Пишем расширения c Roslyn к 2015 студии (часть 1)
Для начала, нам потребуется: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
public static T FindUp
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
У нас есть новое бинарное выражение, но было бы неплохо, чтобы оно как-то оказалось в документе. Сначала порождаем новый корень с замененным выражением:
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: Если вы заинтересованы в каких-то рефакторингах (средствах автоматизации нудных действий), то пишите в комментариях предложения.