教程:编写第一个分析器和代码修复

.NET 编译器平台 SDK 提供了创建自定义诊断(分析器)、代码修复、代码重构和面向 C# 或 Visual Basic 代码的诊断抑制器所需的工具。 分析器包含识别规则冲突的代码。 代码修补程序包含修复冲突的代码。 实现的规则可以是从代码结构到编码样式到命名约定等的任何规则。 .NET 编译器平台提供了用于运行分析的框架,因为开发人员正在编写代码,以及所有用于修复代码的 Visual Studio UI 功能:在编辑器中显示波浪线、填充 Visual Studio 错误列表、创建“灯泡”建议并显示建议修复的丰富预览。

在本教程中,你将了解如何使用 Roslyn API 创建 分析器和 随附 的代码修复 。 分析器是一种向用户报告源代码分析和报告问题的方法。 (可选)可以将代码修复与分析器相关联,以表示对用户的源代码的修改。 本教程将创建一个分析器,用于查找可以使用 const 修饰符声明的但未执行此操作的局部变量声明。 随附的代码修补程序修改这些声明以添加 const 修饰符。

先决条件

需要通过 Visual Studio 安装程序安装 .NET 编译器平台 SDK

安装说明 - Visual Studio 安装程序

Visual Studio 安装程序中查找 .NET 编译器平台 SDK 有两种不同的方法:

使用 Visual Studio 安装程序进行安装 - 工作负载视图

不会自动选择 .NET 编译器平台 SDK 作为 Visual Studio 扩展开发工作负载的一部分。 必须将其选为可选组件。

  1. 运行 Visual Studio 安装程序
  2. 选择“修改”
  3. 检查 Visual Studio 扩展开发 工作负载。
  4. 在摘要树中打开 Visual Studio 扩展开发 节点。
  5. 勾选 .NET 编译器平台 SDK 的框。 将在可选组件最下面找到它。

(可选)还需要 DGML 编辑器 在可视化工具中显示图形:

  1. 在摘要树中打开 “单个组件 ”节点。
  2. 选中框以启用 DGML 编辑器

使用 Visual Studio 安装程序 - 单个组件选项卡进行安装

  1. 运行 Visual Studio 安装程序
  2. 选择“修改”
  3. 选择 “单个组件 ”选项卡
  4. 勾选 .NET 编译器平台 SDK 的框。 可在 编译器、生成工具和运行时 部分的顶部找到它。

(可选)还需要 DGML 编辑器 在可视化工具中显示图形:

  1. 选中“DGML 编辑器”框。 可在 “代码工具” 部分下找到它。

创建和验证分析器有几个步骤:

  1. 创建解决方案。
  2. 注册分析器名称和说明。
  3. 报告分析器警告和建议。
  4. 实现代码修复以接受建议。
  5. 通过单元测试改进分析。

创建解决方案

  • 在 Visual Studio 中,选择“ 文件 > 新建 > 项目...” 以显示“新建项目”对话框。
  • Visual C# > 扩展性下,选择具有代码修复的分析器(.NET Standard)。
  • 将项目命名为“MakeConst”,然后单击“确定”。

注释

