适用于 ImmutableArrays 的 Roslyn 分析器和代码感知库

.NET 编译器平台(“Roslyn”)可帮助你生成代码感知库。 代码感知库提供可以使用和工具(Roslyn 分析器)的功能,以最佳方式使用库或避免错误。 本主题说明如何使用 System.Collections.Immutable NuGet 包生成真实的 Roslyn 分析器来捕获常见错误。 该示例还演示如何为分析器找到的代码问题提供代码修补程序。 用户可在 Visual Studio 灯泡 UI 中看到代码修复,并且可以自动为代码应用修补程序。

开始使用

需要满足以下条件才能生成此示例:

  • Visual Studio 2015(不是 Express Edition)或更高版本。 可以使用免费的 Visual Studio Community Edition
  • Visual Studio SDK。 还可以在安装 Visual Studio 时检查 Common Tools 下的 Visual Studio 扩展性工具来同时安装 SDK。 如果已安装 Visual Studio,也可以转到主菜单“文件>新建>项目”,在左侧导航窗格中选择 C#,然后选择“扩展性”来安装此 SDK。 选择“安装 Visual Studio 扩展性工具”痕迹导航项目模板时,它会提示下载并安装 SDK。
  • .NET 编译器平台 (“Roslyn”) SDK。 还可以转到主菜单“文件>新建>项目”,在左侧导航窗格中选择 C#,然后选择“扩展性”来安装此 SDK。 选择“下载 .NET 编译器平台 SDK”痕迹导航项目模板时,它会提示下载并安装 SDK。 此 SDK 包括 Roslyn 语法可视化工具。 此有用工具可帮助你确定分析器中应查找的代码模型类型。 分析器基础结构针对特定代码模型类型调用代码,因此代码仅在必要时执行,并且只能专注于分析相关代码。

有什么问题?

假设你提供了一个具有 ImmutableArray(例如 System.Collections.Immutable.ImmutableArray<T>)支持的库。 C# 开发人员在 .NET 数组方面有很多经验。 但是,由于实现中使用的 ImmutableArrays 和优化技术的性质,C# 开发人员直觉会导致库的用户编写损坏的代码,如下所述。 此外,在运行时之前,用户不会看到其错误,这不是他们在 Visual Studio 中使用 .NET 时使用的质量体验。

用户熟悉编写代码,如下所示:

var a1 = new int[0];
Console.WriteLine("a1.Length = { 0}", a1.Length);
var a2 = new int[] { 1, 2, 3, 4, 5 };
Console.WriteLine("a2.Length = { 0}", a2.Length);

C# 开发人员熟悉创建空数组以填充后续代码行和使用集合初始值设定项语法。 但是,为 ImmutableArray 在运行时崩溃编写相同的代码:

var b1 = new ImmutableArray<int>();
Console.WriteLine("b1.Length = { 0}", b1.Length);
var b2 = new ImmutableArray<int> { 1, 2, 3, 4, 5 };
Console.WriteLine("b2.Length = { 0}", b2.Length);

第一个错误是由于 ImmutableArray 实现使用结构来包装基础数据存储。 结构必须具有无参数构造函数,以便 default(T) 表达式可以返回包含所有零或 null 成员的结构。 当代码访问 b1.Length时,出现运行时 null 取消引用错误,因为 ImmutableArray 结构中没有基础存储数组。 创建空 ImmutableArray ImmutableArray<int>.Empty的正确方法是。

集合初始值设定项出错,因为 ImmutableArray.Add 每次调用该方法时都会返回新实例。 由于 ImmutableArrays 永远不会更改,因此在添加新元素时,将返回新的 ImmutableArray 对象(这可能会出于性能原因与以前存在的 ImmutableArray 共享存储)。 由于 b2 在调用 Add() 五次之前指向第一个 ImmutableArray, b2 因此是默认的 ImmutableArray。 调用长度也会崩溃并出现 null 取消引用错误。 在不手动调用 Add 的情况下初始化 ImmutableArray 的正确方法是使用 ImmutableArray.CreateRange(new int[] {1, 2, 3, 4, 5})

