C#

Добавление Code Fix в ваш анализатор Roslyn

Алекс Тернер

В этой статье обсуждаются предварительные версии Visual Studio 2015 и .NET Compiler Platform («Roslyn») SDK. Любая изложенная здесь информация может быть изменена.

Продукты и технологии:

.NET Compiler Platform, Visual Studio 2015 Preview

В статье рассматриваются:

  • преобразование неизменяемых синтаксических деревьев;
  • получение идентификатора диагностики;
  • создание исправления кода (code fix) для данной диагностики (diagnostic);
  • обновление кода.

Если вы следовали за моими действиями, описанными в моей предыдущей статье «Use Roslyn to Write a Live Code Analyzer for Your API» (msdn.microsoft.com/magazine/dn879356), то создали анализатор, который показывает ошибки в строках недопустимого шаблона регулярного выражения (regex). Каждый недопустимый шаблон подчеркивается в редакторе красной волнистой линией, и эти линии появляются по мере того, как вы набираете код. Это стало возможным благодаря новым .NET Compiler Platform («Roslyn») API, которые расширили редакторы C# и Visual Basic в Visual Studio 2015 Preview.

Можно ли сделать больше? Если вы знаете предметную область настолько, чтобы не просто заметить ошибку, но и исправить ее, вы можете предложить соответствующее исправление кода через новый значок Visual Studio в виде электрической лампы. Это исправление кода позволит разработчику не только найти ошибку в его коде, но и моментально устранить ее.

В этой статье я покажу, как добавить провайдер исправления кода в диагностический анализатор regex, предлагающий исправления для каждой ошибки в регулярном выражении. Исправление будет добавлено как элемент в меню, открываемое щелчком электрической лампы, что даст возможность предварительно просмотреть исправление и автоматически применить его к коду.

Продолжаем с того места, где мы остановились в прошлый раз

Чтобы приступить к работе, убедитесь, что вы следовали за мной в предыдущей статье. В той статье я показал, как написать первую половину анализатора, который генерирует диагностические волнистые линии под каждой недопустимой строкой шаблона regex. И вы вместе со мной:

  • установили Visual Studio 2015 Preview, ее SDK и соответствующие VSIX-пакеты Roslyn;
  • создали новый проект Diagnostic with Code Fix;
  • добавили код в DiagnosticAnalyzer.cs, чтобы реализовать обнаружение неправильного шаблона regex.

Если же вы не читали ту статью, просмотрите рис. 1, где приведен окончательный код для DiagnosticAnalyzer.cs.

Рис. 1. Полный код для DiagnosticAnalyzer.cs

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace RegexAnalyzer
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class RegexAnalyzerAnalyzer : DiagnosticAnalyzer
  {
    public const string DiagnosticId = "Regex";
    internal const string Title = "Regex error parsing string argument";
    internal const string MessageFormat = "Regex error {0}";
    internal const string Category = "Syntax";
    internal static DiagnosticDescriptor Rule =
      new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
      Category, DiagnosticSeverity.Error, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor>
      SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
    public override void Initialize(AnalysisContext context)
    {
      context.RegisterSyntaxNodeAction(
        AnalyzeNode, SyntaxKind.InvocationExpression);
    }
    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
    {
      var invocationExpr = (InvocationExpressionSyntax)context.Node;
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      if (memberAccessExpr?.Name.ToString() != "Match") return;
      var memberSymbol = context.SemanticModel.
        GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      if (!memberSymbol?.ToString().StartsWith(
        "System.Text.RegularExpressions.Regex.Match") ?? true) return;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      if ((argumentList?.Arguments.Count ?? 0) < 2) return;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      if (regexLiteral == null) return;
      var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
      if (!regexOpt.HasValue) return;
      var regex = regexOpt.Value as string;
      if (regex == null) return;
      try
      {
        System.Text.RegularExpressions.Regex.Match("", regex);
      }
      catch (ArgumentException e)
      {
        var diagnostic =
          Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message);
        context.ReportDiagnostic(diagnostic);
      }
    }
  }
}

Преобразование неизменяемых синтаксических деревьев