可能会收到编译错误(MSB4062:“CompareBuildTaskVersion”任务无法加载”。 若要解决此问题,请使用 NuGet 包管理器更新解决方案中的 NuGet 包,或在包管理器控制台窗口中使用 Update-Package

浏览分析器模板

使用代码修复模板的分析器创建五个项目:

  • 包含分析器的 MakeConst
  • MakeConst.CodeFixes,其中包含代码修复。
  • MakeConst.Package,用于为分析器和代码修复生成 NuGet 包。
  • MakeConst.Test,它是一个单元测试项目。
  • MakeConst.Vsix,它是默认启动项目,用于启动已加载新分析器的第二个 Visual Studio 实例。 按 F5 启动 VSIX 项目。

注释

分析器应面向 .NET Standard 2.0,因为它们可以在 .NET Core 环境(命令行生成)和 .NET Framework 环境(Visual Studio)中运行。

小窍门

运行分析器时,启动 Visual Studio 的第二个副本。 第二个副本使用不同的注册表区来存储设置。 这样,便可以区分 Visual Studio 的两个副本中的视觉设置。 可以为 Visual Studio 的实验性运行选择不同的主题。 此外,不要在设置中漫游,也不要使用 Visual Studio 的实验性运行登录到 Visual Studio 帐户。 这让设置保持不同。

该配置单元不仅包括正在开发的分析器,而且还包括任何以前打开的分析器。 若要重置 Roslyn 配置单元,需要从 %LocalAppData%\Microsoft\VisualStudio 中手动将其删除。 例如,Roslyn hive 的文件夹名称将以 Roslyn 结尾,例如 16.0_9ae182f9Roslyn。 请注意,你可能需要在删除配置单元后清除解决方案并重新生成。

在刚刚启动的第二个 Visual Studio 实例中,创建新的 C# 控制台应用程序项目(任何目标框架都将正常工作 -- 分析器在源级别工作)。将鼠标悬停在带有波浪下划线的令牌上,并显示分析器提供的警告文本。

该模板创建一个分析器,用于报告类型名称包含小写字母的每个类型声明的警告,如下图所示:

分析器报告警告

该模板还提供了一种代码修复功能,可以将任何包含小写字符的类型名称全部更改为大写。 可以单击显示有警告的灯泡以查看建议的更改。 接受建议的更改会更新类型名称和解决方案中对该类型的所有引用。 现在你已看到初始分析器在作中,请关闭第二个 Visual Studio 实例并返回到分析器项目。

无需启动 Visual Studio 的第二个副本,并创建新的代码来测试分析器中的每个更改。 该模板还会为你创建单元测试项目。 该项目包含两个测试。 TestMethod1 显示分析代码而不触发诊断的测试的典型格式。 TestMethod2 显示触发诊断的测试的格式,然后应用建议的代码修复。 构建分析器和代码修复时,您将为不同的代码结构编写测试来验证你的工作。 分析器的单元测试比使用 Visual Studio 以交互方式对其进行测试要快得多。

小窍门

分析器的单元测试是一个非常有效的工具,如果你知道哪些代码构造应该触发或者不应该触发你的分析器。 在 Visual Studio 的另一个副本中加载分析器是探索和查找你可能尚未考虑的构造的好工具。

在本教程中,你将编写一个分析器,该分析器向用户报告任何可转换为本地常量的局部变量声明。 例如,考虑以下代码:

int x = 0;
Console.WriteLine(x);

在上面的代码中, x 分配了常量值,并且永远不会修改。 可以使用 const 修饰符声明:

const int x = 0;
Console.WriteLine(x);

用于确定变量是否可以变为常量的分析涉及复杂的过程,需要语法分析、初始值表达式的常量分析以及数据流分析,以确保该变量从不被赋值。 .NET 编译器平台提供 API,可更轻松地执行此分析。

创建分析器注册

该模板在MakeConstAnalyzer.cs文件中创建初始DiagnosticAnalyzer类。 此初始分析器显示每个分析器的两个重要属性。

  • 每个诊断分析器必须提供 [DiagnosticAnalyzer] 属性,用于描述其操作所用的语言。
  • 每个诊断分析器都必须从 DiagnosticAnalyzer 类中派生(直接或间接)。

该模板还显示了属于任何分析器的基本功能:

  1. 注册操作。 此操作表示应触发分析器以检查存在冲突的代码的代码更改。 当 Visual Studio 检测到与已注册操作匹配的代码编辑时,它将调用你的分析器的注册方法。
  2. 创建诊断。 分析器检测到冲突时,它会创建 Visual Studio 用来通知用户违规的诊断对象。

在重写的 DiagnosticAnalyzer.Initialize(AnalysisContext) 方法中注册操作。 在本教程中,你将访问查找本地声明的 语法节点 ,并查看其中哪些具有常量值。 如果声明可能为常量,则分析器将创建并报告诊断。

第一步是更新注册常量和 Initialize 方法,以便这些常量能够指示您的“生成常量”分析器。 大多数字符串常量都在字符串资源文件中定义。 应遵循这种做法,以便更轻松地进行本地化。 打开 MakeConst 分析器项目的 Resources.resx 文件。 这会显示资源编辑器。 按如下所示更新字符串资源:

  • AnalyzerDescription 更改为 "Variables that are not modified should be made constants."。
  • 将“AnalyzerMessageFormat”更改为“Variable '{0}' can be made constant”。
  • 更改AnalyzerTitle为“Variable can be made constant”。

完成后,资源编辑器应如下图所示:

更新字符串资源

其余更改位于分析器文件中。 在 Visual Studio 中打开 MakeConstAnalyzer.cs 。 将注册的动作从对符号执行的动作更改为对语法执行的动作。 在 MakeConstAnalyzerAnalyzer.Initialize 方法中,找到在符号上注册操作的行:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

将其替换为以下行:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

更改后,可以删除 AnalyzeSymbol 该方法。 此分析器检查 SyntaxKind.LocalDeclarationStatement,而不是 SymbolKind.NamedType 语句。 请注意,AnalyzeNode 下面有红色波浪线。 刚刚添加的代码引用了一个尚未声明的方法 AnalyzeNode。 使用以下代码声明该方法:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

MakeConstAnalyzer.csCategory更改为“Usage”,如以下代码所示:

private const string Category = "Usage";

查找可以是常量的局部声明

是时候编写方法的第 AnalyzeNode 一个版本了。 应查找可以是 const 但实际不是的单个局部声明,如以下代码所示:

int x = 0;
Console.WriteLine(x);

第一步是查找本地声明。 将以下代码添加到AnalyzeNodeMakeConstAnalyzer.cs

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

此强制转换始终会成功,因为分析器注册了对局部声明的更改,并且只注册了局部声明。 没有其他节点类型会触发对您方法 AnalyzeNode 的调用。 接下来,检查声明是否有任何 const 修饰符。 如果找到它们,请立即返回。 以下代码查找本地声明上的任何 const 修饰符:

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

最后,需要检查变量是否为 const。 这意味着要确保在初始化后永远不对它进行赋值。

你将使用SyntaxNodeAnalysisContext进行一些语义分析。 使用 context 参数确定是否可以进行 const局部变量声明。 一个 Microsoft.CodeAnalysis.SemanticModel 表示单个源文件中的所有语义信息。 可以在涵盖 语义模型的文章中了解详细信息。 你将使用该 Microsoft.CodeAnalysis.SemanticModel 函数对本地声明语句执行数据流分析。 然后,使用此数据流分析的结果来确保本地变量不会使用其他任何位置的新值进行写入。 调用GetDeclaredSymbol扩展方法以检索ILocalSymbol变量,并检查该变量是否未包含在DataFlowAnalysis.WrittenOutside集合的数据流分析中。 将以下代码添加到方法的 AnalyzeNode 末尾:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

新添加的代码确保变量不被修改,因此可以使其成为const。 是时候提出诊断了。 将以下代码添加为 AnalyzeNode 中的最后一行:

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

可以通过按 F5 运行分析器来检查进度。 可以加载之前创建的控制台应用程序,然后添加以下测试代码:

int x = 0;
Console.WriteLine(x);

灯泡应出现,分析器应报告诊断结果。 但是,根据你使用的具体版本的 Visual Studio,你将看到以下之一:

  • 灯泡,它仍使用模板生成的代码修补程序,并告知你可以用大写。
  • 编辑器顶部的横幅消息,指出“MakeConstCodeFixProvider”遇到错误并已被禁用。 这是因为代码修复提供程序尚未进行变更,仍然期望查找 TypeDeclarationSyntax 元素而不是 LocalDeclarationStatementSyntax 元素。

下一部分介绍如何编写代码修补程序。

编写代码以进行修复

分析器可以提供一个或多个代码修复。 代码修正是指对报告的问题进行解决的编辑。 对于您创建的分析器,可以提供一种代码修复方案来插入 const 关键字:

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

用户从编辑器中的灯泡 UI 中选择它,Visual Studio 会更改代码。

打开 CodeFixResources.resx 文件并更改为 CodeFixTitle “Make constant”。

打开模板添加的 MakeConstCodeFixProvider.cs 文件。 此代码修补程序已绑定到由诊断分析器生成的诊断 ID,但它尚没有实施正确的代码转换。

接下来,删除 MakeUppercaseAsync 该方法。 它不再适用。

所有代码修复工具派生自 CodeFixProvider. 它们都重写 CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) 以报告可用的代码修补程序。 在RegisterCodeFixesAsync中,将您正在搜索的上级节点类型更改为LocalDeclarationStatementSyntax以匹配诊断。

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

