C# 和 Visual Basic

使用 Roslyn 编写 API 的实时代码分析器

Alex Turner

十年来,Visual Studio 代码分析提供了 C# 和 Visual Basic 程序集的生成时分析,运行针对 Microsoft .NET Framework 2.0 编写的一组特定的 FxCop 规则。这些规则极大地帮助您避免代码中的常见缺陷,但它们所针对都是开发人员在 2005 年已经解决的问题。那么,我们今天要讨论的新语言功能和 API 会如何呢?

通过 Visual Studio 2015 预览版中实时的、基于项目的代码分析器,API 作者可以提供特定于域的代码分析作为其 NuGet 程序包的一部分。由于这些分析器由 .NET 编译器平台(代号为“Roslyn”)提供支持,因此在您键入代码时,甚至在您完成一行之前,它们就能生成警告,不再等到生成代码时才找出您所犯的错误。分析器还可以通过新的 Visual Studio 灯泡图标提示来显现自动代码修补程序,让您立即清理代码。

许多实时代码分析器只占 50 到 100 行代码,您无需成为编译器专业程序员,也可编写这些代码。在本文中,我将介绍如何编写一款分析器,该分析器针对使用 .NET 正则表达式 (regex) 所导致的常见编码问题:如何确保在运行应用之前,您所编写的 regex 模式在语法上是有效的。为此,我将说明如何编写分析器,该分析器包含指出无效 regex 模式的诊断。稍后我将在本文第二部分继续讨论,其中我将说明如何添加代码修补程序,以便清理您的分析器所发现的错误。

准备工作

首先,确保您有必要的 Visual Studio 2015 预览版位:

  • 以这两种方法之一通过 Visual Studio 2015 预览版设置一个框:
    1.       从 aka.ms/vs2015preview 安装 Visual Studio 2015 预览版。
    2.       从 aka.ms/vs2015azuregallery 获取预构建的 Azure VM 映像。
  • aka.ms/vs2015preview 安装 Visual Studio 2015 Preview SDK。即使您正在使用 Azure VM 映像,也需要执行这一操作。
  • aka.ms/roslynsdktemplates 安装 SDK 模板 VSIX 包,以获取 .NET 编译器平台项目模板。
  • aka.ms/roslynsyntaxvisualizer 安装语法可视化工具 VSIX 包,以获取语法可视化工具工具窗口,从而帮助探讨您将要分析的语法树。
  • 您还需要将语法可视化工具安装到您将用于调试分析器的实验性 Visual Studio 沙盒中。我将在下文的语法可视化工具部分中介绍如何执行上述操作。

探索分析器模板

在探索完之后,将其与 Visual Studio 2015、Visual Studio SDK 和必要的 VSIX 包一起运行,以便查看生成分析器的项目模板。

在 Visual Studio 2015 中,请转到“文件”|“新项目”|“C#”|“可扩展性”,并选择带有代码修补程序 (NuGet + VSIX) 模板的诊断,如图 1 中所示。您还可以在 Visual Basic 中创建分析器,但就本文来说,我将使用 C#。请确保在开头将目标 Framework 设置为 .NET Framework 4.5。将您的项目命名为 RegexAnalyzer,并选择“确定”创建此项目。

创建分析器项目
图 1 创建分析器项目

您将看到此模板生成的一组项目(三个):

  • RegexAnalyzer:这是主项目,构建包含诊断和代码修补程序的分析器 DLL。构建此项目还产生包含分析器的项目的本地 NuGet 程序包(.nupkg 文件)。
  • RegexAnalyzer.VSIX:这是将您的分析器 DLL 绑定到 Visual Studio 范围扩展包(.vsix 文件)的项目。如果您的分析器不需要添加影响构建的警告,则可以选择分布此 .vsix 文件而不是分布 NuGet 程序包。无论哪种方式,.vsix 项目允许您按 F5 来测试 Visual Studio 单独(调试对象)实例中的分析器。
  • RegexAnalyzer.Test:这是一个单元测试项目,让您确保分析器正在生成正确的诊断和修补程序,而无需每次都运行 Visual Studio 的调试对象实例。

