Автор: Sergey Teplyakov
В прошлый раз мы рассмотрели одну из возможностей 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).
В данном фрагменте используются некоторые методы расширения, такие как 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