接下来,更改最后一行以注册代码修复。 修复程序将创建一个新文档,该文档的结果是将 const 修饰符添加到现有声明:

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

你会注意到刚在符号 MakeConstAsync 上添加的代码中的红色波浪线。 请为MakeConstAsync添加如下代码定义:

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

新的MakeConstAsync方法会将代表用户源文件的Document转换成现在包含const声明的新Document

创建一个新的 const 关键字标记,以在声明语句的前面插入。 请注意,首先从声明语句的第一个标记中删除任何前导琐碎内容,然后将其附加到 const 标记。 将以下代码添加到 MakeConstAsync 方法中:

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

接下来,使用以下代码将 const 令牌添加到声明:

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

接下来,设置新声明的格式以匹配 C# 格式规则。 设置更改的格式以匹配现有代码可提供更好的体验。 在现有代码后面立即添加以下语句:

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

此代码需要新的命名空间。 将以下 using 指令添加到文件顶部:

using Microsoft.CodeAnalysis.Formatting;

最后一步是进行编辑。 此过程有三个步骤:

  1. 获取现有文档的句柄。
  2. 通过将现有声明替换为新声明来创建新文档。
  3. 返回新文档。

将以下代码添加到方法的 MakeConstAsync 末尾:

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

您的代码修复已准备好进行测试。 按 F5 在 Visual Studio 的第二个实例中运行分析器项目。 在第二个 Visual Studio 实例中,创建新的 C# 控制台应用程序项目,并向 Main 方法添加一些使用常量值初始化的局部变量声明。 您将看到它们被报告为警告信息,如下所示。