如果您在主项目中打开 DiagnosticAnalyzer.cs 文件,则可以看到生成诊断的模板中的默认代码。该诊断执行的操作有点愚笨,它在任何包含小写字母的类型名称下都画波浪线。但是,由于大部分程序都具有这种类型名称,这可以让您轻松地看到工作中的分析器。

请确保 RegexAnalyzer.VSIX 项目为启动项目,并按 F5。运行 VSIX 项目会负载 Visual Studio 的实验性沙盒副本,这让 Visual Studio 追踪一组单独的 Visual Studio 扩展。在开发您自己的扩展,并需要使用 Visual Studio 来调试 Visual Studio 时,这很有用。由于调试对象 Visual Studio 实例是新的实验性沙盒,您将看到第一次运行 Visual Studio 2015 时所出现的相同对话框。像平常一样依次单击这些对话框。当您下载 Visual Studio 自身的调试符号时,也会有一些延迟。在第一次运行之后,Visual Studio 应为您缓存这些符号。

一旦您的调试对象 Visual Studio 实例正在运行,请使用它来创建 C# 控制台应用程序。因为您正在调试的分析器是 Visual Studio 范围的 .vsix 扩展,您应该在几秒钟内就能看到 Program 类定义上出现绿色的波浪线。如果您将鼠标悬停在此波浪线上或查看错误列表,将看到以下消息:“类型名称‘Program’包含小写字母”,如图 2 中所示。在您单击此波浪线时,您将看到左侧出现一个灯泡图标。单击灯泡图标将显示“转换成大写字母”修补程序,通过将类型名称更改成大写字母来清理此诊断。

分析器模板的代码修补程序
图 2 分析器模板的代码修补程序

您还可以从此处调试分析器。在您的 Visual Studio 主实例中,在 Diagnostic­Analyzer.cs 的 Initialize 方法中设置一个断点。随着您在编辑器中键入,分析器会不断地重复计算诊断。下次您在调试对象 Visual Studio 实例的 Program.cs 中键入时,将会看到调试器在该断点停止。

目前,保持控制台应用程序项目打开,因为在下一部分您还要进一步使用它。

使用语法可视化工具检查相关代码

既然您面向分析器模板,现在是时候来规划您将在分析的代码中查找的代码模式,从而决定何时画波浪线。

您的目标是,引入一个会在编写无效的 regex 模式时显示的错误。首先,在控制台应用程序的 Main 方法中,添加以下行,调用带有无效 regex 模式的 Regex.Match:

Regex.Match("my text", @"\pXXX");

请看以下代码并将鼠标悬停在 Regex 和 Match 上,您可以推断出想要生成波浪线时的条件:

  • 有一个对 Regex.Match 方法的调用。
  • 涉及的 Regex 类型来自 System.Text.RegularExpressions 命名空间。
  • 该方法的第二个参数是表示无效正则表达式模式的字符串参数。在实践中,表达式还可以是一个变量或常量引用,或计算的字符串,但对于分析器的初始版本,我将首先关注字符串参数。在您继续支持更多代码模式之前,通常最好让分析器针对简单案例进行端对端的工作。

那么,如何将这些简单限制转换成 .NET 编译器平台代码呢?语法可视化工具是帮助解决这一问题的强大工具。

您将需要在一直用于调试分析器的实验性沙盒中安装该可视化工具。您以前或许安装过可视化工具,但安装程序只将程序包安装到您的主 Visual Studio 中。

既然您还在调试对象 Visual Studio 实例中,请打开“工具”|“扩展与更新”|“联机”,并搜索“语法可视化工具”的 Visual Studio 库。下载并安装 .NET 编译器平台语法可视化工具包。然后选择“立即重新启动”来重新启动 Visual Studio。

