Перейти ко второй части
Для начала, нам потребуется:
1. 2015 студия
2. SDK для разработки расширений
3. Шаблоны проектов
4. Визуализатор синтаксиса
5. Крепкие нервы
Полезные ссылки: исходники roslyn, исходники и документация roslyn, roadmap с фичами С# 6.
Наверное вас смутило, что вам потребуются крепкие нервы и вы хотите пояснения. Все дело в том, что весь API компилятора — это низкоуровненное кодогенерерированное API. Вы будете смеяться, но простейший способ создать код — это распарсить строку. Иначе вы либо погрязнете в куче нечитаемого кода, либо будете писать тысячи extension-методов, чтобы ваш код выглядел синтаксически не как полная кака. И еще две тысячи extension-методов, чтобы оставаться на приемлемом уровне абстракций. Ладно, я вас убедил, что писать Roslyn расширения к студии это плохая идея? И очень хорошо, что убедил, а то кто-то из читающих эту статью может написать второй ReSharper по прожорливости ресурсов. Не убедил? Платформа все еще сырая, бывают баги и не доработки.
Вы все еще здесь? Приступаем. Давайте напишем простейший рефакторинг, который для бинарной операции поменяет местами два аргумента. Например, было: 1 — 5. Стало: 5 — 1.
Сначала создаем проект используя один из предустановленных шаблонов.
Для того, чтобы представить какой-то рефакторинг нужно объявить провайдер рефакторингов. Т.е. штуку, которая будет говорить «О, вы хотите сделать здесь код красивее? Ну, можно вот так вот сделать:…. Нравится?». Вообще, рефакторинги — они не только о том, как сделать красивее. Они больше о том, как автоматизировать какие-то нудные действия.
Ок, давайте напишем SwapBinaryExpressionArgumentsProvider (я надеюсь вам нравится мой стиль именования).
Во-первых, он должен наследоваться от абстрактного класса CodeRefactoringProvider, потому что иначе IDE не сможет работать с ним. Во-вторых, он должен быть помечен аттрибутом ExportCodeRefactoringProvider, потому что иначе IDE не сможет найти ваш провайдер. Аттрибут Shared здесь для красоты.
Теперь, естественно, нужно реализовать наш провайдер. Нужно сделать всего один асинхронный метод, вот такой вот:
CodeRefactoringContext — это просто штуковина, в которой лежит текущий документ (Document), текущее место в тексте (TextSpan), токен для отмены (CancellationToken). А еще он предоставляет возможность зарегистрировать ваше действие с кодом.
Т.е. на входе у нас информация о документе, на выходе обещание чего-нибудь сделать. Почему метод асинхронный? Потому что первичен текст. А всякие ништяки типа распарсенного кода или информации о классах в не сбилденном проекте — это медленно. А еще вы можете написать очень медленный код, а его никто не любит. Даже разработчики студии.
Теперь было бы неплохо получить распарсенное синтаксическое дерево. Делается это так:
Осторожно, root может быть равен null. Впрочем это неважно. Важно другое — ваш код не должен бросать исключений. Поскольку мы тут все не гении, то единственный способ избежать исключений это завернуть ваш код try/catch.
Даже этот код, с пустым блоком catch — это самое лучшее решение, которое можно придумать. Иначе вы будете раздражать юзера тем, что студия кидает MessageBox «вы установили расширение, написанное криворуким мутантом» и больше не даст пользователю воспользоваться вашим расширением даже в другом участке кода (до перезапуска студии). Но лучше все-таки писать в лог и отправлять на ваш сервер для анализа.
Итак, мы получили информацию о синтаксическом дереве, но нас-то просят предложить рефакторинг для участка кода, где стоит курсор пользователя. Найти этот узел можно так:
Но нам нужно найти самый ближайший бинарный оператор. С помощью Roslyn Syntax Visualizer мы можем узнать, что он представляется классом BinaryExpressionSyntax. Т.е. у нас есть узел (SyntaxNode) — он должен быть BinaryExpressionSyntax, либо его предок должен им быть, либо предок-предка,…. Было бы неплохо, если бы у нас был способ из текущего узла попытаться найти какую-нибудь специфичную ноду. Например, чтобы мы могли писать так:
Теперь у нас есть бинарное выражение, которое нужно отрефакторить. Ну или нету, в этом случае делаем просто return.
Теперь нужно сказать среде, что у нас есть способ переписать этот код. Эту концепцию представляет класс CodeAction. Самый простой код:
Вторым параметром идет измененная версия документа. Или измененная версия солюшена. Или асинхронный метод, которые породит измененную версию документа/солюшена. В последнем случае ваши изменения не будут вычисляться до того, как пользователь наведет мышкой на ваше предложение по изменению кода. Простые преобразования не имеет смысла делать асинхронными.
Итак, возвращаемся к нашим баранам. У нас есть BinaryExpressionSyntax expression, нам нужно создать новый, в котором аргументы будут перевернутыми. Важный факт — все неизменяемое. Мы не можем поменять что-то в текущем узле, мы можем только создать новый. У каждого класса, представляющего какую-либо кодосущность есть методы, чтобы породить новую чуточку-измененную кодосущность. У бинарного выражения нам сейчас интересны свойства Left/Right и методы WithLeft/WithRight. Вот так вот:
Nicefy это мой хелпер, который делает из кода конфетку. Он выглядит так:
Дело в том, что мы не можем работать просто с кодом. Мы работаем прежде всего с текстовым представлением кода. Даже если у нас код распарсен — то он все-равно содержит информацию о текстовом представлении кода. В лучшем случае с неправильным текстовым представлением вы получите плохо выглядящий код. Но если вы порождаете код сами и не расставляете форматирования то вы можете получить например «vari=5», что является некорректным кодом.
Аннотация Formatter делает ваш код красивым и синтаксически корректным. Аннотация Simplifier убирает из кода всякие redudant вещи, типа System.String -> string; System.DateTime -> DateTime (последнее делается при условии, что подключен namespace System).
У нас есть новое бинарное выражение, но было бы неплохо, чтобы оно как-то оказалось в документе. Сначала порождаем новый корень с замененным выражением:
Есть важный момент — нам нельзя ставить аннотации Formatter и Simplifier на корень документа. Потому что тем самым мы можем испортить пользователю жизнь. Да и preview действия, которое переделывает пару десятков строк, когда на самом деле заменяет одно выражение — это грустька.
Осталось скомпоновать все в кучу. Мы сделали это! Мы написали первое расширение для студии.
Теперь запускаем его с помощью F5 / Ctrl + F5. При этом запускается новая студия в режиме Roslyn, с пустым набором расширений и дефолтными настройками. Они не сбрасываются после перезапуска, т.е. если вы хотите, то можете настроить этот экземпляр студии под себя.
Пишем какой-нибудь код, типа:
Проверяем, что все работает. Проверили? Все ок? Поздравляю!
Поздравляю, вы написали код, который будет падать и раздражать пользователя в редких случаях. И наш try/catch этому не поможет. Я завел connected issue на этот баг студии
Вкратце, что происходит:
1. Пользователь пишет «1 — 1»
2. Мы порождаем новое синтаксическое дерево, которое выглядит так: «1 — 1»
3. Но при этом оно не является исходным (в смысле reference equality, т.е. равенства ссылок), поэтому студия думает, что исходное и новое дерево абсолютно разные.
4. А раз они абсолютно разные, то падает контракт внутри студии, который проверяет, что исходное и новое дерево абсолютно разные.
Чтобы исправить баг, нужно проверить, что исходное и новое синтаксическое дерево не являются одинаковыми:
В этой части я попытался рассказать какое API для вас представляется; и как сделать простейший рефакторинг кода.
В следующих частях вы узнаете:
— как порождать новый код с помощью SyntaxFactory
— что такое SemanticModel и как с этим работать (на примере расширения, которое позволит вам автоматически заменять List на ICollection, IEnumerable; т.е. заменять тип на базовый/интерфейс)
— как писать юнит тесты на это все дело
— диагностики кода
Если вы хотите двигаться дальше, но вам не хватает примеров кода, то вам помогут примеры от разработчиков, набор диагностик от FxCop и код моего расширения.
Перейти ко второй части
P.S: Если вы заинтересованы в каких-то рефакторингах (средствах автоматизации нудных действий), то пишите в комментариях предложения.
Для начала, нам потребуется:
1. 2015 студия
2. SDK для разработки расширений
3. Шаблоны проектов
4. Визуализатор синтаксиса
5. Крепкие нервы
Полезные ссылки: исходники 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)
.OfType<T>()
.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);
Есть важный момент — нам нельзя ставить аннотации Formatter и Simplifier на корень документа. Потому что тем самым мы можем испортить пользователю жизнь. Да и preview действия, которое переделывает пару десятков строк, когда на самом деле заменяет одно выражение — это грустька.
Осталось скомпоновать все в кучу. Мы сделали это! Мы написали первое расширение для студии.
Теперь запускаем его с помощью 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; т.е. заменять тип на базовый/интерфейс)
— как писать юнит тесты на это все дело
— диагностики кода
Если вы хотите двигаться дальше, но вам не хватает примеров кода, то вам помогут примеры от разработчиков, набор диагностик от FxCop и код моего расширения.
Перейти ко второй части
P.S: Если вы заинтересованы в каких-то рефакторингах (средствах автоматизации нудных действий), то пишите в комментариях предложения.