可以发出常数警告

你取得了很大的进步。 可以进行 const 操作的声明下具有波浪线。 但仍有工作要做。 如果您将const添加到从i开始的声明中,然后添加j,最后添加k,这就很好。 但是,如果按不同的顺序添加 const 修饰符,从 k 开始,分析器将创建错误:k 无法声明 const,除非 ij 都已是 const。 必须进行更多分析,以确保能够处理变量的不同宣告和初始设定方式。

生成单元测试

您的分析器和代码修复功能适用于一个简单的案例,即一个可以设为常量的声明。 这里有许多种可能的声明语句,在这些语句的实现中可能会犯错误。 你将使用模板编写的单元测试库来解决这些情况。 这比重复打开 Visual Studio 的第二个副本要快得多。

在单元测试项目中打开 MakeConstUnitTests.cs 文件。 该模板创建了两个测试,这些测试遵循分析器和代码修复单元测试的两种常见模式。 TestMethod1 显示测试的模式,该模式可确保分析器在不应报告诊断时不会报告诊断。 TestMethod2 显示报告诊断并运行代码修复的模式。

该模板使用 Microsoft.CodeAnalysis.Testing 包进行单元测试。

小窍门

测试库支持特殊的标记语法,包括:

  • [|text|]:表示为text报告了诊断。 默认情况下,此格式只可用于测试由 DiagnosticAnalyzer.SupportedDiagnostics 提供了正好一个 DiagnosticDescriptor 的分析器。
  • {|ExpectedDiagnosticId:text|}:表示针对 text 报告 IdExpectedDiagnosticId 的诊断信息。

将类中的 MakeConstUnitTest 模板测试替换为以下测试方法:

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

运行此测试以确保它通过。 在 Visual Studio 中,通过选择测试>Windows>测试资源管理器来打开“测试资源管理器”。 然后选择“全部运行”。

为有效声明创建测试

作为一般规则,分析器应尽快退出,并执行最少的工作。 Visual Studio 在用户编辑代码时调用已注册的分析器。 响应能力是关键要求。 对于不应引发诊断的代码,有几个测试用例。 您的分析器已经执行了其中的几个测试。 添加以下测试方法来表示这些情况:

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }
        [TestMethod]
        public async Task VariableIsAlreadyConst_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }
        [TestMethod]
        public async Task NoInitializer_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i;
        i = 0;
        Console.WriteLine(i);
    }
}
");
        }

这些测试通过是因为分析器已处理以下条件:

  • 数据流分析检测到初始化后分配的变量。
  • 通过检查是否有 const 关键字,筛选掉已 const 的声明。
  • 没有初始化器的声明由数据流分析处理,该分析检测声明外的赋值。

接下来,为尚未处理的条件添加测试方法:

  • 初始值不是常量的声明,因为这些初始值不能在编译时作为常量:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

它可能更加复杂,因为 C# 允许将多个声明作为一个语句。 请考虑以下测试用例字符串常量:

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

该变量 i 可以是常量,但该变量 j 不能。 因此,此语句不能成为常量声明。

再次运行测试,你将看到最后两个测试用例失败。

更新分析器以忽略正确的声明

您需要对分析器的 AnalyzeNode 方法进行一些功能增强,以筛选符合这些条件的代码。 它们是所有相关条件,因此类似的更改将修复所有这些条件。 对 AnalyzeNode 进行以下更改:

  • 语义分析检查了单个变量声明。 此代码需要位于检查同一 foreach 语句中声明的所有变量的循环中。
  • 每个声明的变量都需要有一个初始值设定项。
  • 每个声明的变量的初始值设定项必须是编译时常量。