在 Visual Studio 重新启动后,打开同一个控制台应用程序项目,并通过选择“视图”|“其他窗口”|“Roslyn 语法可视化工具”打开语法可视化工具。现在您可以在编辑器中四处移动插入点,并查看语法可视化工具向您显示的语法树的相关部分。图 3 介绍您在此处感兴趣的 Regex.Match 调用表达式的视图。

 适用于目标调用表达式的运行中的语法可视化工具
图 3 适用于目标调用表达式的运行中的语法可视化工具

语法树的部件 当您在语法树中进行浏览时,将看到各种元素。

树中的蓝色节点是语法节点,表示在编译器分析文件之后的代码逻辑树结构。

树中的绿色节点是编译器读取源文件时所发现的语法令牌、各个词、数字和符号。令牌显示在树中它们所属的语法节点之下。

树中的红色节点是琐碎内容,代表不是令牌的其他内容:空格、注释等。一些编译器弃用此信息,但 .NET 编译器平台保留它,这样,在修补程序更改用户代码时,您的代码修补程序可以根据需要维护琐碎内容。

通过在编辑器中选择代码,您可以看到树中的相关节点,反之亦然。若要有助于可视化您所关心的节点,可以在语法可视化工具树中右键单击“InvocationExpression”,并选择“查看定向语法图”。这将生成一个 .dgml 图,可视化下面所选节点的树结构,如图 4 中所示。

目标调用表达式的语法图
图 4 目标调用表达式的语法图

在这种情况下,您可以看出您正在寻找调用 Regex.Match 的 Invocation­Expression,其中 ArgumentList 具有包含 StringLiteralExpression 的第二个参数节点。如果字符串值代表一个无效的 regex 模式,如“\pXXX”,则您已经找到要画波浪线的范围。现在,您已经收集到了编写诊断分析器所需的大部分信息。

符号和语义模型:超越语法树虽然语法可视化工具中所示的语法节点、令牌和琐碎内容代表文件的全文,但它们并不会告诉您所有内容。您还需要了解代码中的每个标识符实际所引用的内容。例如,您知道此调用是对带有两个参数的 Regex 类型上的 Match 方法的调用,但不知道该 Regex 类型所处的命名空间,或者不知道调用的是哪个 Match 重载。发现所引用的确切定义需要编译器分析其附近 using 指令的上下文中的标识符。

回答这类问题需要您请求语义模型提供与给定表达式节点相关联的符号。符号代表您的代码定义的逻辑实体,例如您的类型和方法。确定给定表达式所引用的符号的过程被称为绑定。符号还可以代表您所使用的引用库中的实体,如来自基类库 (BCL) 的 Regex 类型。

如果您右键单击“Invocation­Expression”,选择“查看符号”,下面的属性网格将显示所调用方法的方法符号信息,如图 5 中所示。

 在语法可视化工具中查看 Regex.Match 方法符号
图 5 在语法可视化工具中查看 Regex.Match 方法符号

在这种情况下,您可以查看原始定义属性,发现此调用指的是 System.Text.RegularExpressions.Regex.Match 方法,而不是某其他 Regex 类型上的方法。编写诊断的最后一个步骤是绑定该调用,并检查返回的符号是否与字符串 System.Text.RegularExpressions.Regex.Match 匹配。

构建诊断

现在您已经获得了策略,请关闭调试对象 Visual Studio 实例,返回到您的分析器项目来开始构建诊断。

SupportedDiagnostics 属性打开 Diagnostic­Analyzer.cs 文件,查看顶部附近的四个字符串常量。这就是您定义诊断规则元数据的地方。甚至在您的分析器产生任何波浪线之前,规则集编辑器和其他 Visual Studio 功能将使用此元数据了解您的分析器可能生成的诊断详细信息。

更新这些字符串以匹配您打算生成的 Regex 诊断:

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";

