Автор: Sergey Teplyakov

В прошлый раз мы рассмотрели одну из возможностей 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 уже есть в данном классе, но избавление от коллизий не будет такой уж сложной задачей.

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

  • ErrorProne.NET на гитхаб
  • ErrorProne.NET. Часть 1
  • ErrorProne.NET. Часть 2
  • ErrorProne.NET. Часть 3

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).