在您的AnalyzeNode方法中,替换原有的语义分析:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

使用以下代码片段:

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

第一个 foreach 循环使用语法分析检查每个变量声明。 第一个检查保证变量具有初始值。 第二个检查保证初始值设定项是常量。 第二个循环具有原始语义分析。 语义检查位于单独的循环中,因为它对性能有更大的影响。 再次运行测试,应看到它们全部通过。

进行最终润色

即将完成。 分析器要处理的还有一些条件。 当用户编写代码时,Visual Studio 会调用分析器。 通常,分析器将调用不编译的代码。 诊断分析器 AnalyzeNode 的方法不检查常量值是否可转换为变量类型。 因此,当前实现会不经意地将不正确的声明如 int i = "abc" 转换为本地常量。 为此情况添加测试方法:

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

此外,引用类型未被正确处理。 引用类型所允许的唯一常量值是 null,但允许字符串文本的情况 System.String除外。 换句话说, const string s = "abc" 是合法的,但 const object s = "abc" 不是。 此代码片段验证条件:

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

若要彻底,需要添加另一个测试,以确保可以为字符串创建常量声明。 以下代码片段定义了引发诊断的代码,以及应用修复后的代码:

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

最后,如果使用 var 关键字声明变量,代码修复将执行错误操作并生成一个 const var 声明,而 C# 语言不支持该声明。 若要修复此 bug,代码修复必须将关键字 var 替换为推断类型的名称。

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

幸运的是,上述所有 bug 都可以使用刚刚学习的相同技术来解决。

若要修复第一个 bug,请先打开 MakeConstAnalyzer.cs 并找到 foreach 循环,在其中检查每个本地声明的初始值设定项以确保使用常量值分配它们。 在第一个 foreach 循环之前,立即调用 context.SemanticModel.GetTypeInfo() 以检索有关本地声明类型的详细信息:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

然后,在你的 foreach 循环中,检查每个初始值设定项,以确保它可转换为变量类型。 在确保初始值设定项为常量后添加以下检查:

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

下一个更改是在上一项更改的基础上构建的。 在第一个 foreach 循环结束的右大括号之前,添加以下代码以检查常量是字符串或 null 时本地变量声明的数据类型。

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

必须在代码修复提供程序中编写更多代码,才能将 var 关键字替换为正确的类型名称。 返回到 MakeConstCodeFixProvider.cs。 要添加的代码执行以下步骤:

  • 检查声明是否为 var 声明,以及声明是否为:
  • 为推断的类型创建新类型。
  • 请确保类型声明不是别名。 如果是这样,声明 const var是合法的。
  • 请确保 var 此程序中不是类型名称。 (如果是这样, const var 是合法的)。
  • 简化完整类型名称

这听起来像是很多代码。 不是。 将声明和初始化 newLocal 的行替换为以下代码。 在初始化 newModifiers之后立即执行:

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

需要添加一个 using 指令才能使用该 Simplifier 类型:

using Microsoft.CodeAnalysis.Simplification;

运行测试,你会发现它们全部通过。 通过运行已完成的分析器自行庆祝。 按 Ctrl+F5 在加载 Roslyn 预览版扩展的 Visual Studio 第二个实例中运行分析器项目。

  • 在第二个 Visual Studio 实例中,创建新的 C# 控制台应用程序项目,并将 int x = "abc"; 添加到 Main 方法。 由于第一个 bug 已修复,应不会报告针对此局部变量声明的警告(尽管像预期那样出现了编译器错误)。
  • 接下来,将 object s = "abc"; 添加到 Main 方法。 由于修复了第二个 bug,不应报告任何警告。
  • 最后,添加另一个使用关键字的 var 局部变量。 你将看到一个警告和显示在左下方的一个建议。
  • 将编辑器插入点移到波浪下划线,然后按 Ctrl+。 来显示建议的代码修正。 选择代码修复后,请注意 var 关键字现在已得到正确处理。

最后,添加以下代码:

int i = 2;
int j = 32;
int k = i + j;

完成这些更改后,仅在前两个变量上有红色波浪线。 将 const 添加到 ij,然后你会在 k 上收到新的警告,因为它现在可能是 const

祝贺! 你已创建第一个 .NET 编译器平台扩展,该扩展执行实时代码分析以检测问题并提供快速修复方法。 在此过程中,你了解了许多属于 .NET 编译器平台 SDK(Roslyn API)的代码 API。 您可以将您的工作与我们样本 GitHub 存储库中的已完成示例进行比对。

其他资源