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

в 19:14, , рубрики: .net, resharper must die, roslyn, Visual Studio

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

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js