В прошлый раз, когда вы писали диагностический анализатор для обнаружения неправильных шаблонов регулярных шаблонов, первым шагом было применение Syntax Visualizer для идентификации шаблонов в синтаксических деревьях, которые указывали на проблемный код. Затем вы написали метод анализа, выполняемый всякий раз, когда обнаруживался релевантный тип узла. Этот метод делал проверку на шаблон синтаксических узлов, которые подтверждали ошибку.

Написание исправления — процесс аналогичный. Вы имеете дело с синтаксическими деревьями, фокусируясь на нужном новом состоянии файлов кода после применения пользователем вашего исправления. Большинство исправлений кода включает добавление, удаление или замену синтаксических узлов в текущих деревьях для создания новых синтаксических деревьев. Вы можете напрямую оперировать над синтаксическими узлами или использовать API, позволяющие вносить изменения на уровне проекта, такие как переименования.

Очень важно понимать, что синтаксические узлы, деревья и символы в .NET Compiler Platform являются неизменяемыми. После создания синтаксический узел или дерево изменить нельзя — данный объект дерева или узла всегда будет представлять один и тот же код на C# или Visual Basic.

Неизменяемость в API для преобразования исходного кода может показаться нелогичной. Как же добавлять, удалять и заменять дочерние узлы в синтаксическом дереве, если ни дерево, ни его узлы нельзя изменять? Здесь полезно обратить внимание на .NET-тип String — еще один неизменяемый тип, которым вы скорее всего пользуетесь ежедневно. Вы довольно часто выполняете операции для преобразования строк, соединяя их и даже заменяя подстроки с помощью String.Replace. Но ни одна из этих операций на самом деле не изменяет исходный строковый объект. Вместо этого каждый вызов возвращает новый строковый объект, который представляет новое состояние строки. Вы можете назначить этот новый объект обратно исходной переменной, но любой метод, которому вы передавали старую строку, по-прежнему будет располагать ее исходным значением.

Добавление узла Parameter в неизменяемое дерево Чтобы понять, как неизменяемость действует в синтаксических деревьях, давайте вручную выполним простое преобразование в редакторе кода и посмотрим, как это повлияет на синтаксическое дерево.

Создайте в Visual Studio 2015 Preview (с установленным расширением Syntax Visualizer) новый файл кода на C#. Замените все его содержимое следующим кодом:

class C
{
  void M()
  {
  }
}

Откройте Syntax Visualizer, выбрав View | Other Windows | Roslyn Syntax Visualizer, и щелкните в любом месте файла кода, чтобы заполнить дерево. В окне Syntax Visualizer щелкните правой кнопкой мыши корневой узел CompilationUnit и выберите View Directed Syntax Graph. Визуализация этого синтаксического дерева даст граф, подобный показанному на рис. 2 (в этом графе опущены серые и белые дополнительные узлы, которые представляют неотображаемые символы). Синий синтаксический узел ParameterList (здесь синие узлы обозначаются темно-серым цветом) имеет две зеленых дочерних лексемы (здесь зеленые узлы обозначаются светло-серым), представляющие его скобки, и ни одного синего дочернего синтаксического узла, так как список не содержит никаких параметров.

Синтаксическое дерево перед преобразованием
Рис. 2. Синтаксическое дерево перед преобразованием

Моделируемое здесь преобразование — то, при котором был бы добавлен новый параметр типа int. Введите код «int i» в скобках списка параметров метода M и следите за изменениями в Syntax Visualizer по мере набора:

class C
{
  void M(int i)
  {
  }
}

Заметьте: даже до того, как вы закончите набор, когда ваш неполный код содержит ошибки компиляции (показываемые в Syntax Visualizer как узлы с красным фоном), дерево все равно остается согласованным, и компилятор догадывается, что ваш новый код образует правильный узел Parameter. Такая устойчивость синтаксических деревьев к ошибкам компиляции как раз и позволяет средствам IDE и вашей диагностике нормально работать применительно к незаконченному коду.

Снова щелкните правой кнопкой мыши корневой узел CompilationUnit и сгенерируйте новый граф, который должен выглядеть, как на рис. 3 (здесь вновь опущены узлы дополнительной информации).

Синтаксическое дерево после преобразования
Рис. 3. Синтаксическое дерево после преобразования