在生成此诊断时,在错误列表中向用户显示诊断 ID 和填写的消息字符串。用户还可以使用 #pragma 指令中源代码内的 ID 以禁止该诊断的某个实例。在此后续文章中,我将介绍如何使用此 ID 将您的代码修补程序与此规则相关联。

在声明规则字段的行中,您还可以将您要生成的诊断的严重程度更新为错误,而不是警告。如果 regex 字符串不进行分析,Match 方法肯定会在运行时引发异常,那么您应该阻止此构建,正如您对 C# 编译器错误的处理一样。将规则的严重程度更改为 DiagnosticSeverity.Error:

internal static DiagnosticDescriptor Rule =
  new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
   Category, DiagnosticSeverity.Error, isEnabledByDefault: true);

这也是您决定是否应在默认情况下启用此规则的行。您的分析器可以定义大量在默认情况下关闭的规则集,用户可以选择加入部分或全部规则。使此规则在默认情况下启用。

SupportedDiagnostics 属性返回此诊断描述符作为不可变数组的单个元素。在这种情况下,您的分析器将只生成一种诊断,因此这里没有要更改的内容。如果您的分析器可以生成多种诊断,则可以生成多个描述符并将其全部从 SupportedDiagnostics 返回。

Initialize 方法您的诊断分析器的主要入口点是 Initialize 方法。在这种方法中,您注册一组操作来处理编译器遍历您的代码时所触发的各种事件,如查找各种语法节点,或引起新符号的声明。从此模板获取的简单默认分析器调用 RegisterSymbolAction 找出何时更改或引入类型符号。在该情况下,符号操作允许分析器查看每个类型符号,以了解其是否指明无效名称的类型需要画波浪线。

在这种情况下,您需要注册 SyntaxNode 操作找出何时有对 Regex.Match 的新调用。请回想一下,在探索语法可视化工具时,您正在查找的特定节点类型是 InvocationExpression,因此使用以下调用替代 Initialize 方法中的 Register 调用:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);

Regex 分析器只需注册本地生成诊断的语法节点操作。跨多个方法收集数据的更为复杂的分析是怎样的呢?有关此问题的详细信息,请参阅本文中的“处理其他 Register 方法”。

AnalyzeNode 方法删除此模板中您不再需要的 AnalyzeSymbol 方法。在其位置上,您将创建 AnalyzeNode 方法。单击 RegisterSyntaxNodeAction 调用中的 AnalyzeNode,按 Ctrl+Dot 打开新的灯泡图标菜单。从此处选择 Generate 方法来创建带有正确签名的 AnalyzeNode 方法。在生成的 AnalyzeNode 方法中,将参数的名称从“obj”更改为“context”。

既然您已经获得了分析器的核心,现在是时候来检查有疑问的语法节点了,看看您是否应该显现诊断。

首先,您应该按 Ctrl+F5 再次启动 Visual Studio 的调试对象实例(这次不再进行调试)。打开您刚才查看的控制台应用程序,确保语法可视化工具处于可用状态。在您构建 AnalyzeNode 方法时,您将查看可视化工具数次以找到相关详细信息。

获取目标节点您在 AnalyzeNode 方法中采取的第一步就是获取您正在分析的节点对象,并将其转换为相关类型。要找到该类型,请使用您刚才打开的语法可视化工具。选择 Regex.Match 调用,在语法树中导航到 InvocationExpression 节点。在属性网格正上方,您可以看到 InvocationExpression 就是这种类型的节点,其类型为 InvocationExpressionSyntax。

您可以通过类型检查来测试节点的类型,或通过 IsKind 方法来测试其特定类型。但是,在这里您可以保证不进行任何测试即可转换成功,这是因为您请求了 Initialize 方法中的特定类型。您的操作所分析的节点可从上下文参数上的节点属性中获得:

var invocationExpr = (InvocationExpressionSyntax)context.Node;