查找触发分析器的相关语法节点类型

若要开始生成分析器,请先确定需要查找的 SyntaxNode 类型。 从菜单“查看>其他 Windows>Roslyn 语法可视化工具”启动语法可视化工具。

将编辑器的插入点放在声明 b1的行上。 你将看到语法可视化工具显示你位于 LocalDeclarationStatement 语法树的节点中。 此节点有一个 VariableDeclaration,反过来有一个 VariableDeclarator,又有一个 EqualsValueClause,最后有一个 ObjectCreationExpression。 单击节点的语法可视化工具树时,编辑器窗口中的语法将突出显示以显示该节点表示的代码。 SyntaxNode 子类型的名称与 C# 语法中使用的名称匹配。

创建分析器项目

在主菜单中,选择“文件”>“新建”>“项目”。“新建项目”对话框中的左侧导航栏中的 C# 项目下,选择“扩展性”,并在右侧窗格中选择“使用代码修复项目模板的分析器”。 输入名称并确认对话框。

该模板将打开 DiagnosticAnalyzer.cs 文件。 选择编辑器缓冲区选项卡。此文件具有派生自 DiagnosticAnalyzer (Roslyn API 类型) 的分析器类(由你提供项目的名称构成)。 新类具有 DiagnosticAnalyzerAttribute 声明分析器与 C# 语言相关,以便编译器发现并加载分析器。

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ImmutableArrayAnalyzerAnalyzer : DiagnosticAnalyzer
{}

可以使用面向 C# 代码的 Visual Basic 实现分析器,反之亦然。 在 DiagnosticAnalyzerAttribute 中,选择分析器是面向一种语言还是同时面向这两种语言更为重要。 需要对语言进行详细建模的更复杂的分析器只能面向单个语言。 例如,如果分析器仅检查类型名称或公共成员名称,则可能可以在 Visual Basic 和 C# 中使用公共语言模型 Roslyn 产品/服务。 例如,FxCop 警告类实现 ISerializable,但类没有 SerializableAttribute 属性是独立于语言的,适用于 Visual Basic 和 C# 代码。

初始化分析器

在类中 DiagnosticAnalyzer 向下滚动一点即可查看 Initialize 该方法。 编译器在激活分析器时调用此方法。 该方法采用一个 AnalysisContext 对象,该对象允许分析器获取上下文信息,并为要分析的代码类型注册事件的回调。

public override void Initialize(AnalysisContext context) {}

在此方法中打开一个新行并键入“context”。以查看 IntelliSense 完成列表。 可以在完成列表中看到许多 Register... 处理各种事件的方法。 例如,第一个 RegisterCodeBlockAction块调用代码,该块通常是大括号之间的代码。 注册块还会调用代码,以获取字段初始值设定项、给定给属性的值或可选参数的值。

另一个示例是, RegisterCompilationStartAction在编译开始时回调代码,这在需要收集多个位置的状态时非常有用。 可以创建一个数据结构,例如,收集使用的所有符号,每次调用分析器以获取某些语法或符号时,都可以保存有关数据结构中每个位置的信息。 由于编译结束而被调用回来时,可以分析保存的所有位置,例如,报告代码在每个 using 语句中使用的符号。

使用语法可视化工具,你了解了编译器处理 ObjectCreationExpression 时需要调用的。 使用此代码设置回调:

context.RegisterSyntaxNodeAction(c => AnalyzeObjectCreation(c),
                                 SyntaxKind.ObjectCreationExpression);

注册语法节点并仅筛选对象创建语法节点。 根据约定,分析器作者在注册操作时使用 lambda,这有助于使分析器保持无状态。 可以使用 Visual Studio 功能 “从使用情况 生成”来创建 AnalyzeObjectCreation 方法。 这也会为你生成正确的上下文参数类型。

