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. 编写
Analyzer 就是个普通的 Class Library,所以创建项目时选择 Class Library 即可。需要注意的是作为 Roslyn 基础的 .NET CoreFx (开源版本)和 .NET Framework 4.5.x 实际上并不兼容,所以需要创建为 .NET Framework 4.6.x 项目。
添加 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,而不会漏掉基础的部件。
将自动创建的 Class1.cs 重命名为 CSharpAnalyzer.cs,并且确认自动对 Class1 类进行重命名。然后给
CSharpAnalyzer
类添加[DiagnosticAnalyzer(LanguageNames.CSharp)]
Attribute,并且使其继承自DiagnosticAnalyzer
类。通过在未添加 Using 引用的语句上使用 Light Bulb(Ctrl + .)可以快速添加 Using,通过在未实现的基类上使用 Light Bulb,可以快速生成需要 override 的部分。在
CSharpAnalyzer
class 内添加一个 static 的DiagnosticDescriptor
,这个 Descriptor 用于描述我们的 Analyzer 将会发出的调试信息,不但会显示在上图所示的列表中,在创建调试信息的时候也需要使用到。1
2static 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
3public 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。在
AnalyzeClass
上使用 Light Bulb 快速创建我们需要的方法,并改写成如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private 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,但是我没研究,就略了。