Для начала, нам потребуется:
1. 2015 студия
2. SDK для разработки расширений
3. Шаблоны проектов
4. Визуализатор синтаксиса
4. Крепкие нервы
Полезные ссылки: исходники roslyn, исходники и документация roslyn, roadmap с фичами С# 6.
Наверное вас смутило, что вам потребуются крепкие нервы и вы хотите пояснения. Все дело в том, что весь API компилятора — это низкоуровненное кодогенерерированное API. Вы будете смеяться, но простейший способ создать код — это распарсить строку. Иначе вы либо погрязнете в куче нечитаемого кода, либо будете писать тысячи extension-методов, чтобы ваш код выглядел синтаксически не как полная кака. И еще две тысячи extension-методов, чтобы оставаться на приемлемом уровне абстракций. Ладно, я вас убедил, что писать Roslyn расширения к студии это плохая идея? И очень хорошо, что убедил, а то кто-то из читающих эту статью может написать второй ReSharper по прожорливости ресурсов. Не убедил? Платформа все еще сырая, бывают баги и не доработки.
Вы все еще здесь? Приступаем. Давайте напишем простейший рефакторинг, который для бинарной операции поменяет местами два аргумента. Например, было: 1 — 5. Стало: 5 — 1.
Сначала создаем проект используя один из предустановленных шаблонов.
Для того, чтобы представить какой-то рефакторинг нужно объявить провайдер рефакторингов. Т.е. штуку, которая будет говорить «О, вы хотите сделать здесь код красивее? Ну, можно вот так вот сделать:…. Нравится?». Вообще, рефакторинги — они не только о том, как сделать красивее. Они больше о том, как автоматизировать какие-то нудные действия.
Ок, давайте напишем SwapBinaryExpressionArgumentsProvider (я надеюсь вам нравится мой стиль именования).
Во-первых, он должен наследоваться от абстрактного класса CodeRefactoringProvider, потому что иначе IDE не сможет работать с ним. Во-вторых, он должен быть помечен аттрибутом ExportCodeRefactoringProvider, потому что иначе IDE не сможет найти ваш провайдер. Аттрибут Shared здесь для красоты.
[ExportCodeRefactoringProvider("SwapBinary", LanguageNames.CSharp), Shared]
public class SwapBinaryExpressionArgumentsProvider : CodeRefactoringProvider
Теперь, естественно, нужно реализовать наш провайдер. Нужно сделать всего один асинхронный метод, вот такой вот:
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) {
CodeRefactoringContext — это просто штуковина, в которой лежит текущий документ (Document), текущее место в тексте (TextSpan), токен для отмены (CancellationToken). А еще он предоставляет возможность зарегистрировать ваше действие с кодом.
Т.е. на входе у нас информация о документе, на выходе обещание чего-нибудь сделать. Почему метод асинхронный? Потому что первичен текст. А всякие ништяки типа распарсенного кода или информации о классах в не сбилденном проекте — это медленно. А еще вы можете написать очень медленный код, а его никто не любит. Даже разработчики студии.
Теперь было бы неплохо получить распарсенное синтаксическое дерево. Делается это так:
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
Осторожно, root может быть равен null. Впрочем это неважно. Важно другое — ваш код не должен бросать исключений. Поскольку мы тут все не гении, то единственный способ избежать исключений это завернуть ваш код try/catch.
try {
// ваш код
}
catch (Exception ex) {
// TODO: add logging
}
Даже этот код, с пустым блоком catch — это самое лучшее решение, которое можно придумать. Иначе вы будете раздражать юзера тем, что студия кидает MessageBox «вы установили расширение, написанное криворуким мутантом» и больше не даст пользователю воспользоваться вашим расширением даже в другом участке кода (до перезапуска студии). Но лучше все-таки писать в лог и отправлять на ваш сервер для анализа.
Итак, мы получили информацию о синтаксическом дереве, но нас-то просят предложить рефакторинг для участка кода, где стоит курсор пользователя. Найти этот узел можно так:
root.FindNode(context.Span)
Но нам нужно найти самый ближайший бинарный оператор. С помощью Roslyn Syntax Visualizer мы можем узнать, что он представляется классом BinaryExpressionSyntax. Т.е. у нас есть узел (SyntaxNode) — он должен быть BinaryExpressionSyntax, либо его предок должен им быть, либо предок-предка,…. Было бы неплохо, если бы у нас был способ из текущего узла попытаться найти какую-нибудь специфичную ноду. Например, чтобы мы могли писать так:
node.FindUp<BinaryExpressionSyntax>(limit: 3)
. Концепция очень простая — берем текущий узел и его предков, фильтруем чтобы они были определенного типа, возвращаем первый попавшийся.
public static IEnumerable<SyntaxNode> GetThisAndParents(this SyntaxNode node, int limit) {
while (limit> 0 && node != null) {
yield return node;
node = node.Parent;
limit--;
}
}
public static T FindUp<T>(this SyntaxNode node, int limit = int.Max)
where T : SyntaxNode {
return node
.GetThisAndParents(limit)
.Select(n => n as T)
.Where(n => n != null)
.FirstOrDefault();
}
Теперь у нас есть бинарное выражение, которое нужно отрефакторить. Ну или нету, в этом случае делаем просто return.
Теперь нужно сказать среде, что у нас есть способ переписать этот код. Эту концепцию представляет класс CodeAction. Самый простой код:
context.RegisterRefactoring(CodeAction.Create("Хотите, поменяю?", newDocument))
Вторым параметром идет измененная версия документа. Или измененная версия солюшена. Или асинхронный метод, которые породит измененную версию документа/солюшена. В последнем случае ваши изменения не будут вычисляться до того, как пользователь наведет мышкой на ваше предложение по изменению кода. Простые преобразования не имеет смысла делать асинхронными.
Итак, возвращаемся к нашим баранам. У нас есть BinaryExpressionSyntax expression, нам нужно создать новый, в котором аргументы будут перевернутыми. Важный факт — все неизменяемое. Мы не можем поменять что-то в текущем узле, мы можем только создать новый. У каждого класса, представляющего какую-либо кодосущность есть методы, чтобы породить новую чуточку-измененную кодосущность. У бинарного выражения нам сейчас интересны свойства Left/Right и методы WithLeft/WithRight. Вот так вот:
var newExpression = expression
.WithLeft(expression.Right)
.WithRight(expression.Left)
.Nicefy()
Nicefy это мой хелпер, который делает из кода конфетку. Он выглядит так:
public static T Nicefy<T>(this T node) where T : SyntaxNode {
return node.WithAdditionalAnnotations(
Formatter.Annotation,
Simplifier.Annotation);
}
Дело в том, что мы не можем работать просто с кодом. Мы работаем прежде всего с текстовым представлением кода. Даже если у нас код распарсен — то он все-равно содержит информацию о текстовом представлении кода. В лучшем случае с неправильным текстовым представлением вы получите плохо выглядящий код. Но если вы порождаете код сами и не расставляете форматирования то вы можете получить например «vari=5», что является некорректным кодом.
Аннотация Formatter делает ваш код красивым и синтаксически корректным. Аннотация Simplifier убирает из кода всякие redudant вещи, типа System.String -> string; System.DateTime -> DateTime (последнее делается при условии, что подключен namespace System).
У нас есть новое бинарное выражение, но было бы неплохо, чтобы оно как-то оказалось в документе. Сначала порождаем новый корень с замененным выражением:
var newRoot = root.ReplaceNode(expression, newExpression);
И теперь мы можем получить новый документ:
var newDocument = context.Document.WithSyntaxRoot(newRoot);
Осталось скомпоновать все в кучу. Мы сделали это! Мы написали первое расширение для студии.
Теперь запускаем его с помощью F5 / Ctrl + F5. При этом запускается новая студия в режиме Roslyn, с пустым набором расширений и дефолтными настройками. Они не сбрасываются после перезапуска, т.е. если вы хотите, то можете настроить этот экземпляр студии под себя.
Пишем какой-нибудь код, типа:
var a = 5 - 1;
Проверяем, что все работает. Проверили? Все ок? Поздравляю!
Поздравляю, вы написали код, который будет падать и раздражать пользователя в редких случаях. И наш try/catch этому не поможет. Я завел connected issue на этот баг студии
Вкратце, что происходит:
1. Пользователь пишет «1 — 1»
2. Мы порождаем новое синтаксическое дерево, которое выглядит так: «1 — 1»
3. Но при этом оно не является исходным (в смысле reference equality, т.е. равенства ссылок), поэтому студия думает, что исходное и новое дерево абсолютно разные.
4. А раз они абсолютно разные, то падает контракт внутри студии, который проверяет, что исходное и новое дерево абсолютно разные.
Чтобы исправить баг, нужно проверить, что исходное и новое синтаксическое дерево не являются одинаковыми:
!SyntaxFactory.AreEquivalent(root, newRoot, false);
В этой части я попытался рассказать какое API для вас представляется; и как сделать простейший рефакторинг кода.
В следующих частях вы узнаете:
— как порождать новый код с помощью SyntaxFactory
— что такое SemanticModel и как с этим работать (на примере расширения, которое позволит вам автоматически заменять List на ICollection, IEnumerable; т.е. заменять тип на базовый/интерфейс)
— как писать юнит тесты на это все дело
— диагностики кода
Если вы хотите двигаться дальше, но вам не хватает примеров кода, то вам помогут примеры от разработчиков и код моего расширения.
P.S: Если вы заинтересованы в каких-то рефакторингах (средствах автоматизации нудных действий), то пишите в комментариях предложения.
Автор: nsinreal