创建一个 Roslyn Analyzer

Roslyn 是新一代的开源 C#/Visual Basic 编译器,Visual Studio 2015 的 C#/Visual Basic 开发环境就基于 Roslyn,TypeScript 和 XAML 编辑器也使用了 Roslyn 提供部分支持。

基于 Roslyn 可以使用 C#/Visual Basic 快速创建 Analyzer 和 CodeFix,针对 C#/Visual Basic 的 Analyzer 需要分别编写但不一定要使用对应语言。成品可以直接打包成 NuGet package 加入目标项目的 Reference 里,使用非常方便。

本文以非著名二进制序列化格式 MessagePack 所面临的问题,编写一个简单的 Analyzer 进行解决。

1. 问题

MessagePack 默认使用基于数组的序列化,所以在编写作为 contract 的 class 时,需要通过 attributes 确定各个 properties 在数组中的 index。数组的确非常有利于减小序列化后的大小,但是通过复制粘贴添加 attributes 的后果经常是两个 properties 有了一样的 index,而且这个问题需要到运行时才能发现。

2. 准备

在 GitHub 上有 Wiki 描述了正确的开始方法,即使用 .NET Compiler Platform SDK 扩展提供的模板。但是说实话这个模板会一次性给你创建一个 Analyzer + CodeFix,一个 Unit Test 还有一个 VSIX,真是太复杂了。因此本文使用手工方式从头开始。

所以需要准备的只有:Visual Studio 2015

3. 编写

  1. Analyzer 就是个普通的 Class Library,所以创建项目时选择 Class Library 即可。需要注意的是作为 Roslyn 基础的 .NET CoreFx (开源版本)和 .NET Framework 4.5.x 实际上并不兼容,所以需要创建为 .NET Framework 4.6.x 项目。

  2. 添加 Roslyn 的 References,在项目上右键,选择 Manage NuGet Packages...,然后搜索 Microsoft.CodeAnalysis 添加即可。如果仅需要 C# 支持,可以直接添加 Microsoft.CodeAnalysis.CSharp.Workspaces,反之 Visual Basic 则是 Microsoft.CodeAnalysis.VisualBasic.Workspaces

    现在展开项目的 References -> Analyzers,就能看到 Roslyn 的 dll 中包含的 Analyzers 了,这些 Analyzers 会帮助你编写 Analyzer,而不会漏掉基础的部件。

  3. 将自动创建的 Class1.cs 重命名为 CSharpAnalyzer.cs,并且确认自动对 Class1 类进行重命名。然后给 CSharpAnalyzer 类添加 [DiagnosticAnalyzer(LanguageNames.CSharp)] Attribute,并且使其继承自 DiagnosticAnalyzer 类。通过在未添加 Using 引用的语句上使用 Light Bulb(Ctrl + .)可以快速添加 Using,通过在未实现的基类上使用 Light Bulb,可以快速生成需要 override 的部分。

  4. CSharpAnalyzer class 内添加一个 static 的 DiagnosticDescriptor,这个 Descriptor 用于描述我们的 Analyzer 将会发出的调试信息,不但会显示在上图所示的列表中,在创建调试信息的时候也需要使用到。

    1
    2
    static DiagnosticDescriptor Descriptor = new DiagnosticDescriptor("MP0001", "Two properties have same serialization ID.",
    "Properties \"{0}\" and \"{1}\" in class \"{2}\" have same serialization ID {3}.", "Design", DiagnosticSeverity.Error, true);

    然后修改自动生成的 SupportedDiagnostics 属性和 Initialize 方法:

    1
    2
    3
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Descriptor);

    public override void Initialize(AnalysisContext context) => context.RegisterSymbolAction(AnalyzeClass, SymbolKind.NamedType);

    Initialize 方法需要向 Roslyn 注册我们对什么样的东西感兴趣,比如 MessagePack 的 Analyzer 需要对一个 class 的 properties 进行分析,所以此处使用 RegisterSymbolAction 加上 SymbolKind.NamedType,效果将包括 struct, class, delegate 等,会在分析函数内筛选出 class。

  5. AnalyzeClass 上使用 Light Bulb 快速创建我们需要的方法,并改写成如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    private void AnalyzeClass(SymbolAnalysisContext obj)
    {
    var symbol = (INamedTypeSymbol)obj.Symbol;
    if (symbol.TypeKind != TypeKind.Class)
    return;

    Dictionary<int, string> ids = new Dictionary<int, string>();
    foreach (var member in symbol.GetMembers().Where(x => x is IPropertySymbol || x is IFieldSymbol))
    {
    var attribute = member.GetAttributes().FirstOrDefault(x => x.AttributeClass.ToString() == "MsgPack.Serialization.MessagePackMemberAttribute");
    if (attribute == null)
    continue;

    var id = (int)attribute.ConstructorArguments[0].Value;
    if (ids.ContainsKey(id))
    obj.ReportDiagnostic(Diagnostic.Create(Descriptor, member.Locations[0], ids[id], member.Name, symbol.Name, id));
    else
    ids.Add(id, member.Name);
    }
    }

    Roslyn 会将注册过的 Symbol 传递给我们的分析函数,所以此处 obj.Symbol 一定是一个 INamedTypeSymbol

    首先判断这个 Symbol 是否是一个 class,然后对其每个 property 和 field 进行检测,看它是否有 MessagePackMemberAttribute Attribute 并且是否每个 ID 都不同,如果发现相同,则向 Roslyn 进行报告。Roslyn 默认会按照 Descriptor 中提供的警报级别进行,比如我们指定了 DiagnosticSeverity.Error 会让此次 Build 失败。但是用户也可以根据需要,在上面图中列出的 Analyzer 上右键,覆盖默认的警报级别。

4. 使用

将 Analyzer 的 project 编译完毕后,在需要使用的项目的 References 处右键,选择 Add Analyzers,然后添加生成出的 dll 即可。如果 project 仅包含 Analyzer 代码,则不需要直接添加 project 到 References,否则 Roslyn 会将这个 dll 一起复制到生成目录里去。

重新编译,立刻就能看到图1的代码丢出了错误。

也可以将此 Analyzer 打包成 NuGet package,但是我没研究,就略了。