Roslyn-秘籍-全-
Roslyn 秘籍(全)
原文:
zh.annas-archive.org/md5/2134ab55bdcded8205d2f4b0f494825e译者:飞龙
前言
软件开发者日常处理源代码。无论他们工作的技术或编程语言是什么,他们都必须在代码上执行一系列常规任务:
-
将源代码编译成特定运行时的二进制格式
-
分析源代码以识别源代码中的问题
-
编辑源代码以修复问题或重构以提高维护性、可理解性、性能、安全性等
-
导航源代码以搜索模式、引用、定义和关系
-
调试源代码以观察和修复功能、性能、安全性等方面的运行时行为
-
可视化源组件集合(项目)、它们的属性、配置等
.NET 编译器平台(代号Roslyn)是.NET 编程语言 C#和 Visual Basic 的平台,旨在构建工具和扩展以执行这些常规编程任务。值得注意的是,这个平台在 Microsoft 的.NET 编译器和.NET 开发 Visual Studio IDE 之间是共享的。
Roslyn 被划分为多个编程层,每一层都公开 API 以编写定制的工具和扩展:
-
代码分析层:源代码的核心语法和语义表示层。C#和 Visual Basic 编译器(csc.exe和vbc.exe)都是基于这一层编写的。
-
工作空间层:收集一组逻辑上相关源文件的项目和解决方案层。这些与任何特定宿主无关,例如 Visual Studio。
-
功能层:在 CodeAnalysis 和 Workspaces API 之上构建的一组 IDE 功能,如代码修复、重构、IntelliSense、自动完成、查找引用和导航到定义等。这些功能与任何特定宿主无关,例如 Visual Studio。
-
Visual Studio 层:将所有编译器和 IDE 功能汇集和激活的 Visual Studio 工作空间和项目系统。
Roslyn 本质上是一堆服务,它遵循两个核心设计原则:可扩展性(用于上层和第三方插件)和可维护性(它在这些层之间有良好文档和支持的公共 API)。外部开发者或第三方可以在这些服务之上做很多酷的事情:
-
为任何特定编程层编写自己的工具以完成之前提到的任何编程任务。
-
为特定层编写简单的插件(例如诊断分析器、代码修复和重构、自动完成和 IntelliSense 提供者)。
-
在任何特定层执行高级场景,例如实现自己的编译器、工作空间、IDE 或项目系统,以及整个堆栈的其他功能会自动激活。
本书涵盖的内容
第一章,编写诊断分析器,使开发者能够为 C#编译器和 Visual Studio IDE 编写诊断分析器扩展,以分析源代码并报告警告和错误。最终用户将在从命令行或 Visual Studio 构建项目时看到这些诊断,并在 Visual Studio IDE 中编辑源代码时实时看到它们。
第二章,在.NET 项目中使用诊断分析器,使 C#社区的开发者能够利用第三方 Roslyn 诊断分析器为其 C#项目服务。您将学习如何在 Visual Studio 中搜索、安装、查看和配置诊断分析器。
第三章,编写 IDE 代码修复、重构和 IntelliSense 完成提供者,使开发者能够为 Visual Studio IDE 编写代码修复和代码重构扩展,以编辑 C#源代码并修复编译器/分析器诊断以及重构源代码。它还使开发者能够为 Visual Studio IDE 中的 C# IntelliSense 编写完成提供者扩展,以增强代码编辑体验。
第四章,提高 C#代码库的代码维护性,使 C#社区的开发者能够通过使用集成到 Visual Studio IDE 中的分析器和代码修复,以及一些流行的第三方实现,来提高其源代码的代码维护性和可读性。
第五章,在 C#代码中捕捉安全漏洞和性能问题,使 C#社区的开发者能够通过使用流行的第三方分析器,如 PUMA 扫描分析器和 FxCop 分析器,来捕捉其 C#代码库中的安全和性能问题。
第六章,Visual Studio Enterprise 中的实时单元测试,使开发者能够使用 Visual Studio 2017 Enterprise 版中基于 Roslyn 的新功能,该功能允许在后台执行智能实时单元测试(LUT)。LUT 在您编辑代码时自动在后台运行受影响的单元测试,并在编辑器中实时可视化结果和代码覆盖率。
第七章,C#交互式和脚本编程,使开发者能够在 Visual Studio 中使用 C#交互式和脚本功能。C#脚本是一种使用 REPL(读取-评估-打印循环)快速测试 C#和.NET 片段的工具,无需创建多个单元测试或控制台项目。
第八章,向 Roslyn C#编译器开源代码贡献简单功能,使开发者能够向开源的 Roslyn C#编译器添加新功能。你将学习如何实现新的 C#编译器错误,为它们添加单元测试,然后将你的代码更改的 pull request 发送到 Roslyn 仓库,以便将其纳入 C#编译器的下一个版本。
第九章,设计和实现新的 C#语言特性,使开发者能够在开源的 Roslyn C#编译器中设计新的 C#语言特性,并实现该特性的各种编译阶段。你将学习编译设计和实现的以下方面:语言设计、解析、语义分析和绑定,以及代码生成,并伴有合适的代码示例。
第十章,基于 Roslyn API 的命令行工具,使开发者能够使用 Roslyn 编译器和 Workspaces API 编写命令行工具来分析和/或编辑 C#代码。
你需要为本书准备
你需要 Visual Studio 2017 Community/Enterprise 版本来执行本书中的食谱。你可以从www.visualstudio.com/downloads/安装 Visual Studio 2017。此外,有些章节需要你从desktop.github.com/安装 GitHub 桌面工具。
本书面向对象
.NET 开发者和架构师,他们有兴趣充分利用基于 Roslyn 的扩展和工具来改进开发流程,会发现这本书很有用。Roslyn 贡献者,即生产者和 C#社区开发者,也会发现这本书很有用。
习惯用法
在本书中,你将找到多种文本样式,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名和用户输入如下所示:“此外,.editorconfig文件可以与源文件一起提交到仓库,以便为每个为仓库做出贡献的用户强制执行规则。”
代码块应如下设置:
private void Method_PreferBraces(bool flag)
{
if (flag)
{
Console.WriteLine(flag);
}
}
任何命令行输入或输出都应如下编写:
msbuild ClassLibrary.csproj /v:m
新术语和重要词汇以粗体显示。屏幕上看到的词汇,例如在菜单或对话框中,在文本中如下所示:“启动 Visual Studio,点击文件 | 新建 | 项目...,创建一个新的 C#类库项目,并用ClassLibrary/Class1.cs中的代码替换Class1.cs中的代码。”
警告或重要注意事项以如下框的形式出现。
小贴士和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者的反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。
要发送给我们一般性的反馈,只需发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书的标题。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你的账户中下载本书的示例代码文件 www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的“支持”选项卡上。
-
点击“代码下载 & 错误清单”。
-
在搜索框中输入书的名称。
-
选择你想要下载代码文件的书籍。
-
从下拉菜单中选择你购买这本书的地方。
-
点击“代码下载”。
文件下载完成后,请确保使用最新版本解压缩或提取文件夹:
-
Windows 版本的 WinRAR / 7-Zip
-
Mac 版本的 Zipeg / iZip / UnRarX
-
Linux 版本的 7-Zip / PeaZip
本书代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Roslyn-Cookbook。我们还有其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。去看看吧!
下载本书的颜色图片
我们还为你提供了一个包含本书中使用的截图/图表颜色图片的 PDF 文件。这些颜色图片将帮助你更好地理解输出的变化。你可以从 www.packtpub.com/sites/default/files/downloads/RoslynCookbook_ColorImages.pdf 下载此文件。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。
第一章:编写诊断分析器
在本章中,我们将介绍以下内容:
-
在 Visual Studio 中创建、调试和执行分析器项目
-
创建一个符号分析器以报告符号声明的问题
-
创建一个语法节点分析器以报告语言语法的问题
-
创建一个语法树分析器以分析源文件并报告语法问题
-
创建一个方法体分析器以分析整个方法并报告问题
-
创建一个分析整个编译并报告问题的编译分析器
-
为分析器项目编写单元测试
-
发布分析器项目的 NuGet 包和 VSIX
引言
诊断分析器是 Roslyn C# 编译器和 Visual Studio IDE 的扩展,用于分析用户代码并报告诊断。用户在从 Visual Studio 构建项目后,甚至在命令行构建项目时都会看到这些诊断。当在 Visual Studio IDE 中编辑源代码时,他们也会实时看到诊断。分析器可以报告诊断以强制执行特定的代码样式、提高代码质量和维护性、推荐设计指南,甚至报告无法由核心编译器覆盖的非常特定的问题。本章使 C# 开发者能够编写、调试、测试和发布执行不同类型分析的分析器。
如果您不熟悉 Roslyn 的架构和 API 层,建议在继续阅读本章之前,先阅读本书的序言,以获得对 Roslyn API 的基本了解。
诊断分析器建立在 Roslyn 的 CodeAnalysis/编译器层 API 之上。分析器可以通过注册一个或多个分析器操作来分析特定的代码单元,例如符号、语法节点、代码块、编译等。编译器层在编译感兴趣的代码单元时会对分析器进行回调。分析器可以在代码单元上报告诊断,这些诊断被添加到编译器诊断列表中,并报告给最终用户。
根据执行的分析类型,分析器可以大致分为以下两个类别:
-
无状态分析器:通过注册一个或多个分析器操作来报告特定代码单元诊断的分析器:
-
不需要在分析器操作之间维护任何状态。
-
与单个分析器操作的执行顺序无关。
-
例如,一个独立检查每个类声明并报告声明问题的分析器是一个无状态分析器。我们将在本章后面向您展示如何编写无状态符号、语法节点和语法树分析器。
-
有状态的分析器:报告特定代码单元诊断信息,但在包含代码单元的上下文中,例如代码块或编译。这些分析器更复杂,需要强大和广泛的分析,因此需要仔细设计以实现高效的分析器执行而不会出现内存泄漏。这些分析器至少需要以下一种状态操作进行分析:
-
访问包含代码单元的不可变状态对象,例如编译或代码块。例如,访问在编译中定义的某些已知类型。
-
在包含代码单元上执行分析,其中在包含代码单元的启动操作中定义和初始化可变状态,中间嵌套操作访问和/或更新此状态,以及一个结束操作来报告单个代码单元的诊断。
-
例如,一个分析器检查编译中的所有类声明,在分析每个类声明时收集和更新公共状态,然后最终,在分析所有声明后,报告有关这些声明的诊断信息,这是一个有状态的分析器。在本章中,我们将向您展示如何编写有状态的方法体和编译分析器。
默认情况下,分析器可以分析并报告项目中源文件的诊断信息。然而,我们也可以编写一个分析器来分析额外的文件,即项目中包含的非源文本文件,并在额外文件中报告诊断信息。非源文件可以是文件,例如 Web.config 文件在 Web 项目中,cshtml 文件在 Razor 项目中,XAML 文件在 WPF 项目中等等。您可以在 github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md 中了解更多关于如何编写和消费额外文件分析器的信息。
在 Visual Studio 中创建、调试和执行分析器项目
我们将向您展示如何安装 .NET 编译器平台 SDK,从模板创建分析器项目,然后调试和执行默认分析器。
您在此配方中创建的分析器项目可以用于本章后续配方中添加新的分析器和编写单元测试。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的 Visual Studio 2017 社区版。
如何操作...
-
启动 Visual Studio 并点击 File | New | Project。
-
在新建项目对话框右上角的文本框中搜索
Analyzer模板,选择下载 .NET 编译器平台 SDK,然后点击 OK:

- 新项目将默认打开
index.html文件。点击下载 .NET 编译器平台 SDK 模板 >> 按钮,以安装分析器 SDK 模板。

- 在随后的文件下载对话框中,点击打开。

- 在下一个 VSIX 安装程序对话框中点击安装,在随后的提示中点击结束任务以安装 SDK:

-
启动一个新的 Visual Studio 实例,然后点击文件 | 新建 | 项目... 以获取新项目对话框。
-
将项目目标框架组合框更改为 .NET Framework 4.6(或更高版本)。在 Visual C# | 扩展性下,选择具有代码修复功能的分析器(NuGet + VSIX),将项目命名为
CSharpAnalyzers,然后点击确定。

- 您现在应该有一个包含 3 个项目的分析器解决方案:
CSharpAnalyzers (Portable)、CSharpAnalyzers.Test和CSharpAnalyzer.Vsix**:

- 在
CSharpAnalyzers项目中打开源文件DiagnosticAnalyzer.cs并在Initialize和AnalyzeSymbol方法的开始处设置断点(按 F9),如图所示:

-
将
CSharpAnalyzers.Vsix设置为启动项目,然后点击 F5 构建分析器并启动一个新的 Visual Studio 实例,其中启用了分析器并开始调试。 -
在新的 Visual Studio 实例中,创建一个新的 C# 类库项目,例如
ClassLibrary。 -
验证我们在第一个 VS 实例的分析器代码中触发了前面的断点。您可以使用 F10 单步执行分析器代码或点击 F5 继续调试。
-
我们现在应该在错误列表中看到分析器诊断,并在编辑器中看到一个波浪线:

-
将类的名称从
Class1编辑为CLASS1。 -
我们应该在
AnalyzeSymbol方法中再次遇到断点。使用 F5 继续调试,诊断和波浪线应立即消失,展示了强大的实时和可扩展的分析功能。
它是如何工作的...
.NET 编译器平台 SDK 是一个包装项目,它将我们重定向到获取 C# 和 Visual Basic 的分析器 + CodeFix 项目模板。从这些模板创建新项目将创建一个功能齐全的分析器项目,该项目具有默认分析器、单元测试和 VSIX 项目:
-
CSharpAnalyzers: 包含默认分析器实现的核心分析器项目,该实现报告所有包含任何小写字母的类型名称的诊断。 -
CSharpAnalyzers.Test: 包含一些分析器和代码修复单元测试以及测试辅助工具的分析器单元测试项目。 -
CSharpAnalyzers.Vsix: 将分析器打包成 VSIX 的 VSIX 项目。这是解决方案中的启动项目。
点击 F5 以启动调试解决方案,构建并部署分析器到 Visual Studio 扩展存储库,然后从这个存储库启动一个新的 Visual Studio 实例。在我们的分析器中,默认情况下为在此 VS 实例中创建的所有 C# 项目启用。
让我们更详细地探讨一下在 DiagnosticAnalyzers.cs 中定义的诊断分析器源代码。它包含一个名为 CSharpAnalyzersAnalyzer 的类型,该类型继承自 DiagnosticAnalyzer。DiagnosticAnalyzer 是一个抽象类型,具有以下两个抽象成员:
SupportedDiagnostics属性:分析器必须定义一个或多个受支持的诊断描述符。描述符描述了分析器在分析器动作中可以报告的诊断的元数据。它包含诊断 ID、消息格式、标题、描述、诊断文档的超链接等字段。可用于创建和报告诊断:
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
Initialize方法:诊断分析器必须实现Initialize方法以注册对特定代码实体类型的分析器动作回调,对于默认分析器,这个类型被称为类型符号。初始化方法在分析器生命周期内只被调用一次,以允许分析器初始化和注册动作。
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}
private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
...
}
如果你的分析器可以同时处理来自多个线程的动作回调,请在 Initialize 方法中调用 AnalysisContext.EnableConcurrentExecution(),这使分析器驱动程序能够在具有多个核心的机器上更有效地执行分析器。此外,还应在 Initialize 方法中调用 AnalysisContext.ConfigureGeneratedCodeAnalysis() 以配置分析器是否想要分析和/或报告生成代码中的诊断。
分析器动作会在用户源代码中感兴趣的每个代码实体上被调用。此外,当用户编辑代码并创建新的编译时,在代码编辑期间,对于新编译中定义的实体,动作回调会持续调用。错误列表确保它只报告活动编译中的诊断。
使用 source.roslyn.io 进行丰富的语义搜索和导航 Roslyn 源代码,该代码在 github.com/dotnet/roslyn.git 上开源。例如,你可以使用查询 URL source.roslyn.io/#q=DiagnosticAnalyzer 查看对 DiagnosticAnalyzer 的定义和引用。
创建符号分析器以报告关于符号声明的相关问题
符号分析器注册动作回调以分析一种或多种符号声明,例如类型、方法、字段、属性、事件等,并报告关于声明的语义问题。
在本节中,我们将创建一个符号分析器,该分析器扩展了编译器诊断 CS0542(成员名称不能与其封装类型相同)以报告如果成员名称与任何外部父类型相同,则报告诊断。例如,分析器将在此处报告内部最深层类型 NestedClass 的诊断:
public class NestedClass
{
public class InnerClass
{
public class NestedClass
{
}
}
}
准备工作
您需要在 Visual Studio 2017 中创建并打开一个分析器项目,例如 CSharpAnalyzers。请参阅本章的第一个配方以创建此项目。
如何操作...
-
在解决方案资源管理器中,双击
CSharpAnalyzers项目中的Resources.resx文件以在资源编辑器中打开资源文件。 -
将
AnalyzerDescription、AnalyzerMessageFormat和AnalyzerTitle的现有资源字符串替换为新字符串:

- 将
Initialize方法实现替换为以下内容:
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(symbolContext =>
{
var symbolName = symbolContext.Symbol.Name;
// Skip the immediate containing type, CS0542 already covers this case.
var outerType = symbolContext.Symbol.ContainingType?.ContainingType;
while (outerType != null)
{
// Check if the current outer type has the same name as the given member.
if (symbolName.Equals(outerType.Name))
{
// For all such symbols, report a diagnostic.
var diagnostic = Diagnostic.Create(Rule, symbolContext.Symbol.Locations[0], symbolContext.Symbol.Name);
symbolContext.ReportDiagnostic(diagnostic);
return;
}
outerType = outerType.ContainingType;
}
},
SymbolKind.NamedType,
SymbolKind.Method,
SymbolKind.Field,
SymbolKind.Event,
SymbolKind.Property);
}
-
点击 Ctrl + F5 以启动一个新的带有分析器启用的 Visual Studio 实例。
-
在新的 Visual Studio 实例中,使用以下代码创建一个新的 C# 类库:
namespace ClassLibrary
{
public class OuterClass
{
public class NestedClass
{
public class NestedClass
{
}
}
}
}
-
验证编译器在错误列表中报告了诊断 CS0542:
'NestedClass': member names cannot be the same as their enclosing type。 -
将类库代码更改为以下内容:
namespace ClassLibrary
{
public class OuterClass
{
public class NestedClass
{
public class InnerClass
{
public class NestedClass
{
}
}
}
}
}
- 验证 CS0542 诊断不再报告,但错误列表中有我们的分析器诊断:

- 将
NestedClass的内部最深层类型声明替换为字段:public int NestedClass,并验证是否报告了相同的分析器诊断。您应该为具有相同名称的其他成员类型(如方法、属性和事件)获得相同的诊断。
它是如何工作的...
符号分析器注册一个或多个符号动作回调以分析感兴趣的符号类型。请注意,与注册名为 AnalyzeSymbol 的委托方法的默认实现不同,我们注册了一个 lambda 回调。
我们在 RegisterSymbolAction 调用中指定了对所有可以具有封装类型的顶级符号类型的兴趣,即类型、方法、字段、属性和事件:
context.RegisterSymbolAction(symbolContext =>
{
...
},
SymbolKind.NamedType,
SymbolKind.Method,
SymbolKind.Field,
SymbolKind.Event,
SymbolKind.Property);
分析器驱动程序确保为注册的兴趣类型的所有符号调用注册的 lambda。
分析跳过了直接封装类型,因为 C# 编译器已经报告了错误 CS0542,如果成员具有与其封装类型相同的名称。
// Skip the immediate containing type, CS0542 already covers this case.
var outerType = symbolContext.Symbol.ContainingType?.ContainingType;
核心分析通过遍历外部类型,并在符号分析上下文中比较符号的名称与相关外部类型,直到找到匹配项,在这种情况下报告诊断;如果外部类型没有包含类型,则不报告诊断。
while (outerType != null)
{
// Check if the current outer type has the same name as the given member.
if (symbolName.Equals(outerType.Name))
{
// For all such symbols, report a diagnostic.
...
}
outerType = outerType.ContainingType;
}
建议符号操作仅分析并报告关于声明的诊断,而不是其中可执行代码。如果您需要分析符号内的可执行代码,应尝试注册本章后面讨论的其他动作类型。
还有更多...
趣闻:符号分析器的先前实现没有最佳性能。例如,如果您有 n 级别的类型嵌套,以及最内层嵌套类型中的 m 个字段,我们实现的分析将是 O(mn)* 算法复杂度。您能否实现一个替代实现,其中分析可以以更优越的 O(m + n) 复杂度实现?
参见
我们当前的分析器实现是完全无状态的,因为它不需要依赖于多个符号的分析。我们单独分析每个符号并为其报告诊断。然而,如果您需要进行更复杂的分析,这需要从多个符号中收集状态然后进行全局编译分析,您应该编写一个具有符号和编译动作的有状态编译分析器。这将在本章后面的食谱 创建一个分析整个编译并报告问题的编译分析器 中介绍。
创建一个语法节点分析器以报告关于语言语法的相关问题
语法节点分析器注册动作回调以分析一种或多种语法节点,例如运算符、标识符、表达式、声明等,并报告关于语法的语义问题。这些分析器通常需要获取正在分析的不同语法节点的语义信息,并使用编译器语义模型 API 获取这些信息。
在本节中,我们将创建一个语法分析器,该分析器分析 VariableDeclarationSyntax 节点以进行局部声明,并报告一个诊断建议使用显式类型而不是隐式类型声明,即使用关键字 var 定义的变量,例如 var i = new X();. 如果存在编译器语法错误(隐式类型声明不能定义多个变量),或者赋值右侧有错误类型或特殊 System 类型(如 int、char、string 等),分析器将不会报告诊断。例如,分析器不会标记本例中的局部变量 local1、local2 和 local3,但会标记 local4。
int local1 = 0;
Class1 local2 = new Class1();
var local3 = 0;
var local4 = new Class1();
准备工作
您需要创建并打开一个分析器项目,例如在 Visual Studio 2017 中创建名为 CSharpAnalyzers 的项目。请参考本章的第一个食谱来创建此项目。
如何实现...
-
在解决方案资源管理器中,双击
CSharpAnalyzers项目中的Resources.resx文件以在资源编辑器中打开资源文件。 -
将
AnalyzerDescription、AnalyzerMessageFormat和AnalyzerTitle的现有资源字符串替换为新字符串:

- 将
Initialize方法实现替换为以下内容:
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(syntaxNodeContext =>
{
// Find implicitly typed variable declarations.
// Do not flag implicitly typed declarations that declare more than one variables,
// as the compiler already generates error CS0819 for those cases.
var declaration = (VariableDeclarationSyntax)syntaxNodeContext.Node;
if (!declaration.Type.IsVar || declaration.Variables.Count != 1)
{
return;
}
// Do not flag variable declarations with error type or special System types, such as int, char, string, and so on.
var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(declaration.Type, syntaxNodeContext.CancellationToken);
if (typeInfo.Type.TypeKind == TypeKind.Error || typeInfo.Type.SpecialType != SpecialType.None)
{
return;
}
// Report a diagnostic.
var variable = declaration.Variables[0];
var diagnostic = Diagnostic.Create(Rule, variable.GetLocation(), variable.Identifier.ValueText);
syntaxNodeContext.ReportDiagnostic(diagnostic);
},
SyntaxKind.VariableDeclaration);
}
-
点击 Ctrl + F5 以启动一个新的带有分析器的 Visual Studio 实例。
-
在新的 Visual Studio 实例中,创建一个新的 C# 类库,代码如下:
namespace ClassLibrary
{
public class Class1
{
public void M(int param1, Class1 param2)
{
// Explicitly typed variables - do not flag.
int local1 = param1;
Class1 local2 = param2;
}
}
}
-
验证分析器诊断未在错误列表中报告显式类型变量。
-
现在,将以下隐式类型变量声明添加到方法中:
// Implicitly typed variable with error type - do not flag.
var local3 = UndefinedMethod();
// Implicitly typed variable with special type - do not flag.
var local4 = param1;
-
验证分析器诊断不会在错误列表中报告具有错误类型或特殊类型的隐式类型变量。
-
将违反的隐式类型变量声明添加到方法中:
// Implicitly typed variable with user defined type - flag.
var local5 = param2;
- 验证分析器诊断是否报告了此隐式类型变量:

它是如何工作的...
语法节点分析器注册一个或多个语法节点动作回调以分析感兴趣的语法类型。我们在RegisterSyntaxNodeAction调用中指定了对分析VariableDeclaration语法类型的兴趣。
context.RegisterSyntaxNodeAction(syntaxNodeContext =>
{
...
}, SyntaxKind.VariableDeclaration);
分析通过在回调中操作语法节点和从语法节点分析上下文公开的语义模型来工作。我们首先进行语法检查,以验证我们正在操作一个有效的隐式类型声明:
// Do not flag implicitly typed declarations that declare more than one variables,
// as the compiler already generates error CS0819 for those cases.
var declaration = (VariableDeclarationSyntax)syntaxNodeContext.Node;
if (!declaration.Type.IsVar || declaration.Variables.Count != 1)
{
return;
}
然后,我们使用语义模型 API 执行语义检查,以获取类型声明语法节点的语义类型信息,并验证它不是错误类型或原始系统类型:
// Do not flag variable declarations with error type or special System types, such as int, char, string, and so on.
var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(declaration.Type, syntaxNodeContext.CancellationToken);
if (typeInfo.Type.TypeKind == TypeKind.Error || typeInfo.Type.SpecialType != SpecialType.None)
{
return;
}
您可以使用公共语义模型 API 在从SyntaxNodeAnalysisContext公开的语法节点上执行许多强大的语义操作,有关参考,请参阅github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/Compilation/SemanticModel.cs。
如果语法和语义检查都成功,则我们报告关于推荐显式类型而不是var的诊断。
创建一个语法树分析器来分析源文件并报告语法问题
语法树分析器注册动作回调以分析源文件的语法/语法,并报告纯语法问题。例如,语句末尾缺少分号是一个语法错误,而将不兼容的类型分配给没有可能类型转换的符号是一个语义错误。
在本节中,我们将编写一个语法树分析器,该分析器分析源文件中的所有语句,并为任何未在块中(即花括号{})封装的语句生成语法警告。例如,以下代码将为if语句和System.Console.WriteLine调用语句生成警告,但while语句不会被标记:
void Method()
{
while (...)
if (...)
System.Console.WriteLine(value);
}
准备工作
您需要在 Visual Studio 2017 中创建并打开一个分析器项目,例如CSharpAnalyzers。请参阅本章的第一个配方以创建此项目。
如何实现...
-
在解决方案资源管理器中,双击
CSharpAnalyzers项目中的Resources.resx文件以在资源编辑器中打开资源文件。 -
将
AnalyzerDescription、AnalyzerMessageFormat和AnalyzerTitle的现有资源字符串替换为新字符串。

- 将
Initialize方法实现替换为以下内容:
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxTreeAction(syntaxTreeContext =>
{
// Iterate through all statements in the tree.
var root = syntaxTreeContext.Tree.GetRoot(syntaxTreeContext.CancellationToken);
foreach (var statement in root.DescendantNodes().OfType<StatementSyntax>())
{
// Skip analyzing block statements.
if (statement is BlockSyntax)
{
continue;
}
// Report issue for all statements that are nested within a statement,
// but not a block statement.
if (statement.Parent is StatementSyntax && !(statement.Parent is BlockSyntax))
{
var diagnostic = Diagnostic.Create(Rule, statement.GetFirstToken().GetLocation());
syntaxTreeContext.ReportDiagnostic(diagnostic);
}
}
});
}
-
点击Ctrl + F5以启动一个新的带有分析器启用的 Visual Studio 实例。
-
在新的 Visual Studio 实例中,创建一个新的 C#类库,代码如下:
namespace ClassLibrary
{
public class Class1
{
void Method(bool flag, int value)
{
while (flag)
if (value > 0)
System.Console.WriteLine(value);
}
}
}
- 验证分析器诊断既未报告
Method方法块,也未报告while语句,而是报告了if语句和System.Console.WriteLine调用语句:

- 现在,在
System.Console.WriteLine调用语句周围添加花括号,并验证现在只为if语句报告了一个警告:

它是如何工作的...
语法树分析器注册回调以分析编译中所有源文件的语法。我们的分析是通过获取语法树的根并操作根的所有类型为StatementSyntax的子语法节点来工作的。首先,我们注意到一个块语句本身是一个聚合语句,并且根据定义具有花括号,所以我们跳过这些。
// Skip analyzing block statements.
if (statement is BlockSyntax)
{
continue;
}
我们然后对语句语法的父级进行语法检查。如果语句的父级也是一个语句,但不是一个带有花括号的块,那么我们在语句的第一个语法标记上报告一个诊断,建议使用花括号。
// Report issue for all statements that are nested within a statement,
// but not a block statement.
if (statement.Parent is StatementSyntax && !(statement.Parent is BlockSyntax))
{
var diagnostic = Diagnostic.Create(Rule, statement.GetFirstToken().GetLocation());
syntaxTreeContext.ReportDiagnostic(diagnostic);
}
SyntaxTreeAnalysisContext提供给语法树操作的语义模型不暴露源文件,因此无法在语法树操作内执行语义分析。
创建方法体分析器以分析整个方法和报告问题
状态方法体或代码块分析器注册了需要整个方法体分析来报告关于方法声明或可执行代码问题的操作回调。这些分析器通常需要在分析开始时初始化一些可变状态,这些状态在分析方法体时更新,并且最终状态用于报告诊断。
在本节中,我们将创建一个代码块分析器,标记未使用的方法参数。例如,它不会标记param1和param2为未使用,但会标记param3和param4.。
void M(int param1, ref int param2, int param3, params int[] param4)
{
int local1 = param1;
param2 = 0;
}
准备工作
您需要在 Visual Studio 2017 中创建并打开一个分析器项目,例如CSharpAnalyzers。请参考本章的第一个配方来创建此项目。
如何做...
-
在解决方案资源管理器中,双击
CSharpAnalyzers项目中的Resources.resx文件以在资源编辑器中打开资源文件。 -
将
AnalyzerDescription、AnalyzerMessageFormat和AnalyzerTitle的现有资源字符串替换为新字符串。

-
将
Initialize方法实现替换为来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/中名为Initialize.的方法的代码。 -
在您的分析器中添加来自
CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/的私有类UnusedParametersAnalyzer,该类名为UnusedParametersAnalyzer,以执行给定方法的核心理法体分析。 -
点击 Ctrl + F5 以启动一个新的带有分析器的 Visual Studio 实例。
-
在新的 Visual Studio 实例中,创建一个新的 C# 类库,代码如下:
namespace ClassLibrary
{
public class Class1
{
void M(int param1, ref int param2, int param3, params int[] param4)
{
int local1 = param1;
param2 = 0;
}
}
}
- 验证分析器诊断信息对于
param1和param2没有报告,但对于param3和param4有报告:

- 现在,向本地声明语句中添加使用
param3的代码,删除param4,并验证诊断信息是否消失:

它是如何工作的...
代码块分析器注册代码块操作以分析编译中的可执行代码块。您可以注册无状态的 CodeBlockAction 或具有嵌套操作的 CodeBlockStartAction 以分析代码块内的语法节点。我们的分析器注册了一个 CodeBlockStartAction 以执行有状态的分析。
context.RegisterCodeBlockStartAction<SyntaxKind>(startCodeBlockContext =>
{
...
}
分析从几个早期的退出检查开始:我们只对分析方法体内部的可执行代码以及至少有一个参数的方法感兴趣。
// We only care about method bodies.
if (startCodeBlockContext.OwningSymbol.Kind != SymbolKind.Method)
{
return;
}
// We only care about methods with parameters.
var method = (IMethodSymbol)startCodeBlockContext.OwningSymbol;
if (method.Parameters.IsEmpty)
{
return;
}
我们为每个要分析的方法分配一个新的 UnusedParametersAnalyzer 实例。此类构造函数初始化分析中跟踪的可变状态(稍后解释):
// Initialize local mutable state in the start action.
var analyzer = new UnusedParametersAnalyzer(method);
然后,我们在给定方法的给定代码块上下文中注册一个嵌套的语法节点操作,UnusedParametersAnalyzer.AnalyzeSyntaxNode,并注册对代码块内 IdentifierName 语法节点的分析兴趣:
// Register an intermediate non-end action that accesses and modifies the state. startCodeBlockContext.RegisterSyntaxNodeAction(analyzer.AnalyzeSyntaxNode, SyntaxKind.IdentifierName);
最后,我们在代码块分析结束时注册一个嵌套的 CodeBlockEndAction 以在 UnusedParametersAnalyzer 实例上执行。
// Register an end action to report diagnostics based on the final state. startCodeBlockContext.RegisterCodeBlockEndAction(analyzer.CodeBlockEndAction);
嵌套的结束操作总是在同一分析上下文中注册的所有嵌套非结束操作执行完毕后保证执行。
让我们现在了解核心 UnusedParametersAnalyzer 类型的工作原理,以分析特定的代码块。此分析器定义了可变状态字段以跟踪被认为是未使用的参数(及其名称):
#region Per-CodeBlock mutable state
private readonly HashSet<IParameterSymbol> _unusedParameters;
private readonly HashSet<string> _unusedParameterNames;
#endregion
我们在分析器的构造函数中初始化这个可变状态。在分析开始时,我们过滤掉隐式声明的参数和没有源位置的参数——这些永远不会被认为是冗余的。我们将剩余的参数标记为未使用。
#region State intialization
public UnusedParametersAnalyzer(IMethodSymbol method)
{
// Initialization: Assume all parameters are unused, except for:
// 1\. Implicitly declared parameters
// 2\. Parameters with no locations (example auto-generated parameters for accessors)
var parameters = method.Parameters.Where(p => !p.IsImplicitlyDeclared && p.Locations.Length > 0);
_unusedParameters = new HashSet<IParameterSymbol>(parameters);
_unusedParameterNames = new HashSet<string>(parameters.Select(p => p.Name));
}
#endregion
AnalyzeSyntaxNode 已注册为嵌套的语法节点操作,用于分析代码块内的所有 IdentifierName 节点。我们在方法开始时进行一些快速检查,如果 (a) 我们当前分析状态中没有未使用的参数,或者 (b) 标识符名称不匹配任何未使用的参数名称,则退出分析。后者的检查是为了避免尝试计算标识符符号信息的性能损失。
#region Intermediate actions
public void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
{
// Check if we have any pending unreferenced parameters.
if (_unusedParameters.Count == 0)
{
return;
}
// Syntactic check to avoid invoking GetSymbolInfo for every identifier.
var identifier = (IdentifierNameSyntax)context.Node;
if (!_unusedParameterNames.Contains(identifier.Identifier.ValueText))
{
return;
}
然后,我们使用语义模型 API 获取标识符名称的语义符号信息,并检查它是否绑定到当前被视为未使用的参数之一。如果是这样,我们从未使用集合中删除此参数(及其名称)。
// Mark parameter as used.
var parmeter = context.SemanticModel.GetSymbolInfo(identifier, context.CancellationToken).Symbol as IParameterSymbol;
if (parmeter != null && _unusedParameters.Contains(parmeter))
{
_unusedParameters.Remove(parmeter);
_unusedParameterNames.Remove(parmeter.Name);
}
}
#endregion
最后,注册的代码块结束操作遍历未使用集合中的所有剩余参数,并将它们标记为未使用参数。
#region End action
public void CodeBlockEndAction(CodeBlockAnalysisContext context)
{
// Report diagnostics for unused parameters.
foreach (var parameter in _unusedParameters)
{
var diagnostic = Diagnostic.Create(Rule, parameter.Locations[0], parameter.Name, parameter.ContainingSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
#endregion
创建一个分析整个编译并报告问题的编译分析器
一个有状态的编译分析器注册了需要编译范围内符号和/或语法分析的回调,以报告有关声明或可执行代码的问题。这些分析器通常需要在分析开始时初始化一些可变状态,该状态在分析编译时更新,并使用最终状态来报告诊断信息。
在本节中,我们将创建一个执行编译范围内分析和报告的分析器。对于以下场景,诊断安全类型不得实现具有不安全方法的接口:
-
假设我们有一个接口,比如
MyNamespace.ISecureType,这是一个众所周知的安全接口,即它是表示程序集中所有安全类型的标记*。 -
假设我们有一个方法属性,比如
MyNamespace.InsecureMethodAttribute*,它标记了应用此属性的方法为不安全。任何具有此类属性的成员的接口都必须被视为不安全。 -
我们希望报告实现知名安全接口同时也实现任何不安全接口的类型的相关诊断信息。
分析器执行编译范围内的分析以检测此类违规类型,并在编译结束操作中报告它们的诊断信息。
准备工作
您需要创建并打开一个分析器项目,例如在 Visual Studio 2017 中创建 CSharpAnalyzers。请参阅本章的第一个配方以创建此项目。
如何操作...
-
在解决方案资源管理器中,双击
CSharpAnalyzers项目中的Resources.resx文件以在资源编辑器中打开资源文件。 -
将现有的
AnalyzerDescription、AnalyzerMessageFormat和AnalyzerTitle资源字符串替换为新字符串。

-
将
Initialize方法实现替换为来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/方法名为Initialize** 的代码**。 -
在您的分析器中添加一个名为
CompilationAnalyzer的私有类,来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/DiagnosticAnalyzer.cs/类型,以执行给定方法的核心理法体分析。 -
点击 Ctrl + F5 以启动一个新的带有分析器的 Visual Studio 实例。
-
在新的 Visual Studio 实例中,通过遵循此处提供的步骤启用 C# 项目的完整解决方案分析:
msdn.microsoft.com/en-us/library/mt709421.aspx

- 在新的 Visual Studio 实例中,创建一个新的 C# 类库,代码如下:
namespace MyNamespace
{
public class InsecureMethodAttribute : System.Attribute { }
public interface ISecureType { }
public interface IInsecureInterface
{
[InsecureMethodAttribute]
void F();
}
class MyInterfaceImpl1 : IInsecureInterface
{
public void F() {}
}
class MyInterfaceImpl2 : IInsecureInterface, ISecureType
{
public void F() {}
}
class MyInterfaceImpl3 : ISecureType
{
public void F() {}
}
}
- 验证分析器诊断信息对于
MyInterfaceImpl1和MyInterfaceImpl3 没有报告,但对于MyInterfaceImpl2有报告:

- 现在,将
MyInterfaceImpl2改为不再实现IInsecureInterface并验证诊断信息不再报告。
class MyInterfaceImpl2 : ISecureType
{
public void F() {}
}
它是如何工作的...
编译分析器注册编译操作以分析编译中的符号和/或语法节点。您可以注册无状态的 CompilationAction 或具有嵌套操作的 CompilationStartAction 以分析编译内的符号和/或语法节点。我们的分析器注册了一个 CompilationStartAction 来执行有状态分析。
context.RegisterCompilationStartAction(compilationContext =>
{
...
}
分析从几个早期退出检查开始:我们只对具有名为 MyNamespace.ISecureType 和 MyNamespace.InsecureMethodAttribute 的源或元数据类型的编译进行分析。
// Check if the attribute type marking insecure methods is defined.
var insecureMethodAttributeType = compilationContext.Compilation.GetTypeByMetadataName("MyNamespace.InsecureMethodAttribute");
if (insecureMethodAttributeType == null)
{
return;
}
// Check if the interface type marking secure types is defined.
var secureTypeInterfaceType = compilationContext.Compilation.GetTypeByMetadataName("MyNamespace.ISecureType");
if (secureTypeInterfaceType == null)
{
return;
}
我们为要分析的编译分配一个新的 CompilationAnalyzer 实例。此类构造函数初始化分析中跟踪的可变和不可变状态(稍后解释)。
// Initialize state in the start action.
var analyzer = new CompilationAnalyzer(insecureMethodAttributeType, secureTypeInterfaceType);
然后,我们在给定的编译开始上下文中注册一个嵌套符号操作,CompilationAnalyzer.AnalyzeSymbol,以分析给定的编译中的类型和符号。
// Register an intermediate non-end action that accesses and modifies the state. compilationContext.RegisterSymbolAction(analyzer.AnalyzeSymbol, SymbolKind.NamedType, SymbolKind.Method);
最后,我们在编译分析结束时注册一个嵌套的 CompilationEndAction 以在 CompilationAnalyzer 实例上执行。
// Register an end action to report diagnostics based on the final state. compilationContext.RegisterCompilationEndAction(analyzer.CompilationEndAction);
嵌套的编译结束操作始终保证在相同分析上下文中注册的所有嵌套非结束操作执行完毕后执行。
现在,让我们了解核心 CompilationAnalyzer 类型的运作方式,以便分析特定的编译。此分析器为对应于安全接口和不安全方法属性的类型符号定义了一个不可变状态。它还定义了可变状态字段,以跟踪编译中定义的实现安全接口的类型集合以及编译中定义的具有不安全方法属性的方法集合。
#region Per-Compilation immutable state
private readonly INamedTypeSymbol _insecureMethodAttributeType;
private readonly INamedTypeSymbol _secureTypeInterfaceType;
#endregion
#region Per-Compilation mutable state
/// <summary>
/// List of secure types in the compilation implementing secure interface.
/// </summary>
private List<INamedTypeSymbol> _secureTypes;
/// <summary>
/// Set of insecure interface types in the compilation that have methods with an insecure method attribute.
/// </summary>
private HashSet<INamedTypeSymbol> _interfacesWithInsecureMethods;
#endregion
在分析开始时,我们将安全类型和不安全方法接口的集合初始化为空。
#region State intialization
public CompilationAnalyzer(INamedTypeSymbol insecureMethodAttributeType, INamedTypeSymbol secureTypeInterfaceType)
{
_insecureMethodAttributeType = insecureMethodAttributeType;
_secureTypeInterfaceType = secureTypeInterfaceType;
_secureTypes = null;
_interfacesWithInsecureMethods = null;
}
#endregion
AnalyzeSymbol 被注册为嵌套符号操作,以分析编译内的所有类型和方法。对于编译中的每个类型声明,我们检查它是否实现了安全接口,如果是,则将其添加到我们的安全类型集合中。对于编译中的每个方法声明,我们检查其包含的类型是否为接口以及方法是否具有不安全方法属性,如果是,则将包含的接口类型添加到我们的具有不安全方法接口类型集合中。
#region Intermediate actions
public void AnalyzeSymbol(SymbolAnalysisContext context)
{
switch (context.Symbol.Kind)
{
case SymbolKind.NamedType:
// Check if the symbol implements "_secureTypeInterfaceType".
var namedType = (INamedTypeSymbol)context.Symbol;
if (namedType.AllInterfaces.Contains(_secureTypeInterfaceType))
{
_secureTypes = _secureTypes ?? new List<INamedTypeSymbol>();
_secureTypes.Add(namedType);
}
break;
case SymbolKind.Method:
// Check if this is an interface method with "_insecureMethodAttributeType" attribute.
var method = (IMethodSymbol)context.Symbol;
if (method.ContainingType.TypeKind == TypeKind.Interface && method.GetAttributes().Any(a => a.AttributeClass.Equals(_insecureMethodAttributeType)))
{
_interfacesWithInsecureMethods = _interfacesWithInsecureMethods ?? new HashSet<INamedTypeSymbol>();
_interfacesWithInsecureMethods.Add(method.ContainingType);
}
break;
}
}
#endregion
最后,注册的编译结束操作使用编译分析结束时的最终状态来报告诊断。在此操作中,如果既没有安全类型也没有不安全方法的接口,则分析将提前退出。然后,我们遍历所有安全类型和所有不安全方法的接口,并对每一对进行检查,看安全类型或其任何基类型是否实现了不安全接口。如果是这样,我们在安全类型上报告一个诊断。
#region End action
public void CompilationEndAction(CompilationAnalysisContext context)
{
if (_interfacesWithInsecureMethods == null || _secureTypes == null)
{
// No violating types.
return;
}
// Report diagnostic for violating named types.
foreach (var secureType in _secureTypes)
{
foreach (var insecureInterface in _interfacesWithInsecureMethods)
{
if (secureType.AllInterfaces.Contains(insecureInterface))
{
var diagnostic = Diagnostic.Create(Rule, secureType.Locations[0], secureType.Name, "MyNamespace.ISecureType", insecureInterface.Name);
context.ReportDiagnostic(diagnostic);
break;
}
}
}
}
#endregion
为分析器项目编写单元测试
在本节中,我们将向您展示如何编写和执行分析器项目的单元测试。
准备工作
您需要创建并打开一个分析器项目,例如在 Visual Studio 2017 中创建CSharpAnalyzers。请参阅本章的第一个配方以创建此项目。
如何做到这一点...
- 在解决方案资源管理器中打开
CSharpAnalyzers.Test项目中的UnitTests.cs,以查看为模板分析器项目创建的默认单元测试(类型名称不应包含小写字母)。

-
导航到测试 | 窗口 | 测试窗口以打开测试资源管理器窗口,查看项目中的单元测试。默认分析器项目有两个单元测试:
-
TestMethod1:这个测试了分析器诊断在测试代码上没有触发的场景。 -
TestMethod2:这个测试了分析器诊断在测试代码上确实触发的场景。
-

注意,单元测试项目包含对 DiagnosticAnalyzer 和 CodeFixProvider 的单元测试。本章仅处理分析器测试。我们将在本书的后面部分扩展对 CodeFixProvider 的单元测试。
-
通过在测试资源管理器中右键单击“未运行测试”节点,执行“运行选中测试”上下文菜单命令来运行项目的所有单元测试,并验证测试是否通过。
-
编辑
TestMethod1,使测试代码现在有一个小写字母的类型:
[TestMethod]
public void TestMethod1()
{
var test = @"class Class1 { }";
VerifyCSharpDiagnostic(test);
}
- 在编辑器中右键单击
TestMethod1,执行“运行测试”上下文菜单命令,并验证测试现在由于诊断不匹配断言而失败 -预期 "0" 实际 "1":

- 编辑
TestMethod1以现在添加对新测试代码的预期诊断:
var expected = new DiagnosticResult
{
Id = "CSharpAnalyzers",
Message = String.Format("Type name '{0}' contains lowercase letters", "Class1"),
Severity = DiagnosticSeverity.Warning,
Locations = new[] {
new DiagnosticResultLocation("Test0.cs", 11, 15)
}
};
VerifyCSharpDiagnostic(test, expected);
- 再次运行单元测试并注意,测试仍然失败,但现在失败是因为诊断报告的位置(列号)不同。

- 编辑诊断位置以使用正确的预期列号并重新运行测试 - 验证测试现在是否通过。
new DiagnosticResultLocation("Test0.cs", 11, 7)
- 编辑
TestMethod1并将测试代码更改为将Class1重命名为CLASS1:
var test = @"class CLASS1 { }";
- 再次运行单元测试并验证测试现在由于诊断不匹配断言而失败 -
预期 "1" 实际 "0"。

- 编辑
TestMethod1以删除预期诊断并验证测试通过:
var test = @"class CLASS1 { }";
VerifyCSharpDiagnostic(test);
它是如何工作的...
分析器单元测试项目允许我们为分析器在不同代码样本上的执行编写单元测试。每个单元测试都带有 TestMethod 属性,并定义了示例测试代码、分析器在代码上报告的预期诊断(如果有),以及调用测试辅助方法(此处为 VerifyCSharpDiagnostic,)以验证诊断。
//No diagnostics expected to show up
[TestMethod]
public void TestMethod1()
{
var test = @"";
VerifyCSharpDiagnostic(test);
}
单元测试可以使用 DiagnosticResult 类型定义预期诊断,该类型必须指定诊断的 Id、Message、Severity 和 Locations:
var expected = new DiagnosticResult
{
Id = "CSharpAnalyzers",
Message = String.Format("Type name '{0}' contains lowercase letters", "Class1"),
Severity = DiagnosticSeverity.Warning,
Locations = new[] { new DiagnosticResultLocation("Test0.cs", 11, 15) }
};
VerifyCSharpDiagnostic(test, expected);
计算预期诊断的正确行号和列号(例如,(11, 15))可能有点棘手。通常有效的方法是从默认位置 (0, 0) 开始,执行一次测试,然后查看测试资源管理器窗口中的失败文本以获取预期和实际行号。然后,将测试代码中的预期行号替换为实际行号。重新执行测试并重复此过程以获取正确的列号。
包含所有单元测试的 UnitTest 类型还重写了以下方法以返回要测试的 DiagnosticAnalyzer(以及可选的 CodeFixProvider):
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return new CSharpAnalyzersCodeFixProvider();
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new CSharpAnalyzersAnalyzer();
}
现在,让我们更详细地介绍单元测试的测试框架辅助工具。分析器单元测试项目包含两个主要的辅助抽象类型,用于编写分析器和代码修复的单元测试:
-
DiagnosticVerifier: 包含辅助方法以运行DiagnosticAnalyzer单元测试,这些测试验证给定测试源集的分析器诊断。 -
CodeFixVerifier: 包含辅助方法以运行DiagnosticAnalyzer和CodeFixProvider单元测试,这些测试在应用代码修复前后验证给定测试源集的分析器诊断。此类型继承自DiagnosticVerifier。
在默认的分析器项目中,UnitTest 类型继承自 CodeFixVerifier,但也可以改为继承自 DiagnosticVerifier, 如果你只对编写分析器单元测试感兴趣。我们在这里将只关注 DiagnosticVerifier;CodeFixVerifier 将在后面的章节中介绍。
DiagnosticVerifier 类型分为 2 个源文件 DiagnosticVerifier.cs 和 DiagnosticVerifier.Helper.cs.

-
DiagnosticVerifier.Helper.cs包含以下核心功能:-
提供辅助方法以创建基于给定 C# 或 VisualBasic 源代码的源文件编译(前一个截图中的“设置编译和文档”区域)。
-
提供辅助方法以调用前面的功能来创建包含给定 C# 或 VisualBasic 源代码的编译,并在编译上执行给定的
DiagnosticAnalyzer以生成分析器诊断,并返回排序后的诊断以进行验证(前一个截图中的“获取诊断”区域)。
-
-
DiagnosticVerifier.cs包含以下核心功能:-
获取要测试的
DiagnosticAnalyzer类型的方法(在前一个截图中的“测试类要实现区域”中实现)。 -
执行实际的诊断比较和验证以及格式化诊断,以便在单元测试失败时获取实际/预期诊断的字符串表示(前一个截图中的“实际比较和验证区域”和“格式化诊断区域”)。
-
诊断验证方法
VerifyCSharpDiagnostic和VerifyBasicDiagnostic可以通过单元测试调用,以验证在给定的 C# 或 Visual Basic 源代码上生成的分析器诊断(前一个截图中的“验证器包装器”部分)。这些方法调用获取诊断辅助工具来创建编译并获取排序后的分析器诊断,然后调用前面的私有辅助工具来比较和验证诊断。
-
参见
Live 单元测试是 Visual Studio 2017 企业版中的新功能,它会在您编辑代码时在后台自动运行受影响的单元测试,并在编辑器中实时可视化结果和代码覆盖率。请参阅第六章,Visual Studio 企业版中的 Live 单元测试,以启用项目的实时单元测试并可视化在您按照本食谱中的步骤编辑代码后自动执行的单元测试。
发布分析器项目的 NuGet 包和 VSIX
我们将向您展示如何配置、构建和发布一个用于在 Visual Studio 2017 中使用 .NET 编译器平台 SDK 创建的分析器项目的 NuGet 包和 VSIX 包。
在我们深入探讨这些主题之前,让我们了解基于 NuGet 的分析器包和基于 VSIX 的分析器包之间的区别。NuGet 和 VSIX 是微软开发平台为打包文件(如程序集、资源、构建目标、工具等)到单个可安装包的两种基本不同的打包方案。
-
NuGet 是一种更通用的打包方案。NuGet 包(
.nupkg文件)可以直接在 .NET 项目中引用,并使用 Visual Studio 中的 NuGet 包管理器安装到特定的项目或解决方案。基于分析器模板项目的分析器 NuGet 包被安装为项目文件中的 AnalyzerReferences,然后传递给编译器命令行以在构建期间执行。此外,AnalyzerReferences 在 Visual Studio IDE 中设计时解析,并在代码编辑时执行以生成实时诊断。 -
VSIX 包是一个
.vsix文件,它包含一个或多个 Visual Studio 扩展,以及 Visual Studio 用来分类和安装扩展的元数据。分析器 VSIX 包可以全局安装或安装到特定的扩展分叉中,并针对从 Visual Studio 分叉打开的所有项目/解决方案启用。与NuGet包不同,它不能专门安装到项目/解决方案中,并且不会与项目源一起移动。
截至 Visual Studio 2017,通过 NuGet 包安装的 AnalyzerReferences 分析器在命令行构建和 Visual Studio 中的实时代码编辑期间都会执行。通过 Analyzer VSIX 包安装的分析器仅在 Visual Studio 中的实时代码编辑期间执行,不在项目构建期间执行。因此,只有分析器 NuGet 包可以配置为在持续集成 (CI) 构建系统中执行并中断构建。
准备工作
您需要在 Visual Studio 2017 中创建并打开一个分析器项目,例如 CSharpAnalyzers。请参阅本章的第一个配方以创建此项目。
如何操作...
-
在 Visual Studio 中通过执行“构建 | 构建解决方案”命令来构建
CSharpAnalyzers解决方案。 -
在 Windows 资源管理器中打开
CSharpAnalyzers项目的二进制输出文件夹 (<%SolutionFolder%>\CSharpAnalyzers\bin\debug),并验证名为CSharpAnalyzers.1.0.X.Y.nupkg的分析器 NuGet 包是否已生成在文件夹中。 -
在解决方案资源管理器中双击
CSharpAnalyzers项目的Diagnostic.nuspec文件,以查看和配置 nupkg 的属性。

-
重新构建项目以使用新属性重新生成 nupkg。
-
按照此处列出的步骤发布 nupkg 为公共或私有包:
docs.microsoft.com/en-us/nuget/create-packages/publish-a-package。 -
在 Windows 资源管理器中打开
CSharpAnalyzers.Vsix项目的二进制输出文件夹 (<%SolutionFolder%\CSharpAnalyzers.Vsix\bin\debug),并验证名为CSharpAnalyzers.Vsix.vsix的 VSIX 是否存在于文件夹中。 -
在解决方案资源管理器中双击
CSharpAnalyzers.Vsix项目的source.extension.vsixmanifest文件,以查看和配置 VSIX 包的属性。

-
重新构建 VSIX 项目以重新生成 VSIX。
-
按照此处列出的步骤发布到 Visual Studio 扩展库:
msdn.microsoft.com/en-us/library/ff728613.aspx。
第二章:在 .NET 项目中消费诊断分析器
在上一章中,我们向您展示了如何编写诊断分析器来分析并报告有关 .NET 源代码的问题,并将它们贡献给 .NET 开发者社区。在本章中,我们将向您展示如何搜索、安装、查看和配置已在 NuGet 和 VS 扩展库上发布的分析器。我们将涵盖以下食谱:
-
通过 NuGet 包管理器搜索和安装分析器
-
通过 VS 扩展库搜索和安装 VSIX 分析器
-
在 Visual Studio 的解决方案资源管理器中查看和配置分析器
-
使用规则集文件和规则集编辑器配置分析器
简介
诊断分析器是 Roslyn C# 编译器和 Visual Studio IDE 的扩展,用于分析用户代码并报告诊断信息。用户在从 Visual Studio 构建项目后,甚至在命令行构建项目时,都会在错误列表中看到这些诊断信息。当在 Visual Studio IDE 中编辑源代码时,他们也会实时看到诊断信息。分析器可以报告诊断信息,以强制执行特定的代码风格,提高代码质量和维护性,推荐设计指南,甚至报告非常特定于特定领域的问题,这些问题无法由核心编译器覆盖。
分析器可以以 NuGet 包或 VSIX 的形式安装到 .NET 项目中。为了更好地理解这些打包方案,并了解将分析器作为 NuGet 包或 VSIX 安装时的分析器体验差异,建议您阅读第一章中关于如何发布分析器项目的食谱的介绍部分 Publishing NuGet package and VSIX for an analyzer project。
分析器支持各种不同版本的 .NET Standard、.NET Core 和 .NET Framework 项目,例如类库、控制台应用程序等。
通过 NuGet 包管理器搜索和安装分析器
在本食谱中,我们将向您展示如何在 Visual Studio 的 NuGet 包管理器中搜索和安装分析器 NuGet 包,并查看在项目构建中以及 Visual Studio 代码编辑期间的实时分析器诊断信息。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能完成此食谱。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
在 Visual Studio 2017 中创建一个 C# 类库项目,例如
ClassLibrary*。 -
在解决方案资源管理器中,右键单击解决方案或项目节点,并执行“管理 NuGet 包”命令:

- 这将打开 NuGet 包管理器,您可以使用它来搜索和安装 NuGet 包到解决方案或项目:

- 在搜索栏中输入以下文本以查找标记为分析器的 NuGet 包:
Tags:"analyzers"。
注意,一些知名包被标记为 analyzer,因此您可能还想搜索带有 Tags:"analyzer"的标签。
- 通过搜索栏右侧的 Include prerelease 复选框选择或取消选择以搜索或隐藏预发布分析器包。包根据下载次数列出,下载次数最高的包位于顶部:

- 选择要安装的包,例如
System.Runtime.Analyzers,并选择一个特定版本,例如 1.1.0,然后点击安装:

-
在许可接受对话框中点击 I Accept 按钮,以安装 NuGet 包。
-
验证在解决方案资源管理器中的分析器节点下显示的已安装分析器:

- 验证项目文件是否包含一个包含以下分析器引用的新
ItemGroup,这些引用来自已安装的分析器包:
*<ItemGroup>
<Analyzer Include="..\packages\System.Runtime.Analyzers.1.1.0\analyzers\dotnet\cs\System.Runtime.Analyzers.dll" />
<Analyzer Include="..\packages\System.Runtime.Analyzers.1.1.0\analyzers\dotnet\cs\System.Runtime.CSharp.Analyzers.dll" />
</ItemGroup>*
- 将以下代码添加到您的 C# 项目中:
namespace ClassLibrary
{
public class MyAttribute : System.Attribute
{
}
}
- 验证错误列表中显示的已安装分析器的分析器诊断:

- 打开 VS 2017 的开发者命令提示符并构建项目,以验证分析器是否在命令行构建上执行,并且分析器诊断是否被报告:

- 在 VS 2017 中创建一个新的 C# 项目,并将与步骤 10 相同的代码添加到其中。验证错误列表或命令行中没有出现分析器诊断,以确认分析器包仅在步骤 1-6 中安装到所选项目。
注意,CA1018(自定义属性应该有 AttributeUsage 定义)在 FxCop/System.Runtime.Analyzers 包的未来版本中已被移动到单独的分析器程序集中。建议您从(www.nuget.org/packages/Microsoft.CodeAnalysis.FxCopAnalyzers)安装 Microsoft.CodeAnalysis.FxCopAnalyzers NuGet 包以获取最新的 Microsoft 推荐的分析器组。
通过 VS 扩展库搜索和安装 VSIX 分析器
在本教程中,我们将向您展示如何在 Visual Studio 扩展管理器中搜索和安装分析器 VSIX 包,并查看安装的 VSIX 分析器诊断如何在 Visual Studio 代码编辑期间作为实时诊断亮起。
准备工作
您需要在您的机器上安装 Visual Studio 2017 以遵循此教程。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
在 Visual Studio 2017 中创建一个 C# 类库项目,例如
ClassLibrary。 -
从顶级菜单中,导航到“工具”|“扩展和更新”。
-
导航到对话框左侧选项卡上的“在线”|“Visual Studio 市场 place”,以查看 Visual Studio 扩展库/市场中的可用 VSIX:

- 在对话框右上角的搜索框中搜索
analyzers,下载一个分析器 VSIX,例如Refactoring Essentials for Visual Studio:

- 下载完成后,你将在对话框底部收到一条消息,说明安装将在 Visual Studio 和相关窗口关闭后执行:

-
关闭对话框,然后关闭 Visual Studio 实例以开始安装。
-
在 VSIX 安装程序对话框中,单击“修改”以开始安装:

- 随后的消息提示您终止所有活动的 Visual Studio 和辅助进程。保存所有相关工作,并在所有打开的 Visual Studio 实例中单击“结束任务s”以终止这些进程并安装 VSIX:

- 安装完成后,重新启动 VS,点击“工具”|“扩展和更新”,并验证
Refactoring Essentials VSIX是否已安装:

- 创建一个具有以下源代码的新 C# 项目,并在错误列表中验证分析器诊断 RECS0085 (冗余数组创建表达式):
namespace ClassLibrary
{
public class Class1
{
void Method()
{
int[] values = new int[] { 1, 2, 3 };
}
}
}

- 从 Visual Studio 2017 或命令行构建项目,并确认输出窗口或命令行中均未显示分析器诊断,从而确认 VSIX 分析器没有作为构建的一部分执行。
在 Visual Studio 的解决方案资源管理器中查看和配置分析器
在本教程中,我们将向您展示如何使用 Visual Studio 2017 中的解决方案资源管理器查看项目中安装的不同分析器,查看这些程序集实现的分析器规则,以及规则属性(或描述符元数据),并配置规则严重性和持久化新的严重性设置。
准备工作
您需要在 Visual Studio 2017 中创建并打开一个 .NET 项目,并在项目中安装基于 NuGet 的分析器。有关在 .NET 项目中安装分析器的信息,请参阅本章的第一个教程。
如何操作...
-
打开一个 C#项目,例如
ClassLibrary,其中预发布版本 1.2.0-beta2 的分析器 NuGet 包System.Runtime.Analyzers.nupkg已安装。 -
在解决方案资源管理器中,展开引用 | 分析器节点以查看通过分析器 NuGet 包安装的分析器程序集。我们应该看到两个分析器程序集,
System.Runtime.Analyzers和System.Runtime.CSharp.Analyzers:

- 展开 System.Runtime.Analyzers 节点以查看在程序集中实现的全部 CAXXXX 规则,并单击一个特定规则,例如 CA1813:避免未密封的属性, 以查看规则属性,如 ID、消息、标题、描述、类别、有效严重性、默认启用,等等,在属性窗口中:

- 注意,CA1813 规则的默认启用为 False,这意味着规则默认是关闭的。我们可以通过添加以下违反此规则的源代码来确认这一点,因为我们声明了一个公共未密封的属性,但 CA1813 没有报告违反:
using System;
namespace ClassLibrary
{
[AttributeUsage(AttributeTargets.All)]
public class MyAttribute: Attribute
{
}
}
- 右键单击规则节点,点击设置规则集严重性,并将严重性从默认更改为警告:

- 确认 CA1813 现在报告了前面的代码:

-
保存当前项目,然后关闭并重新打开解决方案。
-
验证警告 CA1813 仍然出现在前面的源代码中,确认规则集严重性更改已持久化到项目中。
它是如何工作的...
解决方案资源管理器中的分析器节点以可视方式表示在项目文件中定义的分析器项,这些项对应于手动添加到项目中的分析器程序集或通过分析器 NuGet 包添加的程序集。程序集中的规则来自实现 DiagnosticAnalyzer 类型并对其应用了 DiagnosticAnalyzerAttribute 的程序集中的每个类型。属性窗口中显示的规则属性来自实例化分析器类型并请求其 SupportedDiagnostics。
在解决方案资源管理器中更改规则严重性并将其持久化到项目是通过一个自动生成的规则集文件实现的,该文件被添加到项目中。请参阅下一道菜谱以获取有关基于规则集的分析器配置的更多详细信息。
使用规则集文件和规则集编辑器配置分析器
在本菜谱中,我们将向您展示如何使用 Visual Studio 中的 ruleset 文件和规则集编辑器来配置分析器规则的每个项目严重性,并说明严重性更改如何在 Visual Studio 的实时诊断以及命令行构建中反映出来。
准备工作
您需要在 Visual Studio 2017 中创建并打开一个 .NET 项目,并在项目中安装基于 NuGet 的分析器。请参阅本章的第一个配方,了解如何在 .NET 项目中安装分析器。
如何做到这一点...
-
打开一个安装了分析器 NuGet 包
System.Runtime.Analyzers.nupkg预发布版本 1.2.0-beta2 的 C# 项目,例如ClassLibrary。 -
将以下源代码添加到项目中并验证是否未触发 CA1813: 避免未密封的属性:
using System;
namespace ClassLibrary
{
[AttributeUsage(AttributeTargets.All)]
public class MyAttribute: Attribute
{
}
}
- 在解决方案资源管理器中,导航到 ClassLibary | 引用 | 分析器,右键单击分析器节点并执行上下文菜单命令“打开活动规则集”:

-
在规则集编辑器中,在右上角的文本框中搜索 CA1813。
-
对于 CA1813,在 System.Runtime.Analyzers 下搜索列出的结果,将操作从 None 更改为警告,并保存:

-
我们现在应该在源代码中的属性定义上看到 CA1813 警告。
-
在解决方案资源管理器中验证,项目现在包含新的
ClassLibrary.ruleset项,并在项目文件中添加了新的CodeAnalysisRuleset属性:
*<CodeAnalysisRuleSet>**ClassLibrary.ruleset**</CodeAnalysisRuleSet>*
- 在 Visual Studio 外部的文本编辑器中打开
ClassLibrary.ruleset并验证它是否具有以下针对 CA1813 的规则操作规范:
*<Rules AnalyzerId="System.Runtime.Analyzers" RuleNamespace="System.Runtime.Analyzers">*
*<Rule Id="**CA1813**" Action="**Warning**" />*
*</Rules>*
-
编辑规则集文件,将 CA1813 的
ruleset操作从警告更改为错误并保存文件。 -
切换回 Visual Studio 并确认源代码编辑器现在显示红色波浪线,并且错误列表也报告了 CA1813 错误:

-
在解决方案资源管理器中双击 ClassLibrary.ruleset 以使用规则集编辑器打开它,并验证 CA1813 的规则严重性条目现在显示为错误。
-
构建项目并验证是否报告了错误 CA1813,确认
ruleset设置在命令行构建中也得到保留。
它是如何工作的...
ruleset 文件本质上是一组代码分析规则的集合,您可以将这些规则应用于项目以配置其分析。它以 XML 格式指定,并基于 Visual Studio 一起提供的 XML 架构。它也是开源的,可以在 github.com/dotnet/roslyn/blob/version-2.0.0/src/Compilers/Core/Portable/RuleSet/RuleSetSchema.xsd 找到。可以使用项目文件中的 CodeAnalysisRuleset 属性为项目指定 ruleset。每个 Rules 节点包含一组具有公共分析器 ID 和命名空间的规则规范。每个规则规范都有规则 ID 和有效的操作或严重性。规则操作可以取以下五个值之一:None(抑制)、Hidden(在 IDE 中不可见,主要是代码修复触发器)、Info(信息性消息)、Warning 和 Error。这些规则操作被转换为编译器的编译选项,并覆盖了诊断 ID 的默认严重性。
规则集编辑器是一个强大的图形用户界面,用于搜索、过滤和批量编辑规则配置。
请参阅 msdn.microsoft.com/en-us/library/dd264996.aspx 以获取更详细的说明,以及 Visual Studio 中 ruleset 文件架构和规则集编辑器的文档。
还有更多...
在 Visual Studio 2017 中,可以通过新的 .editorconfig 格式配置内置的编码风格规则分析器,该格式在文件夹级别应用规则配置。有关更多详细信息,请参阅文档 (docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference )。
第三章:编写 IDE 代码修复、重构和智能感知完成提供者
在本章中,我们将涵盖以下食谱:
-
创建、调试和执行
CodeFixProvider以修复编译器警告 -
在不同的作用域(文档、项目、解决方案)中应用批量代码修复(FixAll)
-
创建一个自定义的
FixAllProvider来修复作用域内所有问题的所有出现 -
创建一个
CodeRefactoringProvider来重构源代码,推荐使用 C# 7.0 的元组 -
创建一个
CompletionProvider以在编辑代码时提供额外的智能感知项 -
为
`CodeFixProvider`编写单元测试
简介
代码修复提供者和代码重构提供者是 Visual Studio IDE 的扩展,用于编辑用户源代码以修复问题并重构它,同时不引入功能更改。用户在代码编辑器中看到灯泡图标,可以调用代码操作(修复/重构)以自动编辑他们的代码。此外,代码修复还可以提供 FixAll 支持,允许通过单个代码操作修复文档、项目或解决方案中多个类似的问题。
完成提供者是 Visual Studio IDE 的扩展,用于在用户编辑源代码时在智能感知完成列表中显示额外的完成项,并在用户提交特定完成项时自动生成代码。
本章使 C# 开发者能够编写、调试、执行和测试这些 IDE 扩展。
创建、调试和执行 CodeFixProvider 以修复编译器警告
代码修复提供者是 IDE 扩展,用于修复源代码中的诊断,这些诊断由编译器和分析器报告。这些是在 Roslyn 的 Workspaces 层之上构建的,并操作于正在编辑的当前文档。当用户在 Visual Studio 编辑器中调用如 Ctrl + 点的命令时,IDE 代码修复引擎计算当前行跨度中的所有诊断,并识别所有已注册修复一个或多个报告的诊断的代码修复提供者。然后,每个代码修复提供者都会使用包含当前文档、诊断和跨度的代码修复上下文被调用。修复器通过在树中添加、删除或编辑语法节点来操作与文档关联的底层语法树,并返回带有修复代码的新文档。它们还可能更改包含的项目或解决方案的内容以修复诊断。当用户通过按 Enter 键提交修复时,代码修复引擎将此代码修复应用于用户代码。
在本节中,我们将编写一个CodeFixProvider来修复编译器警告CS0219 (docs.microsoft.com/en-us/dotnet/csharp/misc/cs0219)(变量variable被赋值但从未使用过)。例如,以下代码示例包含两个未使用的变量a和b,代码修复将删除带有未使用变量a的局部声明语句,并在下一个声明语句中删除声明b = 1:
public class MyClass
{
public static void Main()
{
int a = 0; // CS0219 for '*a'*
int b = 1, c = 2; // CS0219 for '*b'*
System.Console.WriteLine(c);
}
}
准备工作
你需要在你的机器上安装 Visual Studio 2017 才能执行本章中的食谱。你可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装免费的 Visual Studio 2017 社区版。
此外,你可以参考第一章中的食谱,在 Visual Studio 中创建、调试和执行分析器项目,来安装分析器+代码修复项目模板,并从模板创建一个默认项目,例如CSharpAnalyzers。
如何操作...
-
在 Visual Studio 中打开
CSharpAnalyzers.sln解决方案,并打开项目CSharpAnalyzers中的源文件CodeFixProvider.cs。 -
将代码修复提供者的
title从"Make Uppercase"更改为"Remove unused local",并将FixableDiagnosticIds属性更改为返回"CS0219"而不是CSharpAnalyzersAnalyzer.DiagnosticId:

-
将
RegisterCodeFixesAsync方法的实现替换为来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/CodeFixProvider.cs/中名为RegisterCodeFixesAsync.的代码。 -
将辅助方法
GetSyntaxNodeToRemoveAsync和RemoveDeclarationAsync添加到源文件中,文件内容来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/CodeFixProvider.cs。 -
在新添加的方法
RegisterCodeFixesAsync和RemoveDeclarationAsync的第一行设置断点。 -
将
CSharpAnalyzers.Vsix设置为启动项目,并按F5启动一个新的带有代码修复提供者启用的 VS 实例。 -
在新的 VS 实例中,创建一个新的 C#类库项目,例如
ClassLibrary,并用以下代码替换现有代码:
public class Class1
{
public void Method1()
{
// Local declaration statement with unused local ('a')
int a = 0;
// Local declaration statement with a used ('c') and ununused local ('b').
int b = 1, c = 2;
System.Console.WriteLine(c);
// Local declaration statement where unused local ('d') initializer is non-constant.
int d = c;
// Local declaration statement with errors ('e').
if (true)
var e = 1;
}
}
-
将光标放在
int a = 0;这一行上,并验证RegisterCodeFixesAsync中的断点是否被触发。移除此断点后,你可以使用F10单步执行方法,或者按F5继续执行。 -
验证在源行下方是否出现灯泡图标,并带有指向
显示潜在修复的超链接:![]()
-
点击灯泡图标,并验证
RemoveDeclarationAsync中的断点是否被触发。移除此断点后,你可以使用F10单步执行方法,或者按F5继续执行。 -
再次单击灯泡并验证是否提供了移除未使用局部代码的修复,并提供修复的代码更改预览:

-
按下回车键应用代码修复并验证未使用的局部声明语句已被删除。
-
将光标移至未使用的局部变量
b并按 Ctrl + 点键,验证是否提供了相同的代码修复,并应用修复删除声明b = 1,但保留c的局部声明:

- 验证对于具有非常量初始化器的局部
d和具有不同编译器错误的局部e,没有提供代码修复。
它是如何工作的...
代码修复提供者是 VS IDE 扩展,可以注册用于修复指定诊断 ID 的编译器或分析器诊断的代码操作。CodeFixProvider 上的主要 API 是:
-
FixableDiagnosticIds属性(抽象):一个不可变的诊断 ID 数组,代码修复提供者可以修复。任何报告了指定诊断 ID 之一的编译器或分析器诊断都是提供者的代码修复的候选者,并且对于每个此类诊断都会调用RegisterCodeFixesAsync。 -
RegisterCodeFixesAsync方法(抽象):这是用于注册可修复诊断的代码操作的函数。每当代码修复引擎需要计算 VS IDE 中当前源行报告的诊断的代码操作时,都会调用此方法。此方法接受一个CodeFixContext参数,它包含一组要修复给定诊断范围和文档的诊断。上下文中的所有诊断都有一个可修复的诊断 ID。CodeFixProvider可以将诊断范围映射到文档中的语法节点,并分析它以注册一个或多个代码操作来修复上下文中的一个或多个诊断。CodeAction包含以下主要成员:-
Title属性:这是当提供代码修复时显示在灯泡旁边的字符串。 -
Callback方法:这是当用户应用已注册的代码操作时将被调用的委托。此方法返回更改后的文档或解决方案,代码修复引擎将更改应用到工作区。 -
EquivalenceKey属性:这是表示此代码操作所属的代码操作等价类的字符串。如果代码修复提供者支持FixAllProvider,则 FixAll 代码修复将批处理调用代码操作的等价类中的所有代码操作,并同时修复它们。 -
GetFixAllProviderAsync方法(虚拟):代码修复提供者可以选择覆盖此方法,并在需要为他们的代码操作提供 FixAll 支持时返回一个非空的FixAllProvider。我们将在下一道菜中更详细地讨论这个问题。
-
本食谱中实现的 CodeFixProvider 有一个可修复的诊断 ID:CS0219,这是一个编译器诊断,标记未使用的局部变量声明。让我们详细说明 RegisterCodeFixesAsync 覆盖及其辅助函数的实现细节。
RegisterCodeFixesAsync 的第一部分计算要删除的语法节点,该节点是通过调用 GetNodeToRemoveAsync标记的变量,如果得到一个空节点,则退出:
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
// Get syntax node to remove for the unused local.
var nodeToRemove = await GetNodeToRemoveAsync(context.Document, diagnostic, context.CancellationToken).ConfigureAwait(false);
if (nodeToRemove == null)
{
return;
}
GetNodeToRemoveAsync 的初始部分计算由诊断标记的语法节点:
private async Task<SyntaxNode> GetNodeToRemoveAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Find the variable declarator identified by the diagnostic.
var variableDeclarator = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().First();
我们首先获取正在修复的文档的语法根。然后我们在根中找到诊断范围开始的语法标记,并找到第一个 VariableDeclaratorSyntax 类型的祖先节点。例如,考虑以下局部声明语句:
int b = 1, c = 2;
整个语句是一个 LocalDeclarationStatementSyntax 节点。它有一个子语法节点 VariableDeclarationSyntax,表示 int b = 1, c = 2,以及分号语法标记。变量声明语法节点包含两个变量声明符节点,每个节点都是 VariableDeclaratorSyntax 类型,分别具有文本 b = 1 和 c = 2。VariableDeclaratorSyntax 节点包含一个 IdentifierName 标记和一个类型为 EqualsValueClauseSyntax 的初始化器语法节点。CS0219 是在未使用的变量声明符的 IdentifierName 标记上报告的。
使用 Roslyn SyntaxVisualizer 来理解给定 C# 或 VB 源代码的解析语法节点/标记:

GetNodeToRemoveAsync 的下一部分实现了一些防御性检查,以便在不注册任何代码操作的情况下提前退出,如下面的代码片段所示:
if (variableDeclarator == null)
{
return null;
}
// Bail out if the initializer is non-constant (could have side effects if removed).
if (variableDeclarator.Initializer != null)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (!semanticModel.GetConstantValue(variableDeclarator.Initializer.Value).HasValue)
{
return null;
}
}
// Bail out for code with syntax errors - parent of a declaration is not a local declaration statement.
var variableDeclaration = variableDeclarator.Parent as VariableDeclarationSyntax;
var localDeclaration = variableDeclaration?.Parent as LocalDeclarationStatementSyntax;
if (localDeclaration == null)
{
return null;
}
如果诊断是在没有 VariableDeclaratorSyntax 祖先的标记上报告的,我们首先退出。如果变量初始化器是一个非常量,因为使用代码修复删除它可能会导致功能变化,不执行初始化器代码。最后,我们检查变量声明符是否有 VariableDeclarationSyntax 父节点,该父节点有一个 LocalDeclarationStatementSyntax 父节点。
代码修复提供程序中的防御性检查非常重要,可以保护我们免受意外第三方分析器报告具有相同诊断 ID 但语法节点类型与我们的修复器期望不同的诊断的影响。我们应该确保我们优雅地退出,而不是意外崩溃或注册错误的代码修复。
最后,该方法通过代码修复计算并返回要删除的语法节点:
// If the statement declares a single variable, the code fix should remove the whole statement.
// Otherwise, the code fix should remove only this variable declaration.
SyntaxNode nodeToRemove;
if (variableDeclaration.Variables.Count == 1)
{
if (!(localDeclaration.Parent is BlockSyntax))
{
// Bail out for error case where local declaration is not embedded in a block.
// Compiler generates errors CS1023 (Embedded statement cannot be a declaration or labeled statement)
return null;
}
nodeToRemove = localDeclaration;
}
else
{
nodeToRemove = variableDeclarator;
}
return nodeToRemove;
}
我们有两个情况要处理:
-
如果局部声明语句只声明了一个变量,那么我们可以删除整个语句。我们还涵盖了一个额外的退出情况,即局部声明不是由块语句父化的,在这种情况下,删除局部声明语句将导致语法错误。鉴于编译器已经为这种情况报告了诊断 CS1023(嵌入语句不能是声明或标记语句),我们只需退出。
-
否则,如果局部声明语句声明了多个变量,我们可以只删除变量声明符。
一旦我们有一个要删除的非空语法节点,我们注册一个代码修复来删除声明节点:
// Register a code action that will invoke the fix.
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
context.RegisterCodeFix(
CodeAction.Create(
title: title,
createChangedDocument: c => RemoveDeclarationAsync(context.Document, root, nodeToRemove, c),
equivalenceKey: title),
diagnostic);
}
我们使用 CodeAction.Create API 创建一个标准的 CodeAction,标题为 Remove unused local,并使用相同的等效键。我们将 RemoveDeclarationAsync 注册为当用户应用代码修复时要调用的回调方法:
private Task<Document> RemoveDeclarationAsync(Document document, SyntaxNode root, SyntaxNode declaration, CancellationToken cancellationToken)
{
var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
var newRoot = syntaxGenerator.RemoveNode(root, declaration);
return Task.FromResult(document.WithSyntaxRoot(newRoot));
}
此方法使用 SyntaxGenerator 辅助实用工具从原始语法根中删除声明节点,并返回使用新语法根创建的新文档。
SyntaxGenerator 是一个强大的语法工厂,具有在语言无关的方式中添加、删除或编辑语法节点的 API。它适用于 VB 和 C# 语法节点,并允许编写修复跨两种语言的代码修复提供程序,而无需特定的语言实现。有关 SyntaxGenerator 的参考源,请参阅 source.roslyn.io/#q=SyntaxGenerator。
在不同作用域(文档、项目、解决方案)中应用批量代码修复(FixAll)
在本节中,您将学习如何应用批量代码修复来修复不同作用域中相似诊断的多个实例。我们将应用默认分析器 + 代码修复模板项目中的 FixAll 代码修复,修复文档、项目和解决方案作用域中的多个类型名称,使它们都只包含大写字母。我们将向您展示如何从编辑器灯泡中调用 FixAll 代码修复,然后使用 FixAll 预览更改对话框来选择性地选择要应用到解决方案中的修复。
准备工作
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本***。
此外,您应执行第一个 第一章,编写诊断分析器中的配方 Creating, debugging, and executing an analyzer project in Visual Studio,以安装分析器 + 代码修复项目模板并从模板创建默认项目,例如 CSharpAnalyzers。
如何操作...
-
在 Visual Studio 中打开
CSharpAnalyzers.sln。将CSharpAnalyzers.Vsix设置为启动项目,并按 F5 启动一个新的 VS 实例,其中启用了代码修复提供程序。 -
在新的 VS 实例中,创建一个新的 C# 类库项目,例如
ClassLibrary,并用以下代码替换现有的代码:
public class Class1
{
public class Class2
{
}
}
public class Class3
{
}
- 向项目添加一个新的源文件,例如
Class4.cs,并使用以下代码:
public class Class4
{
}
- 将新 C# 类库项目
ClassLibrary2.csproj添加到解决方案中,将源文件重命名为Class5.cs,并用以下代码替换其源代码:
public class Class5
{
}
-
在错误列表中验证五个诊断,每个类一个:类型名称 'XXX' 包含小写字母.。
-
将光标放在
Class1上,按 Ctrl + 点号以显示代码修复的灯泡“Make uppercase”。

- 点击超链接“在文档中修复所有实例”以打开预览更改 - 修复所有实例对话框。点击应用按钮将“转换为大写”修复应用到
Class1.cs中的所有类型:

-
验证
Class1、Class2、和Class3分别更改为CLASS1、CLASS2和CLASS3、,但Class4和Class5保持不变。 -
按 Ctrl + Z 撤销批量代码修复,并验证解决方案返回到应用修复之前的状态。
-
再次按 Ctrl + 点号,但这次点击项目中的“修复所有实例”。
-
取消选中
public class CLASS2旁边的复选框,并验证在预览更改对话框中CLASS2是否已切换回Class2:

-
应用修复并验证
Class1、Class3和Class4分别更改为CLASS1、CLASS3和CLASS4,但Class2和Class5保持不变。 -
按 Ctrl + Z 撤销项目级别的代码修复,并验证源文件
Class1.cs和Class4.cs的更改已回滚。 -
再次按 Ctrl + 点号,这次点击解决方案中的“修复所有实例”。
-
应用代码修复并验证所有五个类都已更改为大写,然后按 Ctrl + Z 回滚解决方案中所有类型的更改。
创建一个自定义 FixAllProvider 来修复跨作用域内所有问题的所有实例
在本节中,我们将向您展示如何编写一个自定义的 FixAll 代码修复提供者以批量修复诊断项。我们将使用本章第一个菜谱中实现的代码修复来Remove unused local. 如该菜谱所示,未使用局部变量的移除可能基于封装局部声明语句是否声明了一个或多个变量而具有不同的代码修复。此外,我们可能在单个语句中声明了多个未使用的局部变量,如果语句中声明的所有局部变量都是未使用的,则批量修复应删除整个语句。因此,我们不能使用知名的批量修复器。例如,对于以下代码,批量修复应删除前两个局部声明语句,但仅删除第三个声明语句中的d的声明符:
public class MyClass
{
public static void M()
{
int a = 0; // CS0219 for 'a'
int b = 1, c = 2; // CS0219 for 'b' and 'c'
int d = 3, e = 4; // CS0219 for 'd'
System.Console.WriteLine(e);
}
}
默认的知名BatchFixer仅适用于简单的代码修复。对于其他场景,我们需要编写一个自定义的修复所有提供者。有关 FixAll 提供者和知名 BatchFixer 的限制的文档,请参阅(github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md)。
准备工作
您应该执行本章的第一个菜谱,创建、调试和执行一个代码修复提供者以修复编译器警告来实现一个用于Remove unused local的代码修复提供者。
如何做到这一点...
-
在 Visual Studio 中打开
CSharpAnalyzers.sln项目,并将两个新的源文件添加到CSharpAnalyzers项目中:-
CustomFixAllProvider.cs -
CustomFixAllCodeAction.cs
-
-
在
CustomFixAllProvider.cs中添加代码以实现一个自定义的修复所有提供者,来自CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/CustomFixAllProvider.cs。 -
从
CSharpAnalyzers/CSharpAnalyzers/CSharpAnalyzers/CustomFixAllCodeAction.cs将代码添加到CustomFixAllCodeAction.cs中,以实现一个自定义的CodeAction,该CodeAction由CustomFixAllProvider.GetFixAsync返回。 -
对
CodeFixProvider.cs进行以下修改:-
将
GetNodeToRemoveAsync修改为一个内部静态方法 -
将
GetFixAllProviderAsync修改为返回一个new CustomFixAllProvider()
-
-
将
CSharpAnalyzers.Vsix设置为启动项目,并按F5启动一个新的带有代码修复提供者的 VS 实例。 -
在新的 VS 实例中,创建一个新的 C#类库项目,例如
ClassLibrary,并用以下代码替换现有代码:
public class Class1
{
public static void M()
{
int a = 0; // CS0219 for 'a'
int b = 1, c = 2; // CS0219 for 'b' and 'c'
int d = 3, e = 4; // CS0219 for 'd'
System.Console.WriteLine(e);
}
}
- 向项目中添加一个新的源文件,例如
Class2.cs,并添加以下代码:
public class Class2
{
public static void M()
{
int a = 0; // CS0219 for 'a'
int b = 1, c = 2; // CS0219 for 'b' and 'c'
int d = 3, e = 4; // CS0219 for 'd'
System.Console.WriteLine(e);
}
}
- 将一个新的 C#类库项目添加到解决方案中,例如
ClassLibrary2.csproj,将源文件重命名为Class3.cs,并用以下代码替换其内容:
public class Class3
{
public static void M()
{
int a = 0; // CS0219 for 'a'
int b = 1, c = 2; // CS0219 for 'b' and 'c'
int d = 3, e = 4; // CS0219 for 'd'
System.Console.WriteLine(e);
}
}
-
在错误列表中验证 12 个诊断项,每个类中有一个未使用的变量.
-
将光标放在
Class1.cs中的局部变量'a'上,然后按Ctrl + 点来弹出代码修复Remove unused local****的代码提示。** -
点击文档中的超链接“修复所有出现”以打开“预览更改 - 修复所有出现”对话框。点击“应用”按钮以应用“删除未使用局部变量”以删除
Class1.cs中的所有四个未使用局部变量('*a*'、'*b*'、'*c*'和'*d*'):

- 切换到
Class2.cs中的未使用局部声明,并在解决方案/项目范围内尝试修复所有出现,并验证所选范围内所有未使用的局部变量是否已删除。
它是如何工作的...
FixAll 提供程序是 VS IDE 扩展,它们注册与特定代码修复提供程序注册的代码操作对应的批量修复的代码操作。FixAllProvider上的主要 API 是:
-
GetSupportedFixAllScopes属性(虚拟):此方法获取修复诊断所有出现的支持的范围。默认情况下,它返回文档、项目和解决方案范围。 -
GetSupportedFixAllDiagnosticIds方法(虚拟):此方法获取支持修复所有出现的诊断 ID。默认情况下,它返回相应代码修复提供程序的FixableDiagnosticIds。 -
GetFixAsync方法(抽象):这是主要方法,它接受一个FixAllContext参数,并返回与FixAllContext参数对应的批量修复的代码操作:CodeFixProvider、要修复的诊断 ID、FixAllScope和原始代码操作的EquivalenceKey。
代码修复引擎调用CodeFixProvider.GetFixAllProviderAsync方法以获取代码修复器支持的可选 FixAll 提供程序。在我们的实现中,我们确保为此方法返回CustomFixAllProvider。让我们详细说明CustomFixAllProvider的实现细节。
CustomFixAllProvider仅覆盖GetFixAsync方法。GetFixAsync方法的第一部分计算修复标题和当前FixAllScope要修复的文档:
public override async Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
{
var diagnosticsToFix = new List<KeyValuePair<Document, ImmutableArray<Diagnostic>>>();
string titleFormat = "Remove all unused locals in {0} {1}";
string title = null;
var documentsToFix = ImmutableArray<Document>.Empty;
switch (fixAllContext.Scope)
{
case FixAllScope.Document:
{
documentsToFix = ImmutableArray.Create(fixAllContext.Document);
title = string.Format(titleFormat, "document", fixAllContext.Document.Name);
break;
}
case FixAllScope.Project:
{
documentsToFix = fixAllContext.Project.Documents.ToImmutableArray();
title = string.Format(titleFormat, "project", fixAllContext.Project.Name);
break;
}
case FixAllScope.Solution:
{
foreach (Project project in fixAllContext.Solution.Projects)
{
documentsToFix = documentsToFix.AddRange(project.Documents);
}
title = "Add all items in the solution to the public API";
break;
}
case FixAllScope.Custom:
return null;
default:
break;
}
然后,我们遍历所有计算出的文档,并为每个文档计算要修复的诊断,并将它们存储在一个映射中。我们返回带有计算标题和诊断的CustomFixAllCodeAction:
foreach (Document document in documentsToFix)
{
ImmutableArray<Diagnostic> diagnostics = await fixAllContext.GetDocumentDiagnosticsAsync(document).ConfigureAwait(false);
diagnosticsToFix.Add(new KeyValuePair<Document, ImmutableArray<Diagnostic>>(document, diagnostics));
}
return new CustomFixAllCodeAction(title, fixAllContext.Solution, diagnosticsToFix);
让我们来看一下CustomFixAllCodeAction.的实现细节。自定义代码操作覆盖的主要方法是GetChangedSolutionAsync。此方法获取带有批量修复编辑的新解决方案。当用户尝试应用批量修复时,代码修复引擎会调用此方法。
GetChangedSolutionAsync方法的初始部分计算每个文档中要删除的所有本地声明和变量声明符语法节点,在一个名为nodesToRemove的映射中,执行非常基本的语法节点批量修复:
protected override async Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
var nodesToRemoveMap = new Dictionary<Document, HashSet<SyntaxNode>>();
foreach (KeyValuePair<Document, ImmutableArray<Diagnostic>> pair in _diagnosticsToFix)
{
Document document = pair.Key;
ImmutableArray<Diagnostic> diagnostics = pair.Value;
var nodesToRemove = new HashSet<SyntaxNode>();
foreach (var diagnostic in diagnostics)
{
var nodeToRemove = await CSharpAnalyzersCodeFixProvider.GetNodeToRemoveAsync(document, diagnostic, cancellationToken).ConfigureAwait(false);
if (nodeToRemove != null)
{
nodesToRemove.Add(nodeToRemove);
}
}
第二部分试图识别具有多个变量声明的局部声明语句,其中所有声明的局部变量都没有使用,因此整个语句可以被删除。对于这种情况,我们将局部声明语句添加到nodesToRemove中,并从nodeToRemove映射中移除局部声明语句中的所有单个未使用变量声明符:
var candidateLocalDeclarationsToRemove = new HashSet<LocalDeclarationStatementSyntax>();
foreach (var variableDeclarator in nodesToRemove.OfType<VariableDeclaratorSyntax>())
{
var localDeclaration = (LocalDeclarationStatementSyntax)variableDeclarator.Parent.Parent;
candidateLocalDeclarationsToRemove.Add(localDeclaration);
}
foreach (var candidate in candidateLocalDeclarationsToRemove)
{
var hasUsedLocal = false;
foreach (var variable in candidate.Declaration.Variables)
{
if (!nodesToRemove.Contains(variable))
{
hasUsedLocal = true;
break;
}
}
if (!hasUsedLocal)
{
nodesToRemove.Add(candidate);
foreach (var variable in candidate.Declaration.Variables)
{
nodesToRemove.Remove(variable);
}
}
}
最后,我们遍历所有的{Document, HashSet<SyntaxNode>}对,并为每个文档计算一个新的根,其中从整个树中移除了所有未使用的局部变量。我们创建一个新的文档,并使用新的根应用文档更改到最新的解决方案,该解决方案被跟踪为newSolution。循环结束时,newSolution代表应用了所有文档更改的当前解决方案,并由方法返回:
Solution newSolution = _solution;
foreach (KeyValuePair<Document, HashSet<SyntaxNode>> pair in nodesToRemoveMap)
{
var document = pair.Key;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
var newRoot = syntaxGenerator.RemoveNodes(root, pair.Value);
newSolution = newSolution.WithDocumentSyntaxRoot(document.Id, newRoot);
}
return newSolution;
创建一个CodeRefactoringProvider来重构源代码,推荐使用 C# 7.0 的元组
代码重构提供者是 IDE 扩展,用于重构源代码以优化代码结构,而不影响代码的功能或语义行为。这些是基于 Roslyn 的 Workspaces 层构建的,并操作于正在编辑的当前文档。当用户在 Visual Studio 编辑器中调用如Ctrl + 点的命令时,IDE 代码重构引擎计算所有可以重构编辑器中当前所选文本跨度内代码的重构。然后,每个提供者都会使用包含当前文档和跨度的代码重构上下文被调用。重构通过在树中添加、删除或编辑语法节点来操作与文档关联的底层语法树,并返回带有重构代码的新文档。它们还可能更改包含的项目或解决方案的内容。当用户通过按Enter键提交重构时,代码重构引擎将此重构应用于用户代码。
在本节中,我们将编写一个CodeRefactoringProvider,以建议在返回多个值的函数中使用元组表达式,这是一个 C# 7.0 的特性。在 C# 7.0 之前,想要返回多个值的函数有以下可能的实现方式:
-
声明一个非无返回类型的其中一个返回值,并为剩余的返回值声明
out参数。 -
为每个返回值声明无返回类型和输出参数。
-
声明一个新的类型,将这些值作为字段封装,并返回该类型的实例。
在 C# 7.0 中,推荐的实现方式是声明一个元组返回类型,并为每个返回值的类型定义元素,并且没有输出参数。我们将编写一个重构来识别之前提到的模式 1 的现有代码,并推荐使用元组的重构。例如,考虑以下返回多个返回值的函数:
private int MethodReturningTwoValues(out int x)
{
x = 0;
return 0;
}
private int MethodReturningThreeValues(out int x, int y, out int z)
{
x = 0;
z = 1;
return y;
}
我们的代码重构将提供将这些方法转换为:
private (int, int) MethodReturningTwoValues()
{
int x;
x = 0;
return (0, x);
}
private (int, int, int) MethodReturningThreeValues(int y)
{
int x;
int z;
x = 0;
z = 1;
return (y, x, z);
}
准备工作
你需要在你的机器上安装 Visual Studio 2017 来执行本章中的配方。你可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的 Visual Studio 2017 社区版本。
此外,你应该已经安装了 .NET 编译器平台 SDK 来获取 CodeRefactoring 项目模板。有关参考,请参阅第一章 编写诊断分析器 中的配方,在 Visual Studio 中创建、调试和执行分析器项目,在 Chapter 1。
默认情况下,CodeRefactoring 项目模板针对 .NET Portable v4.5 并引用 Microsoft.CodeAnalysis 包的 1.0.1 版本。由于我们打算使用 C# 7.0 语法,我们需要将 CodeAnalysis 包升级到 2.0.0 或更高版本,这些版本基于 .NET Standard,因此需要引用的项目基于 .NET 标准模板或针对 .NET Framework v4.6 或更高版本。为此配方,我们将项目更改为针对 .NET Framework v4.6。
如何操作...
-
启动 Visual Studio 并点击 文件 | 新建 | 项目...
-
将项目目标框架组合框更改为 .NET Framework 4.6(或更高)。在 Visual C# | 扩展性 下,选择代码重构(VSIX),将你的项目命名为
CodeRefactoring, 并点击确定:

-
现在你应该有一个包含两个项目:
CodeRefactoring和CodeRefactoring.Vsix的解决方案。 -
使用以下步骤将
CodeRefactoring项目更改为针对 .NET Framework v4.6:-
-
在 Visual Studio 中卸载项目并编辑
csproj文件 -
删除属性
ProjectTypeGuids和TargetFrameworkProfile
-
-
将属性
TargetFrameworkVersion从 v4.5 更改为 v4.6.。 -
将文件中的最后一个
Imports元素从可移植目标更改为非可移植目标,即,将以下行<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />替换为<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> -
保存更改并重新加载项目
-
-
在解决方案资源管理器中右键单击项目,点击 管理 NuGet 包,并将
Microsoft.CodeAnalysis.CSharp.Workspaces更新到 2.0.0:

-
在
CodeRefactoringProvider项目中打开CodeRefactoringProvider.cs并将现有的ComputeRefactoringAsync方法实现替换为CodeRefactoring/CodeRefactoring/CodeRefactoringProvider.cs/中名为ComputeRefactoringAsync. 的代码。 -
添加以下辅助程序来从参数列表计算 out 参数:
private static IEnumerable<ParameterSyntax> GetOutParameters(MethodDeclarationSyntax methodDecl)
=> methodDecl.ParameterList.Parameters.Where(parameter => parameter.Modifiers.Any(m => m.Kind() == SyntaxKind.OutKeyword));
-
删除现有的方法
ReverseTypeNameAsync,并用来自CodeRefactoring/CodeRefactoring/CodeRefactoringProvider.cs的UseValueTupleAsync以及几个辅助方法GenerateTupleType和GenerateTupleExpression替换。此外,在文件顶部添加一个新的 using 语句:using Microsoft.CodeAnalysis.Formatting; -
将
CodeRefactoring.Vsix设置为启动项目,然后单击F5来构建重构并启动一个带有启用重构的新实例的 Visual Studio 进行调试。 -
在新的 Visual Studio 实例中,创建一个新的 C#类库项目,例如
ClassLibrary,并将以下两个方法添加到Class1中:
private int MethodReturningTwoValues(out int x)
{
x = 0;
return 0;
}
private int MethodReturningThreeValues(out int x, int y, out int z)
{
x = 0;
z = 1;
return y;
}
-
右键单击项目节点 | 管理 NuGet 包,并将 NuGet 包引用添加到
System.ValueTuple。 -
将光标放在
MethodReturningTwoValues上,然后按Ctrl + 点并验证是否提供了将返回类型更改为ValueTuple的重构操作:

-
应用重构并验证方法签名是否更改为返回值元组。
-
类似地,将光标放在
MethodReturningThreeValues上,然后按Ctrl + 点并验证是否提供了将返回类型更改为ValueTuple的重构操作:

它是如何工作的...
代码重构提供者是 VS IDE 扩展,可以在不引入任何功能更改的情况下注册代码操作以将代码重构为推荐模式。CodeRefactoringProvider的主要 API 是:
-
ComputeRefactoringsAsync方法(抽象):这是一个用于注册代码操作以进行重构的方法。代码重构引擎在需要计算在 VS IDE 当前源行上提供的重构时调用此方法。此方法接受一个CodeRefactoringContext参数,它包含当前的范围和文档。CodeRefactoringProvider可以将范围映射到文档中的语法节点,并分析它以在上下文中注册一个额外的代码操作。CodeAction包含以下主要成员:-
Title属性:当提供代码重构时,显示在灯泡旁边的字符串。 -
回调方法:当用户应用已注册的代码操作时,委托将被调用。此方法返回更改后的文档或解决方案,代码重构引擎将应用这些更改到工作区。
-
让我们进一步探讨ComputeRefactoringsAsync及其辅助方法的实现细节。
ComputeRefactoringsAsync的第一个部分计算当前范围对应的语法节点:
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// Find the node at the selection.
var node = root.FindNode(context.Span);
方法的下一部分实现了某些防御性检查,以便在没有注册任何代码操作的情况下提前退出:
// Only offer a refactoring if the selected node is a method declaration node with non-void return type and at least one 'out' var.
var methodDecl = node as MethodDeclarationSyntax;
if (methodDecl == null ||
methodDecl.ReturnType.Kind() == SyntaxKind.VoidKeyword ||
!GetOutParameters(methodDecl).Any())
{
return;
}
// Check if the compilation references System.ValueTuple
var hasValueTuple = false;
if (context.Document.Project.SupportsCompilation)
{
var compilation = await context.Document.Project.GetCompilationAsync(context.CancellationToken).ConfigureAwait(false);
var systemValueTuple = compilation?.GetTypeByMetadataName(@"System.ValueTuple");
if (systemValueTuple != null && systemValueTuple.ContainingAssembly.Name.Equals(@"System.ValueTuple"))
{
hasValueTuple = true;
}
}
我们首先检查我们是否正在操作一个具有非空返回类型和至少一个输出参数的 MethodDeclarationSyntax 节点。如果分析的编译未在 System.ValueTuple 集合引用中定义名为 System.ValueTuple 的类型,我们也会退出。
最后,该方法注册了一个带有要在灯泡中显示的标题的代码操作,以及一个回调, UseValueTupleAsync,来计算重构:
if (hasValueTuple)
{
// Create a code action to transform the method signature to use tuples.
var action = CodeAction.Create("Use ValueTuple return type", c => UseValueTupleAsync(context.Document, methodDecl, c));
// Register this code action.
context.RegisterRefactoring(action);
}
UseValueTupleAsync 使用 C# 的 SyntaxFactory 辅助工具来编辑方法声明的签名和主体,并返回使用新语法根创建的新文档。
方法的第一部分计算重构代码的新参数列表。我们从参数列表中删除所有 out 参数,并将返回类型更改为元组类型。例如,对于一个返回类型为 T 的方法,具有参数 A a,out B b,out C c,我们返回一个 TupleTypeSyntax (T, B, C) 并将方法参数列表更改为仅包含 `A a
:
// Compute the new parameter list with all the out parameters removed.
var outParameters = GetOutParameters(methodDecl);
var newParameters = methodDecl.ParameterList.Parameters.Where(p => !outParameters.Contains(p));
var newParameterList = methodDecl.ParameterList.Update(
methodDecl.ParameterList.OpenParenToken,
new SeparatedSyntaxList<ParameterSyntax>().AddRange(newParameters),
methodDecl.ParameterList.CloseParenToken);
methodDecl = methodDecl.WithParameterList(newParameterList);
// Compute the new return type: Tuple type with the original return type as first element and
// types for all original out parameters as subsequent elements.1
var newReturnType = GenerateTupleType(methodDecl.ReturnType, outParameters);
methodDecl = methodDecl.WithReturnType(newReturnType);
方法的下一部分在方法体块顶部为原始参数列表中的每个 out 参数添加局部声明语句。对于前面的示例,我们将添加局部声明语句 B b; 和 C c;:
// Add local declaration statements as the start of the method body to declare locals for original out parameters.
var newStatements = new List<StatementSyntax>(outParameters.Count());
foreach (var outParam in outParameters)
{
var variableDeclarator = SyntaxFactory.VariableDeclarator(outParam.Identifier);
var variableDeclarationSyntax = SyntaxFactory.VariableDeclaration(outParam.Type, SyntaxFactory.SingletonSeparatedList(variableDeclarator));
var localDeclarationStatement = SyntaxFactory.LocalDeclarationStatement(variableDeclarationSyntax);
newStatements.Add(localDeclarationStatement);
}
var statements = methodDecl.Body.Statements;
var newBody = methodDecl.Body.WithStatements(methodDecl.Body.Statements.InsertRange(0, newStatements));
methodDecl = methodDecl.WithBody(newBody);
然后,我们收集原始方法实现中的所有 ReturnStatementSyntax 节点,并将它们的表达式替换为通过连接原始返回表达式和新生成局部变量的标识符名称创建的元组表达式。对于我们的示例,这将替换形式为 return x; 的语句为 return (x, b, c);:
// Replace all return statement expressions with tuple expressions: original return expression
// as the first argument and identifier name for original out parameters as subsequent arguments.
var returnStatements = methodDecl.Body.DescendantNodes().OfType<ReturnStatementSyntax>();
var replacementNodeMap = new Dictionary<ReturnStatementSyntax, ReturnStatementSyntax>(returnStatements.Count());
foreach (var returnStatement in returnStatements)
{
var tupleExpression = GenerateTupleExpression(returnStatement.Expression, outParameters);
var newReturnStatement = SyntaxFactory.ReturnStatement(tupleExpression);
replacementNodeMap.Add(returnStatement, newReturnStatement);
}
methodDecl = methodDecl.ReplaceNodes(returnStatements, computeReplacementNode: (o, n) => replacementNodeMap[o]);
最后,我们在方法 decl 上应用格式化器注解,以确保格式化由格式化器引擎完成。然后我们替换原始根中的更新后的 methodDecl 节点,并返回更新后的文档:
// Add formatter annotation to format the edited method declaration and body.
methodDecl = methodDecl.WithAdditionalAnnotations(Formatter.Annotation);
// Return new document with replaced method declaration.
var newRoot = root.ReplaceNode(originalMethodDecl, methodDecl);
return document.WithSyntaxRoot(newRoot);
}
还有更多...
我们当前的重构实现是不完整的——我们更改了方法签名以返回元组类型,但没有更新调用位置以消费它们。例如,这里高亮的调用位置将被我们的重构破坏:
private void M()
{
int x;
int y = MethodReturningTwoValues(out x);
}
private int MethodReturningTwoValues(out int x)
{
x = 0;
return 0;
}
我们将如何增强这个重构以使用 FindReferences API (source.roslyn.io/#q=FindReferencesSearchEngine.FindReferencesAsyn) 来查找方法的调用位置并编辑代码以修复调用位置,留作读者的练习。对于前面的示例,我们需要将 MethodReturningTwoValues 调用替换为高亮代码。
private void M()
{
int x;
(int, int) t1 = MethodReturningTwoValues();
x = t1.Item2;
int y = t1.Item1;
}
创建一个 CompletionProvider,在编辑代码时提供额外的智能感知项。
CompletionProviders 是 IDE 扩展,当用户在 Visual Studio IDE 中编辑代码时,在智能感知列表中提供完成项:

上述截图显示了一个包含当前类型及其基类型中所有可访问实例成员的完成列表,通常在用户在可执行代码中键入 this. 时显示。用户可以按提交字符,例如 Enter 键,以调用所选成员的自动完成。
在本节中,我们将编写一个 CompletionProvider 来提供相同的 可访问成员 完成项,但不需要用户在 . 字符之前输入 this(像我这样的懒惰人欢呼!)。此外,当在静态方法中调用时,完成提供程序将只提供完成列表中的 静态 可访问成员。

准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的 Visual Studio 2017 社区版。
如何操作...
-
打开 Visual Studio,单击文件 | 新建项目 | Visual C# | 类库,确保 .NET 框架组合框设置为 v4.6.2,并创建一个名为
CompletionProvider的项目。 -
在解决方案资源管理器中,右键单击项目节点并执行
管理 NuGet 包命令以打开 NuGet 包管理器。将 NuGet 包引用添加到Microsoft.CodeAnalysis和Microsoft.CodeAnalysis.Features,版本均为 2.0.0.。 -
将源文件
Class1.cs重命名为CustomCompletionProvider.cs,并从CompletionProvider/CompletionProvider/CustomCompletionProvider.cs/Type中的代码示例添加CustomCompletionProvider的源代码。 -
将名为
CompletionProvider.Vsix的 C# VSIX 项目添加到解决方案中。 -
将 VSIX 项目中的
source.extension.vsixmanifest的内容替换为以下内容:
<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" >
<Metadata>
<Identity Id="CompletionProvider.Vsix.ccf1c2f5-d03f-42a2-a1b9-c05d10efda2c" Version="1.0" Language="en-US" Publisher="Packt publishing" />
<DisplayName>CompletionProvider.Vsix</DisplayName>
<Description>Roslyn completion provider.</Description>
</Metadata>
<Installation>
<InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[15.0]" />
</Installation>
<Dependencies>
<Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.5,)" />
</Dependencies>
<Assets>
<Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="CompletionProvider" Path="|CompletionProvider|"/>
</Assets>
<Prerequisites>
<Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,16.0)" DisplayName="Visual Studio core editor" />
<Prerequisite Id="Microsoft.VisualStudio.Component.Roslyn.LanguageServices" Version="[15.0,16.0)" DisplayName="Roslyn Language Services" />
</Prerequisites>
</PackageManifest>
-
从
CompletionProvider.Vsix添加项目到项目引用到CompletionProvider。 -
将
CompletionProvider.Vsix设置为启动项目,并单击 F5 以构建完成提供程序并启动一个启用提供程序的新实例的 Visual Studio 进行调试。 -
在新的 Visual Studio 实例中,创建一个新的 C# 类库项目,例如
ClassLibrary,并将以下代码添加到源文件中:
public class Base
{
protected static int StaticMemberBase;
public int InstanceMemberBase;
}
public class Derived : Base
{
private static int staticMemberDerived;
internal int InstanceMemberDerived;
private void InstanceMethod()
{
.
}
private static void StaticMethod()
{
.
}
}
-
在
InstanceMethod中的.后面放置光标,然后按 Ctrl + SpaceBar 以显示我们的自定义完成列表。验证类型为Derived的所有实例成员以及基类型Base和System.Object的可访问实例成员是否显示在完成列表中。 -
选择一个成员,例如
InstanceMemberDerived,然后按 Enter 键,并验证.是否被替换为this.InstanceMemberDerived。 -
在
StaticMethod中的.后放置光标,然后按 Ctrl + Spacebar 来弹出完成列表。验证类型为 Derived 的所有静态成员以及基类型Base和System.Object的可访问静态成员是否显示在完成列表中。 -
选择一个成员,例如
StaticMemberBase,然后按 Enter 键,并验证.是否被替换为Base.StaticMemberBase。
它是如何工作的……
完成提供者是 VS IDE 扩展,可以在用户编辑源代码时向 Visual Studio IDE 注册要显示的完成项。CompletionProvider 上的主要 API 包括:
-
ShouldTriggerCompletion方法(虚拟):这是决定是否为给定的编辑上下文调用完成的方法。此方法接受以下参数:正在编辑的文档的SourceText、完成调用时的caretPosition、包含触发类型(插入、删除等)和触发字符的CompletionTrigger,以及完成项的OptionSet。 -
ProvideCompletionsAsync方法(抽象):这是注册完成项的方法。当完成引擎需要计算在 VS IDE 中当前完成触发器上提供的完成时,会调用此方法。此方法接受一个CompletionContext参数,它包含当前光标位置、文档、完成触发器和选项。CompletionContext提供了添加一个或多个完成项的方法。CompletionItem包含以下主要组件:要在完成列表中显示的DisplayText、可选的FilterText和SortText以修改默认的过滤和排序,与完成关联的语法元素的文本Span、一个<string, string>属性字典、一个Tags数组以及完成项的处理规则集。 -
GetDescriptionAsync方法(虚拟):此方法获取要显示在每个完成项快速信息中的描述。 -
GetChangeAsync方法(虚拟):此方法获取当用户提交特定完成项时要应用的CompletionChange。CompletionChange包含要应用到文档中的一组文本更改以及提交完成项后的新光标位置。
让我们详细说明我们 CustomCompletionProvider 中前面重写的每个覆盖方法的实现细节。
我们对 ShouldTriggerCompletion 的实现首先检查是否为插入触发器调用了完成:
public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
switch (trigger.Kind)
{
case CompletionTriggerKind.Insertion:
return ShouldTriggerCompletion(text, caretPosition);
default:
return false;
}
}
辅助方法 ShouldTriggerCompletion 检查当前字符是否为 .,以及前一个字符是否为空格、制表符或换行符。如果是这样,我们返回 true;否则返回 false。
private static bool ShouldTriggerCompletion(SourceText text, int position)
{
// Provide completion if user typed "." after a whitespace/tab/newline char.
var insertedCharacterPosition = position - 1;
if (insertedCharacterPosition <= 0)
{
return false;
}
var ch = text[insertedCharacterPosition];
var previousCh = text[insertedCharacterPosition - 1];
return ch == '.' &&
(char.IsWhiteSpace(previousCh) || previousCh == 't' || previousCh == 'r' || previousCh == 'n');
}
ProvideCompletionsAsync 方法的实现首先检查我们是否应该注册任何完成项,如果不是在支持完成的环境中,则退出。我们也会在不在方法体内编辑时退出。
public async override Task ProvideCompletionsAsync(CompletionContext context)
{
var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var text = await model.SyntaxTree.GetTextAsync(context.CancellationToken).ConfigureAwait(false);
if (!ShouldTriggerCompletion(text, context.Position))
{
return;
}
// Only provide completion in method body.
var enclosingMethod = model.GetEnclosingSymbol(context.Position, context.CancellationToken) as IMethodSymbol;
if (enclosingMethod == null)
{
return;
}
然后,我们使用辅助方法GetAccessibleMembersInThisAndBaseTypes和GetBaseTypesAndThis计算当前上下文中的所有可访问成员:
private static ImmutableArray<ISymbol> GetAccessibleMembersInThisAndBaseTypes(ITypeSymbol containingType, bool isStatic, int position, SemanticModel model)
{
var types = GetBaseTypesAndThis(containingType);
return types.SelectMany(x => x.GetMembers().Where(m => m.IsStatic == isStatic && model.IsAccessible(position, m)))
.ToImmutableArray();
}
private static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(ITypeSymbol type)
{
var current = type;
while (current != null)
{
yield return current;
current = current.BaseType;
}
}
然后,我们遍历所有成员以建议,忽略构造函数,并为每个成员创建和注册一个完成项:
var membersToSuggest = GetAccessibleMembersInThisAndBaseTypes(
enclosingMethod.ContainingType,
isStatic: enclosingMethod.IsStatic,
position: context.Position - 1,
model: model);
// Add completion for each member.
foreach (var member in membersToSuggest)
{
// Ignore constructors
if ((member as IMethodSymbol)?.MethodKind == MethodKind.Constructor)
{
continue;
}
// Add receiver and description properties.
var receiver = enclosingMethod.IsStatic ? member.ContainingType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) : "this";
var description = member.ToMinimalDisplayString(model, context.Position - 1);
var properties = ImmutableDictionary<string, string>.Empty
.Add(Receiver, receiver)
.Add(Description, description);
// Compute completion tags to display.
var tags = GetCompletionTags(member).ToImmutableArray();
// Add completion item.
var item = CompletionItem.Create(member.Name, properties: properties, tags: tags);
context.AddItem(item);
}
我们使用成员的Name属性作为完成项的DisplayText值。
我们计算了几个字符串,即Receiver和Description,并将它们作为字符串属性存储在完成项上。这些属性分别用于GetChangeAsync和GetDescriptionAsync方法的重写。Receiver基本上是用户提交完成项时添加到.字符左侧的字符串:例如实例成员的this,静态成员的包含类型的name。Description是显示在每个完成项快速信息中的文本。我们使用符号的最小显示字符串作为描述,但可以增强以显示带颜色的标记并使用符号上的 XML 文档注释中的内容。
我们还计算并附加一组字符串Tags到完成项。这些标签决定了完成项要显示的符号。例如,符号符号:字段、方法、属性等,以及可访问性符号:私有、受保护、内部、公共等。
GetDescriptionAsync重写直接使用存储在完成项上的Description属性来计算CompletionDescription:
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
return Task.FromResult(CompletionDescription.FromText(item.Properties[Description]));
}
GetChangeAsync重写使用计算出的Receiver属性和项的DisplayText来形成用于文本更改的newText "{receiver}.{item.DisplayText}"。文本更改的TextSpan使用item.Span.Start - 1作为起始值,1作为长度,以考虑到要删除的现有.字符:
public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
{
// Get new text replacement and span.
var receiver = item.Properties[Receiver];
var newText = $"{receiver}.{item.DisplayText}";
var newSpan = new TextSpan(item.Span.Start - 1, 1);
// Return the completion change with the new text change.
var textChange = new TextChange(newSpan, newText);
return Task.FromResult(CompletionChange.Create(textChange));
}
为 CodeFixProvider 编写单元测试
在本节中,我们将向您展示如何编写和执行CodeFixProvider的单元测试。
准备工作
您需要创建并打开一个分析器+代码修复器项目,例如 Visual Studio 2017 中的CSharpAnalyzers。请参考第一章中的配方,“在 Visual Studio 中创建、调试和执行分析器项目”,以获取指导。
注意,模板单元测试项目包含了对DiagnosticAnalyzer和CodeFixProvider的单元测试。本章仅处理CodeFixProvider测试。请参考第一章中的配方,“为分析器项目编写单元测试”,以及“编写诊断分析器”部分,以了解诊断分析器单元测试。
如何操作...
- 在解决方案资源管理器中打开
UnitTests.cs文件,该文件位于CSharpAnalyzers.Test项目中,以查看为项目中的默认符号分析器和代码修复提供者创建的默认单元测试(类型名称不应包含小写字母):

-
点击测试 | 窗口 | 测试窗口以打开测试资源管理器窗口,查看项目中的单元测试。默认项目有两个单元测试:
-
TestMethod1: 这个测试检查分析器诊断在测试代码上没有触发的情况。 -
TestMethod2: 这个测试检查分析器诊断在测试代码上触发,并且代码修复提供者修复了诊断。
-
删除TestMethod1,因为我们只关心CodeFixProvider测试。
- 通过在测试资源管理器中右键单击未运行测试节点并执行运行选定测试上下文菜单命令来运行项目的单元测试,并验证
TestMethod2通过:

- 编辑测试源代码,移除所有使用语句,并在测试字符串和
fixTest字符串中TypeName内部添加一个新的嵌套类型TypeName2:
public void TestMethod2()
{
var test = @"
namespace ConsoleApplication1
{
class TypeName
{
class TypeName2
{
}
}
}";
...
var fixtest = @"
namespace ConsoleApplication1
{
class TYPENAME
{
class TypeName2
{
}
}
}";
-
编辑
TestMethod1以修复原始预期诊断的预期行号,并为新的测试代码添加一个新的预期诊断: -
在编辑器中右键单击
TestMethod2并执行运行测试上下文菜单命令,并验证测试现在失败,诊断不匹配断言 - 预期 1,实际 2。
var expected = new[] {
new DiagnosticResult {
Id = "CSharpAnalyzers",
Message = String.Format("Type name '{0}' contains lowercase letters", "TypeName"),
Severity = DiagnosticSeverity.Warning,
Locations =
new[] { new DiagnosticResultLocation("Test0.cs", 4, 15) }
},
new DiagnosticResult {
Id = "CSharpAnalyzers",
Message = String.Format("Type name '{0}' contains lowercase letters", "TypeName2"),
Severity = DiagnosticSeverity.Warning,
Locations =
new[] { new DiagnosticResultLocation("Test0.cs", 6, 19) }
}
};
-
修复预期的
fixTest代码以包含TYPENAME2,并验证测试现在通过。 -
再次运行单元测试,并注意测试仍然失败,但现在失败的原因是固定测试代码中的差异 -
fixTest中的类TypeName2使用小写字母,但实际测试代码中有TYPENAME2。
如何工作...
分析器 + 代码修复单元测试项目允许我们为我们的分析器/代码修复提供者在不同的代码样本上的执行编写单元测试。每个单元测试都带有TestMethod属性,并定义样本测试代码,分析器在代码上报告的预期诊断(如果有),执行代码修复提供者后的预期修复测试代码,以及调用测试辅助方法,这里VerifyCSharpFix,以验证代码修复。
要了解单元测试的基础知识和我们的单元测试框架的测试框架,请参阅食谱中的如何工作...部分,第一章中的为诊断分析器编写单元测试。
在本节中,我们将简要解释我们的单元测试容器从其中派生的抽象类型:CodeFixVerifier。此类型包含用于运行 C#和 VB CodeFixProvider单元测试的辅助方法VerifyCSharpFix和VerifyBasicFix。这些方法调用一个公共辅助方法VerifyFix,其工作方式如下:
-
此方法接受原始和预期样本测试代码作为输入 - 应用代码修复的原始代码,以及应用代码修复后的预期代码。
-
它还获取语言名称、分析器、代码修复提供者,以及要应用的代码操作的索引,以防修复器注册了多个代码操作。
-
它在原始测试代码上运行分析器以获取分析器诊断。它还在测试代码上计算编译器诊断。
-
它使用第一个分析器诊断来创建一个
CodeFixContext,并使用此上下文调用代码修复提供者的RegisterCodeFixesAsync方法。 -
然后,它将在给定的代码修复索引处应用已注册的代码操作来计算新文档。
-
它在新文档上重新执行分析器以获取新的分析器诊断。
-
直到至少有一个新的分析器诊断出现,它将重复步骤 4-6 以在新文档上应用代码修复。
-
最后,它将新文档的内容与预期的修复代码进行验证。
第四章:提高 C# 代码库的代码维护性
在本章中,我们将介绍以下内容:
-
配置 Visual Studio 2017 内置的 C# 代码风格规则
-
使用
.editorconfig文件配置代码风格规则 -
使用公共 API 分析器进行 API 表面维护
-
使用第三方 StyleCop 分析器进行代码风格规则
简介
在当前开源项目时代,众多来自不同组织和世界各地的不同贡献者,维护任何仓库的一个主要要求是在代码库中强制执行代码风格指南。历史上,这通常通过详尽的文档和代码审查来实现,以捕捉任何违反这些编码指南的行为。然而,这种方法有其缺陷,需要大量的人力和时间来维护文档和执行详尽的代码审查。
利用 Visual Studio 2017 内置的自动代码风格和命名规则,用户可以自定义和配置单个规则的执行级别,并对违规行为提供视觉提示,例如编辑器中的建议或波浪线,以及错误列表中的诊断信息,并设置适当的严重性(错误/警告/信息性消息)。此外,规则还包含一个代码修复功能,可以自动修复文档、项目或解决方案中一个或多个违规实例。在 Visual Studio 2017 的新 EditorConfig 支持下,这些规则配置可以在每个文件夹级别通过 .editorconfig 文件进行强制执行和自定义。此外,.editorconfig 文件可以与源代码一起提交到仓库,以确保所有为仓库做出贡献的用户都遵守这些规则。
EditorConfig (editorconfig.org/) 是一种开源文件格式,帮助开发者配置和强制执行格式化和代码风格约定,以实现代码库的一致性和可读性。EditorConfig 文件易于提交到源代码控制,并在仓库和项目级别应用。EditorConfig 约定覆盖个人设置中的等效约定,使得代码库的约定优先于单个开发者的约定。
在本章中,我们将向您介绍这些代码风格规则,展示如何在 Visual Studio 2017 IDE 中配置它们,并将这些设置保存到 EditorConfig 文件中。此外,我们还将向您介绍一个非常流行的第三方 Roslyn 分析器,即 PublicAPI 分析器,它允许通过检入到仓库中的附加非源文本文件来跟踪 .NET 程序集的公共 API 表面,并在出现破坏性 API 变更或未在公共 API 文件中记录的新公共 API 添加时提供诊断和代码修复。我们还将指导您如何为 .NET 项目配置 StyleCop 分析器,这是一个流行的第三方代码风格分析器,也是内置 Visual Studio 代码风格规则的替代方案,用于强制执行代码风格。
配置 Visual Studio 2017 中内置的 C# 代码风格规则
在本节中,我们将向您介绍 Visual Studio 2017 中内置的重要代码风格规则类别,并展示如何在 Visual Studio 中配置它们。
准备中
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
启动 Visual Studio,导航到文件 | 新建 | 项目...,创建一个新的 C# 类库项目,并用
ClassLibrary/Class1.cs中的代码替换Class1.cs中的代码。 -
点击工具 | 选项以打开工具选项对话框,并导航到 C# 代码风格选项(文本编辑器 | C# | 代码风格 | 一般):

- 将 'this.' 预设的严重性更改为建议,预设的类型偏好更改为警告,以及将 'var' 偏好更改为错误。将 'var' 偏好的偏好从“首选显式类型”更改为“首选 'var'”:

- 将代码块偏好的严重性更改为警告,并将方法偏好更改为首选表达式体:

-
确保所有剩余的代码风格规则(表达式偏好、变量偏好和 'null' 检查)的严重性均为建议。
-
在错误列表中验证以下代码风格诊断:

- 双击第一个 IDE0007 错误并验证是否提供了使用 'var' 而不是显式类型的代码修复的灯泡提示。验证按 Enter 键可以修复代码,并将诊断从错误列表中删除。对剩余的 IDE0007 诊断重复此练习:

- 现在,双击第一个 IDE0012 警告,并验证是否提供了一个用于简化名称 'System.Int32' 的灯泡提示。这次,单击“文档中所有出现的修复”超链接,并验证是否出现了一个预览更改对话框,该对话框使用单个批量修复来修复文档中所有 IDE0012 的实例:

- 对错误列表中剩余的每个诊断应用代码修复,并验证代码现在是否完全干净:
using System;
class Class1
{
// Field and Property - prefer predefined type
private int field1;
private int Property1 => int.MaxValue;
private void Method_DoNotPreferThis()
{
Console.WriteLine(field1);
Console.WriteLine(Property1);
Method_PreferExpressionBody();
}
private int Method_PreferVar()
{
var i = 0;
var c = new Class1();
var c2 = Method_PreferExpressionBody();
return i;
}
private void Method_PreferBraces(bool flag)
{
if (flag)
{
Console.WriteLine(flag);
}
}
private Class1 Method_PreferExpressionBody() => Method_PreferPatternMatching(null);
private Class1 Method_PreferPatternMatching(object o)
{
if (o is Class1 c)
{
return c;
}
return new Class1();
}
private int Method_PreferInlineVariableDeclaration(string s)
{
if (int.TryParse(s, out var i))
{
return i;
}
return -1;
}
private void Method_NullCheckingPreferences(object o)
{
var str = o?.ToString();
var o3 = o ?? new object();
}
}
- 将以下新方法添加到
Class1中,并验证是否为新增代码中的代码风格违规抛出了 IDE0007(使用 'var' 而不是显式类型):
private void Method_New_ViolateVarPreference()
{
Class1 c = new Class1();
}
它是如何工作的...
代码风格规则内置在 Visual Studio 2017 中,并分为以下广泛类别:
-
'this.' 偏好
-
预定义类型偏好
-
'var' 偏好
-
代码块偏好
-
表达式偏好
-
变量偏好
-
'null' 检查
每个类别都有一组一个或多个规则,每个规则有两个字段:
-
偏好:一个字符串,用于标识规则的偏好。通常,它有两个可能的值,一个表示规则应该被优先考虑,另一个表示规则不应该被优先考虑。例如,对于 'this.' 偏好规则,可能的值包括:
-
不偏好 'this.':这强制标记带有
'this.'前缀的成员访问的代码。 -
偏好 'this.':这确保了没有 'this.' 前缀的成员访问的代码会被标记。
-
-
严重性:一个枚举,用于标识规则的严重性。它有以下可能的值和视觉效果:
-
错误:规则的违规会在错误列表中产生错误诊断,并在代码编辑器中产生红色波浪线。
-
警告:规则的违规会在错误列表中产生警告诊断,并在代码编辑器中产生绿色波浪线。
-
建议:规则的违规会在错误列表中产生信息性消息诊断,并在代码编辑器中违反语法的第一个几个字符下方产生灰色点。
-
无:规则在编辑器中不受强制,错误列表中没有诊断,编辑器中也没有视觉指示器。
-
用户可以使用“工具 | 选项”对话框根据其要求配置每个规则的偏好和严重性。关闭并重新打开源文档会导致配置更改生效,违规将在错误列表和视觉指示器(波浪线/点)中报告。每个规则都附带代码修复和 FixAll 支持,以修复文档、项目或解决方案中一个或多个违规的实例。
对于内置代码风格规则报告的诊断仅在 Visual Studio 2017 的实时代码编辑期间生成——它们不会中断构建,也不会在命令行构建期间生成。这种行为在未来版本的 Visual Studio 中可能会改变,也可能不会改变。
还有更多...
代码样式规则首选项与用户配置文件设置一起保存,并在同一 Visual Studio 安装的 Visual Studio 会话之间持久化。这意味着在 Visual Studio 中打开的任何项目都将具有相同的代码样式强制执行。然而,在具有不同用户配置文件的不同 Visual Studio 安装或不同机器上打开的相同源将不会具有相同的代码样式强制执行。为了在所有用户之间启用相同的代码样式强制执行,用户需要将代码样式设置持久化到.editorconfig文件中,并将其与源文件一起提交到仓库。有关详细信息,请参阅本章中的使用.editorconfig 文件进行按文件夹配置代码样式规则配方。
在文本编辑器 | C# | 代码样式 | 命名规则下,考虑探索工具 | 选项...对话框中的命名规则。这些规则允许用户强制执行关于每种不同符号应该如何命名的指南。例如,接口名称应该以大写字母"I"开头,类型名称应该是 Pascal 大小写,等等。
使用.editorconfig文件配置代码样式规则
在本节中,我们将向您展示如何使用 EditorConfig 文件配置 Visual Studio 2017 内置的代码样式规则,以及如何在不同的文件夹级别覆盖这些设置。这些 EditorConfig 文件可以与源文件一起提交到仓库中,这确保了代码样式设置对所有仓库贡献者都是持久和强制执行的。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装免费的 Visual Studio 2017 社区版。
从vsixgallery.com/extension/1209461d-57f8-46a4-814a-dbe5fecef941/的 Visual Studio 扩展库中安装EditorConfig Language Service VSIX,以在 Visual Studio 中获得.editorconfig文件的智能感知和自动完成功能。
如何操作...
-
启动 Visual Studio,点击文件 | 新建 | 项目...,创建一个新的 C#类库项目,并用
ClassLibrary/Class1.cs中的代码替换Class1.cs中的代码。 -
在项目中添加一个名为
.editorconfig的新文本文件,内容如下:
# top-most EditorConfig file
root = true
# rules for all .cs files.
[*.cs]
# 'this.' preferences
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
# predefined type preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# 'var' preferences
csharp_style_var_for_built_in_types = true:error
csharp_style_var_when_type_is_apparent = true:error
csharp_style_var_elsewhere = true:error
# code block preferences
csharp_new_line_before_open_brace = all
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_operators = false:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning
# expression preferences
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
# variable preferences
csharp_style_inlined_variable_declaration = true:suggestion
# 'null' checking
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
- 在错误列表中验证以下代码样式诊断:

-
双击第一个 IDE0007 错误,并验证是否提供了一个代码修复选项,使用
var而不是显式类型。验证按Enter键可以修复代码,并将诊断从错误列表中删除。对剩余的 IDE0007 诊断重复此练习。 -
然后,双击第一个 IDE0012 警告,并验证是否提供了一个灯泡图标来简化名称'System.Int32'。这次,点击“在文档中修复所有实例”的超链接,并验证是否出现了一个预览更改对话框,该对话框通过单个批量修复解决了文档中所有 IDE0012 的实例。
-
对错误列表中剩余的每个诊断项应用代码修复,并验证代码现在是否完全干净:
using System;
class Class1
{
// Field and Property - prefer predefined type
private int field1;
private int Property1 => int.MaxValue;
private void Method_DoNotPreferThis()
{
Console.WriteLine(field1);
Console.WriteLine(Property1);
Method_PreferExpressionBody();
}
private int Method_PreferVar()
{
var i = 0;
var c = new Class1();
var c2 = Method_PreferExpressionBody();
return i;
}
private void Method_PreferBraces(bool flag)
{
if (flag)
{
Console.WriteLine(flag);
}
}
private Class1 Method_PreferExpressionBody() => Method_PreferPatternMatching(null);
private Class1 Method_PreferPatternMatching(object o)
{
if (o is Class1 c)
{
return c;
}
return new Class1();
}
private int Method_PreferInlineVariableDeclaration(string s)
{
if (int.TryParse(s, out var i))
{
return i;
}
return -1;
}
private void Method_NullCheckingPreferences(object o)
{
var str = o?.ToString();
var o3 = o ?? new object();
}
}
- 在项目的根目录中添加一个新文件夹,例如
NewFolder,并在该文件夹中添加一个新类,例如Class2.cs,然后验证 IDE0007(使用var代替显式类型)是否因新添加的代码中的代码风格违规而引发。
private void Method_New_ViolateVarPreference()
{
Class1 c = new Class1();
}
- 在
NewFolder中添加一个名为.editorconfig的新文本文件,其内容如下:
# rules for all .cs files in this folder.
[*.cs]
# override 'var' preferences
csharp_style_var_for_built_in_types = false:error
csharp_style_var_when_type_is_apparent = false:error
csharp_style_var_elsewhere = false:error
- 关闭并重新打开
Class2.cs,并验证 IDE0007 不再被报告。
它是如何工作的...
参考本章中食谱的如何工作...部分,配置 Visual Studio 2017 中内置的 C#代码风格规则,以了解 Visual Studio 2017 中的不同内置代码风格规则,以及与这些规则关联的偏好和严重性设置,以及它们如何映射到编辑器配置条目。例如,考虑以下条目:
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_field是规则名称,偏好设置为false,严重性设置为suggestion。这些规则及其设置在每个文件夹级别强制执行,任何文件夹级别的 EditorConfig 文件都会覆盖祖先目录中 EditorConfig 文件的设置,直到达到根文件路径或找到具有root=true的 EditorConfig 文件。我们建议您参考以下文章,以获得对 EditorConfig 和 Visual Studio 2017 中相关支持的详细理解:
-
Editorconfig 文件格式:
EditorConfig.org/ -
VS2017 中.NET 代码风格的 Editorconfig 支持:
blogs.msdn.microsoft.com/dotnet/2016/12/15/code-style-configuration-in-the-vs2017-rc-update/ -
VS2017 中.NET 代码风格的 Editorconfig 参考:
docs.microsoft.com/en-us/visualstudio/ide/EditorConfig-code-style-settings-reference
使用公共 API 分析器进行 API 表面维护
DeclarePublicAPIAnalyzer分析器是在(github.com/dotnet/roslyn-analyzers)仓库中开发的流行第三方分析器,并作为 NuGet 包发布在www.nuget.org/packages/Roslyn.Diagnostics.Analyzers。此分析器通过提供与项目源文件一起存在的可读和可审查的文本文件来帮助跟踪项目的公共表面区域,并提供作为源的 API 文档。例如,考虑以下具有公共和非公共符号的源文件:
public class Class1
{
public int Field1;
public object Property1 => null;
public void Method1() { }
public void Method1(int x) { }
private void Method2() { }
}
此类型的附加 API 表面文本文件将如下所示:
Class1
Class1.Class1() -> void
Class1.Field1 -> int
Class1.Method1() -> void
Class1.Method1(int x) -> void
Class1.Property1.get -> object
每个公共符号都有一个条目:类型Class1,其构造函数及其成员Field1, Method1重载,以及Property1获取器。条目包含整个符号签名,包括返回类型和参数。
使用此 NuGet 包,用户可以在任何时间点跟踪已发布和未发布的公共 API 表面,当公共 API 表面发生变化时,获取实时和构建中断诊断,并应用代码修复来更新这些附加文件以匹配本地的 API 更改。当实际的代码更改很大且分散在代码库中时,这允许进行更丰富和更集中的 API 审查,但只需查看单个文件中的核心签名更改即可审查 API 更改。
DeclarePublicAPIAnalyzer主要编写用于跟踪位于github.com/dotnet/roslyn的 Roslyn 源库的公共 API 表面,并且在所有 Roslyn 贡献者中仍然非常受欢迎。分析器最终被转换为一个通用开源分析器,可以从NuGet.org安装到任何.NET 项目中。
在本节中,我们将向您展示如何为 C#项目安装和配置公共 API 分析器,带您了解跟踪公共 API 表面的附加文本文件,向您展示分析器报告的 API 更改诊断,并最终向您展示如何应用代码修复来修复一个或多个这些诊断的实例以更新 API 表面文本文件。
准备工作
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
启动 Visual Studio,点击文件 | 新建 | 项目...,创建一个新的 C#类库项目,并将
Class1.cs中的代码替换为ClassLibrary/Class1.cs(也在本食谱的简介部分中提到)中的代码样本。 -
安装
Roslyn.Diagnostics.AnalyzersNuGet 包版本1.2.0-beta2。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅配方通过 NuGet 包管理器搜索和安装分析器,在第二章,在.NET 项目中使用诊断分析器。 -
将RS0016和RS0017的严重性从警告升级到错误。有关分析器严重性配置的指导,请参阅配方,在 Visual Studio 解决方案资源管理器中查看和配置分析器,在第二章,在.NET 项目中使用诊断分析器。
-
将两个新的文本文件
PublicAPI.Shipped.txt和PublicAPI.Unshipped.txt添加到项目中。 -
在解决方案资源管理器中选择两个文本文件,使用属性窗口将它们的构建操作从内容更改为 AdditionalFiles,并保存项目:

- 验证编辑器中的波浪线和错误列表中的六个RS0016错误(
*x*不是已声明的 API 的一部分),每个公共符号一个:

- 将光标移至字段符号
Field1,并按Ctrl + 点号(.)以获取自动修复诊断并将符号添加到未发布的公共 API 文本文件的代码修复:

-
通过按Enter键应用代码修复,并验证条目
Class1.Field1 -> int已添加到PublicAPI.Unshipped.txt,并且Field1的诊断和波浪线不再存在。 -
将光标移至
Class1的类型声明处,再次按Ctrl + 点号以获取代码修复,但这次应用 FixAll 代码修复以批量修复整个文档中的所有 RS0016 实例,并将所有公共符号添加到未发布的公共 API 文本文件中。有关应用 FixAll 代码修复的指导,请参阅配方,在不同范围内应用批量代码修复(FixAll):文档、项目和解决方案,在第三章,编写 IDE 代码修复、重构和 Intellisense 完成提供者。 -
剪切
PublicAPI.Unshipped.txt中的全部内容,并将其粘贴到PublicAPI.Shipped.txt中。 -
在
Class1.cs中,尝试通过将已发布的公共符号Field1重命名为Field2来引入破坏性 API 更改*。 -
验证
Field2立即报告RS0016,并提供一个代码修复以添加Field2的公共 API 条目。应用代码修复将Field2添加到公共 API 表面并修复诊断。 -
构建项目,并验证项目在输出窗口中由于破坏性更改出现以下RS0017诊断错误而无法构建:
ClassLibraryPublicAPI.Shipped.txt(3,1,3,21): 错误 RS0017: 符号'Class1.Field1 -> int'是已声明的 API 的一部分,但既不是公共的,也无法找到。 -
在第 11 步和第 12 步中撤销更改。
-
将
Method2改为公共方法,验证它报告了 RS0016,并使用代码修复将其 API 条目添加到PublicAPI.Unshipped.txt。 -
现在,将
unshipped公共符号从c重命名为Method3. 验证Method3报告了 RS0016,并且代码修复将 Method2 的公共 API 条目替换为Method3的条目:

- 应用代码修复并验证构建是否成功,错误列表中没有诊断信息。
它是如何工作的...
DeclarePublicAPIAnalyzer 是一个额外的文件分析器,它通过比较编译中声明的公共符号与已发布和未发布的 API 表面文本文件中的公共 API 条目来工作。它为每个符号使用基于其完全限定名称和签名的唯一字符串表示形式,作为其公共 API 条目。它报告任何缺失或多余的公共 API 条目的诊断信息。您可以在 github.com/dotnet/roslyn-analyzers/blob/master/src/Roslyn.Diagnostics.Analyzers/Core/DeclarePublicAPIAnalyzer.cs 找到该分析器的实现,以及提供的相应代码修复 github.com/dotnet/roslyn-analyzers/blob/master/src/Roslyn.Diagnostics.Analyzers/Core/DeclarePublicAPIFix.cs。
还有更多...
您可以在 github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md 阅读更多关于如何编写和消费额外的文件分析器,如 DeclarePublicAPIAnalyzer 的信息。
使用第三方 StyleCop 分析器进行代码风格规则
在本节中,我们将向您介绍一个流行的第三方分析器包,用于 C# 项目的代码风格规则,即 StyleCop 分析器。我们将介绍如何安装 StyleCop 分析器 NuGet 包,给出 StyleCop 规则类别的示例违规,并展示如何配置和调整单个 StyleCop 规则。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的 Visual Studio 2017 社区版本。
如何操作...
-
启动 Visual Studio,导航到文件 | 新建 | 项目...,创建一个新的 C# 类库项目,并将
Class1.cs中的代码替换为ClassLibrary/Class1.cs中的代码样本。 -
安装
StyleCop.AnalyzersNuGet 包(截至本文撰写时,最新预发布版本为 1.1.0-beta001)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅配方,通过 NuGet 包管理器搜索和安装分析器,位于 第二章,在 .NET 项目中消费诊断分析器。 -
验证以下 StyleCop 诊断是否显示在错误列表中:

-
从命令行或在 Visual Studio 的顶级构建菜单中构建项目,并验证这些诊断是否也来自构建。
-
双击警告 SA1025 代码不得在一行中包含多个空格字符,验证编辑器中是否提供了灯泡以修复间距违规,并通过按 Enter 键应用代码修复来修复它:

-
然后,双击警告 SA1200 使用指令必须在命名空间声明内出现,并验证它是否在
using System;使用语句上报告,因为它位于命名空间Namespace之外。 -
向项目中添加一个名为
stylecop.json的新文件。 -
在解决方案资源管理器中选择
stylecop.json,使用属性窗口将其构建操作从内容更改为 AdditionalFiles,并保存项目:

- 将以下文本添加到
stylecop.json并验证 SA1200 已不再被报告:
{
"settings": {
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace"
}
}
}
- 将
using System;移入命名空间Namespace内,并验证 SA1200 (使用指令必须出现在命名空间声明之外) 现在报告了位于命名空间内的使用语句。
它是如何工作的...
StypeCop 分析器包含以下类别的样式规则:
-
间距规则(SA1000-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SpacingRules.md):强制执行代码中关键字和符号周围间距要求的规则 -
可读性规则(SA1100-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/ReadabilityRules.md):确保代码格式良好且可读的规则 -
排序规则(SA1200-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/OrderingRules.md):强制执行代码内容标准排序方案的规则 -
命名规则(SA1300-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/NamingRules.md):强制执行成员、类型和变量的命名要求的规则 -
可维护性规则(SA1400-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/MaintainabilityRules.md):提高代码可维护性的规则 -
布局规则(SA1500-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/LayoutRules.md):强制执行代码布局和行间距的规则 -
文档规则(SA1600-) (
github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/DocumentationRules.md):验证代码文档内容和格式的规则
我们提供的代码示例按类别具有以下 StyleCop 诊断:
-
间距:
- SA1025(代码不得在一行中包含多个空白字符)
-
可读性:
- SA1101(使用 this 前缀调用局部变量)
-
排序:
- SA1200(使用指令必须位于命名空间声明内)
-
命名:
-
SA1300(元素"method"必须以大写字母开头)
-
SA1303(常量字段名必须以大写字母开头)
-
-
可维护性:
-
SA1401(字段必须是私有的)
-
SA1402(文件只能包含一个类型)
-
-
布局:
-
SA1502(元素不得位于单行)
-
SA1503(大括号不得省略)
-
SA1505(开括号后不得跟空白行)
-
-
文档:
-
SA1600(元素必须进行文档化)
-
SA1633(文件头缺失或未位于文件顶部)
-
StyleCop 分析器包还包含针对某些规则的代码修复,以修复违规行为。
StyleCop 分析器可以作为 NuGet 包安装,用于特定的 C#项目/解决方案,或者作为为所有 C#项目启用的 VSIX 扩展。NuGet 包启用构建时的 StyleCop 诊断,因此是推荐安装分析器的方式。
代码风格通常被认为是一个非常主观的问题,因此允许最终用户选择性地启用、抑制或配置个别规则非常重要。
-
StyleCop 规则可以通过代码分析规则集文件启用或抑制(参见第二章的配方,使用规则集文件和规则集编辑器配置分析器,在第二章,在.NET 项目中使用诊断分析器中参考)。
-
Stylecop 规则可以通过添加到项目中作为额外非源文件的
stylecop.json文件进行配置和调整。例如,考虑排序规则 SA1200(使用指令必须在命名空间声明内出现)在 (www.nuget.org/packages/StyleCop.Analyzers/)。默认情况下,此规则如果使用指令放置在文件顶部 外部 命名空间声明,则会报告违规。然而,此规则可以被配置为其语义相反,并要求使用指令在命名空间声明外部,如果它们在 内部 则报告违规,使用以下stylecop.json:
{
"settings": {
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace"
}
}
}
StyleCop 分析器仓库为每个规则类别提供了详细的文档,以及单个样式规则。您可以在 github.com/DotNetAnalyzers/StyleCopAnalyzers/tree/master/documentation 找到这些文档。
第五章:在 C#代码中识别安全漏洞和性能问题
在本章中,我们将涵盖以下食谱:
-
识别 Web 应用程序中的配置相关安全漏洞
-
识别 Web 应用程序中视图标记文件(
.cshtml、.aspx文件)中的跨站脚本漏洞 -
识别可能导致 SQL 和 LDAP 注入攻击的不安全方法调用
-
识别 Web 应用程序中的弱密码保护和管理工作
-
识别外部组件数据弱验证以防止跨站请求伪造和路径篡改等攻击
-
使用 FxCop 分析器识别源代码的性能改进
简介
在本章中,我们将涵盖两个非常重要且流行的 Roslyn 分析器类别:安全和性能分析器。
-
安全: 考虑到.NET 应用程序的领域极其庞大,每个应用程序都有非常特定的安全漏洞,因此我们拥有特定领域的工具/扩展来捕获这些漏洞至关重要。基于 Roslyn 的安全分析器,如PUMA扫描分析器,在编译时捕获这些漏洞并报告诊断。PUMA 扫描分析器规则分为以下广泛类别:
-
配置 (
www.pumascan.com/rules.html#overview): 捕获 ASP.NET Web 配置文件中的漏洞的规则 -
跨站脚本 (
www.pumascan.com/rules.html#cross-site-scripting): 捕获跨站脚本(XSS)漏洞的规则 -
注入 (
www.pumascan.com/rules.html#injection): 捕获对可能导致 SQL 注入攻击的不安全外部组件方法调用的规则 -
密码管理 (
www.pumascan.com/rules.html#password-management): 捕获密码管理组件中的漏洞的规则 -
验证 (
www.pumascan.com/rules.html#validation): 捕获外部请求的弱验证和身份验证规则,可能导致对其他用户的恶意攻击
-
-
性能:对于所有应用程序来说,运行时性能都很重要,并且有许多不同的方面。.NET 应用程序的一个重要性能标准是 .NET 编译器生成的 MSIL 或 CIL (
en.wikipedia.org/wiki/Common_Intermediate_Language) 的质量。MSIL 的质量受用户代码的质量和生成 MSIL 的编译器的质量共同影响。在本章中,我们将向您介绍 FxCop 分析器中的性能规则,这些规则是微软为识别 .NET 应用程序中的性能改进而编写的代码分析规则 (CAXXXX),以生成更有效的 MSIL。这些规则已被移植到 Roslyn 分析器框架,并在github.com/dotnet/roslyn-analyzers上开源。
识别 Web 应用程序中的配置相关安全漏洞
ASP.NET 允许您指定影响服务器上所有 Web 应用程序、仅影响单个应用程序、影响单个页面或影响 Web 应用程序中的单个文件夹的配置设置。您可以针对功能(如编译器选项、调试、用户身份验证、错误信息显示、连接字符串等)进行配置设置。配置数据存储在名为 Web.config 的 XML 文件中。
您可以在 msdn.microsoft.com/en-us/library/ff400235.aspx 上阅读有关 Web.config 文件中不同类型配置设置的更多详细信息。
在本节中,我们将向您介绍 PUMA 扫描分析器中的规则,以在 ASP.NET Web Forms 项目中捕获 Web 配置中的安全漏洞。
注意,Roslyn 分析器在 .NET 框架项目和 .NET Core 项目上都得到完全支持,因此本章中提到的 PUMA 扫描分析器在 ASP.NET 和 ASP.Net Core Web 项目上都能正常工作。
准备工作
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的食谱。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何操作...
- 启动 Visual Studio,然后单击文件 | 新建 | 项目... 并使用 Web Forms 模板创建一个新的 Visual C# | Web | ASP.NET Web 应用程序,例如
WebApplication:

-
安装
Puma.Security.Rules分析器 NuGet 包(在撰写本文时,最新稳定版本为 1.0.4)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅第二章,通过 NuGet 包管理器搜索和安装分析器的食谱,在 .NET 项目中消费诊断分析器。 -
在解决方案资源管理器中选择 Web.config,并使用属性窗口将其构建操作从内容更改为 AdditionalFiles,然后保存项目:

- 在编辑器中打开 Web.config,并用以下 XML 替换现有的
system.webXML 元素。您可以在msdn.microsoft.com/en-us/library/dayb112d(v=vs.100).aspx上阅读更多关于system.webXML 元素的信息。
<system.web>
<compilation debug="false" targetFramework="4.6.2" />
<customErrors mode="Off" defaultRedirect="/home/error"/>
<httpRuntime enableHeaderChecking="false" enableVersionHeader="true" />
<httpCookies requireSSL="false" httpOnlyCookies="false"/>
<pages enableEventValidation="false" enableViewStateMac="false" viewStateEncryptionMode="Never" validateRequest="false" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
</authentication>
</system.web>
- 在 Visual Studio 或命令行中构建项目,并验证您是否从 PUMA 扫描分析器获得了以下SECXXXX警告:
1>CSC : warning SEC0014: Insecure HTTP cookies C:WebApplicationWeb.config(11): <httpCookies requireSSL="false" httpOnlyCookies="false" />
1>CSC : warning SEC0015: Cookies accessible via script. C:WebApplicationWeb.config(11): <httpCookies requireSSL="false" httpOnlyCookies="false" />
1>CSC : warning SEC0003: Forms authentication does not set requireSSL to true. C:WebApplicationWeb.config(14): <forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
1>CSC : warning SEC0004: Forms authentication does not set the cookieless attribute to UseCookies. C:WebApplicationWeb.config(14): <forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
1>CSC : warning SEC0006: Forms authentication cookie protection attribute is not set to All. C:WebApplicationWeb.config(14): <forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
1>CSC : warning SEC0007: Forms authentication timeout value exceeds the policy of 30 minutes. C:WebApplicationWeb.config(14): <forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
1>CSC : warning SEC0005: Forms authentication does not set the enableCrossAppRedirects attribute to false. C:WebApplicationWeb.config(14): <forms loginUrl="~/Account/Login.aspx" timeout="900" enableCrossAppRedirects="true" protection="None" />
1>CSC : warning SEC0002: Custom errors are disabled. C:WebApplicationWeb.config(9): <customErrors mode="Off" defaultRedirect="/home/error" />
1>CSC : warning SEC0008: HTTP header checking is disabled. C:WebApplicationWeb.config(10): <httpRuntime enableHeaderChecking="false" enableVersionHeader="true" />
1>CSC : warning SEC0009: The Version HTTP response header is enabled. C:WebApplicationWeb.config(10): <httpRuntime enableHeaderChecking="false" enableVersionHeader="true" />
1>CSC : warning SEC0010: Event validation is disabled. C:WebApplicationWeb.config(12): <pages enableEventValidation="false" enableViewStateMac="false" viewStateEncryptionMode="Never" validateRequest="false" />
1>CSC : warning SEC0012: Validate request is disabled. C:WebApplicationWeb.config(12): <pages enableEventValidation="false" enableViewStateMac="false" viewStateEncryptionMode="Never" validateRequest="false" />
1>CSC : warning SEC0013: Pages ViewStateEncryptionMode disabled. C:WebApplicationWeb.config(12): <pages enableEventValidation="false" enableViewStateMac="false" viewStateEncryptionMode="Never" validateRequest="false" />
1>CSC : warning SEC0011: ViewStateMac is disabled. C:WebApplicationWeb.config(12): <pages enableEventValidation="false" enableViewStateMac="false" viewStateEncryptionMode="Never" validateRequest="false" />
- 将
Web.config文件中的system.webXML 元素替换为以下内容(更改以粗体突出显示):
<system.web>
<compilation debug="false" targetFramework="4.6.2" />
<customErrors mode="On" defaultRedirect="/home/error"/>
<httpRuntime enableHeaderChecking="true" enableVersionHeader="false" />
<httpCookies requireSSL="true" httpOnlyCookies="true"/>
<pages enableEventValidation="true" enableViewStateMac="true" viewStateEncryptionMode="Always" validateRequest="true" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login.aspx" timeout="15" enableCrossAppRedirects="false" protection="All" requireSSL="true" cookieless="UseCookies" />
</authentication>
</system.web>
- 再次构建项目并验证它是否编译且没有任何安全警告。
它是如何工作的...
PUMA 扫描分析器能够捕捉到 C# ASP.NET 网络项目中的网络配置文件中的安全漏洞。在前面的菜谱中,我们向您展示了 PUMA 扫描分析器捕捉到的不同类型的网络安全漏洞,例如不安全的表单身份验证、http cookies 配置、头部设置等。您可以在www.pumascan.com/rules.html#configuration上阅读 PUMA 扫描分析器识别的所有网络配置相关安全漏洞的详细描述。
这些安全分析器被编写为附加文件分析器,用于分析项目中标记为AdditionalFiles项目类型的非源文件。用户必须在他们的项目中将web.config文件标记为附加文件,以在构建期间触发安全分析。您可以在github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md上阅读更多关于如何编写和消费附加文件分析器的信息。
在网络应用程序的视图标记文件(.cshtml, .aspx 文件)中识别跨站脚本漏洞
跨站脚本(XSS)是一种通常在 Web 应用程序中发现的计算机安全漏洞。XSS 允许攻击者向其他用户查看的网页中注入客户端脚本。跨站脚本漏洞可能被攻击者用来绕过如同源策略这样的访问控制。截至 2007 年,Symantec 记录的所有安全漏洞中,大约有 84%是由网站上的跨站脚本造成的。其影响可能从微不足道的麻烦到重大的安全风险不等,这取决于受影响网站处理的数据的敏感性以及网站所有者实施的安全缓解措施的性质。
您可以在en.wikipedia.org/wiki/Cross-site_scripting上阅读更多关于跨站脚本的信息。
在本节中,我们将向您介绍 PUMA 扫描分析器中的规则,以捕获可能导致 ASP.NET Web 项目中跨站脚本攻击的安全漏洞。
准备工作
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何操作...
- 启动 Visual Studio,点击“文件”|“新建”|“项目...”并创建一个新的 Visual C# | Web | ASP.NET Web 应用程序,使用 MVC 模板,例如
WebApplication:

-
安装
Puma.Security.Rules分析器 NuGet 包(在撰写本文时,最新稳定版本为 1.0.4)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅第二章 通过 NuGet 包管理器搜索和安装分析器 中的配方,在 .NET 项目中消费诊断分析器。 -
打开“视图”|
_ViewStart.cshtml文件,并在文件末尾添加以下文本:
<div>
@Html.Raw(string.Format("Welcome <span class=\"bold\">{0}</span>!", ViewContext.ViewBag.UserName))
@{
WriteLiteral(string.Format("Welcome <span class=\"bold\">{0}</span>!", ViewContext.ViewBag.UserName));
}
</div>
-
在解决方案资源管理器中选择
_ViewStart.cshtml,并使用下面的属性窗口将其构建操作从内容更改为 AdditionalFiles,然后保存项目。 -
向项目中添加一个新的 Web 表单,例如
WebForm.aspx,并将以下带有原始内联表达式的 HTML 标题添加到表单中:
<div>
<h2>Welcome <%= Request["UserName"].ToString() %></h2>
</div>
-
在解决方案资源管理器中选择
WebForm.aspx,并使用下面的属性窗口将其构建操作从内容更改为 AdditionalFiles,然后保存项目。 -
在 Visual Studio 或命令行中构建项目,并验证您是否收到来自 PUMA 扫描分析器的以下 SECXXXX 警告:

- 将步骤 3 中添加到
_ViewStart.cshtml的 HTML 分区元素替换为以下内容:
<div>
Welcome <span class=\"bold\">@ViewContext.ViewBag.UserName</span>!
</div>
- 将步骤 5 中添加到
WebForm.aspx的 HTML 分区元素替换为以下内容:
<div>
<h2>Welcome <%: Request["UserName"].ToString() %></h2>
</div>
- 再次构建项目,并验证它是否编译且没有任何安全警告。
它是如何工作的...
PUMA 扫描分析器在 C# ASP.NET Web 项目的视图标记文件(.cshtml、.aspx、.ascx)中捕获跨站脚本安全漏洞。在前面的配方中,我们向您展示了 PUMA 扫描分析器捕获的不同类型的安全漏洞,例如使用原始内联和绑定表达式、原始 razor 辅助程序和原始 WriteLiteral 方法将不受信任的数据源写入 HTML 文档的主体,等等。建议在将此类数据写入浏览器之前对其进行 HTML 编码。您可以在 www.pumascan.com/rules.html#cross-site-scripting 读取 PUMA 扫描分析器识别的所有跨站脚本相关安全漏洞的详细描述。
这些安全分析器被编写为额外的文件分析器,用于分析项目中标记为AdditionalFiles项类型的不源文件。用户必须在他们的项目中将视图标记文件标记为额外文件,以在构建期间触发安全分析。您可以在github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md中了解更多关于如何编写和消费额外文件分析器的信息。
识别可能导致 SQL 和 LDAP 注入攻击的不安全方法调用
SQL 注入是一种代码注入技术,用于攻击数据驱动的应用程序,其中恶意 SQL 语句被插入到输入字段中以执行(例如,将数据库内容导出到攻击者)。SQL 注入攻击允许攻击者伪造身份、篡改现有数据、引发诸如取消交易或更改余额的拒绝问题、允许完全披露系统上的所有数据、破坏数据或使其不可用,并成为数据库服务器的管理员。
LDAP 注入是一种用于利用 Web 应用的代码注入技术,可能会泄露敏感用户信息或修改表示在轻量级目录访问协议(LDAP)数据存储中的信息。LDAP 注入通过操纵传递给内部搜索、添加或修改函数的输入参数来利用应用程序中的安全漏洞。
您可以在en.wikipedia.org/wiki/SQL_injection了解更多关于 SQL 注入的详细信息,以及en.wikipedia.org/wiki/LDAP_injection了解更多关于 LDAP 注入的详细信息。
在本节中,我们将向您介绍 PUMA 扫描分析器中的规则,以捕获可能导致数据驱动.NET 项目中 SQL 注入和 LDAP 注入攻击的安全漏洞。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
启动 Visual Studio,点击文件 | 新建 | 项目...并创建一个新的 Visual C# | 类库,例如
ClassLibrary。 -
安装
Puma.Security.Rules分析器 NuGet 包(在撰写本文时,最新稳定版本为1.0.4)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅第二章中关于通过 NuGet 包管理器搜索和安装分析器的配方,在.NET 项目中消费诊断分析器。 -
将以下框架程序集的引用添加到程序集中:
<q>System.Data.Linq.dll</q>和System.DirectoryServices.dll。 -
将
Class1.cs中的默认Class1实现替换为以下代码:
using System.Data.Linq;
using System.Data.SqlClient;
using System.DirectoryServices;
public class Class1
{
private void SQL_Injection(SqlConnection connection, string id)
{
using (DataContext context = new DataContext(connection))
{
context.ExecuteCommand("SELECT * FROM Items WHERE ID = " + id);
}
SqlCommand cmd = new SqlCommand("SELECT * FROM Items WHERE ID = " + id, connection);
string result = cmd.ExecuteScalar().ToString();
}
private void LDAP_Injection(string domain, string userName)
{
DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://DC={0}, DC=COM/", domain));
DirectorySearcher searcher = new DirectorySearcher(entry)
{
SearchScope = SearchScope.Subtree,
Filter = string.Format("(name={0})", userName)
};
SearchResultCollection resultCollection = searcher.FindAll();
}
}
- 在编辑代码以及调用显式构建时,验证您在错误列表和编辑器中的波浪线中是否获得了以下 SECXXX 诊断信息。

-
通过将
id作为调用context.ExecuteCommand方法的第二个参数传递来修复在SQL_Injection方法中报告的 SEC0106:context.ExecuteCommand("SELECT * FROM Items WHERE ID = {0}", id); -
通过使用
*SqlParameter*参数化传递给*new*SqlCommand(...)的查询来修复 SEC0107:
SqlCommand cmd = new SqlCommand("SELECT * FROM Items WHERE ID = @id", connection);
SqlParameter parm = new SqlParameter("@id", id);
cmd.Parameters.Add(parm);
-
通过使用 Web 保护库(也称为 AntiXSS)的 LDAP 编码方法对域和
userName参数进行编码来修复 SEC0114 诊断。-
将 NuGet 包引用添加到 AntiXSS 库
-
将传递给新
DirectoryEntry(...)的域参数替换为Microsoft.Security.Application.Encoder.LdapDistinguishedNameEncode(domain) -
将初始化器中用于筛选器的
string.Format调用传递给userName参数替换为Microsoft.Security.Application.Encoder.LdapFilterEncode(userName)
-
-
验证错误列表中没有诊断信息,并且项目构建时没有错误或警告。
如何工作...
PUMA 扫描分析器可以捕捉数据驱动应用程序源代码中的 SQL 注入和 LDAP 注入安全漏洞。在前面的配方中,我们向您展示了这些分析器捕捉到的不同类型的漏洞,例如将不受信任的数据与 SQL 查询字符串、SQL 命令、LDAP 目录条目路径和筛选器格式连接起来。
通过使用参数化查询(其中不受信任的数据作为显式格式参数传递)可以防止 SQL 注入攻击。
通过使用 LDAP 编码方法对不受信任的数据进行编码可以防止 LDAP 注入攻击。您可以在 www.pumascan.com/rules.html#injection 阅读 PUMA 扫描分析器识别的所有 SQL 和 LDAP 注入安全漏洞的详细描述。
识别网络应用中的弱密码保护和管理工作
负责密码管理应用程序承担着巨大的风险和责任。用户密码必须具有足够长度/复杂性,安全存储,并保护免受暴力破解和破解尝试。
在本节中,我们将向您介绍 PUMA 扫描分析器中的规则,以捕捉与 ASP.NET 网络项目中弱密码管理相关的漏洞。PUMA 扫描分析器目前支持以下密码管理规则:
-
ASP.NET Identity 弱密码复杂性
-
ASP.NET Identity 缺失密码锁定
您可以在 www.pumascan.com/rules.html#password-management 上阅读有关这些规则的更多详细信息。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装 Visual Studio 2017 的免费社区版本。
如何做到这一点...
- 启动 Visual Studio 并点击 File | New | Project 创建一个新的 Visual C# | Web | ASP.NET Web Application,使用 Web Forms 模板,例如命名为
WebApplication。点击 Change Authentication 按钮并将身份验证更改为 Individual User Accounts:

-
安装
Puma.Security.Rules分析器 NuGet 包(在撰写本文时,最新稳定版本为 1.0.4)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅配方,通过 NuGet 包管理器搜索和安装分析器,位于 第二章,在 .NET 项目中消费诊断分析器。 -
构建项目并验证是否从 PUMA 扫描分析器中获得了一堆 SECXXXX 诊断,包括一些与密码保护相关的诊断(SEC0017 和 SEC0018):
WebApplicationApp_StartIdentityConfig.cs(50,41,57,14): warning SEC0017: Password validator settings do not meet the requirements - Minimum Length (12), Numeric Character (True), Lowercase Character (True), Uppercase Character (True), Special Character (True)
...
WebApplicationAccountLogin.aspx.cs(36,121,36,126): warning SEC0018: Password lockout is disabled. To protect accounts from brute force attacks, set the shouldLockout parameter to true.
- 打开
WebApplicationApp_StartIdentityConfig.cs并将所需的最小密码长度更改为12:
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 12,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
- 打开
WebApplicationAccountLogin.aspx.cs并将shouldLockout参数更改为PasswordSignIn调用为 true:
var result = signinManager.PasswordSignIn(Email.Text, Password.Text, RememberMe.Checked, shouldLockout: true);
- 构建项目并验证没有出现 SEC0017 和 SEC0018 诊断。
识别来自外部组件的数据弱验证以防止跨站请求伪造和路径篡改等攻击
在本节中,我们将向您介绍 PUMA 扫描分析器中的规则,以捕获由于输入验证不足可能导致以下类型安全攻击的安全漏洞:
-
跨站请求伪造 (
en.wikipedia.org/wiki/Cross-site_request_forgery): 跨站请求伪造,也称为一键攻击或会话劫持,缩写为 CSRF 或 XSRF,是一种恶意利用网站的攻击方式,其中未经授权的命令从应用程序信任的用户那里传输。与利用用户对特定网站的信任的跨站脚本(XSS)不同,CSRF 利用网站对用户浏览器的信任。 -
路径篡改 (
en.wikipedia.org/wiki/Directory_traversal_attack): 目录遍历(或路径遍历)是通过利用对用户提供的输入文件名的不充分安全验证/清理,使得表示遍历到父目录的字符能够通过文件 API。此攻击的目的是使用受影响的应用程序来获取对文件系统的未授权访问 -
未经验证的跳转 (
www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet): 当一个网络应用程序接受可能导致应用程序将请求重定向到包含在不受信任输入中的 URL 的不受信任输入时,可能发生未经验证的跳转和转发。通过将不受信任的 URL 输入修改为恶意网站,攻击者可能成功发起钓鱼诈骗并窃取用户凭据
准备工作
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装 Visual Studio 2017 的免费社区版本。
如何操作...
-
启动 Visual Studio,点击文件 | 新建 | 项目...并使用 MVC 模板创建一个新的 Visual C# | Web | ASP.NET Web 应用程序,例如
WebApplication。 -
安装
Puma.Security.Rules分析器 NuGet 包(在撰写本文时,最新稳定版本为1.0.4)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅第二章,在.NET 项目中使用诊断分析器中的配方,通过 NuGet 包管理器搜索和安装分析器。 -
将新类
Class1添加到项目中,并用以下代码替换文件内容:
using System.Configuration;
using System.Net.Http;
using System.Web.Mvc;
namespace WebApplication
{
public class Class1
{
[AllowHtml]
public string AllowHtmlProperty { get; set; }
[HttpPost]
[ValidateInput(false)]
public ActionResult Missing_AntiForgeryToken()
{
return null;
}
[HttpPost]
public FileResult Path_Tampering(string fileName)
{
string filePath = ConfigurationManager.AppSettings["DownloadDirectory"].ToString();
return new FilePathResult(filePath + fileName, "application/octet-stream");
}
private void Certificate_Validation_Disabled()
{
using (var handler = new WebRequestHandler())
{
handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
}
}
}
}
- 在编辑代码和调用显式构建时,验证错误列表和编辑器中的波浪线,以获取以下SECXXX诊断信息。

-
通过将
[ValidateAntiForgeryToken]属性应用于Missing_AntiForgeryToken和Path_Tampering方法来修复前两个SEC0019诊断。 -
通过删除
AllowHtmlProperty上的[AllowHtml]属性来修复SEC0022错误。 -
通过删除
Missing_AntiForgeryToken上的[ValidateInput(false)]属性来修复SEC0023错误。 -
通过在
Path_Tampering方法中将return new FilePathResult(...)替换为return new ValidatedFileResult(...)来修复SEC01111错误,并添加以下ValidatedFileResult类型。
private class ValidatedFileResult : FileResult
{
public ValidatedFileResult(string filePath, string fileName, string contentType)
: base(contentType)
{
// Add validation logic.
}
protected override void WriteFile(HttpResponseBase response)
{
// Add write logic
}
}
-
通过删除行
handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;来修复SEC0113错误。 -
再次构建项目并验证它是否编译且没有任何安全警告。
您可以在www.pumascan.com/rules.html#validation上阅读有关 PUMA 扫描验证规则的更多详细信息。
使用 FxCop 分析器识别源代码的性能改进
在本节中,我们将向您介绍一个流行的第三方分析器包,用于 C#项目,即 FxCop 分析器。
我们将向您展示如何安装 FxCop 分析器 NuGet 包,并给出不同性能规则的违规示例,并展示如何使用 NuGet 包中分析器附带代码修复自动修复这些问题。
准备工作
您需要在您的机器上安装 Visual Studio 2017 才能执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装 Visual Studio 2017 的免费社区版本。
如何做到这一点...
-
启动 Visual Studio,点击文件 | 新建 | 项目... 创建一个新的 C#类库项目,并将
Class1.cs中的代码替换为ClassLibrary\Class1.cs中的代码样本。 -
安装
Microsoft.CodeAnalysis.FxCopAnalyzersNuGet 包(写作时的最新预发布版本是1.2.0-beta2)。有关如何在项目中搜索和安装分析器 NuGet 包的指导,请参阅第二章,在.NET 项目中使用诊断分析器中的配方,通过 NuGet 包管理器搜索和安装分析器。 -
通过在解决方案资源管理器中右键单击
ClassLibrary1来卸载项目文件,然后通过在解决方案资源管理器中右键单击未加载的项目来在 Visual Studio 中打开项目文件进行编辑。 -
将以下
PropertyGroup添加到项目中以启用 FxCop 分析器使用的新的 RoslynIOperation功能:
<PropertyGroup>
<Features>IOperation</Features>
</PropertyGroup>
- 重新加载项目并验证以下 FxCop 诊断显示在错误列表中:

-
从命令行或 Visual Studio 的顶级构建菜单中构建项目,并验证这些诊断是否也来自构建。
-
双击警告CA1815(ValueType 应该重写 Equals)并验证编辑器中是否提供了灯泡以实现 equals、
GetHashCode和==以及!=运算符方法的重写:

-
通过按Enter键应用代码修复来验证是否修复了CA1815诊断。请注意,这引入了新的CAXXXX诊断,这是由于重写默认实现。
-
将
Class1.cs的内容替换为以下代码并验证所有CAXXXX诊断都已修复:
using System;
namespace Namespace
{
public class Class1: IDisposable
{
private static int staticField = 3;
private int[][] jaggedArray;
public void Method1(int usedParam)
{
Console.WriteLine(usedParam);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Dispose resources.
}
}
}
public struct ValueType: IEquatable<ValueType>
{
public int Property { get; }
public override bool Equals(object obj)
{
return Equals((ValueType)obj);
}
public bool Equals(ValueType other)
{
return other.Property == Property;
}
public override int GetHashCode()
{
return Property.GetHashCode();
}
public static bool operator ==(ValueType left, ValueType right)
{
return left.Property == right.Property;
}
public static bool operator !=(ValueType left, ValueType right)
{
return left.Property != right.Property;
}
}
}
它是如何工作的...
FxCop 分析器是将最重要的 Microsoft 代码分析规则(CAXXXX)移植而来,这些规则是以 MSIL 为基础的二进制分析。与构建后二进制分析相比,FxCop 分析器在编辑代码时增加了实时分析和诊断的优势,以及丰富的代码修复功能来解决这些问题。
FxCop 包含各种不同类别的规则,例如性能、安全、代码风格、API 设计、可维护性等。在本节所涵盖的示例中,我们关注以下性能规则:
-
CA1801(审查未使用参数)(
msdn.microsoft.com/en-us/library/ms182268.aspx):方法签名中包含一个在方法体中未使用的参数。 -
CA1810(内联初始化引用类型静态字段)(
msdn.microsoft.com/en-us/library/ms182275.aspx):当一个类型声明了显式的静态构造函数时,即时编译器(JIT)会对类型的每个静态方法和实例构造函数添加检查,以确保静态构造函数之前已被调用。静态构造函数检查可能会降低性能。 -
CA1814(优先使用交错数组而非多维数组)(
msdn.microsoft.com/en-us/library/ms182277.aspx):交错数组是一个元素为数组的数组。组成元素的数组可以有不同的大小,这可能导致某些数据集的浪费空间更少。 -
CA1815(在值类型上重写等于和运算符等于)(
msdn.microsoft.com/en-us/library/ms182276.aspx):对于值类型,继承的等于实现使用反射库并比较所有字段的值。反射计算成本高昂,并且比较每个字段以检查相等性可能是不必要的。如果你期望用户比较或排序实例,或者将实例用作哈希表键,则你的值类型应该实现等于。 -
CA1816(正确调用
GC.SuppressFinalize)(msdn.microsoft.com/en-us/library/ms182269.aspx):实现Dispose的方法没有调用GC.SuppressFinalize,或者不是Dispose实现的方法调用了GC.SuppressFinalize,或者方法调用了GC.SuppressFinalize但传递了其他内容。 -
CA1821(移除空终结器)(
msdn.microsoft.com/en-us/library/bb264476.aspx):在可能的情况下,避免使用终结器,因为跟踪对象生命周期的额外性能开销。空终结器会带来额外的开销,但没有任何好处。
您可以在 msdn.microsoft.com/en-us/library/ms182260(v=vs.140).aspx 阅读有关所有 FxCop 性能规则的详细文档。
注意,尽管大多数 Microsoft 代码分析性能规则已移植到 FxCop 分析器包中,但并非所有规则在 NuGet 包中默认启用。您可以通过规则集编辑器查看和配置每个 FxCop 规则的抑制状态和严重性。有关使用规则集编辑器的进一步指导,请参阅第二章 在 .NET 项目中消费诊断分析器 中的配方 使用规则集文件和规则集编辑器配置分析器,第二章。
第六章:Visual Studio Enterprise 中的实时单元测试
在本章中,我们将涵盖以下食谱:
-
在 Visual Studio 中运行基于 NUnit、XUnit 和 MSTest 框架的单元测试项目的实时单元测试(LUT)
-
查看和导航实时单元测试结果
-
理解代码更改的增量实时单元测试执行
-
理解 LUT 的启动/停止/暂停/继续/重启功能,以实现细粒度控制
-
包括和排除实时执行测试的子集
-
使用“工具 | 选项”对话框配置实时单元测试的不同选项
简介
本章使开发者能够使用 Visual Studio 2017 Enterprise 版本中基于 Roslyn 的新功能,在后台实现智能实时单元测试执行。以下是从该 (blogs.msdn.microsoft.com/visualstudio/2016/11/18/live-unit-testing-visual-studio-2017-rc/) Visual Studio 博客文章中摘录的片段和截图,关于 LUT 的功能有很好的概述。
实时单元测试在您编辑代码时自动在后台运行受影响的单元测试,并实时可视化结果和代码覆盖率,在编辑器中实时显示。除了对您更改对现有测试的影响提供反馈外,您还可以立即获得关于您添加的新代码是否已被一个或多个现有测试覆盖的反馈。这将温和地提醒您在修复错误或添加功能时编写单元测试。您将朝着没有代码库测试债务的应许之地迈进!

如文章中所述,任何给定行的三种潜在状态如下:
-
至少被一个失败的测试覆盖的可执行代码行用红色十字标记装饰 (
) -
只被通过测试覆盖的可执行代码行用绿色勾号装饰 (
) -
没有任何测试覆盖的可执行代码行用蓝色破折号装饰 (
)
LUT 使用 Roslyn API 分析您的产品和测试代码的快照,并确定需要运行的项目单元测试集合。此外,它还使用 Roslyn API 分析代码的增量更新,以智能地确定需要重新运行先前测试运行中的单元测试子集。这些分析 API 与 Visual Studio IDE 诊断引擎使用的相同,用于增量更新错误列表中的智能感知/实时诊断和编辑器中的波浪线。
一旦确定了要执行的单元测试集,它将在后台安排它们的执行,并在测试完成时,自动显示它们的通过/失败/排除状态,并在测试方法上显示相应的符号。用户可以在任何给定时间开始/停止/暂停/恢复实时测试执行。此外,他们还可以为 LUT 排除/包含测试/文件/项目子集。他们还可以随时暂停/重启/停止 LUT,并为 LUT 配置不同的选项,例如在低电量时自动暂停、测试执行超时等。
在 Visual Studio 中运行基于 NUnit、XUnit 和 MSTest 框架的单元测试项目。
在本节中,我们将向您介绍如何为您的单元测试项目启用 LUT,查看和理解测试执行中的实时结果。在 VS2017 中,实时单元测试支持以下单元测试项目的测试项目:
-
NUnit:文档位于
www.nunit.org/ -
XUnit:文档位于
xunit.github.io/ -
MSTest:文档位于 (
en.wikipedia.org/wiki/MSTest) 和 (msdn.microsoft.com/en-us/library/ms182489.aspx)
我们将介绍基于前面提到的每个测试框架的单元测试项目中的 LUT(Live Unit Testing)。
入门
您需要在您的机器上安装 Visual Studio 2017 企业版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15 安装授权的企业版。
如何操作
- 打开 Visual Studio 并创建一个新的 C# 类库项目,例如
ClassLibrary,以下为相应的源代码:
namespace ClassLibrary
{
public class Class1
{
public bool Method1()
{
return true;
}
public bool Method2()
{
return false;
}
public bool Method3(Class1 c)
{
return c.Method1();
}
public bool Method4(Class1 c)
{
return c.Method2();
}
}
}
-
[NUnit] 将一个 C# 单元测试项目,例如
NUnitBasedTestProject,添加到解决方案中,并将ClassLibrary的引用添加到该项目中。 -
打开项目的 NuGet 包管理器并卸载现有的 NuGet 包引用
MSTest.TestAdapter和MSTest.TestFramework:

-
将 NUnit 和 NUnit3TestAdapter 的最新稳定版本添加到项目中。
-
将文件
UnitTest1.cs中的源代码替换为以下代码:
using NUnit.Framework;
namespace NUnitBasedTestProject
{
[TestFixture]
public class UnitTest1
{
[Test]
public void TestMethod1()
{
var c = new ClassLibrary.Class1();
Assert.True(c.Method1());
}
[Test]
public void TestMethod2()
{
var c = new ClassLibrary.Class1();
Assert.True(c.Method2());
}
}
}
-
通过执行 Test | Live Unit Testing | Start 命令开始对项目进行实时单元测试。
-
等待几秒钟,并注意添加的单元测试在后台执行,
TestMethod1按预期通过,TestMethod2失败,相应的绿色和红色符号在编辑器中显示。同时,验证输出窗口切换到实时单元测试视图,并显示带有执行时间戳的测试执行日志:

-
[XUnit] 将 C# 单元测试项目,例如
XUnitBasedTestProject,添加到解决方案中,并将ClassLibrary的引用添加到该项目中。 -
打开项目的 NuGet 包管理器,并卸载现有的 NuGet 包引用
MSTest.TestAdapter和MSTest.TestFramework。 -
将 NuGet 包引用添加到 XUnit 和
xunit.runner.visualstudio的最新稳定版本(晚于 2.2.0)。 -
将
UnitTest1.cs文件中的源代码替换为以下源代码:
using Xunit;
namespace XUnitBasedTestProject
{
public class UnitTest1
{
[Fact]
public void TestMethod1()
{
var c = new ClassLibrary.Class1();
Assert.True(c.Method1());
}
[Fact]
public void TestMethod2()
{
var c = new ClassLibrary.Class1();
Assert.True(c.Method2());
}
}
}
-
等待几秒钟,并注意单元测试在后台执行,
TestMethod1通过而TestMethod2失败。验证这些测试在编辑器中分别显示绿色和红色图标。 -
[MSTest] 将 C# 单元测试项目,例如
MSTestBasedTestProject,添加到解决方案中,并将ClassLibrary的引用添加到该项目中。 -
打开项目的 NuGet 包管理器,并将现有的 NuGet 包引用
MSTest.TestAdapter和MSTest.TestFramework更新到最新稳定版本(晚于1.1.17
)。 -
将文件
UnitTest1.cs中的源代码替换为以下源代码:
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MSTestBasedTestProject
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var c = new ClassLibrary.Class1();
Assert.IsTrue(c.Method1());
}
[TestMethod]
public void TestMethod2()
{
var c = new ClassLibrary.Class1();
Assert.IsTrue(c.Method2());
}
}
}
-
等待几秒钟,并注意单元测试在后台执行,
TestMethod1通过而TestMethod2失败。验证这些测试在编辑器中分别显示绿色和红色图标。 -
在
ClassLibrary项目中打开Class1.cs文件,并在编辑器中验证每个方法的测试覆盖率以及通过/失败详情。

查看和导航实时单元测试结果
在本节中,我们将向您展示如何使用测试资源管理器和 Visual Studio 编辑器中的工具提示查看和导航实时测试执行的结果。
入门指南
您需要在您的机器上安装 Visual Studio 2017 企业版才能执行此操作。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15 安装授权的企业版。
如何操作...
-
打开 Visual Studio 并创建一个新的 C# 类库项目,例如
ClassLibrary。 -
将源文件
Class1.cs中的现有代码替换为附带的示例ClassLibrary\Class1.cs中的代码。 -
将 C# 单元测试项目,例如
UnitTestProject,添加到解决方案中,并将ClassLibrary的引用添加到该项目中。 -
打开项目的 NuGet 包管理器,并将现有的 NuGet 包引用
MSTest.TestAdapter和MSTest.TestFramework更新到最新稳定版本(晚于1.1.17
)。 -
将
UnitTest1.cs文件中的源代码替换为附带的示例UnitTestProject\UnitTest1.cs中的代码。 -
通过点击 Test | Window | Test Explorer 打开测试资源管理器窗口。
-
通过执行 Test | Live Unit Testing | Start 命令来为项目启动实时单元测试。
-
等待几秒钟,并注意添加的单元测试在后台执行,
TestMethod1按预期通过,而TestMethod2失败。 -
确认测试结果在测试资源管理器中显示,并且在编辑器中有相应的绿色和红色图标:

- 在
ClassLibrary中打开源文件Class1.cs并点击Method1顶部的测试指示器,其显示为1/1 passing。验证是否弹出一个包含测试名称和状态的工具栏,TestMethod1。

-
双击工具栏中的方法名称
TestMethod1并确保导航到UnitTest1.cs中此方法的定义。 -
切换回
Class1.cs并在Method1附近的绿色勾号 (
) 附近悬停,查看该方法被 1 个测试覆盖:

- 点击勾号 (
) 以弹出另一个包含方法名称的工具栏,并验证双击它是否可以带您到测试方法定义。
理解代码更改的增量实时单元测试执行
在本节中,我们将向您展示如何实时单元测试在配置为运行实时单元测试的解决方案中对测试和产品代码进行更改时增量运行。
入门
您需要在您的机器上安装 Visual Studio 2017 企业版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15. 安装授权的企业版。
此外,克隆本章中“查看和导航实时单元测试结果”配方中附加的解决方案 ClassLibrary.sln,或者您可以在执行此配方之前手动执行该配方中的步骤。
如何操作...
-
使用两个项目:
ClassLibrary和UnitTestProject打开ClassLibrary.sln解决方案,并通过导航到测试 | 实时单元测试 | 开始来启动实时单元测试。 -
将包含以下代码的新源文件
Class2.cs添加到ClassLibrary项目中:
namespace ClassLibrary
{
public class Class2
{
public bool Method5()
{
return false;
}
public bool Method6()
{
return true;
}
}
}
- 打开输出窗口,将“显示输出从:”组合框切换到实时单元测试,并通过按高亮按钮清除窗口中的所有内容:

- 将包含以下代码的新源文件
UnitTest2.cs添加到UnitTestProject项目中:
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MSTestBasedTestProject
{
[TestClass]
public class UnitTest2
{
[TestMethod]
public void TestMethod5()
{
var c = new ClassLibrary.Class2();
Assert.IsTrue(c.Method5());
}
[TestMethod]
public void TestMethod6()
{
var c = new ClassLibrary.Class2();
Assert.IsTrue(c.Method6());
}
}
}
-
等待几秒钟,并注意添加的单元测试在后台执行,
TestMethod5按预期通过,而TestMethod6失败。 -
此外,请注意输出窗口中的文本表示只执行了两个单元测试(新添加的
TestMethod5和TestMethod6)。此外,测试资源管理器显示旧测试TestMethod1和TestMethod2为灰色,因为这些测试在我们添加新的测试代码时没有执行:

-
切换回
ClassLibrary项目中的Class1.cs文件并编辑Method1以返回true。 -
等待几秒钟,让测试在后台执行,并查看
TestMethod1现在显示为通过。 -
注意,在测试资源管理器中,
TestMethod5和TestMxethod6现在已变为灰色,表示它们在上次代码更改后没有执行:

工作原理
在本食谱中,我们向您展示了如何设计实时单元测试执行来分析增量产品和测试更改,并且只执行单元测试项目中的测试子集,这些测试可能会受到这些更改的语义影响。如本章引言部分所述,LUT 使用 Roslyn API 分析您先前测试运行中的增量代码更新。这些是与 Visual Studio IDE 诊断引擎用于增量更新错误列表中的智能感知/实时诊断和编辑器中的波浪线相同的分析 API。
在本食谱中,我们从 ClassLibrary 项目中的单个类 Class1* 和 UnitTestProject 中的单个单元测试类 UnitTest1* 开始。UnitTest1 包含两个方法 TestMethod1 和 TestMethod2,分别测试 Class1 中的 Method1 和 Method2 方法。我们在 ClassLibrary 项目中添加了一个新类 Class2,其中包含 Method5 和 Method6 方法。然后,我们在 UnitTestProject 中添加了一个新的单元测试类 UnitTest2,包含 TestMethod5 和 TestMethod6 方法,分别测试 Method5 和 Method6 方法。在添加这些方法后,LUT 确定类型 UnitTest1 中的现有测试不受新添加的 Class2 和 UnitTest2 的影响,因此没有重新执行它们。随后,当我们编辑 Class1.Method1 时,LUT 只重新执行了 UnitTest1.TestMethod1 和 UnitTest1.TestMethod2,而没有重新执行 UnitTest2 中的测试方法。
理解启动/停止/暂停/继续/重启功能以对 LUT 进行细粒度控制
在本节中,我们将向您展示如何使用启动、停止、暂停、继续和重启命令在 Visual Studio 中控制实时单元测试执行。
入门指南
您需要在您的机器上安装 Visual Studio 2017 企业版才能执行此食谱。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15 安装授权的企业版。
此外,克隆本章节中附带的解决方案ClassLibrary.sln,查看和导航实时单元测试结果,在执行此配方之前。或者,您可以在执行此配方之前手动执行该配方中的步骤。
如何操作...
-
打开包含两个项目:
ClassLibrary和UnitTestProject的解决方案ClassLibrary.sln,通过点击测试 | 实时单元测试 | 开始来启动实时单元测试。同时,通过点击测试 | 窗口 | 测试资源管理器来打开测试资源管理器窗口。 -
将
Class1.Method1改为返回 true 而不是 false。 -
等待几秒钟,并注意单元测试在后台执行,
TestMethod1和TestMethod2都通过了:

- 点击测试 | 实时单元测试 | 暂停以暂时暂停 LUT 执行。注意,一旦暂停 LUT,编辑器中的绿色勾选标记(
)就会消失:

- 将
Class1.Method1改为再次返回 false。这应该会导致在重新执行时TestMethod1失败,但请注意,测试在测试资源管理器窗口中仍然显示为通过,因为测试不是实时运行的:

- 导航到测试 | 实时单元测试 | 继续以恢复 LUT,并注意
TestMethod1立即执行,现在在测试资源管理器和编辑器的图标中显示为失败:

-
点击测试 | 实时单元测试 | 重新启动,并注意编辑器和测试资源管理器窗口中的所有测试结果都会暂时被清除。
-
注意输出窗口的实时单元测试面板显示的消息:构建完成(成功),表示项目已重新构建并重新执行了所有测试。
-
导航到测试 | 实时单元测试 | 停止,并注意编辑器和测试资源管理器窗口中的所有测试结果都会永久清除:

- 注意输出窗口的实时单元测试面板显示的消息:实时单元测试停止
。
,确认 LUT 执行已停止。
包含和排除实时执行中的子集测试
在本节中,我们将向您展示如何选择性地包含和/或排除实时单元测试执行中的子集测试。这个功能对于提高非常大的解决方案的响应速度非常有帮助,因为在构建整个解决方案然后执行所有单元测试可能会很耗时且资源密集。
入门
您需要在您的机器上安装 Visual Studio 2017 企业版才能执行此配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15安装授权的企业版。
如何操作...
- 打开 Visual Studio 2017 并创建一个包含 10 个
ClassLibrary项目(例如ClassLibrary、ClassLibrary1、...、ClassLibrary9)和一个单元测试项目UnitTestProject的 C# 解决方案:

-
在
UnitTestProject中将项目引用添加到所有类库项目。 -
在
UnitTestProject中添加一个新的测试类UnitTest2并将测试方法重命名为TestMethod2.。 -
导航到 Test | Live Unit Testing | Start 并注意以下对话框提示解决方案很大,如果包含测试子集,则响应速度会提高:

-
点击 No 以确保没有测试被包含在实时执行中。
-
验证
UnitTestProject中测试方法前的蓝色破折号 (
),确认已从实时执行中排除单元测试:

- 在编辑器中右键单击类
UnitTest1并执行 Live Tests | Include 以包含此类中的单元测试到 LUT 中。

-
验证
UnitTest1.TestMethod1在 LUT 下立即执行并显示为通过,但UnitTest2.TestMethod2没有执行。 -
再次在编辑器中右键单击类
UnitTest1并执行 Live Tests | Exclude 以排除此类中的单元测试到 LUT。 -
通过按 Enter 键编辑方法
UnitTest1.TestMethod1并验证测试现在已从 LUT 中排除,并且测试结果也已从编辑器和测试资源管理器窗口中清除:

您可以通过在解决方案资源管理器中右键单击项目节点并单击 Live Tests | Include/Exclude 来包含/排除单元测试项目中的所有测试。
使用工具选项对话框配置实时单元测试的不同选项
在本节中,我们将向您展示如何配置 LUT 执行选项,例如在解决方案加载时启动 LUT、配置保持 LUT 启用所需的最低电池百分比以节省电池电量,等等。这使用户能够控制何时自动启动/暂停 LUT,并控制日志记录级别以满足他们的需求。
入门
您需要在您的机器上安装 Visual Studio 2017 Enterprise 版本才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Enterprise&rel=15 安装授权的企业版。
此外,克隆本章中附带的 ClassLibrary.sln 解决方案,查看和导航实时单元测试结果,然后继续此配方。或者,您可以在继续此配方之前手动执行该配方中的步骤。
如何操作...
-
使用包含两个项目,
ClassLibrary和UnitTestProject,的解决方案ClassLibrary.sln,并通过导航到测试 | 实时单元测试 | 开始来启动实时单元测试。同时,通过点击测试 | 窗口 | 测试资源管理器来打开测试资源管理器窗口。 -
点击工具 | 选项,并在搜索栏中搜索
Live Unit Testing,然后点击常规选项卡以查看 LUT 配置选项:

-
在解决方案加载时检查启动实时单元测试,并在对话框中点击确定。
-
通过导航到测试 | 实时单元测试 | 停止来停止解决方案的实时单元测试。
-
关闭并重新打开解决方案,并验证在解决方案加载完成后所有单元测试都会自动执行。
-
再次使用工具 | 选项打开 LUT 配置选项,并将暂停 LUT 的最低电池百分比从 30% 更改为 100%
-
从笔记本电脑断开电源线,并验证 LUT 会立即暂停,并且编辑任何测试方法都不会导致测试以 LUT 重新执行。
-
连接笔记本电脑电源线,并再次验证单元测试;在后台以 LUT 执行。
第七章:C# 交互式和脚本
在本章中,我们将涵盖以下配方:
-
在 Visual Studio 交互式窗口中编写简单的 C# 脚本并评估它
-
在 C# 交互式窗口中使用脚本指令和 REPL 命令
-
在 C# 交互式窗口中使用键盘快捷键评估和导航脚本会话
-
从现有的 C# 项目初始化 C# 交互式会话
-
使用
csi.exe在 Visual Studio 开发者命令提示符中执行 C# 脚本 -
使用 Roslyn 脚本 API 执行 C# 代码片段
简介
本章介绍了基于 Roslyn 编译器 API 的最强大功能/工具之一:C# 交互式和脚本。您可以在 msdn.microsoft.com/en-us/magazine/mt614271.aspx 阅读有关 C# 脚本的概述。以下是前述文章中关于此功能的一个小示例:
C# 脚本是一个用于测试您的 C# 和 .NET 代码片段的工具,无需创建多个单元测试或控制台项目。它提供了一种简单的方法来探索和理解 API,而无需在您的 %TEMP% 目录中创建另一个 CSPROJ 文件。C# 读取-评估-打印循环 (REPL) 作为 Visual Studio 2015 及以后版本中的交互式窗口以及称为 CSI 的新命令行界面 (CLI) 提供。
这是 Visual Studio 中 C# 交互式窗口的截图:

以下是从 Visual Studio 2017 开发者命令提示符中执行的 C# 交互式命令行界面 (csi.exe) 的截图:

在 Visual Studio 交互式窗口中编写简单的 C# 脚本并评估它
在本节中,我们将向您介绍 C# 脚本的基础知识,并展示如何使用 Visual Studio 交互式窗口来评估 C# 脚本。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
- 打开 Visual Studio 并通过点击视图 | 其他窗口 | C# 交互式窗口来启动 C# 交互式窗口:

-
在交互式窗口中输入
Console.WriteLine("Hello, World!")并按 Enter 键来评估 C# 表达式。 -
确认
Hello, World!在交互式窗口中作为结果输出:

-
现在,输入一个类型为
List<int>的变量声明语句,并使用集合初始化器:var myList = new List<int> { 3, 2, 7, 4, 9, 0 };并按 Enter 键。 -
接下来,输入一个表达式语句,该语句访问先前语句中声明的
myList变量,并使用 linq 表达式过滤列表中的所有偶数:myList.Where(x => x % 2 == 0)。 -
按下 Enter 键来评估表达式并验证评估结果输出为一个格式良好的可枚举列表:
Enumerable.WhereListIterator<int> { 2, 4, 0 }. -
输入命令
$"The current directory is { Environment.CurrentDirectory }."。这访问当前目录环境变量并验证当前目录输出。 -
现在,将以下命令输入到交互窗口中,并验证按下 Enter 键会导致根据输入的
await表达式出现 10 秒的 UI 延迟:
> using System.Threading.Tasks;
> await Task.Delay(10000)
>
- 在交互窗口中输入以下类声明并按下 Enter 键:
class C
{
public void M()
{
Console.WriteLine("C.M invoked");
}
}
-
实例化之前声明的类型
C并通过评估以下语句在实例上调用方法M:new C().M();. -
验证交互窗口中的输出为:
C.M invoked。
您可以在附带的文本文件 InteractiveWindowOutput.txt 中查看本食谱的整个交互窗口内容。
它是如何工作的...
在本食谱中,我们编写了一组简单的 C# 交互脚本命令,以执行在常规 C# 代码中常见的多项操作,但无需声明存根类型/主方法或创建源文件/项目。交互会话期间执行的操作包括:
-
评估一个输出字符串到控制台的表达式 (
Console.WriteLine(...)). -
声明一个在交互会话期间存在的局部变量,并用集合
initializer (myList)初始化它。 -
在后续的 linq 语句中访问先前声明的变量并评估结果值 (
myList.Where(...)). -
在 C# 表达式评估中访问环境变量 (
Environment.CurrentDirectory). -
通过使用声明在会话中导入命名空间 (
using System.Threading.Tasks;). -
等待异步表达式 (
await Task.Delay(10000)). -
在交互会话期间声明一个具有方法的 C# 类 (
class C和method M)。 -
在后续语句中实例化先前声明的类并调用方法 (
new C().M()).
让我们简要回顾一下 C# 交互编译器的实现,它使得在交互模式下执行所有前面的常规 C# 操作成为可能。
Csi.main (source.roslyn.io/#csi/Csi.cs,14) 是 C# 交互编译器的入口点。在编译器初始化后,控制最终会到达 CommandLineRunner.RunInteractiveLoop (source.roslyn.io/#Microsoft.CodeAnalysis.Scripting/Hosting/CommandLine/CommandLineRunner.cs,7c8c5cedadd34d79),即 REPL,或读取-评估-打印循环,它读取交互命令并在循环中评估它们,直到用户通过按 Ctrl + C 退出。
对于每条输入的行,REPL 循环会执行 ScriptCompiler.ParseSubmission (source.roslyn.io/#Microsoft.CodeAnalysis.Scripting/ScriptCompiler.cs,54b12302e519f660) 来将给定的源文本解析成一个语法树。如果提交不完整(例如,如果类声明的第一行已经输入),则输出 . 并继续等待更多文本以完成提交。否则,它使用当前提交文本连接到先前的提交并运行新的提交,通过调用核心 C# 编译器 API。提交的结果输出到交互窗口。
关于提交如何链接到先前的提交并在交互编译器中执行的更多详细信息超出了本章的范围。您可以通过 (source.roslyn.io/#q=RunSubmissionsAsync) 导航到脚本编译器的代码库以了解其内部工作原理。
在 C# 交互窗口中使用脚本指令和 REPL 命令
在本节中,我们将向您介绍 C# 交互脚本中可用的常见指令和 REPL 命令,并展示如何在 Visual Studio 交互窗口中使用它们。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此菜谱。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
-
打开 Visual Studio 并通过点击视图 | 其他窗口 | C# 交互来启动 C# 交互窗口。
-
将附带的示例菜谱中的
Newtonsoft.Json.dll复制到您的临时目录%TEMP%。 -
执行以下
#r指令以将此程序集加载到交互会话中:
#r "<%YOUR_TEMP_DIRECTORY%>\Newtonsoft.Json.dll"
- 验证您现在可以引用此程序集中的类型以及创建对象和调用方法。例如,将以下代码片段输入到交互窗口中:
using Newtonsoft.Json.Linq;
JArray array = new JArray();
array.Add("Manual text");
array.Add(new DateTime(2000, 5, 23));
JObject o = new JObject();
o["MyArray"] = array;
o.ToString()
- 验证输出到交互窗口的数组字符串表示:

-
执行 REPL 命令
#clear(或#cls),并验证这清除了交互窗口中的所有文本。 -
将附带的示例菜谱中的
MyScript.csx复制到您的临时目录%TEMP%。 -
执行以下
#load指令以在交互会话中加载并执行此脚本:
#load "<%YOUR_TEMP_DIRECTORY%>\MyScript.csx"
- 验证脚本是否执行,并从执行中获得以下输出:

-
执行
#resetREPL 命令以重置交互会话。 -
现在,尝试在交互会话中引用之前重置之前添加的
Newtonsoft.Json命名空间,并验证您是否得到错误,因为程序集已不再在会话中加载:

- 最后,执行
#helpREPL 命令以打印交互窗口中可用的键盘快捷键、指令和 REPL 命令的帮助文本:

您可以在附带的文本文件InteractiveWindowOutput.txt中查看此菜谱的交互窗口的完整内容。
在 C#交互窗口中使用键盘快捷键评估和导航脚本会话
在本节中,我们将向您介绍 C#交互脚本中常见的键盘快捷键,并展示如何在 Visual Studio 交互窗口中使用它们。
如前一道菜谱的最后一步所示,您可以在交互窗口中使用#help REPL 命令查看 C#交互窗口中可用的所有键盘快捷键的完整列表。
开始使用
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此菜谱。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15.安装免费的社区版。
如何操作...
-
打开 Visual Studio,通过点击视图|其他窗口|C#交互窗口.打开 C#交互窗口。
-
输入字符串常量
"World!"并按Enter键以评估和输出字符串。 -
输入
"Hello, "并将光标移至步骤 2 中的上一个提交,然后按Ctrl + Enter键将上一个提交的文本附加到当前提交。当前提交的文本应更改为"Hello, " + "World!",按Enter键应输出文本"Hello, World!"。 -
输入
@"Hello, World并按Shift + Enter键在当前提交中添加新行。在下一行输入with a new line!"并按Enter键,应输出文本"Hello, World\r\nwith a new line!",如下所示:

-
输入
Hello并按Esc键;这应该清除当前行的文本。 -
同时按下 Alt + 向上箭头键;这应该将当前提交的文本更改为与上一个提交相同,在我们的例子中,是
@"Hello, World. 在新的一行!". -
按下 Enter 键再次输出
"Hello, World\r\nwith a new line!"。 -
按下引号键 ". 这应该自动添加另一个引号。按 Delete 键删除这个自动添加的引号并添加第二个引号。
-
现在,同时按下 Ctrl + Alt + 向上箭头键;这应该将当前提交的文本更改为与之前提交中相同字符的最后一个提交相同,即 ". 在我们的例子中,这是提交
"Hello, " + "World!". -
按下 Enter 键输出
"Hello, World!". -
现在,将光标放在会话中的第一个提交上,即步骤 2 中的提交。
-
同时按下 Ctrl + A 键以选择第一个提交中的全部文本,即
"World!"。然后,同时按下 Ctrl + Enter 键以将此文本复制到当前提交。 -
按下 Enter 键输出
"World!". -
将光标移回之前的提交,并连续两次按下 Ctrl + A 键以选择交互窗口的全部内容。
您可以在附带的文本文件 InteractiveWindowOutput.txt 中查看此菜谱的交互窗口的全部内容。
从现有 C# 项目初始化 C# 交互会话
在本节中,我们将向您展示如何从现有的 C# 项目初始化 C# 交互式脚本会话,然后在使用 Visual Studio 交互窗口中的项目类型。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此菜谱。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15. 免费安装社区版。
如何操作...
-
打开 Visual Studio 并通过单击“视图”|“其他窗口”|“C# 交互”来启动 C# 交互窗口。
-
在交互窗口中声明一个局部变量
int x = 0;并按下 Enter 键。 -
执行
Console.WriteLine(x)并验证输出0以确认变量x已在当前会话中声明。 -
创建一个新的 C# 类库项目,例如
ClassLibrary。 -
将以下方法
M添加到创建的项目中的Class1类型:
public void M()
{
Console.WriteLine("Executing ClassLibrary.Class1.M()");
}
- 在解决方案资源管理器中右键单击项目,然后单击“使用项目初始化交互”:

- 验证项目构建是否开始,以及 C# 交互会话是否已使用项目引用和输出程序集(
ClassLibrary.dll)重置:

-
在交互窗口中输入以下文本
new Class1().M();并按下 Enter 键以执行提交。 -
验证
Executing ClassLibrary.Class1.M()作为结果输出,确认交互会话是用ClassLibrary项目初始化的。 -
尝试引用在步骤 2 中定义的变量 x,即在通过执行
Console.WriteLine(x);初始化项目交互会话之前。 -
验证这导致以下编译时错误,确认当我们从项目初始化会话时,会话状态已完全重置:
> Console.WriteLine(x);
(1,19): error CS0103: The name 'x' does not exist in the current context
您可以在附带的文本文件 InteractiveWindowOutput.txt 中查看此配方的整个交互窗口内容。
在 Visual Studio 开发者命令提示符中使用 csi.exe 执行 C# 脚本
在本节中,我们将向您展示如何使用命令行界面执行 C# 脚本及其交互模式。csi.exe(CSharp Interactive)是 C# 交互的 CLI 可执行文件,它随 C# 编译器工具集和 Visual Studio 一起提供。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
-
启动 Visual Studio 2017 开发命令提示符并执行命令
csi.exe以启动 C# 交互会话.。 -
在控制台输入
Console.WriteLine("Hello, World!")并按 Enter 键以交互模式执行命令:

-
按 Ctrl + C 退出交互模式。
-
创建一个名为
MyScript.csx的脚本文件,包含以下代码以输出脚本参数:
var t = System.Environment.GetCommandLineArgs();
foreach (var i in t)
{
System.Console.WriteLine(i);
}
- 使用参数
1 2 3执行脚本并验证以下输出。注意,执行脚本后,我们返回到命令提示符,而不是交互会话:
c:\>csi MyScript.csx 1 2 3
csi
MyScript.csx
1
2
3
- 现在,在脚本前添加一个额外的
-i参数并执行,验证与之前相同的输出,不过这次我们返回到交互式提示符:
c:\>csi -i MyScript.csx 1 2 3
csi
-i
MyScript.csx
1
2
3
>
-
执行
Console.WriteLine(t.Length)并验证输出为6,确认在当前交互会话中,脚本中声明的并使用命令行参数初始化的变量 t 仍然存在。 -
按 Ctrl + C 退出交互模式。
-
执行
csi -i以以交互模式启动csi.exe并执行 #help 命令以获取可用键盘快捷键、REPL 命令和脚本指令列表:

-
注意,
csi.exe中可用的快捷键、REPL 命令和脚本指令集合是 Visual Studio 交互窗口中相应集合的子集。有关可用的快捷键、命令和指令的详细信息,请参阅本章前面的配方 在 C# 交互窗口中使用脚本指令和 REPL 命令 和 在 C# 交互窗口中使用键盘快捷键评估和导航脚本会话。 -
按 Ctrl + C 退出交互模式。
-
尝试使用参数执行
csi.exe,但不要指定脚本名称,并验证关于缺少源文件的错误 CS2001:
c:\>csi.exe 1
error CS2001: Source file 'c:\1' could not be found.
您可以在 github.com/dotnet/roslyn/wiki/Interactive-Window#repl 了解有关命令行 REPL 和 csi.exe 参数的更多信息。
使用 Roslyn 脚本 API 执行 C# 代码片段
在本节中,我们将向您展示如何编写一个使用 Roslyn 脚本 API 执行 C# 代码片段并消费其输出的 C# 控制台应用程序。脚本 API 允许 .NET 应用程序实例化一个 C# 引擎,并针对宿主提供的对象执行代码片段。脚本 API 也可以直接在交互会话中使用。
开始使用
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
-
打开 Visual Studio 并创建一个新的以 .NET Framework 4.6 或更高版本为目标的 C# 控制台应用程序,例如
ConsoleApp. -
安装
Microsoft.CodeAnalysis.CSharp.ScriptingNuGet 包(在撰写本文时,最新稳定版本是 2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章 在 .NET 项目中消费诊断分析器 中的配方,通过 NuGet 包管理器搜索和安装分析器。 -
将
Program.cs中的源代码替换为附带的代码示例中的源代码\ConsoleApp\Program.cs. -
按 Ctrl + F5 构建并启动不带调试的项目
.exe。 -
验证
EvaluateSimpleAsync评估的第一个输出:*
Executing EvaluateSimpleAsync...
3
-
按任意键继续执行。
-
验证
EvaluateWithReferencesAsync评估的第二个输出:
Executing EvaluateWithReferencesAsync...
<%your_machine_name%>
-
按任意键继续执行。
-
验证
EvaluateWithImportsAsync评估的第三个输出:
Executing EvaluateWithImportsAsync...
1.4142135623731
-
按任意键继续执行。
-
验证
EvaluateParameterizedScriptInLoopAsync评估的最后输出:
Executing EvaluateParameterizedScriptInLoopAsync...
0
1
4
9
16
25
36
49
64
81
Press any key to continue . . .
- 按任意键退出控制台。
请参阅文章 (github.com/dotnet/roslyn/wiki/Scripting-API-Samples) 以获取脚本 API 的更多示例。
它是如何工作的...
在这个菜谱中,我们基于 Roslyn 脚本 API 编写了一个 C# 控制台应用程序,以执行各种常见的脚本操作。丰富的脚本 API 为评估、创建和执行具有配置选项的脚本提供了一个强大的对象模型。
让我们逐步分析这个菜谱中的代码,了解我们是如何实现这些操作的:
public static void Main(string[] args)
{
EvaluateSimpleAsync().Wait();
Console.ReadKey();
EvaluateWithReferencesAsync().Wait();
Console.ReadKey();
EvaluateWithImportsAsync().Wait();
Console.ReadKey();
EvaluateParameterizedScriptInLoopAsync().Wait();
}
Main 方法调用单个方法以执行以下操作:
-
EvaluateSimpleAsync: 对二进制加法表达式的简单评估 -
EvaluateWithReferencesAsync: 一个涉及传递给脚本选项的引用程序集的评估 -
EvaluateWithImportsAsync: 一个涉及导入系统命名空间并从该命名空间调用 API 的评估 -
EvaluateParameterizedScriptInLoopAsync: 通过参数化脚本并在值循环中调用创建和评估脚本
EvaluateSimpleAsync 调用最常用的脚本 API,即 CSharpScript.EvaluateAsync (source.roslyn.io/#q=CSharpScript.EvaluateAsync),并使用一个表达式作为评估该表达式的参数。在我们的例子中,我们将 1 + 2 作为参数传递给 EvaluateAsync,输出结果 3:
private static async Task EvaluateSimpleAsync()
{
Console.WriteLine("Executing EvaluateSimpleAsync...");
object result = await CSharpScript.EvaluateAsync("1 + 2");
Console.WriteLine(result);
}
EvaluateWithReferencesAsync 调用相同的 CSharpScript.EvaluateAsync API (source.roslyn.io/#q=CSharpScript.EvaluateAsync),但使用通过脚本选项传递的额外引用程序集,通过 ScriptOptions.WithReferences API (source.roslyn.io/#q=ScriptOptions.WithReferences)。
在我们的例子中,我们传递 typeof(System.Net.Dns).Assembly 作为评估 System.Net.Dns.GetHostName() 的额外引用,输出机器名:
private static async Task EvaluateWithReferencesAsync()
{
Console.WriteLine("Executing EvaluateWithReferencesAsync...");
var result = await CSharpScript.EvaluateAsync("System.Net.Dns.GetHostName()",
ScriptOptions.Default.WithReferences(typeof(System.Net.Dns).Assembly));
Console.WriteLine(result);
}
EvaluateWithImportsAsync 使用通过脚本选项传递的命名空间导入调用 CSharpScript.EvaluateAsync API (source.roslyn.io/#q=CSharpScript.EvaluateAsync),通过 ScriptOptions.WithImports API (source.roslyn.io/#q=ScriptOptions.WithImports)。在我们的例子中,我们将 System.Math 作为评估 Sqrt(2) 的额外命名空间导入,输出结果 1.4142135623731:
private static async Task EvaluateWithReferencesAsync()
{
Console.WriteLine("Executing EvaluateWithReferencesAsync...");
var result = await CSharpScript.EvaluateAsync("System.Net.Dns.GetHostName()",
ScriptOptions.Default.WithReferences(typeof(System.Net.Dns).Assembly));
Console.WriteLine(result);
}
EvaluateParameterizedScriptInLoopAsync 使用 CSharpScript.Create API (source.roslyn.io/#Microsoft.CodeAnalysis.CSharp.Scripting/CSharpScript.cs,3beb8afb18b9c076) 创建一个参数化的 C# 脚本,该脚本接受要执行的脚本代码和一个全局类型作为参数:
private static async Task EvaluateParameterizedScriptInLoopAsync()
{
Console.WriteLine("Executing EvaluateParameterizedScriptInLoopAsync...");
var script = CSharpScript.Create<int>("X*Y", globalsType: typeof(Globals));
script.Compile();
for (int i = 0; i < 10; i++)
{
Console.WriteLine((await script.RunAsync(new Globals { X = i, Y = i })).ReturnValue);
}
}
然后它调用 Script.Compile API (source.roslyn.io/#q=Script.Compile) 来编译脚本。编译后的脚本随后在循环中使用 Script.RunAsync API (source.roslyn.io/#q=Script.RunAsync) 执行,循环中使用不同实例的全局类型 Globals,字段 X 和 Y 的值递增。每次迭代计算表达式 X * Y 的结果,在我们的例子中,这只是从零到九的循环中所有数字的平方。
第八章:向 Roslyn C#编译器开源代码贡献简单功能
在本章中,我们将介绍以下食谱:
-
设置 Roslyn 征募
-
在 C#编译器代码库中实现新的语法错误
-
在 C#编译器代码库中实现新的语义错误
-
为 C#编译器代码库中的新错误编写单元测试
-
使用 Roslyn 语法可视化器查看源文件的 Roslyn 语法标记和节点
-
向 Roslyn Pull request 提交以贡献 C#编译器和 VS IDE 的下一个版本
简介
本章使开发者能够向 Roslyn C#编译器添加新功能。
让我们简要地浏览一下 Roslyn 源代码树的不同部分。您可以在 VS2017 分支中快速查看 Roslyn 仓库的最顶层源文件夹:github.com/dotnet/roslyn/tree/Visual-Studio-2017/src
最重要的源文件夹及其对应的组件如下:
-
Compilers: 这实现了 Roslyn C#和 VB 编译器以及核心 Microsoft 代码分析层,该层公开了丰富的语言无关 API,用于对源代码进行语法和语义分析。此层的核心概念包括 SyntaxTree(源文件)、SemanticModel(源文件的语义)、Symbol(源中的声明)和 Compilation(源文件和选项的集合)。 -
Workspaces: 这实现了工作区层及其相应的 API,用于对项目和解决方案进行工作区级别的代码分析和重构。此层对在工作区上运行的宿主操作系统(如 Visual Studio 或命令行工具)完全无关。此层的核心概念包括文档(带有相关语义模型的语法树)、项目(由文档和程序集引用组成的集合,构成编译,并具有配置编译的属性)、解决方案(项目的集合)和工作区级别的选项。 -
Features: 建立在 Workspaces 层之上的可扩展 IDE 功能,如代码修复、重构、IntelliSense、完成、查找引用、导航到定义、编辑并继续(EnC)、诊断等,都位于此层。此层与 Visual Studio 无关,可以托管在不同的宿主或命令行工具中。 -
VisualStudio: 在功能和 Workspaces 层之上构建的 Visual Studio 特定组件提供了一个端到端的 C#和 VB IDE 体验。此层的核心概念包括Visual Studio 工作区、项目系统(组件通过填充工作区和启用上述提到的 IDE 功能,在静态程序表示和实时 IDE 表示之间架起桥梁)和语言服务(向项目系统公开的核心语言语义服务)。 -
ExpressionEvaluator: 用于解析和评估简单表达式以及计算运行时结果的 C# 和 VB 表达式评估器。 -
Samples: 展示 Roslyn API 使用的示例和教程。您可以在github.com/dotnet/roslyn/wiki/Samples-and-Walkthroughs中阅读更多详细信息。
您可以在github.com/dotnet/roslyn/wiki/Roslyn%20Overview阅读更多关于 Roslyn 的详细信息。
设置 Roslyn 入队
在本节中,我们将向您介绍安装所需工具、加入 Roslyn、构建 Roslyn 编译器源代码以及部署、调试和运行本地构建的编译工具集测试的步骤。
入门
您需要在您的机器上安装 Visual Studio 2017 以执行本章中的配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装免费的 Visual Studio 2017 社区版本。请确保所选的工作负载包括 C#、VB、MSBuild 和 Visual Studio 扩展性。更具体地说,将 .NET 桌面开发工作负载和 Visual Studio 扩展性工具工作负载添加到您的 VS 安装中。
如何做到这一点...
-
按照以下步骤在
desktop.github.com/安装 GitHub for desktop 并使用您的 GitHub 个人资料登录 GitHub。如果您没有个人资料,您可以在github.com/join创建一个。 -
从 Git Shell 执行以下命令以使用 VS2017 标签加入并恢复 Roslyn 编译器源代码:
-
git clone https://github.com/dotnet/roslyn c:\Roslyn
-
cd c:\Roslyn
-
git checkout tags/Visual-Studio-2017
-
Restore.cmd
-
-
从 VS2017 管理员开发命令提示符构建 Roslyn 编译器子树:
msbuild /m /v:m Compilers.sln。您也可以使用Build.cmd或msbuild /m /v:m Roslyn.sln构建整个 Roslyn 源树。此步骤构建源代码并将本地构建的编译工具集(或整个 Roslyn IDE + 编译工具集)部署到 RoslynDev hive。
注意:如果您由于强名称签名失败而遇到构建错误,请从管理员开发命令提示符执行以下命令以禁用强名称验证:sn -Vr *,然后执行构建。
-
在 VS2017 中打开
Roslyn.sln并将Compilers\CompilerExtension.csproj设置为启动项目。 -
点击 Ctrl + F5 将本地构建的编译工具集部署到单独的 Visual Studio hive 并从这个 hive 启动一个新的 Visual Studio 实例(RoslynDev)。
-
在新的 Visual Studio 实例中,创建一个新的 C# 类库项目。
-
通过打开“工具”|“选项”|“项目和解决方案”|“构建和运行”|“MSBuild 项目构建输出详细程度”,将 msbuild 输出详细程度从“最小”更改为“正常”。
-
构建 C#类库项目,并打开输出窗口以确认是否使用了本地构建的
<q>csc.exe</q>来构建库,并且它是从 Visual Studio RoslynDev 存储库执行的:C:\USERS\<%USER_NAME%>\APPDATA\LOCAL\MICROSOFT\VISUALSTUDIO\15.0_XXXXXXXXROSLYNDEV\EXTENSIONS\MICROSOFT\ROSLYN COMPILERS\42.42.42.42424\csc.exe -
右键单击
Compilers\CSharpCompilerSemanticTest.csproj并打开项目属性|调试页面。通过执行 Start external program 文本框中指定的 xunit 控制台 exe 文件,并使用命令行参数文本框中的参数执行 C#编译器语义单元测试:C:\Users\<%USER_NAME%>\.nuget\packages\xunit.runner.console\<%VERSION%>\tools\xunit.console.x86.exe "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\\Roslyn.Compilers.CSharp.Semantic.UnitTests.dll" -html "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\\xUnitResults\Roslyn.Compilers.CSharp.Semantic.UnitTests.html" -noshadow
您可以在github.com/dotnet/roslyn/blob/dev16/docs/contributing/Building,%20Debugging,%20and%20Testing%20on%20Windows.md获取有关注册、构建和测试 Roslyn 源代码的更详细说明。
在 C#编译器代码库中实现新的语法错误
本节将使 Roslyn 贡献者能够修改 C#解析器以添加新的语法错误。当在符号声明(如字段、方法、局部变量等)中使用不正确的修饰符时,C#解析器报告诊断CS0106(t*he modifier 'modifier' is not valid for this* item)(msdn.microsoft.com/en-us/library/3dd3ha66.aspx)。例如,以下错误代码生成了三个CS0106实例:
class Class
{
// Error CS0106: The modifier 'async' is not valid for this item
async int field;
// Error CS0106: The modifier 'readonly' is not valid for this item
readonly static void M()
{
// Error CS0106: The modifier 'readonly' is not valid for this item
readonly int local = 0;
System.Console.WriteLine(local);
}
}
然而,如果您声明了一个具有不正确修饰符的参数,例如readonly int param,它不会生成CS0106错误,而是生成大量与缺失标记、无效标识符等相关的不太有用的语法错误和波浪线。考虑以下示例:
class Class
{
static void M(readonly int param)
{
}
}
这会生成以下错误和波浪线集:

在本节中,我们将修改 C#解析器以实现更好的错误恢复机制,对于此类无效参数修饰符,将报告单个CS0106语法错误,这对最终用户更有帮助。
入门
您需要确保已安装带有.NET 开发和 VS 扩展性工作负载的 Git 工具、VS2017,并且已使用 VS2017 标签注册和构建了 Roslyn 源代码。有关参考,请参阅本章开头“设置 Roslyn 注册”的配方。
如何操作...
-
在 VS2017 中打开 Roslyn 仓库根目录下的
Roslyn.sln。 -
打开源文件
<%ROOT%>\src\Compilers\CSharp\Portable\Parser\LanguageParser.cs。 -
导航到私有方法
IsPossibleParameter(第 4060 行)并将高亮的||子句添加到默认情况返回语句中:
default:
return IsPredefinedType(this.CurrentToken.Kind) || GetModifier(this.CurrentToken) != SyntaxModifier.None;
- 导航到私有方法
ParseParameterModifiers(第 4234 行),并将现有的while (IsParameterModifier(this.CurrentToken.Kind, allowThisKeyword))替换为while (true)循环,在 while 循环的开始处添加以下 if 语句:
while (true)
{
if (!IsParameterModifier(this.CurrentToken.Kind, allowThisKeyword))
{
if (GetModifier(this.CurrentToken) != SyntaxModifier.None)
{
// Misplaced modifier
var misplacedModifier = this.EatToken();
misplacedModifier = this.AddError(misplacedModifier, ErrorCode.ERR_BadMemberFlag, misplacedModifier.Text);
modifiers.Add(misplacedModifier);
continue;
}
break;
}
...
-
构建解决方案。
-
将
VisualStudio\VisualStudioSetup.Next.csproj设置为启动项目,然后按 Ctrl + F5 启动一个新的 VS 实例,并使用本地构建的编译器和 IDE 工具集。 -
创建一个新的 C# 类库项目,例如
ClassLibrary,并添加以下代码:
class Class
{
static void M(readonly int param)
{
}
}
- 验证错误列表中有一个 CS0106 诊断,用于无效的
readonly修饰符,并且编辑器有一个单条波浪线。

- 构建项目并验证构建输出只有一个 CS0106 诊断。
您可以在 github.com/mavasani/roslyn/commit/02b7be551b46fa9a8e054c3317bc2ae7957b563c. 查看在此配方中做出的所有解析器更改。
它是如何工作的...
解析、语法分析或句法分析是分析一串符号的过程,这些符号可以是自然语言或计算机语言中的,并符合形式语法的规则。
C# 语言解析器是编译器工具链的第一个阶段,它根据 C# 语言规范解析源文件以生成语法树。解析每个源文件的主要入口点是 SyntaxFactory.ParseCompilationUnit (source.roslyn.io/#q=SyntaxFactory.ParseCompilationUnit),它将给定的源文本转换为带有语法节点、标记和语法的 CompilationUnitSyntax。
使用 source.roslyn.io/ 进行丰富的语义搜索和导航 Roslyn 源代码。请注意,该网站索引的源代码版本对应于 Roslyn 仓库 master 分支的最新源代码,可能与 Visual-Studio-2017 标签的源代码不同。
LanguageParser.ParseCompilationUnitCore (source.roslyn.io/#q=LanguageParser.ParseCompilationUnitCore) 是 LanguageParser 的核心方法,它解析最外层的命名空间声明(如果有的话),然后解析命名空间体内的类型和成员声明。它使用 Lexer (source.roslyn.io/#q=Lexer.cs) 来读取下一个标记,并使用非常复杂的错误恢复机制为错误代码提供有意义的语法错误,这些错误代码包含位置不当或无效的标记。
在本配方中,我们确定了一个参数上无效修饰符的案例,其中 C#编译器没有执行最佳错误恢复,并更改了解析器代码以查找参数上的无效修饰符,并使用CS0601诊断解析它们。
在步骤 3 中对IsPossibleParameter默认情况返回语句的以下突出更改确保了当我们遇到参数声明修饰符列表中的成员级修饰符(如 readonly、public、private 等)时,不会完全跳过参数列表。相反,我们将修饰符与参数关联。
default:
return IsPredefinedType(this.CurrentToken.Kind) || GetModifier(this.CurrentToken) != SyntaxModifier.None;
步骤 4 中添加到ParseParameterModifiers方法 while 循环中的If语句确保我们将有效和无效的修饰符都解析到参数修饰符列表中(与原始代码中仅解析有效修饰符相反),并在无效修饰符上生成适当的CS0106语法错误。
在 C#编译器代码库中实现新的语义错误
本节将使 Roslyn 贡献者能够对 C#绑定/语义分析阶段进行更改,以添加新的语义诊断。此外,我们还将展示如何扩展在本地重写(降低)阶段报告的现有语义诊断,以涵盖更多情况。
使用var关键字进行隐式类型声明的使用是一个非常主观的问题。C#编译器仅在无法推断类型或类型无效的隐式类型声明上报告非主观语义错误。然而,在某些情况下,初始化器的类型是有效的并且可以推断,但由于初始化器表达式的转换而不太明显。例如,考虑以下表达式var x = 1 + 1.0, var y = "string" + 1。x和y的初始化器包含二元表达式的左右两侧的隐式转换,这还可能涉及用户定义的隐式运算符转换,因此,变量的推断类型并不明显。我们将扩展 C#绑定以报告针对此类情况的新警告CS0823:警告 CS0823:使用显式类型进行声明,因为初始化器类型'{0}'由于转换而不明显。
此外,在本配方中,我们将扩展CS1717(对同一变量进行赋值;您是想分配其他内容?)(docs.microsoft.com/en-us/dotnet/csharp/misc/cs1717)以报告在自赋值属性访问上的错误。目前,它仅涵盖自赋值字段、局部变量、参数和事件访问。
在这两个更改之后,我们将看到以下新的警告:
class Class
{
int X { get; set; }
void M(int x)
{
// Warning CS1717 Assignment made to same variable; did you mean to
//assign something else?
X = X;
// Warning CS0823 Use an explicit type for declaration as the
//initializer type 'string' is not apparent due to conversions
var y = x + "" ;
}
}
入门
您需要确保已安装Git工具、VS2017 以及.NET 开发,并且已安装 VS 扩展性工作负载,并使用 VS2017 标签登记和构建 Roslyn 源代码。有关参考,请参阅本章开头处的配方设置 Roslyn 登记。
如何做到这一点...
-
在 VS2017 中打开 Roslyn 仓库根目录下的
Roslyn.sln。 -
打开源文件
<%ROOT%>\src\Compilers\CSharp\Portable\Errors\ErrorCode.cs,在行 566 处添加以下新的警告 ID:WRN_ImplicitlyTypedVariableNotRecommended = 823, -
打开 resx 文件
<q><%ROOT%>\src\Compilers\CSharp\Portable\CSharpResources.resx</q>,为警告消息添加以下新的资源字符串:
<data name="WRN_ImplicitlyTypedVariableNotRecommended" xml:space="preserve">
<value>
Use an explicit type for declaration as the initializer type '{0}' is not apparent due to conversions
</value>
</data>
<data name="WRN_ImplicitlyTypedVariableNotRecommended_Title" xml:space="preserve">
<value>
Use an explicit type for declaration as the initializer type is not apparent due to conversions
</value>
</data>
-
打开开源文件
<%ROOT%>\src\Compilers\CSharp\Portable\Errors\ErrorFacts.cs,在方法GetWarningLevel的第 320 行添加一个新的 switch case:ErrorCode.WRN_ImplicitlyTypedVariableNotRecommended: -
打开源文件
<%ROOT%>\src\Compilers\CSharp\Portable\Binder\Binder_Statements.cs,在方法BindInferredVariableInitializer的第 702 行添加以下 if 语句:
if (expression.Kind == BoundKind.BinaryOperator)
{
var binaryOperation = (BoundBinaryOperator)expression;
if (!binaryOperation.Left.GetConversion().IsIdentity || !binaryOperation.Right.GetConversion().IsIdentity)
{
// Use an explicit type for declaration as the initializer type '{0}' is
//not apparent due to conversions.
Error(diagnostics, ErrorCode.WRN_ImplicitlyTypedVariableNotRecommended, errorSyntax, expression.Display);
}
}
- 打开源文件
<%ROOT%>\src\Compilers\CSharp\Portable\Lowering\DiagnosticsPass_Warnings.cs,在方法IsSameLocalOrField的第 204 行添加以下 switch section:
case BoundKind.PropertyAccess:
var prop1 = expr1 as BoundPropertyAccess;
var prop2 = expr2 as BoundPropertyAccess;
return prop1.PropertySymbol == prop2.PropertySymbol &&
(prop1.PropertySymbol.IsStatic || IsSameLocalOrField(prop1.ReceiverOpt, prop2.ReceiverOpt));
-
构建解决方案。
-
将
VisualStudio\VisualStudioSetup.Next.csproj设置为启动项目,然后按Ctrl + F5启动一个新的 VS 实例,使用本地构建的编译器和 IDE 工具集。 -
创建一个新的 C# 类库项目,例如
ClassLibrary,并添加以下代码:
class Class
{
int X { get; set; }
void M(int x)
{
X = X;
var y = x + "" ;
}
}
- 验证新的警告CS0823和CS1717出现在错误列表中,并在编辑器中出现波浪线:

- 构建项目并验证构建输出也包含新的诊断信息。
您可以在github.com/mavasani/roslyn/commit/a155824a41150414966c6f03493b0bb05a45a59e查看为CS0823所做的所有源代码更改,以及github.com/mavasani/roslyn/commit/9f33d6809202d9b2b7ef5e0fa79df0b56ea46110为CS1717所做的更改。
它是如何工作的...
语义分析,也称为上下文相关分析,是编译器构建过程中的一个步骤,通常在解析之后,从源代码中收集必要的语义信息。
C# 绑定器是编译器工具链的第二阶段,它操作于由解析器输出的语法树、节点、标记和 trivia,并根据 C# 语言规范分析代码的语义。此阶段生成绑定树并报告语义诊断。绑定树本质上是一个具有与树中每个节点相关联的丰富语义信息的抽象语法树。在 CodeAnalysis 层提供的所有语义信息都来自与语法相关的绑定节点。绑定语句和表达式的入口点分别是Binder.BindStatement和Binder.BindExpression。
使用source.roslyn.io/进行 Roslyn 源代码的丰富语义搜索和导航。请注意,该网站上索引的源代码版本对应于 Roslyn 仓库的master分支的最新源代码,可能与Visual-Studio-2017标签的源代码不同。
在本配方中,我们向您展示了如何在绑定器中做出以下更改以添加新的诊断CS0823:
-
将新的错误代码添加到
ErrorCode枚举中。 -
为编译器的诊断消息添加新的资源字符串。
-
当初始化器是一个涉及非明显隐式转换的二进制表达式时,向方法添加一个新的语义诊断以绑定隐式变量初始化器。
C#本地重写或降低是编译器工具链的第三阶段,它将绑定树简化为非常简单的绑定节点。此外,它还执行流分析并报告流分析诊断(如不可达代码)。然后本地重写器的输出被馈送到代码生成器,该生成器为简化的绑定树生成 MSIL。
在本配方中,我们将现有的本地重写诊断传递扩展到报告自我赋值属性访问表达式的CS1717。
在 C#编译器代码库中为新错误编写单元测试
本节将使您能够向 C#编译器添加单元测试。Roslyn.sln 中有以下一系列单元测试项目:
-
CSharpCompilerSyntaxTest:解析和语法错误的单元测试 -
CSharpCompilerSemanticTest:语义错误和语义模型 API 的单元测试 -
CSharpCompilerSymbolTest:编译器层定义的符号的单元测试 -
CSharpCommandLineTest:编译器的命令行选项的单元测试 -
CSharpCompilerEmitTest:代码生成阶段的单元测试,用于验证生成的 MSIL
在本节中,我们将为新增的语义错误向CSharpCompilerSemanticTest添加单元测试。
入门
您需要确保已执行本章中此前的配方,在 C#编译器代码库中实现新的语义错误,以向 C#编译器添加新的语义诊断:警告 CS0823:由于初始化器类型 '{0}' 由于转换而不明显,请使用显式类型进行声明。
如何做到这一点...
-
在 VS2017 中打开 Roslyn 仓库根目录下的
Roslyn.sln。 -
打开源文件
<%ROOT%>\src\Compilers\CSharp\Test\Semantic\Semantics\ImplicitlyTypedLocalsTests.cs。 -
将以下新的单元测试添加到源文件中:
[Fact]
public void VarInferredTypeNotApparent()
{
var source = @"
class Class
{
void M(int x, string y)
{
var z = x + y;
}
}";
CreateCompilationWithMscorlib(source).VerifyDiagnostics();
}
- 构建测试项目,并在命令行控制台中使用从项目的
Debug属性页复制的命令行执行单元测试,为新增的单元测试添加-method开关:
*<%USERS_FOLDER%>*\.nuget\packages\xunit.runner.console\2.2.0-beta4-build3444\tools\xunit.console.x86.exe "*<%ROOT%>*\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\Roslyn.Compilers.CSharp.Semantic.UnitTests.dll" -html "C:\roslyn\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\xUnitResults\Roslyn.Compilers.CSharp.Semantic.UnitTests.html" -noshadow -method Microsoft.CodeAnalysis.CSharp.UnitTests.ImplicitlyTypedLocalTests.VarInferredTypeNotApparent
- 验证单元测试因缺少CS0823诊断而失败:
Expected:
Actual:
// (6,7): warning CS0823: Use an explicit type for declaration as the initializer type 'string' is not apparent due to conversions
// var z = x + y;
Diagnostic(ErrorCode.WRN_ImplicitlyTypedVariableNotRecommended, "z = x + y").WithArguments("string").WithLocation(6, 7)
Diff:
++> Diagnostic(ErrorCode.WRN_ImplicitlyTypedVariableNotRecommended, "z = x + y").WithArguments("string").WithLocation(6, 7)
- 将缺失的诊断作为参数添加到我们的单元测试中的
VerifyDiagnostics调用:

- 通过重复步骤 4 重新执行单元测试,并验证测试现在是否通过。
如果您遇到 DirectoryNotFoundException,请确保测试结果目录存在于机器上:<%ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\xUnitResults。
- 添加另一个单元测试以验证诊断不会在初始化二进制表达式没有隐式转换的情况下触发:
[Fact]
public void VarInferredTypeApparent_NoDiagnostic()
{
var source = @"
class Class
{
void M(int x, string y)
{
var z = (string)(x + y);
}
}";
CreateCompilationWithMscorlib(source).VerifyDiagnostics();
}
- 执行新的单元测试并验证它是否通过。
您也可以使用测试资源管理器窗口在 Visual Studio 中执行单元测试,但由于整个解决方案中有数千个单元测试,因此对 Roslyn.sln 的测试发现相当慢。因此,您可能需要等待几分钟才能执行第一个单元测试。
使用 Roslyn 语法可视化器查看源文件的 Roslyn 语法标记和节点
语法可视化器是一个 Visual Studio 扩展,它简化了对 Roslyn 语法树的检查和探索,并在您使用 .NET 编译器平台 (Roslyn) API 开发自己的应用程序时可以作为调试辅助工具使用。
在本节中,我们将向您展示如何安装和使用 Roslyn 语法可视化器来查看 Visual Studio 中 C# 和 Visual Basic 源代码的语法树、节点和属性。您还可以查看与语法节点关联的语义,例如符号信息、类型信息和表达式的编译时常量值。
入门
您需要安装 .NET Compiler Platform SDK 来安装 Roslyn 语法可视化器。有关安装 SDK 的说明,请参阅第一章中的配方,“在 Visual Studio 中创建、调试和执行分析器项目”,编写诊断分析器。
如何操作...
-
打开 Visual Studio,使用命令“查看 | 其他窗口 | 语法可视化器”启动 Roslyn 语法可视化器,并将其停靠在 Visual Studio 窗口的左侧。
-
创建一个新的 C# 类库项目,例如
ClassLibrary,并将以下方法添加到Class1.cs中:
public void Method()
{
Console.WriteLine("Hello world!");
}
- 选择调用
Console.WriteLine("Hello world!")的代码,并查看语法可视化器的层次树视图:CompilationUnit包含一个NamespaceDeclaration,它包含一个ClassDeclaration,它包含一个具有Block的MethodDeclaration,其第一个语句是一个ExpressionStatement,该语句包含一个InvocationExpression:

- 在语法可视化器的 SyntaxTree 面板中右键单击 InvocationExpression 节点,然后单击“查看符号(如果有)”命令。

- 您可以查看绑定到调用的 PE 元数据符号
System.Console.WriteLine的Properties。

- 将一个新的 VB 类库项目添加到解决方案中,例如
ClassLibrary1,并将以下方法添加到现有类中:
Public Sub Method()
Console.Write("Hello World!")
End Sub
- 在编辑器中选择调用表达式
Console.Write("Hello World!"),你可以在 Syntax Visualizer 中查看 VB 代码的语法树、节点和属性:

你可以在 github.com/dotnet/roslyn/wiki/Syntax%20Visualizer 阅读关于 Syntax Visualizer 工具的更详细概述。
向 Roslyn 提交拉取请求以贡献下一个版本的 C# 编译器和 VS IDE
在本节中,我们将指导你完成发送拉取请求以贡献 Roslyn 编译器和 Visual Studio IDE 下一个版本的步骤。
入门
你需要确保你已经安装了 Git 工具,VS2017 带有 .NET 开发和 VS 扩展工作负载,并且已经注册并构建了 Roslyn 源代码。有关参考,请参阅本章开头的配方,设置 Roslyn 注册。
如何做到这一点...
-
建议你在
github.com/dotnet/roslyn/issues上为你的计划工作创建一个 Roslyn 问题,并在编码之前与 Roslyn 团队成员讨论,以避免任何不必要的或重复的工作。 -
在你的本地注册中做出你想要贡献给 Roslyn 代码库的源代码更改。例如,执行本章前面提到的配方,在 C# 编译器代码库中实现新的语义错误。
-
为你的代码更改添加足够的单元测试。例如,执行本章前面提到的配方,为 C# 编译器代码库中的新错误编写单元测试。
-
在仓库根目录下执行
Test.cmd以构建和运行所有测试,以确认你的更改没有出现回归。有关参考,请参阅本章开头的配方,设置 Roslyn 注册。 -
创建一个新的 Git 分支,添加并提交你的更改,然后将它们推送到远程仓库。有关 Git 帮助,请搜索
help.github.com/. -
在发送拉取请求之前,请在
cla2.dotnetfoundation.org/上签署 .NET 贡献者许可协议(CLA)。 -
按照以下步骤
help.github.com/articles/creating-a-pull-request/在你的分支上发起一个新的拉取请求。 -
在拉取请求的描述标签中填写拉取请求模板。你可以在创建拉取请求后编辑此信息。
-
在创建拉取请求后,添加一条新评论标记编译器或/和 IDE 团队以审查更改:
-
编译器团队:
@dotnet/roslyn-compiler -
IDE 团队:
@dotnet/roslyn-ide
-
-
根据审阅者的要求进行代码更改,并确保你的分支中没有合并冲突。
-
在你至少获得两个批准并且所有测试在拉取请求上通过后,你可以请求团队成员合并你的更改。
您可以在github.com/dotnet/roslyn/wiki/Contributing-Code阅读关于 Roslyn 仓库贡献代码的指南以获取更多详细信息。
第九章:设计和实现一个新的 C#语言特性
在本章中,我们将介绍以下内容:
-
设计一个新 C#语言特性的语法和语法规则
-
实现对新 C#语言特性的解析器支持
-
实现对新 C#语言特性的绑定/语义分析支持
-
实现对新 C#语言特性的降低/代码生成支持
-
编写 C#解析、绑定和代码生成阶段的单元测试
简介
本章使开发者能够设计一个新的 C#语言特性,并实现该语言特性的各种编译器阶段。从高层次来看,C#编译器有以下重要阶段:

- 词法分析 (
en.wikipedia.org/wiki/Lexical_analysis): 这将源文件中的字符序列转换成诸如关键字、标识符、运算符等标记。Lexer.Lex(source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Parser/Lexer.cs,5ad0cc36317d33e7) 是进入 C#词法分析器的主要入口点,它获取下一个标记并增加源文本中的当前位置。例如,考虑以下源代码:
class C
{
void Method(int x)
{
x = x + 1;
}
}
在词法分析过程中,这被转换成以下标记序列(为了简洁,省略了空白和换行杂项):
class (Keyword) C (IdToken)
{ (OpenBraceToken)
void (Keyword) Method (IdToken) ( (OpenParenToken) int (Keyword) x (IdToken) ) (CloseParenToken)
{ (OpenBraceToken)
x (IdToken) = (EqualsToken) x (IdToken) + (PlusToken) 1 (NumericalLiteralToken) ; (SemiColonToken)
} (CloseBraceToken)
} (CloseBraceToken)
- 语法分析 (
en.wikipedia.org/wiki/Parsing): 这将词法分析阶段生成的标记序列转换成一个具有节点、标记和杂项的语法树。它还验证语法是否符合 C#语言规范,并生成语法诊断。SyntaxFactory.ParseCompilationUnit(source.roslyn.io/#q=SyntaxFactory.ParseCompilationUnit) 是进入 C#语言解析器的主要入口点,该解析器生成一个CompilationUnitSyntax节点,然后使用该节点创建一个以该节点为根的SyntaxTree(参见CSharpSyntaxTree.Create(source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Syntax/CSharpSyntaxTree.cs,d40da3b7b4e39486)))。对于前面的示例源代码和词法标记,我们得到以下语法树:

- 语义分析或绑定 (
en.wikipedia.org/wiki/Semantic_analysis_(compilers)): 将解析阶段生成的语法树转换为带有BoundNodes的绑定树。绑定树本质上是一个具有与树中每个节点相关联的丰富语义信息的抽象语法树。在 CodeAnalysis 层提供的所有语义信息都来自与语法相关的绑定节点。此阶段分析源代码的语义,例如类型检查、方法重载解析、转换等,并生成语义诊断。绑定语句和表达式的入口点分别是Binder.BindStatement(source.roslyn.io/#q=Binder.BindStatement) 和Binder.BindExpression(source.roslyn.io/#q=Binder.BindExpression)。对于前面的示例,为Method的方法体生成了以下绑定树:
BoundBlockStatement (1 statements) (Syntax: '{ ... }')
BoundExpressionStatement (Syntax: 'x = x + 1;')
BoundSimpleAssignmentExpression (Type: System.Int32) (Syntax: 'x = x + 1')
Left: BoundParameterReferenceExpression (Type: System.Int32) (Syntax: 'x')
Right: BoundBinaryOperatorExpression (IntegerAdd) (Type: System.Int32) (Syntax: 'x + 1')
Left: BoundParameterReferenceExpression (Type: System.Int32) (Syntax: 'x')
Right: BoundLiteralExpression (Type: System.Int32, Constant: 1) (Syntax: '1')
-
降低复杂度: 这将绑定阶段生成的绑定树转换为简化后的绑定树。例如,一个绑定 for 循环节点会被重写为一个带有标签和条件跳转的绑定块(参见
LocalRewriter.RewriteForStatement(source.roslyn.io/#q=RewriteForStatement)).LocalRewriter.Rewrite(source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Lowering/LocalRewriter/LocalRewriter.cs,c30511823bc3c19f) 是编译中每个方法块降低的主要入口点。 -
流分析 (
en.wikipedia.org/wiki/Data-flow_analysis): 此阶段对降低后的绑定树进行基本的数据流和控制流分析,以生成不可达代码和未初始化变量诊断。FlowAnalysisPass.Rewrite(source.roslyn.io/#q=FlowAnalysisPass.Rewrite) 是流分析阶段的主要入口点。 -
代码生成 (
en.wikipedia.org/wiki/Code_generation_(compiler)): 这将降低后的绑定树转换为以字节序列表示的 MSIL,并将其输出到 .NET 程序集。CodeGenerator.Generate(source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/CodeGen/CodeGenerator.cs,c28190700f8e314c) 是代码生成的主要入口点。对于前面的代码示例,C# 编译器为Method生成了以下 MSIL:
.method private hidebysig instance void Method(int32 x) cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.1
IL_0002: ldc.i4.1
IL_0003: add
IL_0004: starg.s x
IL_0006: ret
} // end of method C::Method
您可以在github.com/dotnet/roslyn/wiki/Roslyn%20Overview上阅读关于 Roslyn 的更详细概述。
新语言特性:Switch 运算符(?:😃
在本章中,我们将设计一个新的 C#语言特性,我们称之为Switch 运算符(?:😃。这个特性来源于两个现有的 C#语言结构:switch 语句(docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/switch)和条件运算符(?:)(docs.microsoft.com/en-us/cpp/cpp/conditional-operator-q)。它允许编写可以在多个表达式的值上切换的条件表达式,并返回相应的值或默认值。例如,考虑以下计算整型表达式字符串表示的 switch 语句:
public string GetString(int expression)
{
string expressionStr;
switch (expression)
{
case 1:
expressionStr = "One";
break;
case 2:
expressionStr = "Two";
break;
case 3:
expressionStr = "Three";
break;
default:
expressionStr = "More than three";
break;
}
return expressionStr;
}
这段代码基本上是在不同的可能值上切换表达式,并返回其运行时值的描述性字符串。用户的潜在意图只是为表达式的不同可能值返回一个映射的表达式,并带有一些默认值。本章设计的 switch 运算符将允许您使用单个表达式重写前面的代码:
string expressionStr = expression ?: [1, 2, 3] : ["One", "Two", "Three", "More than three"];
设计新 C#语言特性的语法和语法
语法和语法是实现新语言特性的核心元素。本节将使您能够定义新 C#语言特性的语法(节点和标记)和语法:Switch 运算符(?::)。有关此运算符预期功能的详细信息,请参阅本章开头关于新语言特性: Switch 运算符(?:😃的部分。
入门
您需要确保您已在您的机器上注册并构建了带有VS2017标签的 Roslyn 源。有关进一步指导,请参阅食谱,设置 Roslyn 注册。
第八章,向 Roslyn C#编译器开源代码贡献简单功能。
对于提到在 C#语言规范中定义...的食谱步骤,读者应在(github.com/dotnet/roslyn/issues/new)上创建一个新的 GitHub 问题,并添加标签 Language-C#和 Area-Language Design,让语言团队审查规范。如果获得批准,审阅者将确保将其添加到 C#语言规范中。
如何做到这一点...
- 在 C#语言规范中定义新三元运算符
?::的语法:
switch-expression:
null-coalescing-expression
null-coalescing-expression ?: bracketed-argument-list : bracketed-argument-list
bracketed-argument-list:
[ argument-list ]
- 在 C#语言规范中定义与新运算符和 switch 表达式相关的编译时语义:
形式为expr ?: [label1, label2, ..., labeln] : [val1, val2, ..., valn, valn+1]的 switch 表达式具有以下编译时语义:
-
switch 表达式的控制类型由与 switch 语句控制类型相同的规则集确定。
-
第一个括号参数列表
[label1, label2, ..., labeln]必须包含具有隐式可转换为 switch 控制类型的常量值的表达式 labeli。如果同一个 switch 表达式中的两个或多个labeli指定相同的常量值,则编译时将发生错误。 -
第二个括号参数列表
[val1, val2, ..., valn, valn+1]控制 switch 表达式的结果值类型。对列表中的每个 vali 和 valj 对执行以下检查必须得到类型 Z 的相同值;否则,将发生编译时错误:-
如果
vali的类型为 X,而valj的类型为 Y,则:-
如果存在从 X 到 Y 的隐式转换,但不存在从 Y 到 X 的转换,则 Y 是表达式的类型(Z = Y)。
-
如果存在从 Y 到 X 的隐式转换,但不存在从 X 到 Y 的转换,则 X 是表达式的类型(Z = X)。
-
否则,无法确定表达式类型,编译时将发生错误。
-
-
如果只有
vali或valj中的一个有类型,并且vali和valj都可以隐式转换为类型 Z,则该类型是表达式的类型。 -
否则,无法确定表达式类型,编译时将发生错误。
-
- 在 C# 语言规范中定义新操作符的关联性和优先级:
switch 操作符是右结合的,这意味着操作是从右到左分组的。
switch 操作符与其他三元操作符(如条件操作符 ?:)具有相同的优先级。
- 在 C# 语言规范中定义 switch 表达式的运行时执行语义:
switch 表达式的评估如下:
-
评估表达式
expr并将其转换为控制类型。 -
如果在同一个 switch 表达式中第一个括号列表指定的 n 个常量之一,例如
labeli,等于表达式expr的值,则评估第二个括号列表中的表达式vali并将其转换为类型 Z,并成为表达式的结果值。 -
如果在同一个 switch 表达式中第一个括号列表指定的 n 个常量中没有与表达式
expr的值相等的,则评估第二个括号列表中的最后一个表达式valn+1并将其转换为类型 Z,并成为表达式的结果值。
- 在 Visual Studio 2017 中打开
Roslyn.sln并打开源文件%REPO_ROOT%\src\Compilers\CSharp\Portable\Syntax\SyntaxKind.cs。在 77 行和 334 行分别添加新的SyntaxKinds以支持QuestionColonToken和SwitchExpression:
QuestionColonToken = 8284,
...
SwitchExpression = 8658,
- 在源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Syntax\Syntax.xml中添加新的语法节点SwitchExpressionSyntax的 XML 定义,包含字段Expression、QuestionColonToken、Labels、ColonToken和Values,在 686 行:
<Node Name="SwitchExpressionSyntax" Base="ExpressionSyntax">
<Kind Name="SwitchExpression"/>
<Field Name="Expression" Type="ExpressionSyntax">
<PropertyComment>
<summary>ExpressionSyntax node representing the expression of the switch expression.</summary>
</PropertyComment>
</Field>
<Field Name="QuestionColonToken" Type="SyntaxToken">
<Kind Name="QuestionColonToken"/>
<PropertyComment>
<summary>SyntaxToken representing the question mark.</summary>
</PropertyComment>
</Field>
<Field Name="Labels" Type="BracketedArgumentListSyntax">
<PropertyComment>
<summary>BracketedArgumentListSyntax node representing comma separated labels to switch on.</summary>
</PropertyComment>
</Field>
<Field Name="ColonToken" Type="SyntaxToken">
<Kind Name="ColonToken"/>
<PropertyComment>
<summary>SyntaxToken representing the colon.</summary>
</PropertyComment>
</Field>
<Field Name="Values" Type="BracketedArgumentListSyntax">
<PropertyComment>
<summary>BracketedArgumentListSyntax node representing the comma separated expression results.</summary>
</PropertyComment>
</Field>
<TypeComment>
<summary>Class which represents the syntax node for switch expression.</summary>
</TypeComment>
<FactoryComment>
<summary>Creates a SwitchExpressionSyntax node.</summary>
</FactoryComment>
</Node>
-
构建项目
CSharpCodeAnalysis以自动生成之前添加的新SwitchExpressionSyntax节点生成的源代码。请注意,由于我们尚未将新的公共类型添加到公共 API 表面,构建将因一系列RS0016错误而失败。 -
切换回源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Syntax\SyntaxKind.cs,并在第 334 行使用Ctrl + .调用代码修复,以定义SwitchExpression,并应用项目中的所有出现以修复所有RS0016诊断:

- 再次构建项目并验证这次是否成功。
您可以在github.com/mavasani/roslyn/commit/4b50f662c53e1b9fc83f81a819f29d11b85505d5查看此配方中做出的所有源代码更改。
它是如何工作的...
在本配方的前半部分,我们介绍了定义新 switch 表达式/运算符的语法、编译时和运行时语义、结合性和优先级的步骤。在下半部分,我们在编译器中定义了新的语法类型和语法节点。
新的 switch 表达式/运算符的语法、结合性和优先级与现有条件三元表达式的语法相同。
由于三元运算符在优先级顺序中紧接在空合并运算符(??)之后,语法指定:
switch-expression:
null-coalescing-expression
null-coalescing-expression ?: bracketed-argument-list : bracketed-argument-list
语法指定 switch 表达式的标签和值为一个逗号分隔的参数列表,位于方括号内,例如,[ arg1[,] arg[2], ..., arg[n] ]。以下是一些表达式的示例:
// Valid syntax cases
expression ?: [1, 2, 3] : ["One", "Two", "Three", "More than three"];
expression ?: [MethodCall1()] : ["One", "Two", "Three", "More than three"]; // invalid semantics
// Invalid syntax cases
expression ?: [MethodCall1(), 2] : "One"; // argument lists must be bracketed
expression ?: [MethodCall1(), MethodCall2()]; // missing colon and argument list
编译时语义强制要求被切换的表达式的类型与 switch 语句的 switch 控制类型具有相同的语义要求:
switch 语句的控制类型由 switch 表达式确定。如果 switch 表达式的类型是 sbyte、byte、short、ushort、int、uint、long、ulong、char、string 或枚举类型,那么这就是 switch 语句的控制类型。否则,必须存在从 switch 表达式类型到以下可能控制类型之一的确切用户定义的隐式转换(第 6.4 节):sbyte、byte、short、ushort、int、uint、long、ulong、char、string。如果不存在此类隐式转换,或者存在多个此类隐式转换,则编译时将发生错误。
编译时语义还强制要求:
-
第一个括号内的参数列表必须是所有常量标签,这样就有隐式转换为 switch 控制类型的转换
-
第二个参数列表的长度必须比第一个列表长一个,并且所有参数都必须是表达式,这样它们可以转换为具有隐式转换的公共类型
Z
以下是一些语义有效和无效的 switch 表达式的示例:
string expression = ...
// Valid syntax and semantics
expression ?: [1, 2, 3] : ["One", "Two", "Three", "More than three"];
// Invalid semantics, valid syntax
expression ?: [MethodCall1(), 2] : ["One", "Two", "Three"]; // non constant label
expression ?: [1.0] : ["One", "Two"]; // No implicit conversion from label to switch governing type
expression ?: [1] : ["One", 1.0]; // No implicit conversions to a common type between "One" and 1.0
Switch 表达式的运行时语义与 switch 语句相同。首先评估我们切换的表达式,并将其值与第一个参数列表中的每个标签进行比较。对于匹配项,我们评估第二个列表中的相应表达式并将其转换为类型 Z,这成为表达式的结果。如果没有匹配项,则评估第二个参数列表中的最后一个表达式并将其转换为类型 Z,这成为表达式的默认结果:
Console.WriteLine(expression ?: [1, 2, 3] : ["One", "Two", "Three", "More than three"]);
// expression == 1, prints "One"
// expression == 2, prints "Two"
// expression == 3, prints "Three"
// otherwise, prints "More than three"
实现对新的 C# 语言功能的解析器支持
词法分析 和 语法分析(解析)是 C# 编译器的初始阶段,将输入源文本转换为具有节点和标记的语法树,并报告语法诊断。本节将使您能够为新的 C# 语言功能添加词法分析和解析器支持:Switch 操作符 (?:😃。有关此操作符预期功能的信息,请参阅本章开头的 新语言功能:Switch 操作符 (?:😃 部分。有关此操作符的语法和语法定义的详细信息,请参阅前面的配方。
入门
您需要确保在您的机器上已 enlist 并使用 VS2017 标签构建了 Roslyn 源代码。有关进一步指导,请参阅配方,设置 Roslyn,enlistment 在 第八章,向 Roslyn C# 编译器开源代码贡献简单功能。
此外,在您的 enlistment 上 git commit github.com/mavasani/roslyn/commit/4b50f662c53e1b9fc83f81a819f29d11b85505d5 以获取语法定义并构建 CSharpCodeAnalysis 项目。
如何操作...
-
在 Visual Studio 2017 中打开
Roslyn.sln -
打开源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Parser\Lexer.cs并在方法ScanSyntaxToken的第 565 行添加高亮的else if语句:*
case '?':
if (TextWindow.PeekChar() == '?')
{ ...
}
else if (TextWindow.PeekChar() == ':')
{
TextWindow.AdvanceChar();
info.Kind = SyntaxKind.QuestionColonToken;
}
else
{ ...
}
- 打开源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Parser\LanguageParser.cs并在方法ParseSubExpressionCore的第 9426 行添加高亮显示的else if语句:
if (tk == SyntaxKind.QuestionToken && precedence <= Precedence.Ternary)
{ ...
}
else if (tk == SyntaxKind.QuestionColonToken && precedence <= Precedence.Ternary)
{
var questionColonToken = this.EatToken();
var labels = this.ParseBracketedArgumentList();
var colon = this.EatToken(SyntaxKind.ColonToken);
var values = this.ParseBracketedArgumentList();
leftOperand = _syntaxFactory.SwitchExpression(leftOperand, questionColonToken, labels, colon, values);
}
return leftOperand;
- 在同一文件(方法
CanFollowCast*)的第 10552 行添加高亮显示的 case 子句:
case SyntaxKind.EndOfFileToken:
case SyntaxKind.QuestionColonToken:
return false;
- 打开源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Syntax\SyntaxKindFacts.cs并在方法GetText的第 1278 行添加高亮显示的 case 子句:*
case SyntaxKind.XmlProcessingInstructionEndToken:
return "?>";
case SyntaxKind.QuestionColonToken:
return "?:";
...
-
将
Roslyn.csproj设置为启动项目。 -
将解决方案配置从调试更改为发布(以避免绑定器中的断言)并重新构建解决方案。
-
按 Ctrl + F5 以从
RoslynDevhive 启动 VS 的新实例并应用我们的本地更改。 -
在新的 VS 实例中,创建一个新的 C# 类库项目并添加以下代码,该代码使用新的 switch 操作符:
class Class
{
void M(int expr)
{
var exprStr = expr ?: [1, 2, 3] : ["One", "Two", "Three", "More than three"];
System.Console.WriteLine(exprStr);
}
}
- 从“查看”菜单中选择“其他窗口”中的“Roslyn 语法可视化器”,然后在编辑器中选择 switch 表达式,以查看表达式的解析语法节点和标记。有关语法可视化器的指导,请参考食谱,“使用 Roslyn 语法可视化器查看源文件的 Roslyn 语法标记和节点”在第八章,“向 Roslyn C# 编译器开源代码贡献简单功能”。

- 验证错误列表中没有波浪线或智能感知错误:

- 删除冒号标记和第二个括号内的参数列表,即
: ["One", "Two", "Three", "More than three"],并验证你会在 switch 表达式中因为缺少标记而得到语法错误:
Error CS1003 Syntax error, ':' expected ClassLibrary <%PROJECT_DIR%>\ClassLibrary\Class1.cs 5
Error CS1003 Syntax error, '[' expected ClassLibrary <%PROJECT_DIR%>\ClassLibrary\Class1.cs 5
Error CS1003 Syntax error, ']' expected ClassLibrary <%PROJECT_DIR%>\ClassLibrary\Class1.cs 5
- 撤销步骤 11,然后尝试构建项目并验证它因为
CSC : error CS7038: Failed to emit module 'ClassLibrary'而失败,因为我们尚未为新的结构实现任何绑定或代码生成。
你可以在github.com/mavasani/roslyn/commit/24144442e4faa9c54fe2a4b519455a1a45c29569查看在这个食谱中做出的所有源代码更改。
如何工作...
在这个食谱中,我们为 switch 运算符(?:)添加了基本的词法分析和解析器支持。词法分析器主要负责扫描文本并生成标记。LanguageParser 负责解析词法标记并生成带有节点和标记的语法树。
让我们遍历这个食谱中的代码更改。我们向词法分析器中添加了以下高亮代码:
case '?':
if (TextWindow.PeekChar() == '?')
{ ...
}
else if (TextWindow.PeekChar() == ':')
{
TextWindow.AdvanceChar();
info.Kind = SyntaxKind.QuestionColonToken;
}
else
{ ...
}
在原始代码中,当我们扫描文本并识别到 '?' 字符时,我们会查看下一个字符以识别它是否是另一个 '?' 字符(?? 空合并运算符)或空白(条件运算符的 ? 标记)。我们新的代码添加了一个额外的检查,以确定下一个字符是否是 ':'(switch 运算符的 ?: 标记)。如果是这样,它将文本窗口中的当前字符向前移动,并将当前标记的语法类型设置为 SyntaxKind.QuestionColonToken。
我们向解析器中添加了以下高亮代码:
if (tk == SyntaxKind.QuestionToken && precedence <= Precedence.Ternary)
{ ...
}
else if (tk == SyntaxKind.QuestionColonToken && precedence <= Precedence.Ternary)
{
var questionColonToken = this.EatToken();
var labels = this.ParseBracketedArgumentList();
var colon = this.EatToken(SyntaxKind.ColonToken);
var values = this.ParseBracketedArgumentList();
leftOperand = _syntaxFactory.SwitchExpression(leftOperand, questionColonToken, labels, colon, values);
}
我们扩展了原始代码,该代码在解析器中解析 QuestionToken 时,也检查了 QuestionColonToken 和三元运算符的优先级。如果是这样,我们将下一个标记作为 questionColonToken 消费。然后,我们通过调用 EatToken 并传递冒号标记的预期语法类型来解析冒号标记。此方法处理有效和无效标记的情况:
protected SyntaxToken EatToken(SyntaxKind kind)
{
Debug.Assert(SyntaxFacts.IsAnyToken(kind));
var ct = this.CurrentToken;
if (ct.Kind == kind)
{
MoveToNextToken();
return ct;
}
//slow part of EatToken(SyntaxKind kind)
return CreateMissingToken(kind, this.CurrentToken.Kind, reportError: true);
}
对于期望类型的有效标记,它移动到下一个标记并返回当前冒号标记。如果下一个标记不是期望的类型,它将生成一个缺失的标记,并报告缺失标记的语法诊断:
*Error CS1003 Syntax error, **':' expected** ClassLibrary <%PROJECT_DIR%>\ClassLibrary\Class1.cs 5*
最后,我们将 值 作为另一个括号参数列表进行解析。我们调用新自动生成的语法工厂辅助程序 SwitchExpression 来生成带有解析标记的 SwitchExpressionSyntax 节点。
实现对新的 C# 语言特性的绑定/语义分析支持
语义分析(绑定)是 C# 编译器的中间阶段,将语法树转换为 C# 绑定树并报告语义诊断。本节将使你能够为新的 C# 语言特性添加绑定支持:Switch 操作符 (?::)。有关此操作符预期功能的信息,请参阅本章开头部分,新语言特性:Switch 操作符 (?:😃。有关此操作符的语法和语法定义的详细信息,请参阅本章第一道菜谱,为新的 C# 语言特性设计语法和语法。
入门
你需要确保你在机器上已经使用带有 VS2017 标签的 enlistment 并构建了 Roslyn 源代码。有关进一步指导,请参阅菜谱,设置 Roslyn enlistment 在 第八章,向 Roslyn C# 编译器开源代码贡献简单功能。
此外,在你的 enlistment 上进行以下两个 git 提交以获取语法定义和解析器支持,分别构建 CSharpCodeAnalysis 项目:
-
github.com/mavasani/roslyn/commit/4b50f662c53e1b9fc83f81a819f29d11b85505d5 -
github.com/mavasani/roslyn/commit/24144442e4faa9c54fe2a4b519455a1a45c29569
如何做到这一点...
-
在 Visual Studio 2017 中打开
Roslyn.sln -
打开源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\BoundTree\BoundNodes.xml并在第 437 行添加以下BoundSwitchOperator定义:*
<Node Name="BoundSwitchOperator" Base="BoundExpression">
<!-- Non-null type is required for this node kind -->
<Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/>
<Field Name="Expression" Type="BoundExpression"/>
<Field Name="Labels" Type="ImmutableArray<BoundExpression>"/>
<Field Name="Values" Type="ImmutableArray<BoundExpression>"/>
</Node>
- 打开源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Binder\Binder_Expression.cs并在BindExpressionInternal方法中的第 535 行添加switch部分:
case SyntaxKind.SwitchExpression:
return BindSwitchOperator((SwitchExpressionSyntax)node, diagnostics);
-
从附加的代码示例源文件
CSharpCodeAnalysis\Binder_Operators.cs复制BindSwitchOperator和BindSwitchOperatorArguments方法的实现,并将其粘贴到源文件%REPO_ROOT%\src\Compilers\CSharp\Portable\Binder\Binder_Operators.cs的第 3521 行。 -
从附加的代码示例源文件
CSharpCodeAnalysis\Expression.cs复制BoundSwitchOperator的部分类型定义,并将其粘贴到源文件%REPO_ROOT%\src\Compilers\CSharp\Portable\BoundTree\Expression.cs的第 1221 行。 -
将新的源文件
%REPO_ROOT%\src\Compilers\CSharp\Portable\Lowering\LocalRewriter\LocalRewriter_SwitchOperator.cs添加到项目CSharpCodeAnalysis中,并使用从CSharpCodeAnalysis\LocalRewriter_SwitchOperator.cs复制的 switch 运算符降级实现作为占位符:
public override BoundNode VisitSwitchOperator(BoundSwitchOperator node)
{
// TODO: Implement lowering for switch operator.
return MakeLiteral(node.Syntax,
ConstantValue.Create($"CodeGen not yet implemented for: '{node.Syntax.ToString()}'"),
_compilation.GetSpecialType(SpecialType.System_String));
}
- 在
%REPO_ROOT%\src\Compilers\CSharp\Portable\FlowAnalysis\PreciseAbstractFlowPass_Switch.cs文件的第 260 行添加 switch 运算符的占位符流分析实现:
public override BoundNode VisitSwitchOperator(BoundSwitchOperator node)
{
// TODO: Implement flow analysis for switch operator.
return null;}
-
使用我们的本地更改构建项目
csc.csproj以生成%REPO_ROOT%\Binaries\Debug\Exes\csc\csc.exe。 -
创建一个新的源文件,例如
test.cs,并包含以下源代码:
class Class
{
public static void Main(string[] args)
{
System.Console.WriteLine(args.Length ?: [0, 1, 2] : ["Zero", "One", "Two", "More than two"]);
}}
-
使用本地构建的
csc.exe编译此源文件,并验证构建是否成功。 -
运行生成的可执行文件
test.exe并验证其运行良好,但由于 switch 运算符的占位符代码生成实现,输出仍然不是预期的结果:

您可以在github.com/mavasani/roslyn/commit/7a666595c8bf8d5e8c897540ec85ae3fa9fc5236 查看在此配方中做出的所有源代码更改。
它是如何工作的...
在此配方中,我们为 switch 运算符 (?:) 添加了基本的绑定/语义分析支持,这使得我们能够使用此新运算符编译和执行源代码。请注意,这并不是此运算符绑定阶段的全面实现,需要进一步的工作,例如增强错误报告。请参阅本配方中的下一节,还有更多...,以获取更多详细信息。
C# 绑定器负责对解析器生成的语法树进行语义分析。它将语法树转换为带有 BoundNodes 的绑定树,这本质上是一个与树中每个节点相关联的丰富语义信息的抽象语法树。
我们首先在模板文件 BoundNodes.xml 中添加了一个新的 BoundNode 定义,BoundSwitchOperator,:
<Node Name="BoundSwitchOperator" Base="BoundExpression">
<!-- Non-null type is required for this node kind -->
<Field Name="Type" Type="TypeSymbol" Override="true" Null="disallow"/>
<Field Name="Expression" Type="BoundExpression"/>
<Field Name="Labels" Type="ImmutableArray<BoundExpression>"/>
<Field Name="Values" Type="ImmutableArray<BoundExpression>"/>
</Node>
注意 switch 运算符的语法节点和绑定节点之间的区别。我们不再存储任何纯语法信息,例如问题冒号标记或参数列表周围的括号。switch 运算符所操作的表达式预期绑定到 BoundExpression,标签和值也预期是一个绑定表达式的列表。
构建 CSharpCodeAnalysis 项目作为预构建步骤运行生成器工具对 BoundNodes.xml 文件进行处理,以自动生成包含绑定节点源定义的 BoundNodes.generated.cs 文件。
在最新的 Roslyn 主分支中,CSharpCodeAnalysis 项目的构建过程中不再自动生成 BoundNodes.generated.cs,这是一个在 VS2017 之后的变化。在最新的源代码中,您必须显式运行以下脚本来自动生成此代码:github.com/dotnet/roslyn/blob/master/build/scripts/generate-compiler-code.cmd。
核心绑定支持涉及在 BindExpressionInternal 中追加切换情况,以处理 SyntaxKind.SwitchExpression 的语法节点并调用 BindSwitchOperator 方法。
我们向绑定器添加了新的方法 BindSwitchOperator 来处理 SwitchExpressionSyntax 节点的顶级绑定:
private BoundSwitchOperator ***BindSwitchOperator***(SwitchExpressionSyntax node, DiagnosticBag diagnostics)
{
BoundExpression switchExpr = BindValue(node.Expression, diagnostics, BindValueKind.RValue);
ImmutableArray<BoundExpression> labelsExpr = BindSwitchOperatorArguments(node.Labels, diagnostics);
ImmutableArray<BoundExpression> valuesExpr = BindSwitchOperatorArguments(node.Values, diagnostics);
// TODO: Add semantic validation for arguments and diagnostics.
TypeSymbol type = valuesExpr.Length > 1 ? valuesExpr[0].Type : CreateErrorType();
bool hasErrors = type.IsErrorType();
return new BoundSwitchOperator(node, switchExpr, labelsExpr, valuesExpr, type, hasErrors);
}
我们使用 BindValue (source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Binder/Binder_Expressions.cs,608d49de0066ede1,references) 调用来绑定切换操作符的表达式作为值。这保证了节点表达式是一个实际的具有值的表达式,而不是类型。例如,当违反此规则时,我们会得到以下语义错误:

我们添加了新的方法 BindSwitchOperatorArguments 来绑定切换操作符的 Labels 和 Values:
private ImmutableArray<BoundExpression> BindSwitchOperatorArguments(BracketedArgumentListSyntax node, DiagnosticBag diagnostics)
{
AnalyzedArguments analyzedArguments = AnalyzedArguments.GetInstance();
ImmutableArray<BoundExpression> arguments;
try
{
BindArgumentsAndNames(node, diagnostics, analyzedArguments);
arguments = BuildArgumentsForErrorRecovery(analyzedArguments);
}
finally
{
analyzedArguments.Free();
}
return arguments;
}
此方法调用现有的绑定器方法 BindArgumentsAndNames (source.roslyn.io/#q=BindArgumentsAndNames) 来绑定参数列表,然后构建错误恢复的参数。
我们还添加了 BoundSwitchOperator 的存根部分类型实现,以实现 IOperation (source.roslyn.io/#Microsoft.CodeAnalysis/Operations/IOperation.cs,7743f66521e66763) API:
// TODO: Implement IOperation support for switch operator.
internal partial class BoundSwitchOperator
{
protected override OperationKind ExpressionKind => OperationKind.None;
public override void Accept(OperationVisitor visitor)
{
visitor.VisitNoneOperation(this);
}
public override TResult Accept<TArgument, TResult>(OperationVisitor<TArgument, TResult> visitor, TArgument argument)
{
return visitor.VisitNoneOperation(this, argument);
}
}
IOperation 是一个正在编译器层中实现的新实验性功能,旨在将与编译器绑定节点相关的语义作为公开支持的 API 公开。截至 VS2017,该 API 未经发布或公开支持,未来版本可能会有所变化。
此外,我们还添加了流程分析和降级的存根实现,以便我们能够构建带有切换操作符的源代码,尽管生成的 MSIL 或编译可执行文件的结果与预期的最终结果不同。更具体地说,降级实现只是将整个 BoundSwitchOperator 节点替换为一个绑定的字符串字面量,表明尚未为新的切换操作符实现代码生成:
return MakeLiteral(node.Syntax,
ConstantValue.Create($"CodeGen not yet implemented for: '{node.Syntax.ToString()}'"),
_compilation.GetSpecialType(SpecialType.System_String));
参考本章下一节了解如何实现操作符降级支持。
还有更多...
当前 switch 操作符的绑定实现有一系列待办工作项,主要与更全面的语义验证和错误生成相关。要实现验证的项目包括:
-
添加对 switch 操作符表达式的类型进行语义验证,以确保其类型符合 switch 控制类型的需要;否则,生成编译时错误。
-
为参数列表验证添加新的编译器诊断。例如:
-
确保 values 中的表达式数量比 labels 中的表达式数量多一个;否则,生成编译时错误。
-
确保所有标签都是编译时常量,并且可以隐式转换为控制 switch 的类型。如果不是这样,将生成所需的编译时错误。
-
-
验证 Values 中的表达式类型是否可以隐式转换为表达式类型的公共类型 Z。
这些内容留作读者练习。关于在编译器代码库中实现新的语义错误的进一步指导,请参阅第八章中的食谱,“在 C#编译器代码库中实现新的语义错误”,在“向 Roslyn C#编译器开源代码贡献简单功能”一书中。
我们还添加了以下部分的基本存根实现,需要进一步改进:
IOperation支持 switch 操作符:这涉及到为 switch 表达式创建一个新的OperationKind(source.roslyn.io/#Microsoft.CodeAnalysis/Operations/IOperationKind.cs,bf7324631c03b2e7),添加一个新的接口,例如ISwitchChoiceExpression,具有以下 API 形状,然后在BoundSwitchOperator上实现此接口。
/// <summary>
/// Represents a C# switch operator.
/// </summary>
/// <remarks>
/// This interface is reserved for implementation by its associated APIs. We reserve the right to
/// change it in the future.
/// </remarks>
public interface ISwitchChoiceExpression : IOperation
{
/// <summary>
/// Switch expression to be tested.
/// </summary>
IOperation SwitchExpression { get; }
/// <summary>
/// List of labels to compare the switch expression against.
/// </summary>
ImmutableArray<IOperation> SwitchLabels { get; }
/// <summary>
/// List of values corresponding to the labels.
/// </summary>
ImmutableArray<IOperation> SwitchValues { get; }
}
-
添加对 switch 操作符的 lowering 支持:这将在下一道食谱中介绍。
-
添加对 switch 操作符的流分析支持:这在本书中没有介绍,但应该实现以确保在涉及 switch 操作符的代码中报告正确的流分析诊断。
实现对新 C#语言特性的 lowering/代码生成支持
Lowering是一个中间阶段,在绑定之后执行,将高级绑定树转换为简化绑定树。这些简化绑定树被提供给代码生成阶段,并转换为 MSIL 并输出到.NET 程序集。本节将使您能够添加对新 C#语言特性的 lowering 支持:Switch 操作符(?::)。这将使您能够编写、编译并正确执行使用新操作符的 C#程序。关于此操作符预期功能的具体信息,请参阅本章开头的部分,“新语言特性:Switch 操作符(?:😃”。关于此操作符的语法和语法定义的详细信息,请参阅本章的第一道食谱,“为新的 C#语言特性设计语法和语法”。
入门
您需要确保在您的机器上已注册并构建了带有 VS2017 标签的 Roslyn 源代码。有关进一步指导,请参阅第八章中的配方 设置 Roslyn 注册,向 Roslyn C# 编译器开源代码贡献简单功能。
此外,在您的注册中执行以下三个 git 提交以获取语法定义、解析器支持和绑定支持,分别,并构建 CSharpCodeAnalysis 项目:
-
github.com/mavasani/roslyn/commit/4b50f662c53e1b9fc83f81a819f29d11b85505d5 -
github.com/mavasani/roslyn/commit/24144442e4faa9c54fe2a4b519455a1a45c29569 -
github.com/mavasani/roslyn/commit/7a666595c8bf8d5e8c897540ec85ae3fa9fc5236
如何做到这一点...
-
在 Visual Studio 2017 中打开
Roslyn.sln。 -
从附件中的代码示例源文件
CSharpCodeAnalysis\LocalRewriter_SwitchOperator.cs复制VisitSwitchOperator和RewriteSwitchOperator的方法实现,并将它们粘贴到源文件%REPO_ROOT%\src\Compilers\CSharp\Portable\Lowering\LocalRewriter\LocalRewriter_SwitchOperator.cs中。 -
使用本地更改构建项目
csc.csproj以生成%REPO_ROOT%\Binaries\Debug\Exes\csc\csc.exe。 -
创建一个新的源代码文件,例如
test.cs,包含以下源代码:
class Class
{
public static void Main(string[] args)
{
System.Console.WriteLine(args.Length ?: [0, 1, 2] : ["Zero", "One", "Two", "More than two"]);
}
}
-
使用本地构建的
csc.exe编译此源代码文件并验证构建是否成功。 -
使用不同数量的参数运行生成的可执行文件
test.exe并验证相应的输出是否符合预期的开关操作符。

- 执行
ildasm.exe test.exe命令并验证生成的可执行文件的 MSIL 包含对开关表达式与标签列表的顺序检查以及对相应值的条件分支。
.method public hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 51 (0x33)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldlen
IL_0003: conv.i4
IL_0004: brfalse.s IL_0027
IL_0006: ldarg.0
IL_0007: ldlen
IL_0008: conv.i4
IL_0009: ldc.i4.1
IL_000a: beq.s IL_0020
IL_000c: ldarg.0
IL_000d: ldlen
IL_000e: conv.i4
IL_000f: ldc.i4.2
IL_0010: beq.s IL_0019
IL_0012: ldstr "More than two"
IL_0017: br.s IL_001e
IL_0019: ldstr "Two"
IL_001e: br.s IL_0025
IL_0020: ldstr "One"
IL_0025: br.s IL_002c
IL_0027: ldstr "Zero"
IL_002c: call void [mscorlib]System.Console::WriteLine(string)
IL_0031: nop
IL_0032: ret
} // end of method Class::Main
您可以在github.com/mavasani/roslyn/commit/2c1ec4dc60ab0a64b7e9c01d1ec9a1fbcaa611da查看此配方中进行的所有源代码更改。
它是如何工作的...
在此配方中,我们为开关操作符 (?:) 添加了基本降级支持,这使得我们能够编译和执行带有此新操作符的源代码,并给出预期的运行时结果。请注意,这不是此操作符降级/代码生成阶段的最佳实现,需要进一步工作以生成优化的 MSIL。这留给读者作为练习。
C#降低阶段负责将绑定器中的初始绑定树转换为更简单的绑定树,以便代码生成阶段可以操作。代码生成阶段在降低的绑定树上操作,并将其转换为 MSIL。在本配方中,我们添加了对开关操作符的降低支持,将开关操作符重写为嵌套条件分支。
让我们通过一个例子来澄清降低算法。考虑我们配方中使用的开关表达式:
args.Length ?: [0, 1, 2] : ["Zero", "One", "Two", "More than two"]
此操作符被重写为以下降低(伪)代码:
args.Length == 0
jump to label Val0
args.Length == 1
jump to label Val1
args.Length == 2
jump to label Val2
result = "More than two"
jump to label Exit
Val2:
result = "Two"
jump to label Exit
Val1:
result = "One"
jump to label Exit
Val0:
result = "Zero"
jump to label Exit
Exit:
我们遍历每个常量,并将表达式的值与该常量进行比较。如果成功,则跳转到标签并评估相应的开关操作符值,将其加载到结果中,并跳转到退出标签。如果检查失败,则递归地对剩余的标签和值进行操作,直到表达式不匹配任何常量,然后评估最后一个(默认)值。
现在我们来详细说明在降低阶段添加的代码,该代码实现了前面的算法。LocalRewriter类型实现了绑定树降低/重写。此类型本质上是对BoundTreeRewriter(source.roslyn.io/#q=BoundTreeRewriter)的实现,它使用访问者模式遍历整个绑定树。它为每个绑定节点提供了一个可重写的*VisitXXX*方法,将其转换为更简单的重写绑定节点并返回该节点。我们按照以下方式重写VisitSwitchOperator方法:
/// <summary>
/// Rewrite switch operator into nested conditional operators.
/// </summary>
public override BoundNode VisitSwitchOperator(BoundSwitchOperator node)
{
// just a fact, not a requirement (VisitExpression would have rewritten otherwise)
Debug.Assert(node.ConstantValue == null);
var rewrittenExpression = VisitExpression(node.Expression);
var rewrittenLabels = node.Labels.SelectAsArray(l => VisitExpression(l));
var rewrittenValues = node.Values.SelectAsArray(l => VisitExpression(l));
var rewrittenType = VisitType(node.Type);
var booleanType = _compilation.GetSpecialType(SpecialType.System_Boolean);
return RewriteSwitchOperator(
node.Syntax,
rewrittenExpression,
rewrittenLabels,
rewrittenValues,
rewrittenType,
booleanType);
}
绑定树重写器的一般要求和模式是首先访问绑定节点的每个子节点,并使用重写的子节点进行核心重写功能。我们首先重写开关表达式,然后是标签、值和表达式类型。我们还获取众所周知的 System.Boolean 类型,用于重写辅助器。我们将所有这些值传递到核心重写方法RewriteSwitchOperator:
private static BoundExpression RewriteSwitchOperator(
SyntaxNode syntax,
BoundExpression rewrittenExpression,
ImmutableArray<BoundExpression> rewrittenLabels,
ImmutableArray<BoundExpression> rewrittenValues,
TypeSymbol rewrittenType,
TypeSymbol booleanType)
{
Debug.Assert(rewrittenLabels.Length >= 1);
Debug.Assert(rewrittenLabels.Length + 1 == rewrittenValues.Length);
var label = rewrittenLabels[0];
var consequence = rewrittenValues[0];
var condition = new BoundBinaryOperator(label.Syntax, BinaryOperatorKind.Equal, rewrittenExpression, label, null, null, LookupResultKind.Viable, booleanType);
BoundExpression alternative = rewrittenLabels.Length > 1 ?
RewriteSwitchOperator(syntax, rewrittenExpression, rewrittenLabels.RemoveAt(0), rewrittenValues.RemoveAt(0), rewrittenType, booleanType) :
rewrittenValues[1];
return new BoundConditionalOperator(label.Syntax, condition, consequence, alternative, null, rewrittenType);
}
重写方法首先验证我们正在操作一个具有一个或多个标签的开关操作符,并且值的数量比值的数量多一个(否则我们会生成一个绑定错误,降低阶段就不会执行)。
此方法使用递归方法重写开关操作符。我们首先生成一个具有==操作符的BoundBinaryOperator。rewrittenExpression是操作符的左侧,rewrittenLabels中的第一个标签是右侧。这形成了我们的condition绑定节点。rewrittenValues列表中的第一个值是consequence。
如果我们有多个rewrittenLabels,则递归地使用除了每个列表中的第一个之外的所有剩余rewrittenLabels和rewrittenValues调用RewriteSwitchOperator,这成为alternative。否则,当前rewrittenValues列表中的第二个标签成为替代品。
最后,我们使用前面的 condition (BoundBinaryOperator)、consequence(首先重写的值)和 alternative(表达式其余部分的递归重写)来创建 BoundConditionalOperator condition ? consequence : alternative 并将其作为最终重写的节点返回*。
由于降低的边界树没有新的边界节点类型,我们不需要添加任何新的代码生成支持(它已经处理了条件分支)。有关代码生成器的实现细节,请参阅 CodeGenerator (source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/CodeGen/CodeGenerator.cs,8838d807a9a1d615) 类型。
为 C# 解析、绑定和代码生成阶段编写单元测试
本节将指导你为新的 C# 语言特性添加单元测试:Switch 操作符 (?::)。有关此操作符预期功能的信息,请参阅本章开头的部分,新语言特性:Switch 操作符 (?:😃。
C# 编译器在 Roslyn.sln 中有以下一系列单元测试项目:
-
CSharpCompilerSyntaxTest: 这项单元测试用于解析和语法错误。 -
CSharpCompilerSemanticTest: 这项单元测试用于语义错误和语义模型 API。 -
CSharpCompilerSymbolTest: 这项单元测试用于编译器层定义的符号。 -
CSharpCommandLineTest: 这项单元测试用于检查编译器的命令行选项。 -
CSharpCompilerEmitTest: 这项单元测试用于代码生成阶段,该阶段验证生成的 MSIL。
在本节中,我们将为 CSharpCompilerSyntaxTest、CSharpCompilerSemanticTest 和 CSharpCompilerEmitTest 分别添加单元测试,用于解析、绑定和代码生成支持。
入门指南
你需要确保在你的机器上已经列出了带有 VS2017 标签的 Roslyn 源代码,并已构建。有关进一步指导,请参阅第八章,设置 Roslyn 列表,中的配方。
此外,在你的列表中还需要以下四个 git 提交以获取新操作符的语法定义、解析器支持、绑定器支持和降低支持,并构建 CSharpCodeAnalysis 项目:
-
github.com/mavasani/roslyn/commit/4b50f662c53e1b9fc83f81a819f29d11b85505d5 -
github.com/mavasani/roslyn/commit/24144442e4faa9c54fe2a4b519455a1a45c29569 -
github.com/mavasani/roslyn/commit/7a666595c8bf8d5e8c897540ec85ae3fa9fc5236 -
github.com/mavasani/roslyn/commit/2c1ec4dc60ab0a64b7e9c01d1ec9a1fbcaa611da
如何做到这一点...
-
在 Visual Studio 2017 中打开
Roslyn.sln。 -
打开源文件
<%REPO_ROOT%>\src\Compilers\CSharp\Test\Syntax\Parsing\ExpressionParsingTests.cs。 -
[解析测试] 在源文件末尾添加以下新的单元测试:
[Fact]
public void TestSwitchExpression()
{
var text = @"expr ?: [0, 1, 2] : [""Zero"", ""One"", ""Two"", ""More than two""]";
var expr = SyntaxFactory.ParseExpression(text);
Assert.NotNull(expr);
Assert.Equal(SyntaxKind.SwitchExpression, expr.Kind());
Assert.Equal(text, expr. ToString());
Assert.Equal(0, expr.Errors().Length);
var switchExpr = (SwitchExpressionSyntax)expr;
Assert.NotNull(switchExpr.Expression);
Assert.Equal("expr", switchExpr.Expression.ToString());
Assert.NotNull(switchExpr.QuestionColonToken);
Assert.False(switchExpr.QuestionColonToken.IsMissing);
Assert.NotNull(switchExpr.Labels.OpenBracketToken);
Assert.False(switchExpr.Labels.OpenBracketToken.IsMissing);
Assert.Equal(3, switchExpr.Labels.Arguments.Count);
Assert.Equal("0", switchExpr.Labels.Arguments[0].ToString());
Assert.Equal("1", switchExpr.Labels.Arguments[1].ToString());
Assert.Equal("2", switchExpr.Labels.Arguments[2].ToString());
Assert.NotNull(switchExpr.Labels.CloseBracketToken);
Assert.False(switchExpr.Labels.CloseBracketToken.IsMissing);
Assert.NotNull(switchExpr.ColonToken);
Assert.False(switchExpr.ColonToken.IsMissing);
Assert.NotNull(switchExpr.Values.OpenBracketToken);
Assert.False(switchExpr.Values.OpenBracketToken.IsMissing);
Assert.Equal(4, switchExpr.Values.Arguments.Count);
Assert.Equal(@"""Zero""", switchExpr.Values.Arguments[0].ToString());
Assert.Equal(@"""One""", switchExpr.Values.Arguments[1].ToString());
Assert.Equal(@"""Two""", switchExpr.Values.Arguments[2].ToString());
Assert.Equal(@"""More than two""", switchExpr.Values.Arguments[3].ToString());
Assert.NotNull(switchExpr.Values.CloseBracketToken);
Assert.False(switchExpr.Values.CloseBracketToken.IsMissing);
}
CreateCompilationWithMscorlib(source).VerifyDiagnostics();
}
- 构建测试项目
CSharpCompilerSyntaxTest,并在命令行控制台使用从项目的Debug属性页复制的命令行执行单元测试,并为新添加的单元测试添加-method开关:
<%USERS_FOLDER%>\.nuget\packages\xunit.runner.console\2.2.0-beta4-build3444\tools\xunit.console.x86.exe "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSyntaxTest\Roslyn.Compilers.CSharp.Syntax.UnitTests.dll" -html "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSyntaxTest\xUnitResults\Roslyn.Compilers.CSharp.Syntax.UnitTests.html" -noshadow -method Microsoft.CodeAnalysis.CSharp.UnitTests.ExpressionParsingTexts.TestSwitchExpression
- 验证单元测试成功通过:

如果您收到DirectoryNotFoundException,请确保测试结果目录存在于机器上:<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSyntaxTest\xUnitResults。
-
[绑定测试] 打开源文件
<%REPO_ROOT%>\src\Compilers\CSharp\Test\Semantic\Semantics\BindingTests.cs -
将以下新的单元测试添加到源文件中:
[Fact]
public void TestSwitchExpressionBinding()
{
var source =
@"
class Class
{
public static void Main(string[] args)
{
System.Console.WriteLine(args.Length ?: [0, 1, 2] : [""Zero"", ""One"", ""Two"", ""More than two""]);
}
}
";
var compilation = CreateCompilationWithMscorlib(source);
compilation.VerifyDiagnostics();
var tree = compilation.SyntaxTrees[0];
var model = compilation.GetSemanticModel(tree);
var switchExp = (SwitchExpressionSyntax)tree.GetRoot().DescendantNodes().Where(n => n.IsKind(SyntaxKind.SwitchExpression)).Single();
Assert.Equal(@"args.Length ?: [0, 1, 2] : [""Zero"", ""One"", ""Two"", ""More than two""]", switchExp.ToString());
var symbolInfo = model.GetSymbolInfo(switchExp);
Assert.Null(symbolInfo.Symbol);
var typeInfo = model.GetTypeInfo(switchExp);
Assert.NotNull(typeInfo.Type);
Assert.Equal("string", typeInfo.Type.ToString());
symbolInfo = model.GetSymbolInfo(switchExp.Expression);
Assert.NotNull(symbolInfo.Symbol);
Assert.Equal("System.Array.Length", symbolInfo.Symbol.ToString());
typeInfo = model.GetTypeInfo(switchExp.Expression);
Assert.NotNull(typeInfo.Type);
Assert.Equal("int", typeInfo.Type.ToString());
Assert.Equal(3, switchExp.Labels.Arguments.Count);
var constantValue = model.GetConstantValue(switchExp.Labels.Arguments[0].Expression);
Assert.True(constantValue.HasValue);
Assert.Equal(0, constantValue.Value);
typeInfo = model.GetTypeInfo(switchExp.Labels.Arguments[0].Expression);
Assert.NotNull(typeInfo.Type);
Assert.Equal("int", typeInfo.Type.ToString());
Assert.Equal(4, switchExp.Values.Arguments.Count);
constantValue = model.GetConstantValue(switchExp.Values.Arguments[0].Expression);
Assert.True(constantValue.HasValue);
Assert.Equal("Zero", constantValue.Value);
typeInfo = model.GetTypeInfo(switchExp.Values.Arguments[0].Expression);
Assert.NotNull(typeInfo.Type);
Assert.Equal("string", typeInfo.Type.ToString());
}
- 构建测试项目
CSharpCompilerSemanticTest,并在命令行控制台使用从项目的Debug属性页复制的命令行执行单元测试,并为新添加的单元测试添加-method开关:
<%USERS_FOLDER%>\.nuget\packages\xunit.runner.console\2.2.0-beta4-build3444\tools\xunit.console.x86.exe "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\Roslyn.Compilers.CSharp.Semantic.UnitTests.dll" -html "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerSemanticTest\xUnitResults\Roslyn.Compilers.CSharp.Semantic.UnitTests.html" -noshadow -method Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics.BindingTests.TestSwitchExpressionBinding
-
验证单元测试成功通过。
-
[代码生成测试] 打开源文件
<%REPO_ROOT%>\src\Compilers\CSharp\Test\Emit\CodeGen\CodeGenTests.cs。 -
将以下新的单元测试添加到源文件中:
[Fact]
public void TestSwitchExpressionCodeGen()
{
string source = @"
class Class
{
public static void Main(string[] args)
{
System.Console.WriteLine(args.Length ?: [0, 1, 2] : [""Zero"", ""One"", ""Two"", ""More than two""]);
}
}";
var compilation = CompileAndVerify(source, options: TestOptions.DebugExe);
compilation.VerifyIL("Class.Main", @"
{
// Code size 51 (0x33)
.maxstack 2
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldlen
IL_0003: conv.i4
IL_0004: brfalse.s IL_0027
IL_0006: ldarg.0
IL_0007: ldlen
IL_0008: conv.i4
IL_0009: ldc.i4.1
IL_000a: beq.s IL_0020
IL_000c: ldarg.0
IL_000d: ldlen
IL_000e: conv.i4
IL_000f: ldc.i4.2
IL_0010: beq.s IL_0019
IL_0012: ldstr ""More than two""
IL_0017: br.s IL_001e
IL_0019: ldstr ""Two""
IL_001e: br.s IL_0025
IL_0020: ldstr ""One""
IL_0025: br.s IL_002c
IL_0027: ldstr ""Zero""
IL_002c: call ""void System.Console.WriteLine(string)""
IL_0031: nop
IL_0032: ret
}
");
}
- 构建测试项目
CSharpCompilerEmitTest,并在命令行控制台使用从项目的Debug属性页复制的命令行执行单元测试,并为新添加的单元测试添加-method开关:
<%USERS_FOLDER%>\.nuget\packages\xunit.runner.console\2.2.0-beta4-build3444\tools\xunit.console.x86.exe "<%REPO_ROOT%>\Binaries\Debug\UnitTests\CSharpCompilerEmitTest\xUnitResults\Roslyn.Compilers.CSharp.Emit.UnitTests.html" -noshadow -method Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen.CodeGenTests.TestSwitchExpressionCodeGen
- 验证单元测试成功通过。
您也可以使用 Visual Studio 中的测试资源管理器窗口执行单元测试,但由于解决方案中包含数千个单元测试,因此对*Roslyn.sln*的测试发现相当慢。因此,您可能需要等待几分钟才能执行第一个单元测试。
您可以在github.com/mavasani/roslyn/commit/ca1b555aef3d3f5dbe4efecda3580822d382a56f查看此配方中做出的所有源代码更改。
第十章:基于 Roslyn API 的命令行工具
在本章中,我们将介绍以下食谱:
-
基于编译器语法 API 编写应用程序以解析和转换源文件
-
基于编译器语义 API 编写应用程序以显示诊断和重载解析结果
-
基于编译器分析 API 编写应用程序以执行诊断分析器和显示分析器诊断
-
基于工作空间 API 编写应用程序以格式化和简化解决方案中的所有源文件
-
基于工作空间 API 编写应用程序以编辑解决方案中的项目并显示项目属性
简介
本章使开发者能够使用 Roslyn 编译器和工作空间 API 编写命令行工具,用于分析和/或编辑 C#代码。(github.com/dotnet/roslyn/wiki/Roslyn%20Overview)中的文章为这些层的 Roslyn API 提供了非常好的介绍。
我们将为您提供文章中的代码片段:

-
编译器 API:编译器层包含与编译器管道每个阶段公开的信息相对应的对象模型,包括语法和语义。编译器层还包含编译器单次调用的不可变快照,包括程序集引用、编译器选项和源代码文件。有两个不同的 API 代表 C#语言和 Visual Basic 语言。这两个 API 在形状上相似,但针对每个语言进行了定制,以实现高保真度。这一层不依赖于 Visual Studio 组件。
-
工作空间 API:工作空间层包含工作空间 API,这是在整个解决方案上进行代码分析和重构的起点。它帮助您将解决方案中所有关于项目的信息组织到一个单一的对象模型中,提供直接访问编译层对象模型的能力,无需解析文件、配置选项或管理项目间的依赖关系。这一层不依赖于 Visual Studio 组件。
基于编译器语法 API 编写应用程序以解析和转换源文件
在本节中,我们将编写一个基于 Roslyn 编译器 API 的 C#控制台应用程序,将给定的源文件解析为语法树,然后执行以下语法转换:
-
编辑所有没有显式可访问性修饰符的类声明,添加内部修饰符。
-
将所有没有文档注释的公共类声明添加文档注释语法技巧。
-
删除没有成员的空类声明。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此菜谱。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装免费的社区版。
如何做到这一点...
-
打开 Visual Studio 并创建一个新的针对.NET Framework 4.6 或更高版本的 C#控制台应用程序,例如
ConsoleApp.。 -
安装
Microsoft.CodeAnalysis.CSharpNuGet 包(截至本文写作,最新稳定版本是2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章中通过 NuGet 包管理器搜索和安装分析器的菜谱,在.NET 项目中使用诊断分析器。 -
将
Program.cs中的源代码替换为附带的代码示例\ConsoleApp\Program.cs中的源代码。 -
构建项目。
-
打开 Visual Studio 开发者命令提示符,将目录更改为项目根目录,然后不带参数执行
bin\Debug\ConsoleApp.exe。 -
验证输出显示不正确的用法:
Usage: ConsoleApp.exe <%file_path%> -
在项目根目录下创建一个文本文件,例如
test.cs,包含以下代码:
// Class with no accessibility modifier
class C1
{
void M() {}
}
// Public class with no documentation comments
public class C2
{
void M() {}
}
// Empty class with no members
public class C3
{
}
-
现在,使用
test.cs作为参数执行应用程序:bin\Debug\ConsoleApp.exe test.cs。 -
验证预期的转换后的源代码在输出中显示:

它是如何工作的...
在这个菜谱中,我们基于 Roslyn 编译器 API 编写了一个 C#控制台应用程序来解析和转换源文本。如前所述,我们的应用程序演示了对解析树的三个核心语法操作:编辑、添加和删除。让我们逐行代码来了解我们如何实现这些操作:
public static void Main(string[] args)
{
// Parse arguments to get source file.
var filePath = ParseArguments(args);
if (filePath == null)
{
return;
}
// Parse text into SyntaxTree.
var tree = Parse(filePath);
var root = (CompilationUnitSyntax)tree.GetRoot();
// Transform syntax tree to edit/add/remove syntax.
root = EditClassDeclarations(root);
root = AddDocCommentsToClassDeclarations(root);
root = RemoveEmptyClassDeclarations(root);
Console.WriteLine("Transformed source:" + Environment.NewLine);
Console.WriteLine(root.ToFullString());
}
Main方法调用单个方法执行以下操作:
-
ParseArguments用于扫描要解析和转换的输入文件。 -
从此源文件中读取文本并将其
Parse成语法树。 -
获取解析树的编译单元根并对其执行以下转换:
-
EditClassDeclarations:向没有可访问性修饰符的类添加内部修饰符。 -
AddDocCommentsToClassDeclarations:向没有文档注释的公共类添加占位符文档注释。 -
RemoveEmptyClassDeclarations:删除没有成员的类声明。
-
-
在控制台上显示转换后的文本。
ParseArguments方法期望一个参数,该参数必须是磁盘上现有文件的完整路径。否则,它将显示错误并退出,返回 null。
private static string ParseArguments(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine(@"Usage: ConsoleApp.exe <%file_path%>");
return null;
}
if (!File.Exists(args[0]))
{
Console.WriteLine($"File '{args[0]}' does not exist");
return null;
}
return args[0];
}
Parse方法从输入文件读取文件内容,并使用解析 API CSharpSyntaxTree.ParseText进行解析。
private static SyntaxTree Parse(string filePath)
{
var text = File.ReadAllText(filePath);
return CSharpSyntaxTree.ParseText(text);
}
EditClassDeclarations 方法遍历根节点的后代节点以找到所有修饰符列表没有可访问修饰符(公共、私有、内部或受保护)的类声明节点。
private static CompilationUnitSyntax EditClassDeclarations(CompilationUnitSyntax root)
{
// Get class declarations with no accessibility modifier.
var classDeclarations = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => !c.Modifiers.Any(m => SyntaxFacts.IsAccessibilityModifier(m.Kind())));
// Add modifier to these class declarations and replace in the original tree.
return root.ReplaceNodes(classDeclarations,
computeReplacementNode: (o, n) => AddModifier(n));
}
然后,它对根语法节点调用 ReplaceNodes API,用 AddModifier 辅助函数返回的更新节点替换每个此类声明。此辅助函数在类声明的当前修饰符列表开头添加一个新的内部修饰符。它还负责将类声明的任何现有前导杂注移动到新修饰符,并在新修饰符后添加一个空白杂注:
private static ClassDeclarationSyntax AddModifier(ClassDeclarationSyntax classDeclaration)
{
var internalModifier = SyntaxFactory.Token(SyntaxKind.InternalKeyword)
.WithTrailingTrivia(SyntaxFactory.Whitespace(" "));
if (classDeclaration.HasLeadingTrivia)
{
// Move leading trivia for the class declaration to the new modifier.
internalModifier = internalModifier.WithLeadingTrivia(classDeclaration.GetLeadingTrivia());
classDeclaration = classDeclaration.WithLeadingTrivia();
}
var newModifiers = classDeclaration.Modifiers.Insert(0, internalModifier);
return classDeclaration.WithModifiers(newModifiers);
}
AddDocCommentsToClassDeclarations 方法遍历根节点的后代节点以找到所有修饰符列表具有公共可访问修饰符且其第一个标记没有前导文档注释杂注的类声明节点。
private static CompilationUnitSyntax AddDocCommentsToClassDeclarations(CompilationUnitSyntax root)
{
// Get public class declarations with no documentation comments.
var classDeclarations = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Modifiers.Any(m => m.Kind() == SyntaxKind.PublicKeyword) &&
!c.GetFirstToken().LeadingTrivia.Any(IsDocumentationComment));
// Add stub documentation comment to these class declarations and replace in the original tree.
return root.ReplaceNodes(classDeclarations,
computeReplacementNode: (o, n) => AddDocumentationComment(n));
}
然后,它对根语法节点调用 ReplaceNodes API,用 AddDocumentationComment 辅助函数返回的更新节点替换每个此类声明。此辅助函数创建一个带有 TODO 注释的 XML 摘要元素,并创建一个单行文档注释,包含此存根摘要元素,并将其添加到类声明的当前前导杂注的末尾。
private static ClassDeclarationSyntax *AddDocumentationComment*(ClassDeclarationSyntax classDeclaration)
{
var summaryElement = SyntaxFactory.XmlSummaryElement(SyntaxFactory.XmlText("TODO: Add doc comments"));
var documentationComment = SyntaxFactory.DocumentationComment(summaryElement);
var newLeadingTrivia = classDeclaration.GetLeadingTrivia()
.Add(SyntaxFactory.Trivia(documentationComment))
.Add(SyntaxFactory.EndOfLine(Environment.NewLine));
return classDeclaration.WithLeadingTrivia(newLeadingTrivia);
}
RemoveEmptyClassDeclarations 方法遍历根节点的后代节点以找到所有没有成员声明(方法、字段、嵌套类型等)的类声明节点,然后对根语法节点调用 ReplaceNodes API 来删除所有此类声明。
private static CompilationUnitSyntax RemoveEmptyClassDeclarations(CompilationUnitSyntax root)
{
// Get class declarations with no members.
var classDeclarations = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.Members.Count == 0);
// Remove these class declarations from the original tree.
return root.RemoveNodes(classDeclarations, SyntaxRemoveOptions.KeepNoTrivia);
}
基于编译器语义 API 编写应用程序以显示诊断和重载解析结果
在本节中,我们将编写一个基于 Roslyn 编译器 API 的 C# 控制台应用程序,从给定的源文件创建编译,然后执行以下语义分析:
-
计算并显示如果文件被编译,C# 编译器将生成的编译诊断信息。
-
计算源文件中每个调用(方法调用)的符号信息,并显示每个调用以下语义信息:
-
重载解析结果 (
msdn.microsoft.com/en-us/library/aa691336(v=vs.71).aspx):成功或失败的原因。 -
如果重载解析成功,则将方法符号绑定到调用。
-
否则,如果重载解析失败并且我们有多个候选符号,则显示每个候选符号。
-
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
-
打开 Visual Studio 并创建一个新的 C# 控制台应用程序,目标为 .NET Framework 4.6 或更高版本,例如
ConsoleApp.。 -
安装
Microsoft.CodeAnalysis.CSharpNuGet 包(截至本文写作,最新稳定版本为 2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章 通过 NuGet 包管理器搜索和安装分析器 中的菜谱 在 .NET 项目中消费诊断分析器。 -
将
Program.cs中的源代码替换为附带的代码示例中的源代码\ConsoleApp\Program.cs。 -
构建项目。
-
打开 Visual Studio 开发者命令提示符,将目录更改为项目根目录,然后不带参数执行
bin\Debug\ConsoleApp.exe。 -
验证输出显示不正确的用法:
Usage: ConsoleApp.exe <%file_path%>。 -
在项目根目录中创建一个文本文件,例如
test.cs,包含以下代码:
class C1
{
void F()
{
M1();
M2(0);
M2(null);
}
void M1()
{
}
void M2()
{
}
void M2(int x)
{
}
}
-
现在,使用
test.cs作为参数执行应用程序:bin\Debug\ConsoleApp.exe test.cs。 -
验证预期的诊断和重载解析结果显示在输出中:

如何工作...
在这个菜谱中,我们基于 Roslyn 编译器 API 编写了一个 C# 控制台应用程序,用于创建编译、分析和显示调用表达式的诊断和重载解析语义。这些操作与 C# 编译器在编译源代码时所做的操作非常相似。让我们逐行分析代码,了解我们是如何实现这些操作的:
public static void Main(string[] args)
{
// Parse arguments to get source file.
var filePath = ParseArguments(args);
if (filePath == null)
{
return;
}
// Parse text and create a compilation.
var compilation = CreateCompilation(filePath);
// Display diagnostics in the compilation.
DisplayDiagnostics(compilation);
// Display semantic information about invocations in the file.
DisplayInvocations(compilation);
}
Main 方法调用单个方法执行以下操作:
-
ParseArguments用于扫描要解析和转换的输入文件。 -
使用
CreateCompilation从此源文件中的文本创建的解析语法树创建 C# 编译。 -
使用
DisplayDiagnostics计算编译器诊断,然后显示它们。 -
使用
DisplayInvocations分析语法树中的每个调用(方法调用)并显示重载解析结果和绑定符号。
ParseArguments 的实现与之前的菜谱 基于编译器语法 API 编写解析和转换源文件的程序 中的实现相同。有关此方法的进一步解释,请参阅该菜谱的 如何工作... 部分。
CreateCompilation 方法首先从输入文件中读取文件内容,并使用解析 API CSharpSyntaxTree.ParseText 进行解析。然后,它使用包含对象类型的程序集的位置为系统程序集(corlib)创建元数据引用。它创建输出类型为 DynamicallyLinkedLibrary(.dll)的编译选项,并使用这些输入创建 C# 编译:
private static Compilation CreateCompilation(string filePath)
{
var text = File.ReadAllText(filePath);
var tree = CSharpSyntaxTree.ParseText(text);
var systemAssembly = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
var options = new CSharpCompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary);
return CSharpCompilation.Create("TestAssembly",
syntaxTrees: new[] { tree },
references: new[] { systemAssembly },
options: options);
}
DisplayDiagnostics 方法使用 Compilation.GetDiagnostics API 计算编译器诊断,然后显示每个诊断的数量和字符串表示形式。
private static void DisplayDiagnostics(Compilation compilation)
{
var diagnostics = compilation.GetDiagnostics();
Console.WriteLine($"Number of diagnostics: {diagnostics.Length}");
foreach (var diagnostic in diagnostics)
{
Console.WriteLine(diagnostic.ToString());
}
Console.WriteLine();
}
DisplayInvocations 方法首先获取编译中的语法树的语义模型。然后,它遍历根的子节点以获取所有 InvocationExpressionSyntax 节点。对于每个这样的调用,它从语义模型中查询表达式的符号信息。符号信息包含有关调用语义的信息。我们根据候选原因是否为 CandidateReason.None 来显示重载解析成功/失败结果。然后,我们分别显示成功和失败情况下的绑定符号或候选符号:
private static void DisplayInvocations(Compilation compilation)
{
var tree = compilation.SyntaxTrees.Single();
var semanticModel = compilation.GetSemanticModel(tree);
var invocations = tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>();
foreach (var invocation in invocations)
{
Console.WriteLine($"Invocation: '{invocation.ToString()}'");
var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var overloadResolutionResult = symbolInfo.CandidateReason == CandidateReason.None ? "Succeeded" : symbolInfo.CandidateReason.ToString();
Console.WriteLine($" Overload resolution result: {overloadResolutionResult}");
if (symbolInfo.Symbol != null)
{
Console.WriteLine($" Method Symbol: {symbolInfo.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}");
}
else if (!symbolInfo.CandidateSymbols.IsDefaultOrEmpty)
{
Console.WriteLine($" {symbolInfo.CandidateSymbols.Length} candidate symbols:");
foreach (var candidate in symbolInfo.CandidateSymbols)
{
Console.WriteLine($" Candidate Symbol: {candidate.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)}");
}
}
Console.WriteLine();
}
}
基于编译器分析器 API 编写应用程序以执行诊断分析器和显示分析器诊断
在本节中,我们将编写一个基于 Roslyn 编译器 API 的 C# 控制台应用程序,该程序加载给定的分析器程序集,在给定的源文件上执行该程序集中定义的所有诊断分析器,并输出所有报告的分析器诊断。
入门
您需要在您的机器上安装 Visual Studio 2017 社区版才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的社区版。
如何操作...
-
打开 Visual Studio 并创建一个新的针对 .NET Framework 4.6 或更高版本的 C# 控制台应用程序,例如
ConsoleApp。 -
安装
Microsoft.CodeAnalysis.CSharpNuGet 包(截至本文撰写时,最新稳定版本为 2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章 在 .NET 项目中消费诊断分析器 中的配方 通过 NuGet 包管理器搜索和安装分析器。 -
将
Program.cs中的源代码替换为附带的代码示例中的源代码\ConsoleApp\Program.cs。 -
将一个针对 .NET Framework 4.6 或更高版本的 C# 类库项目添加到解决方案中,例如 Analyzer。
-
将
Microsoft.CodeAnalysisNuGet 包安装到该项目中(截至本文撰写时,最新稳定版本为 2.1.0)。 -
将
Class1.cs中的源代码替换为附带的代码示例\Analyzer\Class1.cs中的诊断分析器源代码。此文件包含用于报告包含任何小写字母的类型名称的诊断的默认符号分析器的代码。 -
构建解决方案。
-
打开 Visual Studio 开发者命令提示符,将目录更改为
ConsoleApp的项目根目录,并使用无参数执行bin\Debug\ConsoleApp.exe。 -
验证输出显示不正确的用法:
用法:ConsoleApp.exe <%analyzer_file_path%> <%source_file_path%>。 -
在
ConsoleApp的项目根目录中创建一个文本文件,例如test.cs,并包含以下代码:
class ClassWithLowerCase
{
}
class OuterClassWithLowerCase
{
class NestedClassWithLowerCase
{
}
}
class CLASS_WITH_UPPER_CASE
{
}
-
现在,使用分析器程序集的相对路径和
test.cs作为参数执行应用程序:bin\Debug\ConsoleApp.exe ..\..\..\Analyzer\bin\Debug\Analyzer.dll test.cs。 -
验证预期的分析器诊断是否显示在输出中:
Number of diagnostics: 3
(1,7): warning CSharpAnalyzers: Type name 'ClassWithLowerCase' contains lowercase letters
(5,7): warning CSharpAnalyzers: Type name 'OuterClassWithLowerCase' contains lowercase letters
(7,9): warning CSharpAnalyzers: Type name 'NestedClassWithLowerCase' contains lowercase letters
如何工作...
在这道菜谱中,我们编写了一个基于 Roslyn 编译器 API 的 C# 控制台应用程序,用于从分析器程序集加载和执行诊断分析器,并报告分析器报告的诊断。这些操作与你在使用 /analyzer:<%analyzer_file_path%> 命令行开关编译源文件时 C# 编译器所执行的操作非常相似。让我们通过代码了解我们如何实现这些操作:
public static void Main(string[] args)
{
// Parse arguments to get analyzer assembly file and source file.
var files = ParseArguments(args);
if (files.analyzerFile == null || files.sourceFile == null)
{
return;
}
// Parse source file and create a compilation.
var compilation = CreateCompilation(files.sourceFile);
// Create compilation with analyzers.
var compilationWithAnalyzers = CreateCompilationWithAnalyzers(files.analyzerFile, compilation);
// Display analyzer diagnostics in the compilation.
DisplayAnalyzerDiagnostics(compilationWithAnalyzers);
}
主方法调用单个方法执行以下操作:
-
ParseArguments用于扫描:-
分析器程序集文件。
-
要执行分析器的输入源文件。
-
-
CreateCompilation用于从输入源文件的文本创建的解析语法树中创建 C# 编译。 -
CreateCompilationWithAnalyzers用于创建一个包含附加了给定分析器程序集文件的分析器诊断分析器的编译实例。 -
使用
DisplayAnalyzerDiagnostics执行分析器以计算分析器诊断并显示它们。
ParseArguments 和 CreateCompilation 的实现与上一道菜谱中的相同,即 基于编译器语法 API 编写解析和转换源文件的程序。有关这些方法的进一步解释,请参阅该菜谱的 如何工作... 部分。
CreateCompilationWithAnalyzers 方法接受编译和分析器程序集作为参数:
private static CompilationWithAnalyzers CreateCompilationWithAnalyzers(string analyzerFilePath, Compilation compilation)
{
var analyzerFileReference = new AnalyzerFileReference(analyzerFilePath, new AnalyzerAssemblyLoader());
var analyzers = analyzerFileReference.GetAnalyzers(LanguageNames.CSharp);
var options = new CompilationWithAnalyzersOptions(
new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty),
onAnalyzerException: (exception, analyzer, diagnostic) => throw exception,
concurrentAnalysis: false,
logAnalyzerExecutionTime: false);
return new CompilationWithAnalyzers(compilation, analyzers, options);
}
首先,它创建 AnalyzerFileReference (source.roslyn.io/#q=AnalyzerFileReference),包含分析器文件路径和分析器程序集加载器(IAnalyzerAssemblyLoader - 详细内容见本节后续部分 (source.roslyn.io/#q=IAnalyzerAssemblyLoader)) 的一个实例。它在这个分析器文件引用上调用 AnalyzerReference.GetAnalyzers API (source.roslyn.io/#q=AnalyzerReference.GetAnalyzers),使用给定的分析器程序集加载器加载分析器程序集,然后创建在此程序集中定义的诊断分析器的实例。
它创建一个默认的 CompilationWithAnalyzersOptions (source.roslyn.io/#q=CompilationWithAnalyzersOptions) 集合,用于配置分析器执行。可能的选项包括:
-
AnalyzerOptions:分析器选项包含传递给分析器的额外非源文本文件集。在这个配方中,我们使用一个空集。您可以在github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md中了解更多关于额外文件的信息。 -
onAnalyzerException委托:当分析器抛出异常时将被调用的委托。在这个配方中,我们只是重新抛出这个异常。 -
concurrentAnalysis:控制分析器是否应该并发运行的标志。在这个配方中,我们默认设置为 false。 -
logAnalyzerExecutionTime:控制是否应该跟踪每个分析器的相对执行时间。如果设置为 true,则可以通过公共 APICompilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync(source.roslyn.io/#q=CompilationWithAnalyzers.GetAnalyzerTelemetryInfoAsync) 获取每个分析器的这些数据。这返回一个AnalyzerTelemetryInfo,它有一个名为ExecutionTime(http://source.roslyn.io/#q=AnalyzerTelemetryInfo.ExecutionTime)的属性。在这个配方中,我们默认设置为 false。
最后,该方法创建并返回一个具有给定编译、分析器文件引用和选项的CompilationWithAnalyzers (source.roslyn.io/#Microsoft.CodeAnalysis/DiagnosticAnalyzer/CompilationWithAnalyzers.cs,7efdf3edc21e904a) 实例。
我们简要提到了上面传递给AnalyzerFileReference构造函数的我们的自定义AnalyzerAssemblyLoader。它在我们的代码中实现如下:
private class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
{
void IAnalyzerAssemblyLoader.AddDependencyLocation(string fullPath)
{
}
Assembly IAnalyzerAssemblyLoader.LoadFromPath(string fullPath)
{
return Assembly.LoadFrom(fullPath);
}
}
这个分析器程序集加载器处理使用执行平台上的.NET API 进行程序集加载来加载分析器程序集。在这个配方中,我们使用.NET Framework API Assembly.LoadFrom (msdn.microsoft.com/en-us/library/system.reflection.assembly.loadfrom(v=vs.110).aspx) 从给定路径加载程序集。
在我们的自定义AnalyzerAssemblyLoader中,我们忽略了添加分析器依赖位置的反调,因为我们的测试分析器程序集没有依赖项。我们可以增强这个程序集加载器以跟踪这些位置并处理依赖项的加载。
DisplayAnalyzerDiagnostics接受之前创建的CompilationWithAnalyzers实例,并使用GetAnalyzerDiagnosticsAsync API 在底层编译上执行分析器。然后它遍历所有分析器诊断,并为每个诊断输出消息,包括行和列信息:
private static void DisplayAnalyzerDiagnostics(CompilationWithAnalyzers compilationWithAnalyzers)
{
var diagnostics = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(CancellationToken.None).Result;
Console.WriteLine($"Number of diagnostics: {diagnostics.Length}");
foreach (var diagnostic in diagnostics)
{
Console.WriteLine(diagnostic.ToString());
}
Console.WriteLine();
}
return newSolution;
}
您可以通过调用CompilationWithAnalyzers.GetAnalysisResultAsync(source.roslyn.io/#q=CompilationWithAnalyzers.GetAnalysisResultAsync)公共 API 来获取分析结果的更精细视图。返回的AnalysisResult(source.roslyn.io/#Microsoft.CodeAnalysis/DiagnosticAnalyzer/AnalysisResult.cs,86a401660972cfb8)允许您获取每个诊断分析器报告的单独语法、语义和编译诊断,并且还允许您获取每个分析器的分析器遥测信息。
基于 Workspaces API 编写应用程序以格式化和简化解决方案中的所有源文件
在本节中,我们将编写一个基于 Roslyn Workspaces API 的 C#控制台应用程序,将 C#解决方案加载到工作区中,然后执行以下操作:
-
使用自定义缩进大小将解决方案中的制表符更改为空格。这是一个语法代码重构。
-
简化解决方案,将局部声明更改为具有显式类型指定而不是 var。这是一个语义代码重构。
您可以在以下链接中阅读 Formatter 和 Simplifier 的 XML 文档注释和实现细节,以获取有关这些操作的更多信息:source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Formatting/Formatter.cs,f445ffe3c814c002 和 source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Simplification/Simplifier.cs,1d256ae3815b1cac,分别。
入门
您需要在您的机器上安装 Visual Studio 2017 Community Edition 才能执行此配方。您可以从www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15安装免费的 Community Edition。
如何做到这一点...
-
打开 Visual Studio 并创建一个新的针对.NET Framework 4.6 或更高版本的 C#控制台应用程序,例如
ConsoleApp.。 -
安装
Microsoft.CodeAnalysis.CSharp.WorkspacesNuGet 包(截至本文写作,最新稳定版本为2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章中的配方通过 NuGet 包管理器搜索和安装分析器。 -
将
Program.cs中的源代码替换为附带的代码示例\ConsoleApp\Program.cs中的源代码。 -
构建项目。
-
打开 Visual Studio 开发者命令提示符,切换到项目根目录,并使用无参数执行
bin\Debug\ConsoleApp.exe。 -
验证输出显示使用错误:
Usage: ConsoleApp.exe <%solution_file_path%>。 -
创建一个新的 C# 控制台应用程序,例如
TestSolution,并在Main方法中添加一个隐式声明的局部变量和一个显式声明的局部变量(注意缩进大小为 4):

-
现在,使用完整路径
TestSolution.sln作为参数执行应用程序:bin\Debug\ConsoleApp.exe <%test_solution_path%>。 -
验证控制台输出:
Loading solution '<%test_sln_path%>'...
Formatting solution...
Simplifying solution...
Solution updated.
- 现在验证 TestSolution 中的源文件新内容缩进大小为 2,并且没有显式声明的局部变量:

它是如何工作的...
在这个菜谱中,我们基于 Roslyn 工作区 API 编写了一个 C# 控制台应用程序,用于格式化和简化解决方案中的所有源文件。这些操作与在设置相应的工具选项后应用格式化和简化快速修复时 Visual Studio IDE 会执行的操作非常相似。让我们通过代码来了解我们是如何实现这些操作的:
public static void Main(string[] args)
{
// Parse arguments to get solution.
var slnPath = ParseArguments(args);
if (slnPath == null)
{
return;
}
// Create workspace.
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
// Open solution within the workspace.
Console.WriteLine($"Loading solution '{slnPath}'...");
Solution solution = workspace.OpenSolutionAsync(slnPath).Result;
// Format the solution.
solution = FormatSolution(solution, workspace.Options);
// Simplify the solution.
solution = SimplifySolution(solution, workspace.Options);
// Apply changes.
ApplyChanges(workspace, solution);
}
Main 方法调用单个方法执行以下操作:
-
ParseArguments:扫描要解析和转换的输入文件。 -
MSBuildWorkspace.Create:创建工作区,Workspace.OpenSolutionAsync:将给定的解决方案加载到工作区中。 -
FormatSolution:格式化解决方案中的所有文档。 -
SimplifySolution:简化解决方案中的所有文档。 -
ApplyChanges:将格式化和简化更改应用到工作区,并将这些更改持久化到磁盘。
ParseArguments 的实现与菜谱中的实现相同,基于编译器语法 API 编写解析和转换源文件的程序. 请参阅该菜谱的 如何工作... 部分,以获取关于此方法的进一步说明。
MSBuildWorkspace (source.roslyn.io/#q=MSBuildWorkspace) 是 Roslyn 工作区核心的定制实现,它使用 MSBuild (docs.microsoft.com/en-us/visualstudio/msbuild/msbuild) 项目模型来加载解决方案/项目文件,并允许在项目中读取和写入单个文档。
FormatSolution 方法格式化解决方案中的所有文档。首先,它修改选项以优先使用空格而不是制表符,缩进大小为 2(默认为 4):
-
FormattingOptions.UseTabs:值设置为false。 -
FormattingOptions.IndentationSize:值设置为2。
private static Solution FormatSolution(Solution originalSolution, OptionSet options)
{
Console.WriteLine("Formatting solution...");
// Prefer whitespaces over tabs, with an indentation size of 2.
options = options
.WithChangedOption(FormattingOptions.UseTabs, LanguageNames.CSharp, false)
.WithChangedOption(FormattingOptions.IndentationSize, LanguageNames.CSharp, 2);
Solution newSolution = originalSolution;
foreach (var documentId in originalSolution.Projects.SelectMany(p => p.DocumentIds))
{
Document document = newSolution.GetDocument(documentId);
// Format the document.
Document newDocument = Formatter.FormatAsync(document, options).Result;
// Update the current solution.
newSolution = newDocument.Project.Solution;
}
return newSolution;
}
它跟踪当前解决方案快照在 newSolution,,该快照初始化为 originalSolution。然后它遍历解决方案中的所有文档,并执行以下操作:
-
通过调用
Formatter.FormatAsync公共 API(source.roslyn.io/#q=Formatter.FormatAsync)并使用当前文档和选项来格式化文档。 -
将
newSolution更新为指向格式化后的newDocument的解决方案。
注意我们为什么不能简单地遍历 originalSolution.Projects 或 project.Documents,因为这将返回来自未修改的 originalSolution 的对象,而不是来自 newSolution。我们需要使用 ProjectId/DocumentIds(不会改变)来查找 newSolution 中的相应快照。
最后,在所有文档格式化完成后,它返回 newSolution。
SimplifySolution 方法简化了解决方案中的所有文档。首先,它修改选项以优先使用隐式类型局部声明,即用户 var 而不是显式类型指定,通过将 SimplificationOptions.PreferImplicitTypeInLocalDeclaration 设置为 true:
private static Solution SimplifySolution(Solution originalSolution, OptionSet options)
{
Console.WriteLine("Simplifying solution...");
// Prefer 'var' over explicit type specification.
options = options.WithChangedOption(SimplificationOptions.PreferImplicitTypeInLocalDeclaration, true);
Solution newSolution = originalSolution;
foreach (var documentId in originalSolution.Projects.SelectMany(p => p.DocumentIds))
{
Document document = newSolution.GetDocument(documentId);
// Add simplification annotation to the root.
var newRoot = document.GetSyntaxRootAsync().Result.WithAdditionalAnnotations(Simplifier.Annotation);
// Simplify the document.
Document newDocument = Simplifier.ReduceAsync(document.WithSyntaxRoot(newRoot), options).Result;
// Update the current solution.
newSolution = newDocument.Project.Solution;
}
return newSolution;
}
SimplifySolution 方法与 FormatSolution 方法在迭代文档、通过调用 Simplifier.ReduceAsync 公共 API(source.roslyn.io/#q=Simplifier.ReduceAsync)简化它们方面有非常相似的实现,在处理每个文档后,将最新的快照存储在 newSolution 中,并在最后返回新的解决方案快照。尽管如此,它有一个重要的区别。Simplifier.ReduceAsync 只处理具有特殊语法注解的节点:Simplifier.Annotation(source.roslyn.io/#q=Simplifier.Annotation)。因此,在调用此 API 之前,我们需要将此语法注解添加到文档的根节点。
ApplyChanges 方法通过将新的解决方案快照传递给 Workspace.TryApplyChanges 公共 API(source.roslyn.io/#q=Workspace.TryApplyChanges)来应用解决方案快照中的更改到工作区。这也导致 MSBuildWorkspace 将这些更改持久化到磁盘:
private static void ApplyChanges(Workspace workspace, Solution solution)
{
// Apply solution changes to the workspace.
// This persists the in-memory changes into the disk.
if (workspace.TryApplyChanges(solution))
{
Console.WriteLine("Solution updated.");
}
else
{
Console.WriteLine("Update failed!");
}
}
基于 Workspaces API 编写应用程序以编辑解决方案中的项目并显示项目属性
在本节中,我们将基于 Roslyn Workspaces API 编写一个 C# 控制台应用程序,将 C# 解决方案加载到工作区中,然后执行以下操作:
-
显示项目属性,例如项目文件路径、输出文件路径、项目语言、程序集名称、引用计数、文档计数等。
-
向解决方案中添加一个新的项目。
-
从解决方案中移除现有的项目。
-
编辑项目以添加项目引用。
入门指南
您需要在您的机器上安装 Visual Studio 2017 Community Edition 才能执行此配方。您可以从 www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15 安装免费的 Community Edition。
如何做到这一点...
-
打开 Visual Studio 并创建一个新的以 .NET Framework 4.6 或更高版本为目标平台的 C# 控制台应用程序,例如
ConsoleApp. -
安装
Microsoft.CodeAnalysis.CSharp.WorkspacesNuGet 包(截至本文写作,最新稳定版本是 2.1.0)。有关如何在项目中搜索和安装 NuGet 包的指导,请参阅第二章 通过 NuGet 包管理器搜索和安装分析器 中的菜谱 在 .NET 项目中消费诊断分析器。 -
将
Program.cs中的源代码替换为附带的代码示例\ConsoleApp\Program.cs中的源代码。 -
构建项目。
-
打开 Visual Studio 开发者命令提示符,将目录更改为项目根目录,并执行
bin\Debug\ConsoleApp.exe,不带任何参数。 -
验证输出显示使用不正确:
Usage: ConsoleApp.exe <%solution_file_path%>. -
创建一个新的 C# 类库解决方案,例如
TestSolution,并将另一个类库项目,例如ClassLibrary, 添加到解决方案中。现在,使用TestSolution.sln的完整路径作为参数执行应用程序:bin\Debug\ConsoleApp.exe <%test_solution_path%>。 -
验证控制台输出显示初始解决方案的两个项目
TestSolution和ClassLibrary的项目属性:

- 按任意键继续并注意以下操作已执行:将项目添加到解决方案中,从解决方案中移除项目,以及编辑现有项目(添加项目引用)。

- 按任意键继续并验证解决方案现在包含两个项目
TestSolution和AddedClassLibrary,并且从AddedClassLibrary到TestSolution存在一个项目引用:

它是如何工作的...
在这个菜谱中,我们编写了一个基于 Roslyn Workspaces API 的 C# 控制台应用程序,以在解决方案中的项目上执行各种操作:添加、移除、编辑和显示项目属性。丰富的 Workspaces API 为您提供了一个强大的对象模型,用于分析和编辑解决方案中的项目和文档。让我们通过代码了解我们如何实现这些操作:
public static void Main(string[] args)
{
// Parse arguments to get solution.
string slnPath = ParseArguments(args);
if (slnPath == null)
{
return;
}
// Create workspace.
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
// Open solution within the workspace.
Console.WriteLine($"Loading solution '{slnPath}'...");
Solution solution = workspace.OpenSolutionAsync(slnPath).Result;
// Display project properties.
DisplayProjectProperties(solution);
// Add project AddedClassLibrary.
WaitForKeyPress();
solution = AddProject(solution, "AddedClassLibrary");
// Remove project ClassLibrary.
solution = RemoveProject(solution, "ClassLibrary");
// Add project reference from AddedClassLibrary to TestSolution.
solution = AddProjectReference(solution, referenceFrom: "AddedClassLibrary", referenceTo: "TestSolution");
// Display project properties.
WaitForKeyPress();
DisplayProjectProperties(solution);
}
Main 方法调用单个方法以执行以下操作:
-
ParseArguments:用于扫描要解析和转换的输入文件 -
MSBuildWorkspace.Create:用于创建工作区,Workspace.OpenSolutionAsync用于在工作区中加载给定的解决方案 -
DisplayProjectProperties:用于显示解决方案中所有项目的常见属性。 -
AddProject:用于将新项目添加到解决方案中 -
RemoveProject:用于从解决方案中移除现有项目 -
AddProjectReference:用于向解决方案中的现有项目添加项目引用
ParseArguments 的实现与配方中的实现相同,即 基于 Compiler Syntax API 编写解析和转换源文件的程序。请参阅该配方中的 如何工作... 部分,以获取关于此方法的进一步解释。
MSBuildWorkspace (source.roslyn.io/#q=MSBuildWorkspace) 是 Roslyn 工作区核心的定制实现,它使用 MSBuild 项目模型来加载解决方案/项目文件,并允许在项目中读取和写入单个文档。
DisplayProjectProperties 显示常见的项目属性,如项目名称、语言、程序集名称、引用、文档等:
private static void DisplayProjectProperties(Solution solution)
{
Console.WriteLine($"Project count: {solution.Projects.Count()}");
foreach (var project in solution.Projects)
{
Console.WriteLine($" Project: {project.Name}");
Console.WriteLine($" Assembly name: {project.AssemblyName}");
Console.WriteLine($" Language: {project.Language}");
Console.WriteLine($" Project file: {project.FilePath}");
Console.WriteLine($" Output file: {project.OutputFilePath}");
Console.WriteLine($" Documents: {project.Documents.Count()}");
Console.WriteLine($" Metadata references: {project.MetadataReferences.Count()}");
Console.WriteLine($" Project references: {project.ProjectReferences.Count()}");
Console.WriteLine();
}
Console.WriteLine();
}
AddProject 方法创建一个具有给定 projectName、唯一项目 ID、版本戳、程序集名称和 C# 语言名称的裸骨 ProjectInfo (source.roslyn.io/#q=ProjectInfo):
private static Solution AddProject(Solution originalSolution, string projectName)
{
Console.WriteLine($"Adding project '{projectName}'...");
var projectInfo = ProjectInfo.Create(
id: ProjectId.CreateNewId(),
version: new VersionStamp(),
name: projectName,
assemblyName: "AddedProjectAssembly",
language: LanguageNames.CSharp);
return originalSolution.AddProject(projectInfo);
}
然后,它调用 Solution.AddProject API (source.roslyn.io/#q=Solution.AddProject) 将创建的项目信息添加到解决方案中。
RemoveProject 方法从解决方案中移除具有给定 projectName 的现有项目。它使用 Solution.RemoveProject API (source.roslyn.io/#q=Solution.RemoveProject) 来移除项目:
private static Solution RemoveProject(Solution originalSolution, string projectName)
{
Console.WriteLine($"Removing project '{projectName}'...");
var project = originalSolution.Projects.SingleOrDefault(p => p.Name == projectName);
return originalSolution.RemoveProject(project.Id);
}
AddProjectReference 方法从给定的 referenceFrom 项目向给定的 referenceTo 项目添加项目引用。它搜索解决方案中具有给定名称的现有项目,为 referenceFrom 项目创建一个具有项目 ID 的 ProjectReference (source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Workspace/Solution/ProjectReference.cs,944b5173649705e4),并使用 Solution.AddProjectReference (source.roslyn.io/#q=Solution.AddProjectReference) 来添加所需的引用。
private static Solution AddProjectReference(Solution originalSolution, string referenceFrom, string referenceTo)
{
Console.WriteLine($"Adding project reference from '{referenceFrom}' to '{referenceTo}'...");
var projectReferenceFrom = originalSolution.Projects.SingleOrDefault(p => p.Name == referenceFrom);
var projectReference = new ProjectReference(projectReferenceFrom.Id);
var projectReferenceTo = originalSolution.Projects.SingleOrDefault(p => p.Name == referenceTo);
return originalSolution.AddProjectReference(projectReferenceTo.Id, projectReference);
}



)
)
)
) 附近悬停,查看该方法被 1 个测试覆盖:
)就会消失:
),确认已从实时执行中排除单元测试:
浙公网安备 33010602011771号