既然您有调用节点,则需要检查它是否为需要波浪线的 Regex.Match 调用。

**检查 1:这是否为对 Match 方法的调用?**您需要首先检查的是确保此调用是对正确 Regex.Match 的调用。由于此分析器将在编辑器中的每个击键上运行,因此最好首先执行最快测试,只要这些初始测试通过,就询问开销更大的 API 问题。

成本最低的测试旨在查看调用语法是否是对名为 Match 的方法的调用。这是可以在编译器执行任何特定工作来了解这是哪一种特定的 Match 方法之前就可确定的。

回看一下语法可视化工具,会发现 Invocation­Expression 具有两个主要的子节点,即 SimpleMemberAccess­Expression 和 ArgumentList。通过选择图 6 中所示的编辑器中的 Match 标识符,您会发现,正在寻找的节点就是 Simple­MemberAccessExpression 中的第二个 IdentifierName。

查找语法树中的 Match 标识符
图 6 查找语法树中的 Match 标识符

在构建分析器时,您将经常深入研究像这样的语法和符号,以便找到您需要在代码中引用的相关类型和属性值。在构建分析器时,准备带有语法可视化工具的目标项目是非常便利的。

回到您的分析器代码,您可以浏览 IntelliSense 中的 invocationExpr 成员,查找与每个 InvocationExpression 子节点(一个名为 Expression,一个名为 ArgumentList)对应的属性。在这种情况下,您需要名为 Expression 的属性。由于在参数列表前面的调用部分可以拥有多种形式(例如,它可能是本地变量的委托调用),此属性返回通用基类型 ExpressionSyntax。从语法可视化工具中,可以看到预期的具体类型为 MemberAccessExpressionSyntax,因此将其转换为该类型:

var memberAccessExpr =
  invocationExpr.Expression as MemberAccessExpressionSyntax;

当您深入研究 memberAccessExpr 上的属性时,会发现类似的细目。有一个代表点之前任意表达式的 Expression 属性,以及代表点右侧的标识符的 Name 属性。由于您想要查看是否正在调用 Match 方法,请查看 Name 属性的字符串值。对于语法节点而言,获取字符串值是获取该节点源文本的快速方式。您可以使用新的 C#“?.”运算符处理以下情况:正在分析的表达式不是真正的成员访问,导致上一行的“as”子句返回 null 值:

if (memberAccessExpr?.Name.
    ToString() != "Match") return;

如果被调用的方法没有命名为 Match,则只需退出。这样,以最小成本完成了您的分析,且没有诊断生成。

**检查 2:这是否为对真正的 Regex.Match 方法的调用?**如果此方法命名为 Match,请执行更复杂的检查,让编译器准确确定代码所调用的 Match 方法。确定准确的 Match 需要使上下文的语义模型绑定此表达式来获取引用的符号。

在语义模型上调用 GetSymbolInfo 方法,并向此方法传递想要其符号的表达式:

var memberSymbol =
  context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;

返回的符号对象与您在语法可视化工具中通过右键单击“SimpleMemberAccessExpression”并选择“查看符号”所预览的内容相同。在这种情况下,您选择将此符号转换为常见的 IMethodSymbol 接口。由语法可视化工具中提到该符号的内部 PEMethodSymbol 类型实现此接口。

既然您有符号,可以将其与预期来自真正的 Regex.Match 方法的完全限定的名称进行比较。对于符号,获取字符串值将向您提供其完全限定的名称。您实际上并不在乎正在调用哪一个重载,因此您只需检查单词“Match”:

