Пишем расширения 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(limit: 3) . Концепция очень простая — берем текущий узел и его предков, фильтруем чтобы они были определенного типа, возвращаем первый попавшийся. public static IEnumerable GetThisAndParents (this SyntaxNode node, int limit) { while (limit> 0 && node!= null) { yield return node; node = node.Parent; limit--; } }

public static T FindUp(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(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: Если вы заинтересованы в каких-то рефакторингах (средствах автоматизации нудных действий), то пишите в комментариях предложения.

© Habrahabr.ru