Skip to content

诊断 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);
        }
    }
}

工作原理:

  1. 定义诊断规则 (DiagnosticDescriptor)
  2. 声明分析器支持的诊断
  3. 注册分析动作 (当遇到类声明时调用)
  4. 实现检查逻辑并报告问题

示例 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.Error

4. 启用并发执行

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);

🔗 相关资源

进阶学习

完整参考

相关主题


⏭️ 下一步

完成本指南后,建议:

  1. 实践 - 创建一个简单的命名规则分析器
  2. 学习中级 - 了解更复杂的诊断场景
  3. 探索任务索引 - 查看更多实际应用场景

最后更新: 2025-01-21

基于 MIT 许可发布