Теперь у ParameterList есть три дочерних элемента: две лексемы, относящиеся к скобкам, как и раньше, плюс новый синтаксический узел Parameter. Когда вы ввели «int i» в редакторе, Visual Studio заменила предыдущее синтаксическое дерево документа новым, которое представляет ваш новый исходный код.

Полная замена хорошо работает для малых строк, которые являются отдельными объектами, но как насчет синтаксических деревьев? В большом файле кода могут содержаться тысячи или десятки тысяч синтаксических узлов, и вы явно не захотите, чтобы все эти узлы заново создавались всякий раз, когда кто-то вводит символ в содержимое этого файла. В ином случае это приводило бы к генерации тонн осиротевших объектов для сборщика мусора, которые ему пришлось бы очищать, что серьезно ухудшило бы производительность.

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

Это повторное использование узла подразумевает, что при каждом нажатии алфавитно-цифровых клавиш заново создаются лишь те узлы в дереве, у которых изменился по крайней мере один потомок, а именно: узкая цепочка узлов-предков вплоть до корня, как показано на рис. 4. Остальные узлы используются повторно как есть.

Узлы-предки, замененные при преобразовании
Рис. 4. Узлы-предки, замененные при преобразовании

В данном случае основное изменение — создание нового узла Parameter и последующая замена ParameterList новым ParameterList, у которого новый Parameter вставлен как дочерний узел. Замена ParameterList также требует замены цепочки узлов-предков, поскольку список дочерних узлов каждого предка изменяется для включения замененного узла. Далее в этой статье вы будете делать такую замену для своего анализатора regex методом SyntaxNode.ReplaceNode, который берет на себя замену всех узлов-предков.

Теперь вы видите общий шаблон планирования исправления кода: вы начинаете с кода до состояния, которое инициирует диагностику. Затем вы вручную вносите изменения, которые должно вводить исправление, и наблюдаете за его влиянием на синтаксическое дерево. Наконец, вы разрабатываете код, необходимый для создания заменяющих узлов и возврата нового синтаксического дерева, содержащего эти узлы.

Убедитесь, что ваш проект открыт и содержит диагностику, созданную вами в прошлый раз. Чтобы реализовать свое исправление кода, нужно копать файл CodeFixProvider.cs.

Метод GetFixableDiagnosticIds

Исправления и диагностики, разрешаемые ими, свободно связываются идентификаторами диагностики. Каждое исправление кода ориентировано на один или более таких идентификаторов. Всякий раз, когда Visual Studio видит диагностику с совпадающим идентификатором, она запрашивает провайдер ваших исправлений кода, если вы можете предложить такие исправления. Свободное связывание на основе строки-идентификатора позволяет одному анализатору предоставлять исправление для диагностики, созданной чьим-то другим анализатором, или даже исправление для встроенных в компилятор сообщений об ошибках и предупреждений.

В нашем случае анализатор создает и диагностику, и исправление кода. Как видите, метод GetFixableDiagnosticIds уже возвращает идентификатор диагностики, который вы определили в своем типе Diagnostic, поэтому здесь менять ничего не надо.

Метод ComputeFixesAsync

Метод ComputeFixesAsync — главный драйвер для исправления кода. Этот метод вызывается всякий раз, когда один или более совпадающих Diagnostic обнаруживаются в данном блоке кода.

Реализация по умолчанию метода ComputeFixesAsync шаблона извлекает первый Diagnostic из контекста (в большинстве случаев ожидается лишь один контекст) и получает интервал диагностики (diagnostic span). Затем в следующей строке ведется поиск синтаксического дерева из этого интервала, чтобы найти ближайших узел объявления типа. В случае правила шаблона по умолчанию это релевантный узел, содержимое которого нужно исправить.

В нашем случае анализатор диагностики искал узлы вызовов, проверяя, являются ли они вызовами Regex.Match. Чтобы было проще сделать логику между диагностикой и исправлением кода общей, измените тип, упомянутый в фильтре OfType дерева поиска, чтобы находить тот же узел InvocationExpressionSyntax. Кроме того, переименуйте локальную переменную в invocationExpr:

