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 索引
- 下一篇: Diagnostic 报告
- 相关: 严重级别
其他参考
返回: 诊断 API 索引