Roslyn 分析器开发
大约 10 分钟约 3059 字
Roslyn 分析器开发
简介
Roslyn 是 .NET 编译器平台,提供完整的 C#/VB 编译器 API。通过 Roslyn 分析器(Analyzer),可以在编写代码时实时检测问题、强制编码规范、自动修复代码。本文深入讲解 Roslyn 分析器的开发、测试和发布。
特点
分析器项目结构
创建分析器项目
# 安装模板
dotnet new install Roslyn.Templates
# 创建分析器项目
dotnet new roslyn-analyzer -n MyAnalyzer
# 项目结构
# MyAnalyzer/
# ├── MyAnalyzer.csproj # 分析器项目
# ├── MyAnalyzer.cs # 诊断分析器
# ├── MyAnalyzerCodeFixProvider.cs # 代码修复
# ├── MyAnalyzer.Test/ # 测试项目
# └── MyAnalyzer.Package/ # NuGet 打包csproj 配置
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>诊断分析器开发
基本分析器结构
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncVoidMethodAnalyzer : DiagnosticAnalyzer
{
// 定义诊断规则
public static readonly DiagnosticDescriptor Rule = new(
id: "MY001",
title: "避免使用 async void 方法",
messageFormat: "方法 '{0}' 是 async void,建议改为 async Task",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "async void 方法中的异常无法被捕获,应使用 async Task。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
// 注册语法节点分析器
context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);
}
private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
var methodDecl = (MethodDeclarationSyntax)context.Node;
// 检查是否是 async void
if (methodDecl.Modifiers.Any(SyntaxKind.AsyncKeyword) &&
methodDecl.ReturnType is PredefinedTypeSyntax { Keyword: { ValueText: "void" } })
{
var diagnostic = Diagnostic.Create(Rule, methodDecl.Identifier.GetLocation(),
methodDecl.Identifier.ValueText);
context.ReportDiagnostic(diagnostic);
}
}
}高级分析:检测 Dispose 未调用
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DisposableNotDisposedAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor Rule = new(
id: "MY002",
title: "IDisposable 对象未调用 Dispose",
messageFormat: "变量 '{0}' 实现了 IDisposable 但未被 using 或 Dispose 管理",
category: "Reliability",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeLocalDeclaration, SyntaxKind.LocalDeclarationStatement);
}
private void AnalyzeLocalDeclaration(SyntaxNodeAnalysisContext context)
{
var localDecl = (LocalDeclarationStatementSyntax)context.Node;
// 已经在 using 中,跳过
if (localDecl.UsingKeyword != default) return;
foreach (var variable in localDecl.Declaration.Variables)
{
if (variable.Initializer == null) continue;
// 获取语义信息
var typeInfo = context.SemanticModel.GetTypeInfo(variable.Initializer.Value);
var type = typeInfo.Type;
if (type == null) continue;
// 检查是否实现 IDisposable
var disposable = context.SemanticModel.Compilation.GetTypeByMetadataName("System.IDisposable");
if (disposable == null) return;
if (!type.AllInterfaces.Contains(disposable)) continue;
// 检查变量是否在 using 或 try-finally 中
var method = variable.FirstAncestorOrSelf<MethodDeclarationSyntax>();
if (method == null) continue;
// 简化检查:如果方法中没有调用 Dispose 或未使用 using
var methodName = variable.Identifier.ValueText;
var hasDispose = method.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Any(inv => inv.Expression.ToString().Contains($"{methodName}.Dispose"));
if (!hasDispose)
{
context.ReportDiagnostic(Diagnostic.Create(Rule,
variable.Identifier.GetLocation(), methodName));
}
}
}
}Code Fix Provider
自动修复实现
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncVoidCodeFixProvider))]
[Shared]
public class AsyncVoidCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create("MY001");
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var declaration = root!.FindToken(diagnosticSpan.Start)
.Parent!.AncestorsAndSelf()
.OfType<MethodDeclarationSyntax>()
.First();
// 注册修复操作
context.RegisterCodeFix(
CodeAction.Create(
title: "将 async void 改为 async Task",
createChangedDocument: c => ConvertToAsyncTask(context.Document, declaration, c),
equivalenceKey: "ConvertToAsyncTask"),
diagnostic);
}
private async Task<Document> ConvertToAsyncTask(
Document document, MethodDeclarationSyntax methodDecl, CancellationToken ct)
{
var root = await document.GetSyntaxRootAsync(ct);
// 创建新的返回类型 Task
var taskType = SyntaxFactory.IdentifierName("Task")
.WithLeadingTrivia(methodDecl.ReturnType.GetLeadingTrivia());
// 替换返回类型
var newMethod = methodDecl.WithReturnType(taskType);
var newRoot = root!.ReplaceNode(methodDecl, newMethod);
return document.WithSyntaxRoot(newRoot);
}
}SyntaxNode 操作
语法树遍历
// SyntaxWalker — 深度遍历语法树
class MethodWalker : CSharpSyntaxWalker
{
public List<MethodDeclarationSyntax> Methods { get; } = new();
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
Methods.Add(node);
base.VisitMethodDeclaration(node); // 继续遍历子节点
}
}
// 使用
var tree = CSharpSyntaxTree.ParseText(sourceCode);
var walker = new MethodWalker();
walker.Visit(tree.GetRoot());
foreach (var method in walker.Methods)
{
Console.WriteLine($"方法: {method.Identifier}, 参数: {method.ParameterList.Parameters.Count}");
}
// SyntaxRewriter — 转换语法树
class AsyncToSyncRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node)
{
// 移除 async 修饰符
var newModifiers = node.Modifiers
.Where(m => !m.IsKind(SyntaxKind.AsyncKeyword))
.ToTokenList();
// 将 Task<T> 返回类型改为 T
TypeSyntax newReturnType = node.ReturnType;
if (node.ReturnType is GenericNameSyntax { Identifier.ValueText: "Task" } generic)
{
newReturnType = generic.TypeArgumentList.Arguments[0];
}
else if (node.ReturnType is IdentifierNameSyntax { Identifier.ValueText: "Task" })
{
newReturnType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword));
}
return node.WithModifiers(newModifiers).WithReturnType(newReturnType);
}
}SemanticModel 语义分析
// SemanticModel 提供类型信息、符号解析等
void AnalyzeWithSemantic(SemanticModel semanticModel, SyntaxNode node)
{
// 获取类型信息
var typeInfo = semanticModel.GetTypeInfo(node);
Console.WriteLine($"Type: {typeInfo.Type?.Name}");
Console.WriteLine($"ConvertedType: {typeInfo.ConvertedType?.Name}");
// 获取符号信息
var symbolInfo = semanticModel.GetSymbolInfo(node);
if (symbolInfo.Symbol is IMethodSymbol method)
{
Console.WriteLine($"Method: {method.Name}");
Console.WriteLine($"ReturnType: {method.ReturnType}");
Console.WriteLine($"Parameters: {string.Join(", ", method.Parameters.Select(p => p.Type))}");
}
// 检查是否是扩展方法
if (symbolInfo.Symbol is IMethodSymbol { IsExtensionMethod: true } ext)
{
Console.WriteLine($"扩展方法: {ext.Name}, 定义在: {ext.ContainingType}");
}
// 检查方法是否被标记为过时
if (symbolInfo.Symbol?.GetAttributes().Any(a =>
a.AttributeClass?.Name == "ObsoleteAttribute") == true)
{
Console.WriteLine("此成员已过时");
}
}测试分析器
单元测试
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
public class AsyncVoidAnalyzerTests
{
[Fact]
public async Task AsyncVoidMethod_ShouldReportWarning()
{
var testCode = @"
using System.Threading.Tasks;
class TestClass
{
async void {|#0:MyMethod|}()
{
await Task.Delay(100);
}
}";
var expected = new DiagnosticResult("MY001", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("MyMethod");
await new CSharpAnalyzerTest<AsyncVoidMethodAnalyzer, DefaultVerifier>
{
TestCode = testCode,
ExpectedDiagnostics = { expected }
}.RunAsync();
}
[Fact]
public async Task AsyncTaskMethod_ShouldNotReportWarning()
{
var testCode = @"
using System.Threading.Tasks;
class TestClass
{
async Task MyMethod()
{
await Task.Delay(100);
}
}";
await new CSharpAnalyzerTest<AsyncVoidMethodAnalyzer, DefaultVerifier>
{
TestCode = testCode,
ExpectedDiagnostics = { } // 无诊断
}.RunAsync();
}
}Code Fix 进阶:多选项修复
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DisposableCodeFixProvider))]
[Shared]
public class DisposableCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create("MY002");
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var diagnostic = context.Diagnostics.First();
var span = diagnostic.Location.SourceSpan;
var localDecl = root!.FindToken(span.Start)
.Parent!.AncestorsAndSelf()
.OfType<LocalDeclarationStatementSyntax>()
.First();
// 修复选项 1:包裹 using 声明
context.RegisterCodeFix(
CodeAction.Create(
title: "使用 using 声明",
createChangedDocument: c => AddUsingDeclaration(context.Document, localDecl, c),
equivalenceKey: "AddUsingDeclaration"),
diagnostic);
// 修复选项 2:包裹 using 块
context.RegisterCodeFix(
CodeAction.Create(
title: "使用 using 块",
createChangedDocument: c => AddUsingBlock(context.Document, localDecl, c),
equivalenceKey: "AddUsingBlock"),
diagnostic);
}
private async Task<Document> AddUsingDeclaration(
Document document, LocalDeclarationStatementSyntax localDecl, CancellationToken ct)
{
var root = await document.GetSyntaxRootAsync(ct);
// var conn = new SqlConnection(...); -> using var conn = new SqlConnection(...);
var usingToken = SyntaxFactory.Token(SyntaxKind.UsingKeyword);
var newDecl = localDecl.WithUsingKeyword(usingToken);
var newRoot = root!.ReplaceNode(localDecl, newDecl);
return document.WithSyntaxRoot(newRoot);
}
private async Task<Document> AddUsingBlock(
Document document, LocalDeclarationStatementSyntax localDecl, CancellationToken ct)
{
var root = await document.GetSyntaxRootAsync(ct);
// 将 var x = ...; 包裹为 using (var x = ...) { ... }
var usingStatement = SyntaxFactory.UsingStatement(
localDecl.Declaration, null,
SyntaxFactory.Block());
var newRoot = root!.ReplaceNode(localDecl, usingStatement);
return document.WithSyntaxRoot(newRoot);
}
}Source Generator vs 分析器
Roslyn 分析器 vs Source Generator 对比:
| 维度 | 分析器 (Analyzer) | Source Generator |
|-------------|---------------------------|---------------------------|
| 作用 | 检测代码问题,报告诊断 | 生成新的 C# 源代码 |
| 输出 | 诊断信息(警告/错误) | 编译时添加的 C# 文件 |
| 修改代码 | 通过 Code Fix 建议 | 自动注入源代码 |
| 运行时机 | 编译时 + IDE 实时 | 编译时 |
| 典型场景 | 代码规范、Bug 检测 | 减少样板代码、序列化生成 |
| 代表项目 | Roslyn Analyzers | System.Text.Json 源生成器 |
选择建议:
- 检测问题 → 分析器
- 减少样板代码 → Source Generator
- 两者可以配合使用:分析器检测问题,Source Generator 提供生成方案命名约定分析器示例
/// <summary>
/// 检测私有字段命名约定:应以 _ 开头(如 _connectionString)
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PrivateFieldNamingAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor Rule = new(
id: "MY003",
title: "私有字段命名约定",
messageFormat: "私有字段 '{0}' 应以下划线开头,如 _{0}",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://docs.example.com/naming-conventions");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeField, SyntaxKind.FieldDeclaration);
}
private void AnalyzeField(SyntaxNodeAnalysisContext context)
{
var fieldDecl = (FieldDeclarationSyntax)context.Node;
// 只检查私有字段
var isPrivate = fieldDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword));
var isStatic = fieldDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword));
var isReadOnly = fieldDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.ReadOnlyKeyword));
if (!isPrivate) return;
foreach (var variable in fieldDecl.Declaration.Variables)
{
var name = variable.Identifier.ValueText;
// 跳过 static readonly(通常是常量风格,用 PascalCase 或 UPPER_CASE)
if (isStatic && isReadOnly) continue;
// 跳过已以下划线开头的字段
if (name.StartsWith("_")) continue;
var diagnostic = Diagnostic.Create(Rule, variable.Identifier.GetLocation(), name);
context.ReportDiagnostic(diagnostic);
}
}
}分析器性能优化
分析器性能优化要点:
1. 使用 EnableConcurrentExecution() 启用并发分析
2. 使用 ConfigureGeneratedCodeAnalysis(None) 跳过生成代码
3. 精确注册语法节点(避免 RegisterCompilationStartAction 等粗粒度注册)
4. 避免在分析器中执行耗时操作(文件 I/O、网络请求等)
5. 缓存计算结果(使用 ImmutableDictionary 等线程安全结构)
6. 尽早退出:先做简单条件判断,再做昂贵的语义分析
7. 避免使用 context.SemanticModel 除非必要(语法分析比语义分析快得多)
典型分析流程优化:
1. 先检查语法(快速)→ 2. 再检查符号信息(中等)→ 3. 最后做数据流分析(慢)分析器调试技巧
# 方式一:使用 Visual Studio 调试
# 在分析器项目属性中设置:
# Start Action: Start external program
# -> C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe
# Command line arguments: /rootsuffix Exp
# 方式二:使用 VS Code 调试
# .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Analyzer",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["build", "--no-incremental", "${workspaceFolder}/TestProject"],
"cwd": "${workspaceFolder}"
}
]
}
# 方式三:单元测试中调试
# 直接在测试项目中打断点,运行测试即可分析器打包与分发
<!-- NuGet 包自动集成分析器到项目 -->
<!-- MyAnalyzer.Package.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<ItemGroup>
<!-- 将分析器 DLL 打入 tools/analyzer 目录 -->
<None Include="..\MyAnalyzer\bin\Release\netstandard2.0\MyAnalyzer.dll"
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
<!-- 使用方只需安装 NuGet 包,分析器自动生效 -->
<!-- dotnet add package MyAnalyzer -->
<!-- 或在 Directory.Packages.props 中统一管理 -->分析器分发最佳实践:
- 通过 NuGet 包分发,放入 analyzers/dotnet/cs 目录自动激活
- 使用 .globalconfig 或 .editorconfig 控制规则严重级别
- 团队共享:将分析器 NuGet 包放入 Directory.Packages.props
- CI 集成:构建时将分析器警告视为错误(TreatWarningsAsErrors)
- 提供文档:每个规则附带 helpLinkUri 指向详细说明优点
缺点
总结
Roslyn 分析器通过 DiagnosticAnalyzer 基类注册语法节点或符号分析回调,在 IDE 中实时检测代码问题。CodeFixProvider 提供自动修复能力,通过 SyntaxRewriter 转换语法树。SemanticModel 提供类型信息和符号解析,用于实现更精确的分析。使用 Microsoft.CodeAnalysis.Testing 包编写单元测试,通过标记 {|#0:...|} 指定期望诊断位置。分析器通过 NuGet 包分发,项目引用即可生效。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《Roslyn 分析器开发》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《Roslyn 分析器开发》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Roslyn 分析器开发》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Roslyn 分析器开发》最大的收益和代价分别是什么?