var invocationExpr = root.FindToken(
  diagnosticSpan.Start).Parent.AncestorsAndSelf()
  .OfType<InvocationExpressionSyntax>().First();

Теперь у вас есть ссылка на тот же узел вызова, с которого начинал диагностический анализатор. В следующем выражении вы передаете этот узел методу, вычисляющему изменения в коде, которые вы предложите для этого исправления. Переименуйте этот метод из MakeUppercaseAsync в FixRegexAsync и измените описание исправления на «Fix regex»:

context.RegisterFix(
  CodeAction.Create("Fix regex", c => FixRegexAsync(
  context.Document, invocationExpr, c)), diagnostic);

Каждый вызов метода RegisterFix контекста связывает новое действие кода (code action) с нужным Diagnostic и создает элемент меню, открываемом щелчком значка с лампочкой. Заметьте, что на самом деле вы не вызываете метод FixRegexAsync, который еще выполняет преобразование кода. Вместо этого вызов метода обертывается в лямбда-выражение, которое Visual Studio может вызвать позже. Дело в том, что результат вашего преобразования понадобится, только когда пользователь выберет ваш элемент Fix regex. После этого Visual Studio запускает ваше действие для генерации окна предварительного просмотра или для применения исправления. А до тех пор Visual Studio избегает запуска вашего метода исправления — просто на случай, если вы будете выполнять операции с высокими издержками, например переименования в рамках всего решения.

Заметьте, что провайдер исправлений кода не обязан создавать исправление кода для каждого экземпляра данного Diagnostic. Зачастую вы предлагаете исправление только для некоторых случаев, которые способен распознавать ваш анализатор. Если ваши исправления рассчитаны только не некоторые случаи, вы должны сначала протестировать ComputeFixesAsync в любых нужных вам условиях, чтобы определять, можете ли вы исправить проблему в конкретной ситуации. Если условия не подходят, вы должны вернуть управление из ComputeFixesAsync, не вызывая RegisterFix.

В этом примере вы предложите исправление для всех экземпляров диагностики, поэтому проверка условий не потребуется.

Метод FixRegexAsync

Теперь мы добрались до сердцевины исправления кода. Метод FixRegexAsync в том виде, в каком он на данный момент написан, принимает Document и создает обновленный Solution. Хотя диагностические анализаторы проверяют конкретные узлы и символы, исправления могут изменять код в рамках всего решения. Здесь код шаблона вызывает Renamer.RenameSymbolAsync, который модифицирует не только объявление типа в символе, но и любые ссылки на этот символ по всему решению.

В данном случае вы намерены внести лишь локальные изменения в строку шаблона в текущий документ, поэтому вы можете сменить возвращаемый тип метода с Task<Solution> на Task<Document>. Эта сигнатура по-прежнему совместима с лямбда-выражением в ComputeFixesAsync, так как CodeAction.Create имеет другую перегруженную версию, которая принимает Document, а не Solution. Кроме того, вам понадобиться обновить параметр typeDecl, чтобы он соответствовал узлу InvocationExpressionSyntax, который вы передаете из метода ComputeFixesAsync:

private async Task<Document> FixRegexAsync(Document document,
  InvocationExpressionSyntax invocationExpr, CancellationToken cancellationToken)

Поскольку вам не требуется никакой логики для преобразования букв в верхний регистр, удалите и тело этого метода.

Поиск заменяемого узла Первая половина вашего средства исправления будет сильно похожа на первую половину вашего же диагностического анализатора — вам нужно изучить InvocationExpression, чтобы найти релевантные части вызова метода, который будет информировать о вашем исправлении. По сути, вы можете просто скопировать первую половину метода AnalyzeNode в блок try-catch. Но пропустите первую строку, потому что вы уже принимаете invocationExpr как параметр. Поскольку вам известно, что это код, для которого вы успешно нашли диагностику, вы можете удалить все проверки «if». Остается лишь еще одно изменение — получение семантической модели из аргумента Document, так как у вас больше нет контекста, который напрямую предоставляет семантическую модель.

После внесения этих изменений тело вашего метода FixRegexAsync должно выглядеть так:

var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var memberAccessExpr =
  invocationExpr.Expression as MemberAccessExpressionSyntax;
