此文章由机器翻译。

C#

将代码修补程序添加到您的 Roslyn 分析器

Alex Turner

如果您遵循的步骤在我以前的文章,"使用 Roslyn 到写活代码分析器为您 API"(msdn.microsoft.com/magazine/dn879356),你写显示实时错误无效的正则表达式 (regex) 模式字符串分析器。每个无效的模式在编辑器中,获取红色的波浪线,就像你会看到编译器错误,和那些乱七八糟的文字出现活的当您键入您的代码。这权力的 C# 和 Visual Basic 编辑经验在 Visual Studio 2015 预览中的新.NET 编译器平台 ("罗斯林") 的 Api,使成为可能。

你可以做更多吗?如果你有知识,看到不只是有什么不对,但也如何修复它,你可以建议通过新的 Visual Studio 灯泡的相关代码修复。此代码修复程序将允许开发人员使用您的分析器不只是在他的代码中查找错误 — — 他立刻可以还清理它。

在这篇文章,我将向你展示如何将代码修补程序提供程序添加到您提供的修补程序的 regex 诊断分析仪在每条曲线都正则表达式。此修复程序将作为一个项目列入灯泡菜单,让用户预览此修复程序并将其应用于她的代码会自动添加。

捡回你停下来的地方

若要开始,请确保您已经遵循与上一篇文章中的步骤。在那篇文章,我将向您展示如何编写第一半您的分析器,生成每个无效的正则表达式模式字符串根据诊断的花体字。那篇文章你穿过:

  • 安装 Visual Studio 2015 预览、 SDK 和相关的罗斯林 VSIX 包。
  • 创建一个新的诊断代码修复项目。
  • 将代码添加到 DiagnosticAnalyzer.cs 来执行无效的正则表达式模式检测。

如果你想要快速赶上,查阅图 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);
      }
    }
  }
}

转型不变的语法树

最后一次,当你在写诊断分析仪,检测到无效的正则表达式模式,第一步是使用语法可视化工具以识别模式的语法树,正表明问题的代码。你然后写跑每次相关的节点类型的分析方法被发现。方法检查有必要误差曲线的语法节点的模式。

写一个修补程序是一个类似的过程。你处理语法树,专注于所需的新状态的代码文件的用户应用您修复后。大多数代码修复包括添加、 删除或替换语法节点从当前的树,产生新的语法树。你可以直接在语法节点上操作或使用让你的 Api 进行项目范围的更改,如重命名。

一个非常重要的属性,了解语法节点、 树木和.NET 编译器平台中的符号是不可变的。一旦创建了一个语法节点或一棵树,它不能修改 — — 一个给定的树或节点对象将始终表示相同的 C# 或 Visual Basic 代码。

用于转换的源代码 API 中不变性似乎有悖常理。如何可以添加、 移除和替换在语法树中的子节点,如果既不是这棵树,也不是它的节点可以改变吗?在这里有用是考虑.NET 字符串类型,另一个不可变类型最有可能使用每一天。您执行操作,很多时候,转换字符串连接在一起,甚至替换子字符串使用 String.Replace。 但是,没有这些操作实际上更改原始字符串对象。相反,每次调用返回新的字符串对象,它表示字符串的新的状态。你可以将这个新对象分配回你原来的变量,但您传递到老的字符串的任何方法将仍有原始值。

将参数节点添加到永恒不变的树探索如何不变性适用于语法树,会在代码编辑器中,手动执行一个简单的转换和查看它如何影响语法树。

在视觉工作室 2015年预览 (扩展名语法可视化工具安装,请参阅以前的文章) 内, 创建一个新的 C# 代码文件。它的所有内容替换为以下代码:

class C
{
  void M()
  }
}

打开语法可视化工具,选择查看 |其他窗口 |罗斯林语法可视化工具和代码文件内的任意位置单击来填充这棵树。在语法视觉­izer 窗口中,右键单击根 CompilationUnit 节点,然后选择视图定向语法图。可视化此语法树结果像一个在图中图 2 (此处所示的图省略表示空白的灰色和白色的琐事节点)。蓝色的参数­列表语法节点都有两个绿色的孩子令牌代表它的圆括号和没有蓝子语法节点,如列表中不包含任何参数。