if (!memberSymbol?.ToString().
  StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return;

和前面的测试一样,检查符号是否与您期望的名称匹配,如果不匹配,或者该符号事实上不是方法符号,则退出。虽然在这里对字符串执行操作有一些奇怪,但字符串比较是编译器中的常见操作。

剩余的检查现在您的测试开始达成共鸣。在每一步骤中,您都进一步研究了树,检查语法节点或语义模型,以测试您是否仍处于错误情形。每次您都可以使用语法可视化工具来查看期望的类型和属性,因此您知道在什么情况下返回,以及在什么情况下继续。您将遵循此模式来检查接下来的几个条件。

确保 ArgumentList 至少有两个参数:

var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
if ((argumentList?.Arguments.Count ?? 0) < 2) return;

然后,确保第二个参数是 LiteralExpression,因为您正期待一个字符串参数:

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;

验证 Regex 模式至此,您已经得到了所需的全部数据。您知道您正在调用 Regex.Match,并且已经获取了模式表达式的字符串值。那么,如何来验证它呢?

只需调用同一个 Regex.Match 方法,并传入该模式字符串。由于您仅查找模式字符串中的分析错误,因此可以传递一个空的字符串输入作为第一个参数。在 try-catch 块中进行调用,以便您捕获 Regex.Match 发现无效模式字符串时所引发的 ArgumentException:

try
{
  System.Text.RegularExpressions.Regex.Match("", regex);
}
catch (ArgumentException e)
{
}

如果模式字符串分析没有产生错误,您的 AnalyzeNode 方法将正常退出,无需进行报告。如果出现分析错误,您将捕获参数异常,此时可以准备报告诊断了!

报告诊断在 catch 块中,您使用之前填充的 Rule 对象创建 Diagnostic 对象,这代表您想要生成的某种特定波浪线。特定于该实例,每个诊断都需要以下两个主要内容:应该画波浪线的代码范围,以及插入到您之前定义的消息格式的填充字符串:

var diagnostic =
    Diagnostic.Create(Rule,
    regexLiteral.GetLocation(), e.Message);

在这种情况下,您想要在字符串参数下画波浪线,因此传入其位置作为诊断的范围。还可以提取描述模式字符串存在问题的异常消息,并将其包含在诊断消息中。

最后一步是进行此诊断,并将诊断报告回传递给 AnalyzeNode 的上下文,因此 Visual Studio 知道要向错误列表添加一行,并在编辑器中添加波浪线:

context.ReportDiagnostic(diagnostic);

现在,您在 DiagnosticAnalyzer.cs 中的代码应该如图 7 所示。

图 7 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);
      }
    }
  }
}

尝试一下就是这样,您的诊断现已完成!若要尝试一下,只需按 F5(确保 RegexAnalyzer.VSIX 为启动项目)并重新打开 Visual Studio 调试对象实例中的控制台应用程序。您应该马上就能看到模式表达式上出现指出分析失败原因的红色波浪线,如图 8 中所示。

尝试一下您的诊断分析器
图 8 尝试一下您的诊断分析器

如果您看到波浪线,那么祝贺您!如果没有看到波浪线,则可以在 AnalyzeNode 方法中设置断点,在模式字符串内键入字符来触发重新分析,然后逐步执行分析器代码来查看分析器在何处过早退出。您还可以针对显示 DiagnosticAnalyzer.cs 完整代码的图 7 来检查您的代码。

诊断分析器的使用案例

概括来说,从分析器模板开始,编写大约 30 行新代码,您就能标识用户代码中的真正问题并画出波浪线。最重要的是,无需成为 C# 编译器操作的资深专家就可执行这一操作。您能够专注于正则表达式的目标域,并使用语法可视化工具指导您获得少数几个与您的分析相关的语法节点和符号。

