诊断 API 基础
⏱️ 10-15 分钟 | 📚 基础 | 前置知识: 无
🎯 学习目标
完成本指南后,你将能够:
- [ ] 理解诊断 API 的基本概念
- [ ] 创建诊断描述符 (DiagnosticDescriptor)
- [ ] 报告诊断到 IDE
- [ ] 选择合适的严重级别
- [ ] 实现简单的诊断分析器
📖 什么是诊断 API?
诊断 API 是 Roslyn 提供的工具,用于创建自定义的代码分析器。通过诊断 API,你可以:
- 检测代码问题 - 发现命名不规范、性能问题、潜在错误
- 提供实时反馈 - 在 IDE 中实时显示波浪线和错误信息
- 引导最佳实践 - 帮助团队遵循编码规范
- 自动化代码审查 - 减少人工审查的工作量
典型应用场景:
- 强制执行团队的命名规范
- 检测常见的性能问题
- 发现潜在的空引用错误
- 确保 API 的正确使用
🔧 核心概念
1. DiagnosticDescriptor (诊断描述符)
诊断描述符定义了诊断的元数据,包括 ID、标题、消息格式等。
csharp
using Microsoft.CodeAnalysis;
// 创建诊断描述符
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: "DEMO001", // 唯一标识符
title: "类名应该以大写字母开头", // 标题
messageFormat: "类名 '{0}' 应该以大写字母开头", // 消息格式
category: "Naming", // 类别
defaultSeverity: DiagnosticSeverity.Warning, // 严重级别
isEnabledByDefault: true); // 是否默认启用2. Diagnostic (诊断实例)
诊断实例表示一个具体的问题,包含位置和消息参数。
csharp
// 创建诊断实例
var diagnostic = Diagnostic.Create(
descriptor: Rule, // 使用的描述符
location: node.GetLocation(), // 问题位置
messageArgs: className); // 消息参数
// 报告诊断
context.ReportDiagnostic(diagnostic);3. DiagnosticAnalyzer (诊断分析器)
分析器是实现诊断逻辑的类,继承自 DiagnosticAnalyzer。
csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyzer : DiagnosticAnalyzer
{
// 声明支持的诊断
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
// 初始化分析器
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
}
// 实现分析逻辑
private void Analyze(SyntaxNodeAnalysisContext context) { }
}💡 实践示例
示例 1: 检查类名命名规范
这是一个完整的示例,检查类名是否以大写字母开头。
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ClassNameAnalyzer : DiagnosticAnalyzer
{
// 1. 定义诊断描述符
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: "NAMING001",
title: "类名应该以大写字母开头",
messageFormat: "类名 '{0}' 应该以大写字母开头",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 2. 声明支持的诊断
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Rule);
// 3. 初始化分析器
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
// 注册分析动作
context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration);
}
// 4. 实现分析逻辑
private void AnalyzeClass(SyntaxNodeAnalysisContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
var className = classDeclaration.Identifier.Text;
// 检查类名是否以大写字母开头
if (!char.IsUpper(className[0]))
{
// 创建并报告诊断
var diagnostic = Diagnostic.Create(
Rule,
classDeclaration.Identifier.GetLocation(),
className);
context.ReportDiagnostic(diagnostic);
}
}
}工作原理:
- 定义诊断规则 (DiagnosticDescriptor)
- 声明分析器支持的诊断
- 注册分析动作 (当遇到类声明时调用)
- 实现检查逻辑并报告问题
示例 2: 检查方法名命名规范
检查方法名是否使用 PascalCase。
csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MethodNameAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
id: "NAMING002",
title: "方法名应该使用 PascalCase",
messageFormat: "方法名 '{0}' 应该使用 PascalCase",
category: "Naming",
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(AnalyzeMethod, SyntaxKind.MethodDeclaration);
}
private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var methodName = methodDeclaration.Identifier.Text;
// 检查是否为 PascalCase
if (!IsPascalCase(methodName))
{
var diagnostic = Diagnostic.Create(
Rule,
methodDeclaration.Identifier.GetLocation(),
methodName);
context.ReportDiagnostic(diagnostic);
}
}
private bool IsPascalCase(string name)
{
return !string.IsNullOrEmpty(name) &&
char.IsUpper(name[0]) &&
!name.Contains('_');
}
}🎨 严重级别
诊断有四个严重级别,决定了在 IDE 中的显示方式:
Error (错误)
- 显示: 红色波浪线
- 影响: 阻止编译
- 用途: 必须修复的问题
csharp
defaultSeverity: DiagnosticSeverity.Error使用场景:
- 违反语言规则
- 会导致运行时错误
- 违反强制性约束
Warning (警告)
- 显示: 绿色波浪线
- 影响: 不阻止编译
- 用途: 应该修复的问题
csharp
defaultSeverity: DiagnosticSeverity.Warning使用场景:
- 命名约定违规
- 性能问题
- 可能的错误
Info (信息)
- 显示: 灰色点状下划线
- 影响: 不阻止编译
- 用途: 建议性改进
csharp
defaultSeverity: DiagnosticSeverity.Info使用场景:
- 代码风格建议
- 可选的优化
- 文档建议
Hidden (隐藏)
- 显示: 不显示
- 影响: 不阻止编译
- 用途: 代码修复触发
csharp
defaultSeverity: DiagnosticSeverity.Hidden使用场景:
- 只通过代码修复触发
- 标记不必要的代码
📋 常用方法
创建诊断描述符
csharp
// 基本创建
var descriptor = new DiagnosticDescriptor(
id: "MYLIB001",
title: "标题",
messageFormat: "消息 '{0}'",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 带描述和帮助链接
var descriptor = new DiagnosticDescriptor(
id: "MYLIB001",
title: "标题",
messageFormat: "消息 '{0}'",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "详细描述",
helpLinkUri: "https://docs.example.com/rules");报告诊断
csharp
// 基本报告
var diagnostic = Diagnostic.Create(Rule, location);
context.ReportDiagnostic(diagnostic);
// 带消息参数
var diagnostic = Diagnostic.Create(Rule, location, arg1, arg2);
context.ReportDiagnostic(diagnostic);
// 带附加位置
var diagnostic = Diagnostic.Create(
Rule,
mainLocation,
additionalLocations,
null,
arg1);
context.ReportDiagnostic(diagnostic);注册分析动作
csharp
// 语法节点分析
context.RegisterSyntaxNodeAction(
AnalyzeClass,
SyntaxKind.ClassDeclaration);
// 多种节点类型
context.RegisterSyntaxNodeAction(
AnalyzeNode,
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.InterfaceDeclaration);
// 符号分析
context.RegisterSymbolAction(
AnalyzeSymbol,
SymbolKind.NamedType);✅ 最佳实践
1. 使用一致的 ID 命名
csharp
// ✅ 好的做法
private const string IdPrefix = "MYLIB";
public const string ClassNameRule = "MYLIB1001";
public const string MethodNameRule = "MYLIB1002";
// ❌ 不好的做法
public const string Rule1 = "ABC123";
public const string Rule2 = "XYZ";2. 提供清晰的消息
csharp
// ✅ 好的做法
messageFormat: "类名 '{0}' 应该使用 PascalCase 命名"
// ❌ 不好的做法
messageFormat: "名称不正确"3. 选择合适的严重级别
csharp
// ✅ 好的做法
// 命名问题使用 Warning
defaultSeverity: DiagnosticSeverity.Warning
// ❌ 不好的做法
// 所有问题都使用 Error
defaultSeverity: DiagnosticSeverity.Error4. 启用并发执行
csharp
// ✅ 好的做法
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
}5. 使用精确的位置
csharp
// ✅ 好的做法 - 只标记标识符
var diagnostic = Diagnostic.Create(
Rule,
classDeclaration.Identifier.GetLocation(),
className);
// ❌ 不好的做法 - 标记整个类
var diagnostic = Diagnostic.Create(
Rule,
classDeclaration.GetLocation(),
className);🔗 相关资源
进阶学习
- 诊断 API 中级指南 - 多位置诊断、自定义属性
- 诊断 API 高级指南 - 复杂分析器、性能优化
完整参考
- 诊断 API 完整参考 - 完整的 API 文档
相关主题
⏭️ 下一步
完成本指南后,建议:
- 实践 - 创建一个简单的命名规则分析器
- 学习中级 - 了解更复杂的诊断场景
- 探索任务索引 - 查看更多实际应用场景
最后更新: 2025-01-21