语法树转换之前
图 2 语法树转换之前

你会在这里模拟的变换是那个会添加一个新的参数属于 int 类型。键入代码"int 我"括号的方法 M 的参数列表和手表内语法可视化工具作为你的变化类型:

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

请注意,甚至在您完成键入时您不完整的代码包含编译错误 (语法可视化工具中显示为节点具有红色背景),这棵树是仍然连贯,编译器猜测您的新代码将形成一个有效的参数节点之前。这种弹性的编译器错误语法树是什么允许 IDE 功能和您的诊断程序来有效的对付不完整的代码。

再次右键单击根 CompilationUnit 节点上,生成一个新的图,应该看起来像图 3 (再次,描绘在这里没有琐事)。

语法树变换后
图 3 语法树变换后

请注意 ParameterList 现在有三个孩子,两个括号标记之前,再加上一个新的参数语法节点。作为您键入"int 我"在编辑器中,Visual Studio 替换文档的以前的语法树这新的语法树,它表示您新的源代码。

执行全额重置作品不够好,小的字符串,是单个对象,但是语法树呢?一个大型的代码文件可能包含数千或数万数以千计的语法节点和你当然不希望所有这些节点必须重新创建每次有人键入一个字符在一个文件内。这将产生孤立对象的垃圾回收器清理和严重伤害性能的吨。

幸运的是,语法节点的不变性质还提供了在这里的转义。因为当你做一个小小改变,就不会受到影响大部分的文档中的节点,这些节点可以安全地重用作为为新树中的儿童。幕后的内部节点中存储的数据对于给定的语法节点只能向下指向节点的子节点。因为这些内部的节点没有父级指针,它是安全的相同的内部节点,让在一遍遍在许多迭代中的给定的语法树,只要代码的那一部分保持不变。

此节点再利用是指在一棵树,需要对每一次击键重新创建中的唯一节点是那些至少一个子体发生了变化,即窄窄的链子的祖先节点到根,如中所示图 4。所有其他节点都按原样重用。

在转换期间替换下来的祖先节点
图 4 在转换期间替换下来的祖先节点

在这种情况下,核心变化是创建您新的参数节点,然后用新的 ParameterList 具有新的参数作为一个子节点插入替换 ParameterList。替换 ParameterList 也需要替换的祖先节点链作为每一个祖先更改列表的子节点,包括替换的节点。本文后面会用 SyntaxNode.ReplaceNode 方法,照顾所有祖先节点都替换为你为你的正则表达式分析器做替换那种。

你现在见到规划代码修复的一般模式:你开始在中使用代码之前触发诊断程序的状态。然后您手动更改此修复程序应使,观察对语法树的影响。最后,你算出所需更换节点创建并返回一个新的语法树,包含它们的代码。

请确保你有你的项目打开,包含您创建最后一次的诊断。若要实现你的代码修复,就会挖 CodeFixProvider.cs。

GetFixableDiagnosticIds 方法

修补程序和诊断程序他们解决由诊断 Id 是松散耦合。每个代码修复的目标是一个或多个诊断 Id。每当 Visual Studio 见到具有匹配 ID 的诊断,它会询问您代码修补程序提供商是否您有代码修复提供。松散耦合的基础的 ID 字符串允许一个分析器来的别人的分析器中或甚至修复内置编译器错误和警告产生的诊断提供一种解决方案。

在这种情况下,您的分析器产生诊断程序和代码修复。你可以看到的 GetFixableDiagnosticIds 方法已经返回的诊断的 ID,您定义在您诊断的类型,所以没什么可在此处进行更改。

ComputeFixesAsync 方法

ComputeFixesAsync 方法是代码修复的主要驱动力。每当一个或多个匹配诊断发现给定时段内的代码时,将调用此方法。

你可以看到模板的默认实现的 ComputeFixesAsync 方法掏出的第一次诊断从上下文 (在大多数情况下,只想要一个),并获取诊断的范围。下一行然后搜索语法树从那要找到最近的类型声明节点的范围。在默认的模板规则,是其内容需要固定的相关节点。