为分析器的用户设置属性

以便分析器在 Visual Studio UI 中正确显示,查找并修改以下代码行以标识分析器:

internal const string Category = "Naming";

"Naming" 更改为 "API Guidance"

接下来,使用解决方案资源管理器查找并打开项目中的 Resources.resx 文件。 可以输入分析器、标题等的说明。现在可以将所有这些 "Don't use ImmutableArray<T> constructor" 值更改为现在。 可以在字符串({0}等 {1})中放置字符串格式参数,并在稍后调用 Diagnostic.Create()时提供 params 要传递的参数数组。

分析对象创建表达式

该方法 AnalyzeObjectCreation 采用代码分析器框架提供的不同类型的上下文。 此方法InitializeAnalysisContext允许注册操作回调以设置分析器。 例如,有 SyntaxNodeAnalysisContext一个 CancellationToken 可以传递的。 如果用户开始在编辑器中键入内容,Roslyn 将取消正在运行的分析器以保存工作并提高性能。 另一个示例是,此上下文具有返回对象创建语法节点的 Node 属性。

获取节点,可以假定为筛选语法节点操作的类型:

var objectCreation = (ObjectCreationExpressionSyntax)context.Node;

首次使用分析器启动 Visual Studio

通过生成和执行分析器(按 F5启动 Visual Studio。 由于解决方案资源管理器中的启动项目是 VSIX 项目,因此运行代码会生成代码和 VSIX,然后启动安装了该 VSIX 的 Visual Studio。 以这种方式启动 Visual Studio 时,它会使用不同的注册表配置单元启动,以便在生成分析器时,主要使用 Visual Studio 不会受到测试实例的影响。 首次以这种方式启动时,Visual Studio 会执行与安装 Visual Studio 后首次启动 Visual Studio 时类似的几个初始化。

创建控制台项目,然后将数组代码输入控制台应用程序 Main 方法:

var b1 = new ImmutableArray<int>();
Console.WriteLine("b1.Length = {0}", b1.Length);
var b2 = new ImmutableArray<int> { 1, 2, 3, 4, 5 };
Console.WriteLine("b2.Length = {0}", b2.Length);

具有 ImmutableArray 波形曲线的代码行,因为需要获取不可变的 NuGet 包并将语句添加到 using 代码。 按解决方案资源管理器中项目节点上的右指针按钮,然后选择“管理 NuGet 包”。 在 NuGet 管理器中,在搜索框中键入“Immutable”,然后在左窗格中选择“System.Collections.Immutable”项(请勿选择 Microsoft.Bcl.Immutable),然后按右窗格中的“安装按钮。 安装包会添加对项目引用的引用。

你仍然看到红色波浪线下方ImmutableArray,因此将插入符号放在该标识符中,然后按 Ctrl+。(句点)显示建议的修复菜单,然后选择添加相应的using语句。

立即保存所有并关闭 Visual Studio 的第二个实例,使你处于干净状态以继续。

使用编辑并继续完成分析器

在 Visual Studio 的第一个实例中,通过按第一行上的插入符号按 F9,在方法开头AnalyzeObjectCreation设置断点。

使用 F5 再次启动分析器,并在 Visual Studio 的第二个实例中重新打开上次创建的控制台应用程序。

在断点处返回到 Visual Studio 的第一个实例,因为 Roslyn 编译器看到对象创建表达式并调用到分析器中。

获取对象创建节点。 单步执行通过按 F10 设置变量的行,并在即时窗口中计算表达式"objectCreation.ToString()"objectCreation 你会看到变量指向的语法节点是代码 "new ImmutableArray<int>()",只是你要查找的内容。

获取 ImmutableArray<T> Type 对象。 如果要创建的类型为 ImmutableArray,则需要检查。 首先,获取表示此类型的对象。 使用语义模型检查类型,以确保你拥有正确的类型,并且不会比较字符串。ToString() 在函数末尾输入以下代码行:

var immutableArrayOfTType =
    context.SemanticModel
           .Compilation
           .GetTypeByMetadataName("System.Collections.Immutable.ImmutableArray`1");

使用反杆(')和泛型参数数在元数据中指定泛型类型。 这就是为什么你看不到“...元数据名称中的 ImmutableArray<T>”。

语义模型具有许多有用的内容,可用于询问有关符号、数据流、变量生存期等的问题。Roslyn 出于各种工程原因(性能、建模错误代码等)将语法节点与语义模型分开。 你希望编译模型查找引用中包含的信息,以便进行准确的比较。

可以在编辑器窗口左侧拖动黄色执行指针。 将其拖到设置objectCreation变量的行上,并使用 F10 单步执行新代码行。 如果将鼠标指针悬停在变量 immutableArrayOfType上,则会看到我们在语义模型中找到了确切的类型。

获取对象创建表达式的类型。 本文中的几种方法使用“Type”,但这意味着如果你有“new Foo”表达式,则需要获取 Foo 的模型。 需要获取对象创建表达式的类型,以查看它是否为 ImmutableArray<T> 类型。 再次使用语义模型在对象创建表达式中获取类型符号(ImmutableArray)的符号信息。 在函数末尾输入以下代码行:

var symbolInfo = context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol as INamedTypeSymbol;

由于分析器需要处理编辑器缓冲区中不完整或不正确的代码(例如缺少using语句),因此应检查symbolInfonull 需要从符号信息对象获取命名类型(INamedTypeSymbol),才能完成分析。

比较类型。 由于我们正在寻找的 T 的开放泛型类型,并且代码中的类型是一种具体的泛型类型,因此,可以查询该类型所构造的类型(开放泛型类型)的符号信息,并将该结果与 immutableArrayOfTType该结果进行比较。 在方法末尾输入以下内容:

if (symbolInfo != null &&
    symbolInfo.ConstructedFrom.Equals(immutableArrayOfTType))
{}

报告诊断。 报告诊断非常简单。 使用在项目模板中创建的规则,该项目模板是在 Initialize 方法之前定义的。 由于代码中的这种情况是一个错误,因此可以更改初始化规则的行,以将 DiagnosticSeverity.Warning (绿色波浪线)替换为 DiagnosticSeverity.Error (红色波浪线)。 规则的其余部分从在演练开头附近编辑的资源进行初始化。 还需要报告波形曲线的位置,这是对象创建表达式的类型规范的位置。 在 if 块中输入以下代码:

context.ReportDiagnostic(Diagnostic.Create(Rule, objectCreation.Type.GetLocation()));

函数应如下所示(可能采用不同的格式):

private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
{
    var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
    var immutableArrayOfTType =
        context.SemanticModel
               .Compilation
               .GetTypeByMetadataName(
                   "System.Collections.Immutable.ImmutableArray`1");
    var symbolInfo = context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol as
        INamedTypeSymbol;
    if (symbolInfo != null &&
        symbolInfo.ConstructedFrom.Equals(immutableArrayOfTType))
    {
        context.ReportDiagnostic(
            Diagnostic.Create(Rule, objectCreation.Type.GetLocation()));
    }
}

删除断点,以便可以看到分析器正常工作(并停止返回到 Visual Studio 的第一个实例)。 将执行指针拖到方法的开头,然后按 F5 继续执行。 切换回 Visual Studio 的第二个实例时,编译器将再次开始检查代码,并将调用分析器。 可以看到下面的 ImmutableType<int>波浪线。

为代码问题添加“代码修复”

在开始之前,请关闭 Visual Studio 的第二个实例,并在 Visual Studio 的第一个实例(在其中开发分析器)中停止调试。

添加新类。 使用解决方案资源管理器中项目节点上的快捷菜单(右指针按钮),然后选择添加新项。 添加名为 BuildCodeFixProvider.. 的类。 此类需要派生自CodeFixProvider,需要使用 Ctrl+(句点)调用添加正确using语句的代码修补程序。 此类还需要使用 ExportCodeFixProvider 属性进行批注,并且需要添加语句 using 来解析 LanguageNames 枚举。 应该有一个类文件,其中包含以下代码:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;

namespace ImmutableArrayAnalyzer
{
    [ExportCodeFixProvider(LanguageNames.CSharp)]
    class BuildCodeFixProvider : CodeFixProvider
    {}

存根派生成员。 现在,将编辑器的插入点放在标识符CodeFixProvider中,然后按 Ctrl+。(句点)可保留此抽象基类的实现。 这将为你生成属性和方法。

实现 属性。 使用以下代码填充 FixableDiagnosticIds 属性的 get 正文:

return ImmutableArray.Create(ImmutableArrayAnalyzerAnalyzer.DiagnosticId);

Roslyn 通过匹配这些标识符(只是字符串)将诊断和修补程序组合在一起。 项目模板为你生成了诊断 ID,你可以随意更改它。 属性中的代码仅返回分析器类中的 ID。

RegisterCodeFixAsync 方法采用上下文。 上下文很重要,因为代码修复可以应用于多个诊断,或者代码行上可能存在多个问题。 如果在方法正文中键入“context”。IntelliSense 完成列表将显示一些有用的成员。 有一个 CancellationToken 成员,你可以检查查看某些内容是否要取消修复。 有一个文档成员有很多有用的成员,让你能够访问项目和解决方案模型对象。 有一个 Span 成员,它是报告诊断时指定的代码位置的开始和结束。

使该方法成为异步方法。 首先需要修复生成的方法声明作为方法 async 。 用于消除抽象类实现的代码修复不包括async关键字 (keyword),即使该方法返回 aTask.

获取语法树的根。 若要修改代码,需要使用代码修复所做的更改生成新的语法树。 你需要 Document 从上下文中调用 GetSyntaxRootAsync。 这是一种异步方法,因为获取语法树的工作未知,可能包括从磁盘获取文件、分析该文件以及为其生成 Roslyn 代码模型。 此时,Visual Studio UI 应具有响应性,这使用 async 启用。 将方法中的代码行替换为以下内容:

var root = await context.Document
                        .GetSyntaxRootAsync(context.CancellationToken);

找到有问题的节点。 传入上下文范围,但找到的节点可能不是必须更改的代码。 报告诊断仅提供类型标识符(其中波形曲线所属)的跨度,但你需要替换整个对象创建表达式,包括new开头的关键字 (keyword)和末尾的括号。 将以下代码添加到方法(并使用 Ctrl+ 添加using语句):ObjectCreationExpressionSyntax

var objectCreation = root.FindNode(context.Span)
                         .FirstAncestorOrSelf<ObjectCreationExpressionSyntax>();

为灯泡 UI 注册代码修补程序。 注册代码修复时,Roslyn 会自动插入 Visual Studio 灯泡 UI。 最终用户将看到他们可以使用 Ctrl+。(句号)当分析器波浪线使用错误的ImmutableArray<T>构造函数时。 由于代码修复提供程序仅在出现问题时执行,因此可以假定你具有要查找的对象创建表达式。 在上下文参数中,可以通过将以下代码添加到方法末尾 RegisterCodeFixAsync 来注册新代码修复:

context.RegisterCodeFix(
            CodeAction.Create("Use ImmutableArray<T>.Empty",
                              c => ChangeToImmutableArrayEmpty(objectCreation,
                                                               context.Document,
                                                               c)),
            context.Diagnostics[0]);

需要将编辑器的插入点放在标识符中,CodeAction然后使用 Ctrl+。(句点)添加此类型的相应using语句。

然后将编辑器的插入点放在标识符中ChangeToImmutableArrayEmpty,然后使用 Ctrl+ 再次生成此方法存根。

你添加的最后一个代码片段通过传递 CodeAction 所发现问题的种类和诊断 ID 来注册代码修复。 在此示例中,此代码仅提供一个诊断 ID,因此只需传递诊断 ID 数组的第一个元素即可。 创建 CodeAction时,将传入灯泡 UI 应用作代码修复的说明的文本。 还可以传入一个采用 CancellationToken 并返回新文档的函数。 新文档具有一个新的语法树,其中包含调用 ImmutableArray.Empty的修补代码。 此代码片段使用 lambda,以便它可以关闭 objectCreation 节点和上下文的文档。

构造新的语法树。 ChangeToImmutableArrayEmpty在前面生成的存根的方法中,输入代码行: ImmutableArray<int>.Empty; 如果再次查看 语法可视化工具 窗口,可以看到此语法是 SimpleMemberAccessExpression 节点。 这就是此方法在新文档中构造和返回的内容。

要添加async的第一个更改ChangeToImmutableArrayEmpty是之前Task<Document>添加,因为代码生成器不能假定该方法应该是异步的。

使用以下代码填充正文,使方法如下所示:

private async Task<Document> ChangeToImmutableArrayEmpty(
    ObjectCreationExpressionSyntax objectCreation, Document document,
    CancellationToken c)
{
    var generator = SyntaxGenerator.GetGenerator(document);
    var memberAccess =
        generator.MemberAccessExpression(objectCreation.Type, "Empty");
    var oldRoot = await document.GetSyntaxRootAsync(c);
    var newRoot = oldRoot.ReplaceNode(objectCreation, memberAccess);
    return document.WithSyntaxRoot(newRoot);
}

需要将编辑器的插入符号放在标识符中SyntaxGenerator,并使用 Ctrl+。(句点)添加此类型的相应using语句。

此代码使用 SyntaxGenerator,这是用于构造新代码的有用类型。 获取包含代码问题的文档的生成器后, ChangeToImmutableArrayEmpty 调用 MemberAccessExpression并传递具有要访问的成员的类型,并将成员的名称作为字符串传递。

接下来,该方法提取文档的根目录,由于这可以涉及常规情况下的任意工作,因此代码将等待此调用并传递取消令牌。 Roslyn 代码模型是不可变的,例如使用 .NET 字符串;更新字符串时,会返回一个新的字符串对象。 调用 ReplaceNode时,将返回新的根节点。 大多数语法树都是共享的(因为它是不可变的),但 objectCreation 节点将替换为 memberAccess 节点,以及语法树根的所有父节点。

尝试代码修复

现在可以按 F5 在 Visual Studio 的第二个实例中执行分析器。 打开之前使用的控制台项目。 现在,应会看到灯泡显示在新对象创建表达式所在的 ImmutableArray<int>位置。 如果按 Ctrl+。(句点),则会看到代码修复,并在灯泡 UI 中看到自动生成的代码差异预览。 Roslyn 为你创建这个。

专业提示: 如果启动 Visual Studio 的第二个实例,并且没有看到带代码修复的灯泡,则可能需要清除 Visual Studio 组件缓存。 清除缓存会强制 Visual Studio 重新检查组件,因此 Visual Studio 应选取最新的组件。 首先,关闭 Visual Studio 的第二个实例。 然后,在 Windows 资源管理器,导航到 %LOCALAPPDATA%\Microsoft\VisualStudio\16.0Roslyn\。 (“16.0”使用 Visual Studio 从版本更改为版本。删除子目录 ComponentModelCache

演讲视频和完成代码项目

可在此处查看所有已完成的代码。 子文件夹 DoNotUseImmutableArrayCollectionInitializerDoNotUseImmutableArrayCtor 都有一个 C# 文件,用于查找问题和 C# 文件,用于实现 Visual Studio 灯泡 UI 中显示的代码修复。 请注意,完成的代码有更多的抽象,以避免一遍又一遍地提取 ImmutableArray<T> 类型对象。 它使用嵌套注册的操作在子操作(分析对象创建和分析集合初始化)执行时可用的上下文中保存类型对象。