var memberSymbol =
  semanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
var regexLiteral =
  argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
var regexOpt = semanticModel.GetConstantValue(regexLiteral);
var regex = regexOpt.Value as string;

Генерация заменяющего узла Теперь, когда у вас снова есть regexLiteral, который представляет ваш старый строковый литерал, вам нужно сгенерировать новый. Точное вычисление того, какая строка понадобится для исправления произвольного шаблона regex, — большая задача, которая выходит далеко за рамки этой статьи. Пока в качестве подмены вы будете пользоваться просто строкой допустимого regex, которое на самом деле является строкой допустимого шаблона регулярного выражения.

Низкоуровневый способ создания новых синтаксических узлов для заменяющей вставки в ваше дерево — использование членов SyntaxFactory. Эти методы позволяют создавать каждый тип синтаксического узла точно в том виде, в каком вы выбрали. Однако зачастую легче разобрать нужное выражение из текста, дав возможность компилятору взять на себя всю тяжелую работу, связанную с созданием узлов. Чтобы разобрать какой-то фрагмент кода, просто вызовите SyntaxFactory.ParseExpression и укажите код для строкового литерала:

var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"");

Этот новый литерал в большинстве случаев должен нормально работать в качестве замены, но в нем кое-чего не хватает. Вспомните: синтаксические лексемы могут иметь подключенные дополнительные данные, которые представляют неотображаемые символы или комментарии. Вам потребуется скопировать все такие данные из старого выражения-литерала, чтобы не потерять любые пробелы (интервалы) или комментарии из старого кода. Также считается хорошей практикой помечать созданные новые узлы аннотацией «Formatter», которая информирует механизм исправлений кода о том, что вы хотите отформатировать новый узел согласно стилю пользовательского кода. Вам нужно добавить директиву using для пространства имен Microsoft.CodeAnalysis.Formatting. С этими дополнениями вызов ParseExpression выглядит следующим образом:

var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"")
  .WithLeadingTrivia(regexLiteral.GetLeadingTrivia())
  .WithTrailingTrivia(regexLiteral.GetTrailingTrivia())
  .WithAdditionalAnnotations(Formatter.Annotation);

Заменяющая вставка нового узла в синтаксическое дерево Теперь, когда у вас есть новый синтаксический узел для строкового литерала, вы можете заменить старый узел в синтаксическом дереве, тем самым создав новое дерево с исправленной строкой шаблона regex.

Сначала вы получаете корневой узел из синтаксического дерева текущего документа:

var root = await document.GetSyntaxRootAsync();

После этого можно вызвать метод ReplaceNode применительно к этому синтаксическому корню, чтобы заменить старый узел новым:

var newRoot = root.ReplaceNode(regexLiteral, newLiteral);

Помните, что здесь вы генерируете новый корневой узел. Замена любого синтаксического узла также требует замены его предков на всем пути вверх по иерархии к корню. Как вы уже видели, все синтаксические узлы в .NET Compiler Platform неизменяемы. Поэтому данная операция замены на самом деле просто возвращает новый корневой синтаксический узел с замененными целевым узлом и его предками.

После того как у вас появился новый синтаксический корень с исправленным строковым литералом, можно подняться на уровень выше по дереву, чтобы создать новый объект Document, который содержит обновленный корень. Чтобы заменить корень, вызовите метод WithSyntaxRoot в Document:

var newDocument = document.WithSyntaxRoot(newRoot);

Это все тот же шаблон With API, который вы только что видели при вызове WithLeadingTrivia и других методов применительно к разбираемому выражению. Вы будете часто встречать этот шаблон при преобразовании существующих объектов в неизменяемой объектной модели Roslyn. Эта идея похожа на используемую в .NET-методе String.Replace, который возвращает новый строковый объект.

Преобразовав документ, вы можете вернуть его из FixRegexAsync:

return newDocument;

Ваш код в CodeFixProvider.cs должен теперь выглядеть так, как на рис. 5.