你的情况,你写的诊断分析仪在寻找,看看他们是否对 Regex.Match 的调用的调用。 为了帮助分享您的诊断和你代码修复之间的逻辑,请更改树搜索 OfType 筛选器所述要找到那相同的 InvocationExpressionSyntax 节点的类型。重命名为"invocationExpr,"局部变量:

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

现在可以对诊断分析仪开头相同的调用节点的引用。在 next 语句中,您将传递此节点到会计算你会果脯的代码更改的方法­gesting 使此修复程序。重命名该方法从 MakeUppercaseAsync 到 FixRegexAsync,并更改修复程序描述为修复正则表达式:

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

每次调用上下文的 RegisterFix 方法将新的代码操作与诊断波形曲线上时,相关联,会产生光灯泡内的菜单项。请注意你实际上并没有打电话尚未执行代码转换的 FixRegexAsync 方法。相反,该方法调用被包裹在 Visual Studio 能过一会再打一个 lambda 表达式。这是转换的因为您的结果转换的只需要当用户实际上选择您修复 regex 的项目。当修复项目是突出显示或选择时,Visual Studio 调用你的行动,以生成预览或应用此修复程序。直到那时,Visual Studio 避免运行您的修复方法,只是以防万一你执行昂贵的操作例如,重命名解决方案范围。

请注意代码修复提供程序并不义务产生的每个实例的给定的诊断代码修复。它往往是你能修复建议仅对某些情况你分析仪可以波形曲线的情况。如果你只能修复一些情况下,您应该首先在测试 ComputeFixesAsync 您需要确定是否可以修复的具体情况的任何条件。如果这些条件得不到满足,您应该返回从 ComputeFixesAsync 而无需调用 RegisterFix。

对于此示例,你会提供修补程序的所有实例的诊断,所以没有更多的条件来检查。

FixRegexAsync 方法

现在,您得到的代码修复心脏。目前写的 FixRegexAsync 方法将文档并生成已更新的解决方案。虽然诊断分析仪看看特定节点和符号,代码修复可以更改在整个解决方案的代码。你可以看看这里的模板代码打电话 Renamer.RenameSymbol­异步,改变不只是符号的类型声明,但也对整个解决方案那标志的任何引用。

在这种情况下,您只希望对在当前文档中,模式字符串进行本地更改,所以您可以从任务更改方法的返回类型<解决方案>任务<文档>。此签名是仍然符合 ComputeFixes 中的 lambda 表达式­异步,作为 CodeAction.Create 具有接受一份文件,而不是解决办法的另一个重载。你也将需要更新的 typeDecl 参数,以匹配你正在从 ComputeFixesAsync 方法传递中的 InvocationExpressionSyntax 节点:

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

因为你不需要任何"让大写"逻辑,删除的方法,以及身体。

寻找到替换节点你固定在今年上半年将看上去很像第一一半你诊断分析仪 — — 你需要挖进 InvocationExpression 找到可否告知您修复的方法调用的相关部分。事实上,你可以只是在 try-catch 块 AnalyzeNode 法在今年上半年将复制。跳过的第一行,尽管,你已作为 invocationExpr 作为一个参数。因为你知道这是为你成功地发现了一个诊断的代码,您可以删除所有的"如果"检查。只有其他转变,使是语义模型提取文档参数,如你不再有直接提供的语义模型的上下文。

当你完成这些更改时,您的 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,代表你旧的字符串文本,您需要生成新的一个。计算到底什么你需要修复一个任意的正则表达式模式的字符串一项大的任务,远远超出本文的范围。作为现在的替身,您将只使用字符串有效正则表达式,这确实是一个有效的正则表达式模式的字符串。如果您决定要你自己去进一步,你应该开始小和目标你在非常特殊的正则表达式问题的修复。

低级的方式来产生新的语法节点的代入你的树是通过上 SyntaxFactory 的成员。这些方法让你在完全您选择的形状中创建每种类型的语法节点。然而,往往证明只是解析的表达你想要从文本,让编译器做所有繁重的操作来创建节点变得更容易。若要分析的代码片段,只是调用 SyntaxFactory.ParseExpression 并指定字符串的代码:

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

