вторник, 12 апреля 2016 г.

ErrorProne.NET. Часть 4. Реализуем Extract Method

В прошлый раз мы рассмотрели одну из возможностей ErrorProne.NET, которая уведомляет о некорректной обработке предусловий в блоке итераторов и в асинхронных методах. Сам анализ не является сложным и не представляет особого интереса, но реализация фикса довольно любопытна.

Анализатор определяет, что асинхронный метод содержит «невалидную» проверку аргументов и предлагает выделить метод для решения этой проблемы:

clip_image002

Каждый фиксер должен указать, какую проблему он должен решать. Для этого нужно унаследоваться от класса 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) и разбить его на два – в первом оставить проверку предусловий, а в новый метод перенести тело, но без предусловия.

Более детально решение должно выглядеть так:

  1. Найти блок с предусловиями в исходном методе
  2. Выделить метод:
    • Склонировать исходный метод оставив исходную сигнатуру (новый метод должен иметь тот же тип возвращаемого значения и тот же набор параметров).
    • Сделать метод закрытым
    • Удалить из нового метода все предусловия
    • Изменить исходный метод
    • Убрать контекстное ключевое слово async из декларации метода (да, метод все еще возвращает Task или Task<T>, но его реализация не будет перекорежена компилятором в конечный автомат).
  3. Оставить в теле метода проверку предусловий
  4. Делегировать работу методу, созданному на этапе 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 уже есть в данном классе, но избавление от коллизий не будет такой уж сложной задачей.

Дополнительные ссылки

Комментариев нет:

Отправить комментарий