适用于 ImmutableArrays 的 Roslyn 分析器和代码感知库
.NET 编译器平台(“Roslyn”)可帮助你生成代码感知库。 代码感知库提供的功能包括您可以使用的工具(如 Roslyn 分析器),帮助您以最佳方式使用库以及避免错误。 本主题演示如何构建实际应用的 Roslyn 分析器来检测使用 System.Collections.Immutable NuGet 包时的常见错误。 该示例还演示如何为分析器找到的代码问题提供代码修补程序。 用户可以在 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
时,由于 ImmutableArray 结构中没有底层存储数组,因此会出现运行时空引用错误。 创建空 ImmutableArray 的正确方法是 ImmutableArray<int>.Empty
。
集合初始值设定项出错,因为每次调用 ImmutableArray.Add
方法都会返回新实例。 由于 ImmutableArrays 永远不会更改,因此在添加新元素时,将返回新的 ImmutableArray 对象(这可能会出于性能原因与以前存在的 ImmutableArray 共享存储)。 由于在调用 Add()
五次之前,b2
指向第一个 ImmutableArray,因此 b2
是默认的 ImmutableArray。 对其调用 Length 也会崩溃,并出现 null 取消引用错误。 在不手动调用 Add 的情况下初始化 ImmutableArray 的正确方法是使用 ImmutableArray.CreateRange(new int[] {1, 2, 3, 4, 5})
。
查找触发分析器的相关语法节点类型
若要开始生成分析器,请先确定需要查找的 SyntaxNode 类型。 从“视图”>“其他窗口”>“Roslyn Syntax Visualizer”菜单启动 Syntax Visualizer>>。
将编辑器的插入点放在声明 b1
的行上。 你将看到【语法可视化工具】显示你位于语法树的 LocalDeclarationStatement
节点中。 此节点有一个 VariableDeclaration
,反过来又有一个 VariableDeclarator
,又有一个 EqualsValueClause
,最后还有一个 ObjectCreationExpression
。 当您在语法可视化工具树中单击节点时,编辑器窗口的语法会被高亮显示,以展示该节点所代表的代码。 SyntaxNode 子类型的名称与 C# 语法中使用的名称匹配。
创建分析器项目
在主菜单中,选择 “文件”>“新建>项目”。 在 新建项目 对话框中,在左侧导航栏中的 C# 项目下,选择 扩展性,并在右侧窗格中选择具有代码修复 项目模板的 分析器。 输入名称并确认对话框。
该模板将打开 DiagnosticAnalyzer.cs 文件。 选择编辑器缓冲区选项卡。此文件具有从 DiagnosticAnalyzer
(Roslyn API 类型)派生的分析器类(由你提供项目的名称构成)。 新类具有 DiagnosticAnalyzerAttribute
声明分析器与 C# 语言相关,以便编译器发现并加载分析器。
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ImmutableArrayAnalyzer : DiagnosticAnalyzer
{}
可以使用面向 C# 代码的 Visual Basic 实现分析器,反之亦然。 在 DiagnosticAnalyzerAttribute 中,选择分析器是面向一种语言还是同时面向这两种语言更为重要。 需要对语言进行详细建模的更复杂的分析器只能面向单个语言。 例如,如果您的分析器仅检查类型名称或公共成员名称,那么可以使用 Roslyn 提供的适用于 Visual Basic 和 C# 的通用语言模型。 例如,FxCop 警告类实现了 ISerializable,但该类缺少 SerializableAttribute 属性。此警告与语言无关,适用于 Visual Basic 和 C# 代码。
初始化分析器
在 DiagnosticAnalyzer
类中向下滚动一点,以查看 Initialize
方法。 编译器在激活分析器时调用此方法。 该方法采用一个 AnalysisContext
对象,该对象允许分析器获取上下文信息,并为要分析的代码类型注册事件的回调。
public override void Initialize(AnalysisContext context) {}
在此方法中打开一个新行并键入“context.”以查看 IntelliSense 完成列表。 在完成列表中可以看到用于处理各种事件的许多 Register...
方法。 例如,第一个方法 (RegisterCodeBlockAction
) 会回调你的代码以用于块(通常是大括号之间的代码)。 注册块也会回调到你的代码,以用于字段的初始化器、赋予属性的值或可选参数的值。
作为另一个示例,RegisterCompilationStartAction
会在编译开始时调用你的代码,这在需要汇总多个位置的状态时非常有用。 可以创建一个数据结构,例如,收集使用的所有符号,每次调用分析器以获取某些语法或符号时,都可以保存有关数据结构中每个位置的信息。 因编译结束而回调时,可以分析保存的所有位置,例如报告代码所用的每个 using
语句中的符号。
使用 Syntax Visualizer,你了解到你希望在编译器处理 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
方法采用代码分析器框架提供的不同类型的上下文。 Initialize
方法的 AnalysisContext
允许注册操作回调来设置分析器。 例如,SyntaxNodeAnalysisContext
有一个可以传递的 CancellationToken
。 如果用户开始在编辑器中键入内容,Roslyn 将取消正在运行的分析器以保存工作并提高性能。 另一个示例是,此上下文具有返回对象创建语法节点的 Node 属性。
获取节点,该节点可以假定为筛选语法节点操作所对应的类型:
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
首次使用分析器启动 Visual Studio
通过构建和执行分析器来启动 Visual Studio(按 F5)。 由于解决方案资源管理器中的启动项目是 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
变量的那一行,在 即时窗口 计算表达式 "objectCreation.ToString()"
。 你会看到变量指向的语法节点就是代码 "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
语句),因此应检查 symbolInfo
是否为 null
。 需要从符号信息对象获取命名类型(INamedTypeSymbol),才能完成分析。
比较类型。 由于我们正在寻找的 T 的开放泛型类型,并且代码中的类型是具体的泛型类型,因此可以查询该类型构造的类型(开放泛型类型)的符号信息,并将该结果与 immutableArrayOfTType
进行比较。 在方法末尾输入以下内容:
if (symbolInfo != null &&
symbolInfo.ConstructedFrom.Equals(immutableArrayOfTType))
{}
报告诊断。 报告诊断非常简单。 您使用项目模板中为您创建的规则,该规则是在 Initialize 方法之前定义的。 由于代码中的这种情况是错误的,因此可以更改初始化规则的行,以将 DiagnosticSeverity.Warning
(绿色波浪线)替换为 DiagnosticSeverity.Error
(红色波浪线)。 Rule 的其余部分会从在演练开头附近编辑的资源进行初始化。 你还需要报告波形曲线的位置,即对象创建表达式的类型规范的位置。 在 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(ImmutableArrayAnalyzer.DiagnosticId);
Roslyn 通过匹配这些标识符(只是字符串)来汇总诊断和修复。 项目模板为你生成了诊断 ID,你可以随意更改它。 属性中的代码仅返回分析器类中的 ID。
RegisterCodeFixAsync 方法获取上下文。 上下文很重要,因为代码修复可以应用于多个诊断,或者代码行上可能有多个问题。 如果在方法正文中键入“context.”,IntelliSense 补全列表将显示一些有用的成员。 有一个 CancellationToken 成员,你可以检查该成员以了解某些内容是否需要取消修复。 有一个 Document 成员,该成员包含许多有用的成员,并让你能够访问项目和解决方案模型对象。 有一个 Span 成员,该成员表示你在报告诊断时指定的代码位置的起始和结束。
使该方法成为异步方法。 首先需要修复生成的方法声明,将其作为 async
方法。 用于消除抽象类实现的代码修复不包括 async
关键字,即使该方法返回 Task
也是如此。
获取语法树的根。 若要修改代码,需要使用代码修复所做的更改生成新的语法树。 需要上下文中的 Document
才能调用 GetSyntaxRootAsync
。 这是一种异步方法,因为获取语法树的工作未知,可能包括从磁盘获取文件、分析该文件以及为其生成 Roslyn 代码模型。 此时,Visual Studio UI 应积极响应,使用 async
可实现这点。 将方法中的代码行替换为以下内容:
var root = await context.Document
.GetSyntaxRootAsync(context.CancellationToken);
找到有问题的节点。 虽然你传入了上下文范围,但你找到的节点可能不是必须更改的代码。 报告的诊断仅提供了类型标识符的范围(即波浪线所在的位置),但您需要替换整个对象创建表达式,包括开头的 new
关键字和末尾的括号。 将以下代码添加到方法(并使用 Ctrl+。 为 ObjectCreationExpressionSyntax
添加 using
语句):
var objectCreation = root.FindNode(context.Span)
.FirstAncestorOrSelf<ObjectCreationExpressionSyntax>();
请为灯泡 UI 注册代码修复。 注册代码修复后,Roslyn 会自动插入 Visual Studio 灯泡 UI。 当分析器提示 ImmutableArray<T>
构造函数使用错误时,最终用户将会看到他们可以使用 Ctrl+.(句点)。 由于代码修复提供程序仅在出现问题时执行,因此可以假定你具有要查找的对象创建表达式。 在上下文参数中,可以通过将以下代码添加到 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 并返回新 Document 的函数。 新 Document 有一个新的语法树,其中包含调用 ImmutableArray.Empty
的修补代码。 此代码片段使用 lambda,以便它可以关闭 objectCreation 节点和上下文的 Document。
构造新的语法树。 在前面生成的存根的 ChangeToImmutableArrayEmpty
方法中,输入代码行:ImmutableArray<int>.Empty;
。 如果再次查看 语法可视化工具 工具窗口,可以看到此语法是 SimpleMemberAccessExpression 节点。 这就是此方法需要在新文档中构造并返回的对象。
ChangeToImmutableArrayEmpty
的第一个更改是在 Task<Document>
之前添加 async
,因为代码生成器不能假定该方法应该是异步的。
使用以下代码填充主体部分,使您的方法看起来与以下内容相似:
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 。
讨论视频和完成代码项目
在此处可以查看所有已完成的代码。 子文件夹 DoNotUseImmutableArrayCollectionInitializer 和 DoNotUseImmutableArrayCtor 各包含一个用于查找问题的 C# 文件,以及一个用于实现 Visual Studio 灯泡 UI 中显示的代码修复的 C# 文件。 请注意,完成的代码进行了更高程度的抽象,以避免重复提取 ImmutableArray<T> 类型对象。 它使用嵌套注册的操作在子操作(分析对象创建和分析集合初始化)执行时可用的上下文中保存类型对象。
相关内容
- \\微软Build 2015会议演讲
- 代码已在 GitHub 上完成
- GitHub 上的几个示例,分为三种分析器
- GitHub OSS 站点上的其他文档
- 使用 GitHub 上的 Roslyn 分析器实现的 FxCop 规则