Skip to content

DiagnosticDescriptor 详解

本文档详细介绍 DiagnosticDescriptor 类,包括如何创建和配置诊断描述符。

📚 文档信息

  • 难度级别: 中级
  • 预计阅读时间: 20 分钟
  • 前置知识:
    • C# 基础语法
    • Roslyn 基本概念

🎯 学习目标

通过本文档,你将学会:

  • ✅ 创建诊断描述符
  • ✅ 配置诊断的各个参数
  • ✅ 使用一致的 ID 命名约定
  • ✅ 设计清晰的诊断消息
  • ✅ 选择合适的类别和严重级别

📖 快速导航

主题描述
什么是 DiagnosticDescriptor基本概念
创建诊断描述符基本用法
核心参数详解参数说明
完整示例实际应用
真实使用场景场景示例
最佳实践推荐做法
反模式和常见错误避免陷阱

什么是 DiagnosticDescriptor?

DiagnosticDescriptor 定义了诊断的元数据,包括 ID、标题、消息格式、类别和严重级别。每个诊断规则都需要一个描述符。

它就像是诊断的"身份证",包含了诊断的所有基本信息。

创建诊断描述符

DiagnosticDescriptor 的构造函数接受以下参数:

csharp
using Microsoft.CodeAnalysis;

public static class BasicDiagnosticDescriptor
{
    public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        // id: 诊断的唯一标识符
        id: "MYLIB001",
        
        // title: 诊断的简短标题
        title: "类名应该使用 PascalCase",
        
        // messageFormat: 诊断消息的格式字符串,可以包含参数占位符
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名",
        
        // category: 诊断的类别(如 "Naming", "Performance", "Design" 等)
        category: "Naming",
        
        // defaultSeverity: 默认的严重级别
        defaultSeverity: DiagnosticSeverity.Warning,
        
        // isEnabledByDefault: 是否默认启用此诊断
        isEnabledByDefault: true,
        
        // description: 诊断的详细描述(可选)
        description: "类名应该遵循 PascalCase 命名约定,即每个单词的首字母大写。",
        
        // helpLinkUri: 帮助文档的链接(可选)
        helpLinkUri: "https://docs.example.com/naming-rules",
        
        // customTags: 自定义标签(可选)
        customTags: new[] { WellKnownDiagnosticTags.Telemetry });
}

核心参数详解

id(诊断 ID)

诊断 ID 是诊断的唯一标识符,应该遵循一致的命名约定:

csharp
public static class DiagnosticIds
{
    // ID 命名格式:[前缀][类别代码][序号]
    
    // 前缀:通常使用库名、公司名或项目名
    private const string Prefix = "MYLIB";
    
    // 命名规则 (Naming) - 1xxx 系列
    public const string ClassNameRule = "MYLIB1001";
    public const string MethodNameRule = "MYLIB1002";
    public const string PropertyNameRule = "MYLIB1003";
    public const string FieldNameRule = "MYLIB1004";
    public const string ParameterNameRule = "MYLIB1005";
    
    // 性能规则 (Performance) - 2xxx 系列
    public const string AvoidBoxing = "MYLIB2001";
    public const string AvoidStringConcat = "MYLIB2002";
    public const string AvoidObjectCreationInLoop = "MYLIB2003";
    public const string UseStringBuilder = "MYLIB2004";
    
    // 安全规则 (Safety) - 3xxx 系列
    public const string NullCheck = "MYLIB3001";
    public const string BoundsCheck = "MYLIB3002";
    public const string UnsafeCast = "MYLIB3003";
    public const string ResourceDisposal = "MYLIB3004";
    
    // 设计规则 (Design) - 4xxx 系列
    public const string InterfaceImplementation = "MYLIB4001";
    public const string AbstractMemberImplementation = "MYLIB4002";
    public const string SealedClassInheritance = "MYLIB4003";
    
    // 可维护性规则 (Maintainability) - 5xxx 系列
    public const string MethodComplexity = "MYLIB5001";
    public const string ClassComplexity = "MYLIB5002";
    public const string MethodLength = "MYLIB5003";
}

messageFormat(消息格式)

消息格式支持参数占位符,用于在报告诊断时插入动态内容:

csharp
public static class MessageFormatExamples
{
    // 无参数的消息
    public static readonly DiagnosticDescriptor SimpleMessage = new DiagnosticDescriptor(
        id: "MYLIB001",
        title: "简单消息",
        messageFormat: "这是一个简单的诊断消息",
        category: "Example",
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true);
    
    // 单个参数的消息
    public static readonly DiagnosticDescriptor SingleParameter = new DiagnosticDescriptor(
        id: "MYLIB002",
        title: "单参数消息",
        messageFormat: "类名 '{0}' 不符合命名规范",
        category: "Example",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    // 多个参数的消息
    public static readonly DiagnosticDescriptor MultipleParameters = new DiagnosticDescriptor(
        id: "MYLIB003",
        title: "多参数消息",
        messageFormat: "方法 '{0}' 的参数 '{1}' 类型为 '{2}',应该改为 '{3}'",
        category: "Example",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    // 带格式化的消息
    public static readonly DiagnosticDescriptor FormattedMessage = new DiagnosticDescriptor(
        id: "MYLIB004",
        title: "格式化消息",
        messageFormat: "方法复杂度为 {0:N0},超过了阈值 {1:N0}",
        category: "Example",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
}

category(类别)

类别用于组织和分类诊断规则:

csharp
public static class DiagnosticCategories
{
    // 标准类别
    public const string Naming = "Naming";              // 命名规则
    public const string Performance = "Performance";    // 性能问题
    public const string Design = "Design";              // 设计问题
    public const string Maintainability = "Maintainability";  // 可维护性
    public const string Reliability = "Reliability";    // 可靠性
    public const string Security = "Security";          // 安全性
    public const string Usage = "Usage";                // 使用方式
    public const string Style = "Style";                // 代码风格
    
    // 自定义类别
    public const string Documentation = "Documentation";  // 文档
    public const string Testing = "Testing";            // 测试
    public const string Localization = "Localization";  // 本地化
}

defaultSeverity(默认严重级别)

严重级别决定了诊断在 IDE 中的显示方式:

csharp
public static class SeverityExamples
{
    // Error:错误,阻止编译
    public static readonly DiagnosticDescriptor ErrorRule = new DiagnosticDescriptor(
        id: "MYLIB001",
        title: "必须实现接口成员",
        messageFormat: "类 '{0}' 必须实现接口 '{1}' 的成员 '{2}'",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);
    
    // Warning:警告,不阻止编译但需要注意
    public static readonly DiagnosticDescriptor WarningRule = new DiagnosticDescriptor(
        id: "MYLIB002",
        title: "避免在循环中创建对象",
        messageFormat: "在循环中创建 '{0}' 对象可能导致性能问题",
        category: "Performance",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    // Info:信息,提供建议
    public static readonly DiagnosticDescriptor InfoRule = new DiagnosticDescriptor(
        id: "MYLIB003",
        title: "可以使用更简洁的语法",
        messageFormat: "可以使用 '{0}' 替代 '{1}'",
        category: "Style",
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true);
    
    // Hidden:隐藏,不在编辑器中显示但可以通过代码修复触发
    public static readonly DiagnosticDescriptor HiddenRule = new DiagnosticDescriptor(
        id: "MYLIB004",
        title: "代码修复可用",
        messageFormat: "可以简化此代码",
        category: "Style",
        defaultSeverity: DiagnosticSeverity.Hidden,
        isEnabledByDefault: true);
}

完整示例:诊断描述符集合

以下是一个完整的诊断描述符集合示例:

csharp
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;

public static class CompleteDiagnosticDescriptors
{
    // 诊断 ID 前缀
    private const string IdPrefix = "MYLIB";
    
    // 类别常量
    private const string CategoryNaming = "Naming";
    private const string CategoryPerformance = "Performance";
    private const string CategorySafety = "Safety";
    private const string CategoryDesign = "Design";
    private const string CategoryMaintainability = "Maintainability";
    
    // 所有诊断描述符的集合
    public static ImmutableArray<DiagnosticDescriptor> AllDescriptors { get; } =
        ImmutableArray.Create(
            ClassNameRule,
            MethodNameRule,
            AvoidObjectCreationInLoop,
            AvoidStringConcatenation,
            NullReferenceCheck,
            UnsafeCast,
            MustImplementInterfaceMember,
            MethodTooComplex);
    
    // 命名规则
    public static readonly DiagnosticDescriptor ClassNameRule = new DiagnosticDescriptor(
        id: $"{IdPrefix}1001",
        title: "类名应该使用 PascalCase",
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名(首字母大写)",
        category: CategoryNaming,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "类名应该遵循 PascalCase 命名约定,即每个单词的首字母大写。例如:MyClass, UserAccount, OrderProcessor。",
        helpLinkUri: "https://docs.microsoft.com/dotnet/csharp/fundamentals/coding-style/identifier-names",
        customTags: new[] { WellKnownDiagnosticTags.Telemetry });
    
    public static readonly DiagnosticDescriptor MethodNameRule = new DiagnosticDescriptor(
        id: $"{IdPrefix}1002",
        title: "方法名应该使用 PascalCase",
        messageFormat: "方法名 '{0}' 应该使用 PascalCase 命名",
        category: CategoryNaming,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "方法名应该遵循 PascalCase 命名约定。",
        customTags: new[] { WellKnownDiagnosticTags.Telemetry });
    
    // 性能规则
    public static readonly DiagnosticDescriptor AvoidObjectCreationInLoop = new DiagnosticDescriptor(
        id: $"{IdPrefix}2001",
        title: "避免在循环中创建对象",
        messageFormat: "在循环中创建 '{0}' 对象可能导致性能问题,考虑在循环外创建或使用对象池",
        category: CategoryPerformance,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "在循环中重复创建对象会增加 GC 压力,影响性能。应该考虑对象重用或使用对象池。");
    
    public static readonly DiagnosticDescriptor AvoidStringConcatenation = new DiagnosticDescriptor(
        id: $"{IdPrefix}2002",
        title: "避免在循环中使用字符串拼接",
        messageFormat: "在循环中使用字符串拼接(+)会导致性能问题,建议使用 StringBuilder",
        category: CategoryPerformance,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "字符串是不可变的,每次拼接都会创建新的字符串对象。在循环中应该使用 StringBuilder。");
    
    // 安全规则
    public static readonly DiagnosticDescriptor NullReferenceCheck = new DiagnosticDescriptor(
        id: $"{IdPrefix}3001",
        title: "可能的空引用",
        messageFormat: "'{0}' 可能为 null,应该添加空值检查",
        category: CategorySafety,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "访问可能为 null 的引用会导致 NullReferenceException。");
    
    public static readonly DiagnosticDescriptor UnsafeCast = new DiagnosticDescriptor(
        id: $"{IdPrefix}3002",
        title: "不安全的类型转换",
        messageFormat: "从 '{0}' 到 '{1}' 的转换可能失败,建议使用 'as' 或 'is' 模式匹配",
        category: CategorySafety,
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true,
        description: "直接类型转换可能抛出 InvalidCastException,建议使用更安全的转换方式。");
    
    // 设计规则
    public static readonly DiagnosticDescriptor MustImplementInterfaceMember = new DiagnosticDescriptor(
        id: $"{IdPrefix}4001",
        title: "必须实现接口成员",
        messageFormat: "类 '{0}' 必须实现接口 '{1}' 的成员 '{2}'",
        category: CategoryDesign,
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        description: "实现接口时必须提供所有接口成员的实现。");
    
    // 可维护性规则
    public static readonly DiagnosticDescriptor MethodTooComplex = new DiagnosticDescriptor(
        id: $"{IdPrefix}5001",
        title: "方法过于复杂",
        messageFormat: "方法 '{0}' 的圈复杂度为 {1},超过了阈值 {2}",
        category: CategoryMaintainability,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "高圈复杂度的方法难以理解和维护,应该考虑重构。");
}

真实使用场景

场景 1:命名规则检查器

csharp
public class NamingRuleChecker
{
    private static readonly DiagnosticDescriptor ClassNameRule = new DiagnosticDescriptor(
        id: "NAMING001",
        title: "类名应该使用 PascalCase",
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名",
        category: "Naming",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "类名应该遵循 PascalCase 命名约定。");
    
    public bool IsPascalCase(string name)
    {
        if (string.IsNullOrEmpty(name))
            return false;
        
        // 首字母应该大写
        if (!char.IsUpper(name[0]))
            return false;
        
        // 不应该包含下划线
        if (name.Contains('_'))
            return false;
        
        return true;
    }
}

场景 2:性能分析器

csharp
public class PerformanceAnalyzer
{
    private static readonly DiagnosticDescriptor AvoidObjectCreationInLoop = new DiagnosticDescriptor(
        id: "PERF001",
        title: "避免在循环中创建对象",
        messageFormat: "在循环中创建 '{0}' 对象可能导致性能问题",
        category: "Performance",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "在循环中重复创建对象会增加 GC 压力。");
    
    private static readonly DiagnosticDescriptor AvoidStringConcatenation = new DiagnosticDescriptor(
        id: "PERF002",
        title: "避免在循环中使用字符串拼接",
        messageFormat: "在循环中使用字符串拼接会导致性能问题,建议使用 StringBuilder",
        category: "Performance",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "字符串拼接会创建新的字符串对象。");
}

场景 3:安全检查器

csharp
public class SecurityChecker
{
    private static readonly DiagnosticDescriptor NullReferenceCheck = new DiagnosticDescriptor(
        id: "SAFE001",
        title: "可能的空引用",
        messageFormat: "'{0}' 可能为 null,应该添加空值检查",
        category: "Safety",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "访问可能为 null 的引用会导致 NullReferenceException。");
    
    private static readonly DiagnosticDescriptor UnsafeCast = new DiagnosticDescriptor(
        id: "SAFE002",
        title: "不安全的类型转换",
        messageFormat: "从 '{0}' 到 '{1}' 的转换可能失败",
        category: "Safety",
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true,
        description: "直接类型转换可能抛出异常。");
}

最佳实践

1. 使用一致的 ID 命名约定

csharp
// ✅ 正确:使用一致的前缀和类别编码
public const string ClassNameRule = "MYLIB1001";  // 命名规则
public const string AvoidBoxing = "MYLIB2001";    // 性能规则

// ❌ 错误:不一致的命名
public const string Rule1 = "ABC123";
public const string SomeRule = "XYZ";

原因:一致的命名约定使诊断 ID 更容易理解和管理。

2. 提供清晰的标题和消息

csharp
// ✅ 正确:清晰、具体的标题和消息
messageFormat: "类名 '{0}' 应该使用 PascalCase 命名"

// ❌ 错误:模糊的消息
messageFormat: "名称不正确"

原因:清晰的消息帮助开发者快速理解问题。

3. 选择合适的严重级别

csharp
// ✅ 正确:根据问题的严重程度选择级别
// 错误:阻止编译的问题
defaultSeverity: DiagnosticSeverity.Error

// 警告:应该修复但不阻止编译
defaultSeverity: DiagnosticSeverity.Warning

// 信息:建议性的改进
defaultSeverity: DiagnosticSeverity.Info

// ❌ 错误:所有问题都使用 Error
defaultSeverity: DiagnosticSeverity.Error  // 过于严格

原因:合适的严重级别帮助开发者区分问题的优先级。

4. 提供详细的描述和帮助链接

csharp
// ✅ 正确:提供详细描述和帮助链接
description: "类名应该遵循 PascalCase 命名约定,即每个单词的首字母大写。",
helpLinkUri: "https://docs.microsoft.com/dotnet/csharp/fundamentals/coding-style/identifier-names"

// ❌ 错误:没有描述和帮助链接
description: null,
helpLinkUri: null

原因:详细的描述和帮助链接帮助开发者理解规则和如何修复。

5. 使用合适的类别

csharp
// ✅ 正确:使用标准类别
category: "Naming"
category: "Performance"
category: "Design"

// ❌ 错误:使用不明确的类别
category: "Other"
category: "Misc"

原因:标准类别使诊断更容易分类和过滤。

6. 合理使用 isEnabledByDefault

csharp
// ✅ 正确:重要的规则默认启用
isEnabledByDefault: true  // 命名规则、性能问题

// ✅ 正确:可选的规则默认禁用
isEnabledByDefault: false  // 代码风格偏好

// ❌ 错误:所有规则都默认启用
isEnabledByDefault: true  // 包括可选的风格规则

原因:默认启用重要规则,可选规则让用户选择。

7. 使用参数化的消息格式

csharp
// ✅ 正确:使用参数占位符
messageFormat: "类名 '{0}' 应该使用 PascalCase 命名"

// ❌ 错误:硬编码具体值
messageFormat: "类名 MyClass 应该使用 PascalCase 命名"

原因:参数化的消息更灵活,可以适应不同的情况。

8. 添加遥测标签(可选)

csharp
// ✅ 正确:为重要的诊断添加遥测标签
customTags: new[] { WellKnownDiagnosticTags.Telemetry }

// 这允许收集诊断的使用统计

原因:遥测数据帮助了解诊断的使用情况。

9. 组织和管理描述符

csharp
// ✅ 正确:使用静态类组织描述符
public static class DiagnosticDescriptors
{
    public static ImmutableArray<DiagnosticDescriptor> AllDescriptors { get; } =
        ImmutableArray.Create(Rule1, Rule2, Rule3);
    
    public static readonly DiagnosticDescriptor Rule1 = ...;
    public static readonly DiagnosticDescriptor Rule2 = ...;
}

// ❌ 错误:分散在多个地方
public class Analyzer1
{
    private static readonly DiagnosticDescriptor Rule = ...;
}

原因:集中管理描述符更容易维护。

10. 使用常量定义类别和 ID

csharp
// ✅ 正确:使用常量
private const string CategoryNaming = "Naming";
private const string IdPrefix = "MYLIB";

public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    id: $"{IdPrefix}1001",
    category: CategoryNaming,
    ...);

// ❌ 错误:硬编码字符串
public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    id: "MYLIB1001",
    category: "Naming",
    ...);

原因:使用常量避免拼写错误,更容易重构。

反模式和常见错误

反模式 1:使用不一致的 ID

csharp
// ❌ 反模式:ID 没有规律
public const string Rule1 = "ABC001";
public const string Rule2 = "XYZ123";
public const string Rule3 = "PERF1";

// ✅ 正确做法:使用一致的前缀和编号
public const string Rule1 = "MYLIB1001";
public const string Rule2 = "MYLIB1002";
public const string Rule3 = "MYLIB2001";

问题:不一致的 ID 难以管理和理解。

反模式 2:消息过于简单或模糊

csharp
// ❌ 反模式:消息不清楚
messageFormat: "错误"
messageFormat: "这里有问题"

// ✅ 正确做法:提供具体信息
messageFormat: "类名 '{0}' 应该使用 PascalCase 命名"
messageFormat: "方法 '{0}' 的圈复杂度为 {1},超过了阈值 {2}"

问题:模糊的消息让开发者不知道如何修复。

反模式 3:滥用 Error 级别

csharp
// ❌ 反模式:所有问题都是 Error
defaultSeverity: DiagnosticSeverity.Error  // 命名问题
defaultSeverity: DiagnosticSeverity.Error  // 代码风格

// ✅ 正确做法:根据严重程度选择
defaultSeverity: DiagnosticSeverity.Warning  // 命名问题
defaultSeverity: DiagnosticSeverity.Info     // 代码风格

问题:过多的错误会让开发者忽略真正重要的问题。

反模式 4:没有提供帮助信息

csharp
// ❌ 反模式:没有描述和帮助链接
description: null,
helpLinkUri: null

// ✅ 正确做法:提供详细信息
description: "类名应该遵循 PascalCase 命名约定。",
helpLinkUri: "https://docs.example.com/naming-rules"

问题:开发者不知道为什么这是问题以及如何修复。

反模式 5:使用不明确的类别

csharp
// ❌ 反模式:类别不清楚
category: "Other"
category: "General"
category: "Misc"

// ✅ 正确做法:使用标准类别
category: "Naming"
category: "Performance"
category: "Design"

问题:不明确的类别难以分类和过滤。

🔗 相关文档

其他参考


返回: 诊断 API 索引

基于 MIT 许可发布