这个新的文本会工作以及在大多数情况下,替代,但它失去了一些东西。如果你还记得,语法标记可以有附加表示空白或注释的琐事。您将需要复制任何琐事从旧的文本表达式,以确保不从旧代码中删除任何间距或评论。它也是良好的做法来标记您用"格式化程序"批注,通知您想要您新的节点,根据最终用户代码样式设置格式的代码修复引擎创建的新节点。您需要添加一条 using 指令 Microsoft.CodeAnalysis.Formatting 命名空间。与这些电子束­,你的 ParseExpression 电话看起来像这样:

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

交换新的节点到语法树现在,你有一个新的语法节点字符串,您可以替换旧的节点在语法树中,生产与一个固定的正则表达式模式字符串的新树。

第一,你得到当前文档语法树的根节点:

var root = await document.GetSyntaxRootAsync();

现在,可以给出旧的语法节点交换和交换在新一个该语法根上调用 ReplaceNode 方法:

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

请记住您正在生成一个新的根节点。替换任何语法节点也要求您替换它到根的父母。正如你看到之前,.NET 编译器平台中的所有语法节点都是不可变的。此替换操作实际上只是返回一个新的根语法节点与目标节点和更换指示其祖先。

既然您已经有一个新的语法根用点燃的固定字符串­,你可以走上一个更多级别的树,以生成新的文档对象,其中包含您最新的根。若要替换的根,在文档上使用 WithSyntaxRoot 方法:

var newDocument = document.WithSyntaxRoot(newRoot);

这是你刚才看到时调用 WithLeadingTrivia 和其他方法对你解析的表达的相同与 API 模式。经常转换 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;
      // Find the invocation expression identified by the diagnostic.
      var invocationExpr =   
        root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
        .OfType<InvocationExpressionSyntax>().First();
      // Register a code action that will invoke the fix.
      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;
    }
  }
}

尝试它就是这样 !现在,您已经定义了代码修复其变换运行时用户会遇到你的诊断和修复从菜单中选择灯泡。尝试代码修复,请按 F5 再次在 Visual Studio 的主实例,打开控制台应用程序。这一次,当您将光标放在你波形曲线上时,你应该看到左侧显示一个灯泡。点击灯泡应该弹出一个菜单包含修复正则表达式代码操作定义,如中所示图 6。此菜单显示预览与内联比较旧的文档和新文档之间创建,这表示您的代码的状态,如果您选择要应用此修复程序。

尝试您的代码修复
图 6 尝试您的代码修复

如果您选择该菜单项,Visual Studio 将新文档,并且采用它作为该源文件的编辑缓冲区的当前状态。现今已应用到您修复 !

祝贺

你做到了 !在大约 70 全行新代码,您确定问题在您的用户代码中,活着,他打字、 以及它的错误,如红字弯弯曲曲地浮出水面的一个代码修复程序,可以把它收拾干净。你转换语法树,生成新的语法节点,一路走来,所有在你熟悉的目标域中的正则表达式操作时。

虽然你可以不断地细化的诊断程序和代码修复你写,我发现分析仪用.NET 编译器平台建成让你在短的时间内完成很多任务。一旦你得到舒适的建筑分析仪,你就会开始发现各种常见的问题您编码生活的日常和检测重复修补程序,您可以自动执行。

你将分析什么?


Alex Turner 是的微软,在那里他已经酝酿了 C# 和 Visual Basic.NET 编译器平台 ("罗斯林") 项目善良的托管语言团队高级项目经理。他毕业于纽约州立大学石溪分校的计算机科学硕士,曾在生成、 PDC、 TechEd、 TechDays 和混合。

感谢以下的微软技术专家对本文的审阅:条例草案辅以辣椒和卢西安 Wischik
卢西恩 • Wischik 是基于 VB / C# 语言设计团队在微软,尤其负责 vb。在加入微软之前他在学术界对并发性理论与异步工作。他是一个热衷于水手和长距离游泳的选手。

条例草案辣椒工作语言 (CMU Common Lisp,迪伦,IronPython,和 C#),开发人员工具的大部分他的职业生涯。他花了过去 17 年来在工作一切从核心 Visual Studio 功能,对动态语言运行时的语言,C# 的微软开发部)。