Пишемо розширення 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<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.: Якщо ви зацікавлені в якихось рефакторингах (засоби автоматизації нудних дій), то пишіть в коментарях пропозиції.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.