Рис. 5. Полный код для CodeFixProvider.cs

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
namespace RegexAnalyzer
{
  [ExportCodeFixProvider("RegexAnalyzerCodeFixProvider",
    LanguageNames.CSharp), Shared]
  public class RegexAnalyzerCodeFixProvider : CodeFixProvider
  {
    public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
    {
      return ImmutableArray.Create(RegexAnalyzer.DiagnosticId);
    }
    public sealed override FixAllProvider GetFixAllProvider()
    {
      return WellKnownFixAllProviders.BatchFixer;
    }
    public sealed override async Task ComputeFixesAsync(CodeFixContext context)
    {
      var root =
        await context.Document.GetSyntaxRootAsync(context.CancellationToken)
        .ConfigureAwait(false);
      var diagnostic = context.Diagnostics.First();
      var diagnosticSpan = diagnostic.Location.SourceSpan;
      // Находим выражение вызова,
      // идентифицированное диагностикой
      var invocationExpr =   
        root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
        .OfType<InvocationExpressionSyntax>().First();
      // Регистрируем действие кода,
      // которое будет вызывать исправление
      context.RegisterFix(
        CodeAction.Create("Fix regex", c =>
        FixRegexAsync(context.Document, invocationExpr, c)), diagnostic);
    }
    private async Task<Document> FixRegexAsync(Document document,
      InvocationExpressionSyntax invocationExpr,
      CancellationToken cancellationToken)
    {
      var semanticModel =
        await document.GetSemanticModelAsync(cancellationToken);
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      var memberSymbol =
        semanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      var regexOpt = semanticModel.GetConstantValue(regexLiteral);
      var regex = regexOpt.Value as string;
      var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"")
        .WithLeadingTrivia(regexLiteral.GetLeadingTrivia())
        .WithTrailingTrivia(regexLiteral.GetTrailingTrivia())
        .WithAdditionalAnnotations(Formatter.Annotation);
      var root = await document.GetSyntaxRootAsync();
      var newRoot = root.ReplaceNode(regexLiteral, newLiteral);
      var newDocument = document.WithSyntaxRoot(newRoot);
      return newDocument;
    }
  }
}

Проверка Вот и все! Теперь вы определили исправление кода, чье преобразование выполняется, когда пользователь встречает вашу диагностику и выбирает исправление из меню, открываемого щелчком значка с лампочкой. Чтобы опробовать этот Code Fix, снова нажмите F5 в главном экземпляре Visual Studio и откройте консольное приложение. На этот раз, когда вы помещаете курсор в участок, выделенный волнистой линией, вы должны увидеть, что слева появился значок лампочки. При щелчке этого значка должно появиться меню, содержащее определенное вами действие кода Fix regex (рис. 6). Это меню показывает превью с построчными различиями между старым Document и новым, представляя состояние вашего кода, если вы выберете применение этого исправления.

Проверка вашего Code Fix
Рис. 6. Проверка вашего Code Fix

Если вы выбираете этот элемент меню, Visual Studio принимает новый Document и адаптирует его как текущее состояние буфера редактора для соответствующего файла исходного кода. Теперь ваше исправление применено!

Мои поздравления

Вы сделали это! Около 70 строк нового кода позволяют идентифицировать проблему в пользовательском коде по мере его набора, выделить проблемное место красной волнистой линией как ошибку и предложить исправление этой ошибки. Попутно вы преобразовывали синтаксические деревья и генерировали новые синтаксические узлы, не выходя за рамки привычной вам предметной области, касающейся регулярных выражений.

Хотя вы можете постоянно улучшать свои диагностику и исправления кода, я нахожу, что анализаторы, встроенные в .NET Compiler Platform, позволяют вам делать очень многое в кратчайшие сроки. Как только вы освоитесь с созданием анализаторов, вы сможете обнаруживать все виды распространенных проблем в коде, который вы пишете ежедневно, и автоматизировать повторяющиеся исправления.


Алекс Тернер (Alex Turner) — старший менеджер программ в группе Managed Languages в Microsoft, где он занимается созданием полезных средств для C# и Visual Basic в проекте .NET Compiler Platform («Roslyn»). Окончил Stony Brook University со степенью магистра наук в области компьютерных наук, выступает на конференциях Build, PDC, TechEd, TechDays и MIX.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Биллу Чайлзу (Bill Chiles) и Лучиану Вишику (Lucian Wischik).