在日常编码中有许多域,可快速编写诊断分析器且很有用:

  • 作为团队的开发人员或领导者,在您执行代码检查时,可能会经常看到其他人犯相同的错误。现在,您可以编写简单的分析器,对反模式画波浪线,并将分析器签入源控件,以确保引入这种 bug 的任何人都会在键入时发现此问题。
  • 作为定义组织业务对象的共享层的维护者,针对难以在类型系统中进行编码的对象,尤其是这些对象涉及数值或涉及某些操作应始终在其他操作之前进行的过程中的步骤时,您或许有正确使用这些对象的业务规则。现在,您可以强制这些控制您的层的使用方式的不明显规则来弥补类型系统的不足。
  • 作为开源或商业 API 程序包的所有者,您可能对于在论坛重复回答相同的问题感到厌倦。您可能甚至已经编写了白皮书和文档,却发现许多客户仍继续提出那些同样的问题,因为他们没有读过您所写的内容。现在,您可以将您的 API 和相关代码分析捆绑到一个 NuGet 程序包,确保使用您的 API 的每个人都能从一开始就看到相同的指导。

希望本文能为您带来灵感,思考构建用于提升自己项目的分析器。通过 .NET 编译器平台,Microsoft 完成了繁重的工作,揭示了 C# 和 Visual Basic 的深入语言理解和丰富代码分析,剩下的就看您的了!

接下来会怎样呢?

现在您让 Visual Studio 显示无效 regex 模式下的错误波浪线。您能实现更多功能吗?

如果您已拥有正则表达式域知识,不仅仅可查看模式字符串的问题所在,还可知道如何修复该问题,则您可以在灯泡图标中建议一个修补程序,正如您在模板的默认分析器中所看到的那样。

由于您了解如何更改您的语法树,在下一篇文章中,我将介绍如何编写该代码修补程序。请继续关注!

处理其他 Register 方法

您可以深入研究 Initialize 方法的上下文参数来查看一整套可以调用的 Register 方法。图 A 中的方法允许您在编译器管道中挂接各种事件。

要注意的关键一点是,使用任何 Register 方法注册的顶级操作不得在分析器类型上的实例字段中存储任何状态。Visual Studio 将针对整个 Visual Studio 会话重用此分析器类型的某个实例,以避免重复分配。在分析未来的编译时,您存储和重用的任何状态都很可能是过时的,如果该状态阻止回收旧的语法节点或符号垃圾,甚至可能导致内存泄漏。

如果您需要此状态在多个操作中保持不变,则应该调用 RegisterCodeBlockStartAction 或 RegisterCompilationStartAction,并将状态储存为该操作方法中的局部变量。传递给这些操作的上下文对象允许您将嵌套操作注册为 lambda 表达式,之后这些嵌套操作可以关闭外部操作中的局部变量来保持状态。

图 A 挂接各种事件的 Register 方法

RegisterSyntaxNodeAction 在分析特定类型的语法节点时触发
RegisterSymbolAction 在分析特定类型的符号时触发
RegisterSyntaxTreeAction 在分析文件的整棵语法树时触发
RegisterSemanticModelAction 在语义模型可用于整个文件时触发

RegisterCodeBlockStartAction

RegisterCodeBlockEndAction

在分析方法主体或其他代码块之前/之后触发

RegisterCompilationStartAction

RegisterCompilationEndAction

在分析整个项目之前/之后触发

Alex Turner是 Microsoft 托管语言团队的高级项目经理,他一直酝酿在 .NET 编译器平台(“Roslyn”)项目上实施 C# 和 Visual Basic 的精华内容。他毕业于石溪大学(Stony Brook University),获得计算机科学专业硕士学位,并在 Build、PDC、TechEd、TechDays 和 MIX 等会议中发表过演讲。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Bill Chiles 和 Lucian Wischik
Bill Chiles 在他的职业生涯中,主要研究语言(CMU Common Lisp、Dylan、IronPython 和 C#)和开发人员工具。在过去 17 年中,他在 Microsoft 开发部门致力于研究从核心 Visual Studio 功能到动态语言运行时再到 C# 的一切内容。

Lucian Wischik 在 Microsoft Visual Basic/C# 语言设计团队工作,特别负责 Visual Basic。在加入 Microsoft 之前,他从事学术界的并发理论和异步的研究工作。在研究方面他目光敏锐,并且具有持之以恒的决心。