Пишем расширения c Roslyn к 2015 студии (часть 1)

  • Tutorial
Перейти ко второй части

Для начала, нам потребуется:

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

Подробнее
Реклама

Комментарии 14

    0
    Если у вас есть пожелания к изложению материала или вы желаете узнать поподробнее про какие-нибудь аспекты, то пишите, буду рад предложениям.
      +3
      откуда качать крепкие нервы? :)
        +2
        Даются от рождения. В качестве кратковременной замены можно использовать успокоительное по предварительной консультации с врачом.
        0
        Немного не понятно что происходит после
        context.RegisterRefactoring(CodeAction.Create("Хотите, поменяю?", newDocument))
        

        В newDocument будет новое дерево со всем нашим кодом в файле?
        Он будет заменять его целиком?
        Просто ситуация: у нас в файле больше 1000 строк, поменяли что то в последней, что произойдет?
        Одно дело если мы поменяли одну ноду, тут я могу предположить что он заменит только ее. А другое дело, если мы что то добавляли, переименовывали и т.д. Наверное все же придется менять все дерево, а это должно быть не так уж и быстро, поменять и перерисовать, провести анализ, наложить глифи и т.д.
          0
          Нет, все зависит от количества изменений. Под изменением подразумевается замена, добавление, удаление. В roslyn проделали большую работу, они поддерживают два персистентных неизменяемых дерева (красное и зеленое). В результате любая операция с нодами не приводит к большому оверхеду. Чуточку можно почитать здесь: blogs.msdn.com/b/ericlippert/archive/2012/06/08/persistence-facades-and-roslyn-s-red-green-trees.aspx. Суммируя: не бойтесь, но делайте профайлинг вашего кода. На скорость работы отрисовки в студии — забейте. Рефакторинги применяются только если юзер пожелает (для этого и есть асинхронные CodeAction), и отрисовываются уж точно только если юзер пожелает
        +2
        Вместо вот такой конструкции:
                 .Select(n => n as T)
                 .Where(n => n != null)
        
        обычно пишут просто .OfType<T>()
          –2
          OfType кажется кинет Exception если будет null
            +2
            Нет, не кинет.
            0
            Да, верно, раньше там был Чуточку другой код, поэтому OfType использовать было нельзя
            0
            Не знаете, как-то можно заставить это работать хотя бы в 2013 студии? Или даже в 2012.
              0
              Увы, это требует поддержку от IDE. Я не уверен, но скорее всего можно написать костыль с ограниченной функциональностью, который будет реализовывать часть фич из 2015-й студии (а именно — выдергивание провайдеров рефакторингов, передача им проанализированного документа, применение изменений, ну и естественно нужно как-то получить проанализированный документ). В мои цели это не входит. Я жду релиза VS 2015, C# 6 и asp vnext. До этого я работаю на превью VS 2015.
              Хотя вспомнил, Roslyn появлялся еще как CTP для 2010-й студии; причем уже тогда были шаблоны проектов Code Refactorings. Думаю если порыскать по интернету, то можно найти инструкции как завести Roslyn в 2012/2013-студии. Но искать скорее всего придется долго + API наверное будет отличаться.
                0
                Спасибо, буду гуглить. Я просто разрабатываю коммерческое расширение для студии, в котором некоторые возможности рефакторинга весьма пригодились бы, и заставлять людей покупать VS 2015 — не решение, когда много у кого еще стоит 2012, которая их вполне устраивает. Похоже, придется приводить все к наименьшему общему знаменателю и делать все под API студии 2012, при этом таская с собой 15 мегабайт сборок Roslyn =/

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое