В прошлый раз мы рассмотрели одну из возможностей ErrorProne.NET, которая уведомляет о некорректной обработке предусловий в блоке итераторов и в асинхронных методах. Сам анализ не является сложным и не представляет особого интереса, но реализация фикса довольно любопытна.
Анализатор определяет, что асинхронный метод содержит «невалидную» проверку аргументов и предлагает выделить метод для решения этой проблемы:
Каждый фиксер должен указать, какую проблему он должен решать. Для этого нужно унаследоваться от класса CodeFixProvider и переопределить свойство FixableDiagnosticIds:
[ExportCodeFixProvider("AsyncMethodPreconditionCodeFixProvider",
LanguageNames.CSharp), Shared]
public sealed class AsyncMethodPreconditionCodeFixProvider : CodeFixProvider
{
private const string FixText =
"Extract preconditions into separate non-async method";
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(RuleIds.SuspiciousPreconditionInAsyncMethod);
И теперь нужно реализовать метод RegisterCodeFixesAsync, задача которого состоит в регистрации действия, которое будет выполнено для осуществления фикса. Именно здесь и сосредоточена основная бизнес-логика.
Данный метод получает CodeFixContext в качестве параметра, через который мы можем получить объект диагностики. Сделать это можно достаточно обобщенным образом:
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync();
var method = context.GetFirstNodeWithDiagnostic<MethodDeclarationSyntax>(root);
Где GetFirstNodeWithDiagnostic – это метод расширения, который может использоваться повторно:
public static T GetFirstNodeWithDiagnostic<T>(
this CodeFixContext context, SyntaxNode root) where T : SyntaxNode
{
Contract.Requires(root != null);
Contract.Ensures(Contract.Result<T>() != null);
var diagnostic = context.Diagnostics.First();
var node = root.FindNode(diagnostic.Location.SourceSpan);
return node.AncestorsAndSelf().OfType<T>().First();
}
Далее, нам нужно реализовать специальный случай паттерна Extract Method: нужно взять исходный метод (например, FooAsync) и разбить его на два – в первом оставить проверку предусловий, а в новый метод перенести тело, но без предусловия.
Более детально решение должно выглядеть так:
- Найти блок с предусловиями в исходном методе
- Выделить метод:
- Склонировать исходный метод оставив исходную сигнатуру (новый метод должен иметь тот же тип возвращаемого значения и тот же набор параметров).
- Сделать метод закрытым
- Удалить из нового метода все предусловия
- Изменить исходный метод
- Убрать контекстное ключевое слово async из декларации метода (да, метод все еще возвращает Task или Task<T>, но его реализация не будет перекорежена компилятором в конечный автомат).
- Оставить в теле метода проверку предусловий
- Делегировать работу методу, созданному на этапе 2: return DoMethodAsync(args).
// Пункт 1: получаем семантическую модель и получаем блок "контрактов" метода
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var preconditionBlock =
PreconditionsBlock.GetPreconditions(method, semanticModel);
Contract.Assert(preconditionBlock.Preconditions.Count != 0,
"Метод должен иметь как минимум одно предусловие!");
// Получаем хэш-сет с операторами предусловий. Это упростит их удаление в новом методе
var preconditionStatements =
preconditionBlock.Preconditions
.Select(p => p.IfThrowStaement).ToImmutableHashSet();
// Выделяем тело нового метода: оно содержит все операторы исходного метода,
// но без блока предусловий
var extractedMethodBody =
method.Body.Statements
.Where(s => !preconditionStatements.Contains(s));
// Пункт 2: "Клонируем" исходный метод, путем замены тела метода и путем
// изменения имени и видимости
var extractedMethod =
method.WithStatements(extractedMethodBody)
.WithIdentifier(Identifier($"Do{method.Identifier.Text}"))
.WithVisibilityModifier(VisibilityModifier.Private);
// Пункт 3: изменяем тело текущего метода и удаляем все тело
// кроме предусловий
var updatedMethodBody =
method.Body.Statements
.Where(s => preconditionStatements.Contains(s)).ToList();
// Создаем выражение вызова метода
var originalMethodCallExpression = CreateMethodCallExpression(extractedMethod, method.ParameterList.AsArguments());
// Пункт 4: добавляем return DoExtractedMethod();
updatedMethodBody.Add(SyntaxFactory.ReturnStatement(originalMethodCallExpression));
// И удаляем ключевое слово async
var updatedMethod =
method.WithStatements(updatedMethodBody)
.WithoutModifiers(t => t.IsKind(SyntaxKind.AsyncKeyword));
В данном фрагменте используются некоторые методы расширения, такие как ParameterList.AsArguments() для получения аргументам по списку параметров метода, или WithoutModifiers, который позволяет удалить модификаторы из метода. Все они достаточно простые и вы их можете найти в коде на github, но они не слишком большие и не должны влиять на понимание происходящего в этом фрагменте. Также я не привожу исходный код класса PreconditionBlock, который также не очень сложен и не слишком важен.
Теперь, все что нам осталось, это зарегистрировать действие внутри контекста:
// Заменяем метод парой узлов: новым методом и выделенным методом
var newRoot = root.ReplaceNode(method, new[] {updatedMethod, extractedMethod});
var codeAction = CodeAction.Create(FixText,
ct => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)));
context.RegisterCodeFix(codeAction, context.Diagnostics.First());
Последний этап довольно простой, но очень важно сделать обновление атомарным. Например, следующий код работать не будет:
var newRoot =
root.ReplaceNode(method, updatedMethod)
.InsertNodesAfter(updatedMethod, new[] { extractedMethod });
В этом случае, исходный метод будет заменен на обновленный метод, но выделенный метод «вставлен» не будет. Произойдет это потому, что InsertNoesAfter не сможет найти обновленный метод в обновленном дереве.
Заключение
Относительная простота реализации выделения метода связана с тем, что мы делаем простую и весьма частную операцию. Благодаря иммутабельности Розлиновских деревяшек, любой метод WithXXX клонирует исходный узел дерева, что является хорошей отправной точкой для реализации этого рефакторинга. К тому же, в данном случае нам не нужно анализировать, какие переменные находятся в скоупе, и нужно ли их переносить в новый метод или нет. Мы знаем, что в новом методе нужно удалить предусловия, а в старом оставить их.
Данная реализация не защищает от коллизий, если метод DoMethodName уже есть в данном классе, но избавление от коллизий не будет такой уж сложной задачей.
Комментариев нет:
Отправить комментарий