诊断和错误报告
项目位置
06-Diagnostics/
学习目标
- 理解诊断 API 的核心概念
- 掌握不同严重级别的使用
- 学习错误消息设计原则
- 实现位置精确的错误报告
- 支持本地化错误消息
- 提供代码修复建议
学习如何在源生成器中实现诊断功能,向用户报告错误、警告和信息。
为什么需要诊断?
诊断功能是源生成器的重要组成部分,它可以:
- 提供清晰的错误消息: 当用户代码不符合要求时,给出明确的错误提示
- 指出问题位置: 在 IDE 中精确标记出问题代码的位置
- 提供不同级别的反馈: 错误(Error)、警告(Warning)、信息(Info)
- 改善开发体验: 让用户快速定位和修复问题
诊断严重级别
C# 编译器支持四种诊断严重级别:
| 级别 | 说明 | IDE 显示 | 是否阻止编译 |
|---|---|---|---|
| Error | 错误,必须修复 | 红色波浪线 | 是 |
| Warning | 警告,建议修复 | 黄色波浪线 | 否 |
| Info | 信息,提供建议 | 蓝色波浪线 | 否 |
| Hidden | 隐藏,不显示但可配置 | 不显示 | 否 |
定义诊断描述符
csharp
public static class DiagnosticDescriptors
{
// 错误级别
public static readonly DiagnosticDescriptor InvalidTargetError = new(
id: "DIAG0001",
title: "特性目标无效",
messageFormat: "特性 '{0}' 只能应用于 partial 类",
category: "Diagnostics.Generator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
// 警告级别
public static readonly DiagnosticDescriptor MissingPartialWarning = new(
id: "DIAG0002",
title: "缺少 partial 修饰符",
messageFormat: "类 '{0}' 应该声明为 partial",
category: "Diagnostics.Generator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 信息级别
public static readonly DiagnosticDescriptor PerformanceInfo = new(
id: "DIAG0003",
title: "性能优化建议",
messageFormat: "字段 '{0}' 建议使用 readonly 修饰符",
category: "Diagnostics.Generator",
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);
}报告诊断信息
csharp
private static void ValidateAndGenerate(
ClassDeclarationSyntax classSyntax,
SourceProductionContext context)
{
// 验证:检查类是否为 partial
if (!classSyntax.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.InvalidTargetError,
classSyntax.Identifier.GetLocation(),
"GenerateCode",
className);
context.ReportDiagnostic(diagnostic);
return; // 错误情况下不生成代码
}
// 继续生成代码...
}诊断 ID 命名约定
诊断 ID 应该遵循一致的命名约定:
[项目前缀][数字]示例:
DIAG0001- 第 1 个诊断DIAG0002- 第 2 个诊断SG0001- Source Generator 项目的第 1 个诊断
建议:
- 使用 4 位数字(0001-9999)
- 按严重级别分组:0001-1999 错误,2000-3999 警告,4000-9999 信息
何时使用不同的严重级别
| 级别 | 使用场景 |
|---|---|
| Error | 代码无法生成或会导致运行时错误 |
| Warning | 代码可以生成但可能不是用户想要的 |
| Info | 提供优化建议或最佳实践 |
| Hidden | 用于代码修复或重构建议 |
诊断消息最佳实践
好的诊断消息:
- ✅ 清晰描述问题:
类 'Person' 必须声明为 partial - ✅ 提供解决方案:
添加 partial 关键字到类声明 - ✅ 使用用户的术语:
类、属性、方法 - ✅ 包含具体信息:类名、属性名等
不好的诊断消息:
- ❌ 模糊不清:
无效的目标 - ❌ 技术术语:
INamedTypeSymbol 不满足约束 - ❌ 没有上下文:
错误
常见错误场景
1. 类未声明为 partial
csharp
// 错误
[GenerateCode]
class MyClass { }
// 正确
[GenerateCode]
partial class MyClass { }2. 没有公共属性
csharp
// 警告
[GenerateCode]
partial class MyClass
{
private string _data;
}
// 正确
[GenerateCode]
partial class MyClass
{
public string Data { get; set; }
}学到的知识点
- Diagnostic API 核心概念
- 不同严重级别的使用场景
- 位置信息的重要性
- 诊断消息的最佳实践
- 错误处理策略
相关资源
完整项目结构
诊断流程图
严重级别图
诊断 API 详解
完整诊断描述符定义
csharp
using Microsoft.CodeAnalysis;
namespace Diagnostics.Generator
{
public static class DiagnosticDescriptors
{
private const string Category = "Diagnostics.Generator";
// 错误:类必须是 partial
public static readonly DiagnosticDescriptor MustBePartial = new(
id: "DIAG0001",
title: "Class must be partial",
messageFormat: "The class '{0}' must be declared as partial to use code generation",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Classes that use source generation must be declared with the partial keyword.",
helpLinkUri: "https://docs.example.com/DIAG0001");
// 错误:缺少必需属性
public static readonly DiagnosticDescriptor MissingRequiredProperty = new(
id: "DIAG0002",
title: "Missing required property",
messageFormat: "The class '{0}' is missing required property '{1}'",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
// 警告:属性应该是公共的
public static readonly DiagnosticDescriptor PropertyShouldBePublic = new(
id: "DIAG1001",
title: "Property should be public",
messageFormat: "The property '{0}' should be public for code generation",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 警告:命名约定
public static readonly DiagnosticDescriptor NamingConvention = new(
id: "DIAG1002",
title: "Naming convention violation",
messageFormat: "The {0} '{1}' does not follow naming conventions (expected: {2})",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 信息:性能优化建议
public static readonly DiagnosticDescriptor PerformanceOptimization = new(
id: "DIAG2001",
title: "Performance optimization suggestion",
messageFormat: "Consider using '{0}' instead of '{1}' for better performance",
category: Category,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);
// 信息:最佳实践
public static readonly DiagnosticDescriptor BestPractice = new(
id: "DIAG2002",
title: "Best practice recommendation",
messageFormat: "Consider {0} for better code quality",
category: Category,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true);
}
}报告诊断的完整示例
csharp
[Generator]
public class DiagnosticsGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => GetClassInfo(ctx)
)
.Where(info => info != null);
context.RegisterSourceOutput(
classDeclarations,
(spc, classInfo) => ValidateAndGenerate(spc, classInfo!)
);
}
private static void ValidateAndGenerate(
SourceProductionContext context,
ClassInfo classInfo)
{
// 验证 1:检查是否是 partial 类
if (!classInfo.IsPartial)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.MustBePartial,
classInfo.Location,
classInfo.Name);
context.ReportDiagnostic(diagnostic);
return; // 错误情况下不继续
}
// 验证 2:检查是否有公共属性
if (classInfo.Properties.Length == 0)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.MissingRequiredProperty,
classInfo.Location,
classInfo.Name,
"at least one public property");
context.ReportDiagnostic(diagnostic);
return;
}
// 验证 3:检查属性命名约定(警告)
foreach (var prop in classInfo.Properties)
{
if (!char.IsUpper(prop.Name[0]))
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.NamingConvention,
prop.Location,
"property",
prop.Name,
$"{char.ToUpper(prop.Name[0])}{prop.Name.Substring(1)}");
context.ReportDiagnostic(diagnostic);
}
}
// 验证 4:性能优化建议(信息)
foreach (var prop in classInfo.Properties)
{
if (prop.Type == "List<string>")
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.PerformanceOptimization,
prop.Location,
"ImmutableArray<string>",
"List<string>");
context.ReportDiagnostic(diagnostic);
}
}
// 所有验证通过,生成代码
GenerateCode(context, classInfo);
}
}错误消息设计
设计原则
- 清晰明确:直接说明问题
- 提供上下文:包含相关的名称和位置
- 给出解决方案:告诉用户如何修复
- 使用用户语言:避免技术术语
- 保持一致:使用统一的格式和风格
好的错误消息示例
csharp
// ✅ 好的错误消息
"The class 'Person' must be declared as partial to use code generation"
// - 明确指出问题(必须是 partial)
// - 包含类名(Person)
// - 说明原因(使用代码生成)
"The property 'Name' is missing the [Required] attribute"
// - 指出具体属性(Name)
// - 说明缺少什么([Required] 特性)
"Consider using 'ImmutableArray<T>' instead of 'List<T>' for better performance"
// - 提供具体建议
// - 说明原因(更好的性能)不好的错误消息示例
csharp
// ❌ 不好的错误消息
"Invalid target"
// - 太模糊,不知道什么无效
"Error in code generation"
// - 没有具体信息
"INamedTypeSymbol does not satisfy constraints"
// - 使用技术术语,用户难以理解
"Failed"
// - 完全没有信息错误消息模板
csharp
// 模板 1:缺少必需元素
"The {element_type} '{element_name}' is missing {required_thing}"
// 示例:"The class 'Person' is missing the partial keyword"
// 模板 2:不正确的使用
"The {element_type} '{element_name}' cannot be used with {feature}"
// 示例:"The property 'Id' cannot be used with [GenerateBuilder]"
// 模板 3:建议改进
"Consider {action} for {benefit}"
// 示例:"Consider using readonly for better performance"
// 模板 4:命名约定
"The {element_type} '{actual_name}' does not follow naming conventions (expected: {expected_name})"
// 示例:"The property 'userName' does not follow naming conventions (expected: UserName)"高级功能
功能 1:本地化错误消息
csharp
// 使用资源文件支持多语言
public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor MustBePartial = new(
id: "DIAG0001",
title: new LocalizableResourceString(
nameof(Resources.DIAG0001_Title),
Resources.ResourceManager,
typeof(Resources)),
messageFormat: new LocalizableResourceString(
nameof(Resources.DIAG0001_MessageFormat),
Resources.ResourceManager,
typeof(Resources)),
category: "Diagnostics.Generator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
// Resources.resx (英文)
// DIAG0001_Title: "Class must be partial"
// DIAG0001_MessageFormat: "The class '{0}' must be declared as partial"
// Resources.zh-CN.resx (中文)
// DIAG0001_Title: "类必须是 partial"
// DIAG0001_MessageFormat: "类 '{0}' 必须声明为 partial"功能 2:自定义诊断配置
csharp
// 允许用户通过 .editorconfig 配置诊断级别
// .editorconfig
[*.cs]
dotnet_diagnostic.DIAG0001.severity = error
dotnet_diagnostic.DIAG1001.severity = warning
dotnet_diagnostic.DIAG2001.severity = suggestion
// 在生成器中读取配置
private static DiagnosticSeverity GetConfiguredSeverity(
AnalyzerConfigOptions options,
string diagnosticId,
DiagnosticSeverity defaultSeverity)
{
if (options.TryGetValue($"dotnet_diagnostic.{diagnosticId}.severity", out var value))
{
return value switch
{
"error" => DiagnosticSeverity.Error,
"warning" => DiagnosticSeverity.Warning,
"suggestion" => DiagnosticSeverity.Info,
"silent" => DiagnosticSeverity.Hidden,
"none" => DiagnosticSeverity.Hidden,
_ => defaultSeverity
};
}
return defaultSeverity;
}功能 3:诊断抑制
csharp
// 支持通过特性抑制诊断
[SuppressDiagnostic("DIAG1001")]
public class MyClass
{
private string _name; // 不会报告 DIAG1001 警告
}
// 在生成器中检查抑制
private static bool IsDiagnosticSuppressed(
ISymbol symbol,
string diagnosticId)
{
return symbol.GetAttributes()
.Any(a => a.AttributeClass?.Name == "SuppressDiagnosticAttribute" &&
a.ConstructorArguments.Any(arg =>
arg.Value?.ToString() == diagnosticId));
}真实应用场景
场景 1:验证特性使用
csharp
// 验证特性只能用于 partial 类
private static void ValidateAttributeUsage(
SourceProductionContext context,
ClassInfo classInfo)
{
if (!classInfo.IsPartial)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MustBePartial,
classInfo.Location,
classInfo.Name));
}
}场景 2:验证属性类型
csharp
// 验证属性类型是否支持
private static void ValidatePropertyTypes(
SourceProductionContext context,
ClassInfo classInfo)
{
var supportedTypes = new[] { "string", "int", "bool", "DateTime" };
foreach (var prop in classInfo.Properties)
{
if (!supportedTypes.Contains(prop.Type))
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.UnsupportedType,
prop.Location,
prop.Type,
string.Join(", ", supportedTypes)));
}
}
}场景 3:性能警告
csharp
// 警告性能问题
private static void CheckPerformance(
SourceProductionContext context,
ClassInfo classInfo)
{
// 检查是否有大量属性
if (classInfo.Properties.Length > 50)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.TooManyProperties,
classInfo.Location,
classInfo.Name,
classInfo.Properties.Length));
}
}最佳实践 vs 反模式
对比表格
| 方面 | ✅ 最佳实践 | ❌ 反模式 | 原因 |
|---|---|---|---|
| 错误消息 | 清晰具体 | 模糊不清 | 帮助用户快速定位问题 |
| 位置信息 | 精确到符号 | 使用 Location.None | IDE 可以直接跳转 |
| 严重级别 | 根据影响选择 | 全部使用 Error | 避免过度警告 |
| 帮助链接 | 提供文档链接 | 不提供链接 | 用户可以查看详细说明 |
| 诊断 ID | 使用一致的命名 | 随意命名 | 便于管理和配置 |
| 本地化 | 支持多语言 | 只有英文 | 提高国际化支持 |
详细示例
1. 错误消息质量
csharp
// ❌ 反模式:模糊的错误消息
var diagnostic = Diagnostic.Create(
new DiagnosticDescriptor(
"ERR001",
"Error",
"Invalid",
"Error",
DiagnosticSeverity.Error,
true),
Location.None);
// ✅ 最佳实践:清晰的错误消息
var diagnostic = Diagnostic.Create(
new DiagnosticDescriptor(
"DIAG0001",
"Class must be partial",
"The class '{0}' must be declared as partial to use code generation",
"Diagnostics.Generator",
DiagnosticSeverity.Error,
true,
helpLinkUri: "https://docs.example.com/DIAG0001"),
classDecl.Identifier.GetLocation(),
className);2. 位置信息
csharp
// ❌ 反模式:不提供位置
context.ReportDiagnostic(Diagnostic.Create(
descriptor,
Location.None, // 用户不知道问题在哪里
className));
// ✅ 最佳实践:精确的位置
context.ReportDiagnostic(Diagnostic.Create(
descriptor,
classDecl.Identifier.GetLocation(), // 精确到类名
className));常见问题
1. 如何选择诊断严重级别?
决策树:
是否阻止代码生成?
├─ 是 → Error
└─ 否 → 是否影响功能?
├─ 是 → Warning
└─ 否 → 是否是建议?
├─ 是 → Info
└─ 否 → Hidden2. 诊断 ID 如何命名?
推荐格式:[PREFIX][CATEGORY][NUMBER]
- PREFIX: 项目前缀(如 DIAG, SG)
- CATEGORY: 类别(0=错误, 1=警告, 2=信息)
- NUMBER: 序号(001-999)
示例:
DIAG0001- 第 1 个错误DIAG1001- 第 1 个警告DIAG2001- 第 1 个信息
3. 如何测试诊断?
csharp
[Fact]
public void ReportsError_WhenClassIsNotPartial()
{
var source = @"
[GenerateCode]
class MyClass { }";
var (diagnostics, _) = GetGeneratedOutput(source);
Assert.Single(diagnostics);
Assert.Equal("DIAG0001", diagnostics[0].Id);
Assert.Equal(DiagnosticSeverity.Error, diagnostics[0].Severity);
}扩展练习
练习 1:添加代码修复
实现 CodeFixProvider 自动修复诊断问题。
练习 2:支持批量抑制
允许在项目级别抑制特定诊断。
练习 3:诊断分析器
创建分析器统计项目中的诊断分布。
🔗 相关资源
深入学习
- 快速开始 - 开始使用源生成器
- Hello World 示例 - 最简单的入门示例
- Builder 生成器 - 复杂的代码生成模式
- 增量生成器示例 - 学习增量生成器的使用
- 测试示例 - 了解如何测试源生成器
API 参考
官方文档
相关文档
总结
诊断功能是源生成器的重要组成部分,通过提供清晰的错误消息和精确的位置信息,可以大大改善开发体验。关键要点:
- 选择合适的严重级别
- 提供清晰的错误消息
- 包含精确的位置信息
- 提供帮助链接
- 支持本地化
文档字数统计:约 5,500 字
🎯 高级诊断场景
场景 1: 多语言支持
实现支持多语言的诊断消息:
csharp
public class LocalizedDiagnostics
{
private static readonly LocalizableString Title = new LocalizableResourceString(
nameof(Resources.AnalyzerTitle),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(
nameof(Resources.AnalyzerMessageFormat),
Resources.ResourceManager,
typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(
nameof(Resources.AnalyzerDescription),
Resources.ResourceManager,
typeof(Resources));
public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
"GEN001",
Title,
MessageFormat,
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);
}
// Resources.resx
// AnalyzerTitle: Invalid Usage
// AnalyzerMessageFormat: Type '{0}' cannot be used here
// AnalyzerDescription: This analyzer checks for invalid type usage场景 2: 上下文相关的诊断
根据代码上下文提供不同的诊断:
csharp
public void AnalyzeContext(SyntaxNodeAnalysisContext context)
{
var node = context.Node;
var semanticModel = context.SemanticModel;
// 检查是否在异步方法中
var containingMethod = node.Ancestors()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault();
if (containingMethod != null)
{
var methodSymbol = semanticModel.GetDeclaredSymbol(containingMethod);
if (methodSymbol?.IsAsync == true)
{
// 在异步方法中的特定诊断
context.ReportDiagnostic(Diagnostic.Create(
AsyncMethodRule,
node.GetLocation(),
methodSymbol.Name));
}
else
{
// 在同步方法中的诊断
context.ReportDiagnostic(Diagnostic.Create(
SyncMethodRule,
node.GetLocation(),
methodSymbol.Name));
}
}
}场景 3: 批量诊断报告
一次性报告多个相关的诊断:
csharp
public void AnalyzeClass(INamedTypeSymbol classSymbol, SourceProductionContext context)
{
var diagnostics = new List<Diagnostic>();
// 检查类名
if (!IsValidClassName(classSymbol.Name))
{
diagnostics.Add(Diagnostic.Create(
InvalidClassNameRule,
classSymbol.Locations.FirstOrDefault(),
classSymbol.Name));
}
// 检查属性
foreach (var property in classSymbol.GetMembers().OfType<IPropertySymbol>())
{
if (!IsValidProperty(property))
{
diagnostics.Add(Diagnostic.Create(
InvalidPropertyRule,
property.Locations.FirstOrDefault(),
property.Name));
}
}
// 检查方法
foreach (var method in classSymbol.GetMembers().OfType<IMethodSymbol>())
{
if (!IsValidMethod(method))
{
diagnostics.Add(Diagnostic.Create(
InvalidMethodRule,
method.Locations.FirstOrDefault(),
method.Name));
}
}
// 批量报告所有诊断
foreach (var diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic);
}
}📊 诊断统计和分析
诊断收集器
创建一个诊断收集器来分析诊断模式:
csharp
public class DiagnosticCollector
{
private readonly Dictionary<string, List<Diagnostic>> _diagnosticsByFile = new();
private readonly Dictionary<DiagnosticSeverity, int> _diagnosticsBySeverity = new();
public void Collect(Diagnostic diagnostic)
{
// 按文件分组
var filePath = diagnostic.Location.SourceTree?.FilePath ?? "Unknown";
if (!_diagnosticsByFile.ContainsKey(filePath))
{
_diagnosticsByFile[filePath] = new List<Diagnostic>();
}
_diagnosticsByFile[filePath].Add(diagnostic);
// 按严重级别统计
if (!_diagnosticsBySeverity.ContainsKey(diagnostic.Severity))
{
_diagnosticsBySeverity[diagnostic.Severity] = 0;
}
_diagnosticsBySeverity[diagnostic.Severity]++;
}
public DiagnosticReport GenerateReport()
{
return new DiagnosticReport
{
TotalCount = _diagnosticsByFile.Values.Sum(list => list.Count),
ErrorCount = _diagnosticsBySeverity.GetValueOrDefault(DiagnosticSeverity.Error, 0),
WarningCount = _diagnosticsBySeverity.GetValueOrDefault(DiagnosticSeverity.Warning, 0),
InfoCount = _diagnosticsBySeverity.GetValueOrDefault(DiagnosticSeverity.Info, 0),
FileCount = _diagnosticsByFile.Count,
DiagnosticsByFile = _diagnosticsByFile.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Count)
};
}
}
public class DiagnosticReport
{
public int TotalCount { get; set; }
public int ErrorCount { get; set; }
public int WarningCount { get; set; }
public int InfoCount { get; set; }
public int FileCount { get; set; }
public Dictionary<string, int> DiagnosticsByFile { get; set; }
public override string ToString()
{
return $@"
Diagnostic Report
=================
Total: {TotalCount}
Errors: {ErrorCount}
Warnings: {WarningCount}
Info: {InfoCount}
Files: {FileCount}
";
}
}🔄 诊断流程图
完整的诊断处理流程
💡 诊断设计模式
模式 1: 链式验证
使用链式验证模式组织复杂的验证逻辑:
csharp
public class ValidationChain
{
private readonly List<Func<ISymbol, ValidationResult>> _validators = new();
public ValidationChain AddValidator(Func<ISymbol, ValidationResult> validator)
{
_validators.Add(validator);
return this;
}
public List<Diagnostic> Validate(ISymbol symbol)
{
var diagnostics = new List<Diagnostic>();
foreach (var validator in _validators)
{
var result = validator(symbol);
if (!result.IsValid)
{
diagnostics.Add(result.Diagnostic);
}
}
return diagnostics;
}
}
// 使用示例
var chain = new ValidationChain()
.AddValidator(symbol => ValidateName(symbol))
.AddValidator(symbol => ValidateAccessibility(symbol))
.AddValidator(symbol => ValidateAttributes(symbol));
var diagnostics = chain.Validate(classSymbol);模式 2: 诊断工厂
使用工厂模式创建一致的诊断:
csharp
public class DiagnosticFactory
{
public static Diagnostic CreateError(
string id,
string title,
string message,
Location location,
params object[] messageArgs)
{
var descriptor = new DiagnosticDescriptor(
id,
title,
message,
"Generator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
return Diagnostic.Create(descriptor, location, messageArgs);
}
public static Diagnostic CreateWarning(
string id,
string title,
string message,
Location location,
params object[] messageArgs)
{
var descriptor = new DiagnosticDescriptor(
id,
title,
message,
"Generator",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
return Diagnostic.Create(descriptor, location, messageArgs);
}
public static Diagnostic CreateInfo(
string id,
string title,
string message,
Location location,
params object[] messageArgs)
{
var descriptor = new DiagnosticDescriptor(
id,
title,
message,
"Generator",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
return Diagnostic.Create(descriptor, location, messageArgs);
}
}
// 使用示例
var diagnostic = DiagnosticFactory.CreateError(
"GEN001",
"Invalid Class",
"Class '{0}' is invalid",
classDecl.GetLocation(),
className);🎓 学习建议
初学者
- 从简单的错误检测开始
- 学习如何创建 DiagnosticDescriptor
- 理解不同的严重级别
- 练习报告诊断
中级
- 实现自定义验证规则
- 学习位置和范围的使用
- 添加代码修复建议
- 处理复杂的验证场景
高级
- 实现多语言支持
- 创建诊断分析工具
- 优化诊断性能
- 设计诊断架构
📚 相关资源
❓ 常见问题解答 (FAQ)
Q2: 如何为诊断添加代码修复?
A: 使用 CodeFixProvider:
csharp
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MyCodeFixProvider))]
public class MyCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create("GEN001");
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 node = root.FindNode(diagnosticSpan);
context.RegisterCodeFix(
CodeAction.Create(
title: "Fix the issue",
createChangedDocument: c => FixIssue(context.Document, node, c),
equivalenceKey: "FixIssue"),
diagnostic);
}
private async Task<Document> FixIssue(
Document document,
SyntaxNode node,
CancellationToken cancellationToken)
{
// 实现修复逻辑
var newNode = node.WithAdditionalAnnotations();
var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root.ReplaceNode(node, newNode);
return document.WithSyntaxRoot(newRoot);
}
}Q3: 如何测试诊断报告?
A: 使用 Roslyn 测试框架:
csharp
[Fact]
public async Task Analyzer_ReportsDiagnostic_ForInvalidCode()
{
var test = @"
[Generate]
public class TestClass
{
// 无效的代码
}
";
var expected = new DiagnosticResult
{
Id = "GEN001",
Message = "Invalid class definition",
Severity = DiagnosticSeverity.Error,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 2, 9)
}
};
await VerifyCSharpDiagnosticAsync(test, expected);
}最后更新: 2025-01-21
🚀 实战项目:完整的诊断系统
项目概述
构建一个完整的诊断系统,包括:
- 多种诊断规则
- 严重级别管理
- 诊断收集和报告
- 代码修复建议
项目结构
DiagnosticSystem/
├── Descriptors/
│ ├── ErrorDescriptors.cs
│ ├── WarningDescriptors.cs
│ └── InfoDescriptors.cs
├── Analyzers/
│ ├── ClassAnalyzer.cs
│ ├── PropertyAnalyzer.cs
│ └── MethodAnalyzer.cs
├── Reporters/
│ ├── DiagnosticReporter.cs
│ └── DiagnosticCollector.cs
└── Fixers/
├── ClassFixer.cs
└── PropertyFixer.cs实现示例
1. 诊断描述符管理
csharp
public static class DiagnosticDescriptors
{
private const string Category = "Generator";
// 错误级别
public static class Errors
{
public static readonly DiagnosticDescriptor MissingAttribute = new(
"GEN001",
"Missing Required Attribute",
"Type '{0}' is missing the required '{1}' attribute",
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "All generated types must have the required attribute.");
public static readonly DiagnosticDescriptor InvalidType = new(
"GEN002",
"Invalid Type",
"Type '{0}' cannot be used for code generation",
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
// 警告级别
public static class Warnings
{
public static readonly DiagnosticDescriptor PerformanceIssue = new(
"GEN101",
"Performance Issue",
"Type '{0}' may cause performance issues",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor ObsoleteUsage = new(
"GEN102",
"Obsolete Usage",
"Type '{0}' uses obsolete pattern",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
// 信息级别
public static class Info
{
public static readonly DiagnosticDescriptor CodeGenerated = new(
"GEN201",
"Code Generated",
"Generated code for type '{0}'",
Category,
DiagnosticSeverity.Info,
isEnabledByDefault: true);
}
}2. 综合分析器
csharp
public class ComprehensiveAnalyzer
{
public List<Diagnostic> Analyze(INamedTypeSymbol typeSymbol)
{
var diagnostics = new List<Diagnostic>();
// 分析类
diagnostics.AddRange(AnalyzeClass(typeSymbol));
// 分析属性
foreach (var property in typeSymbol.GetMembers().OfType<IPropertySymbol>())
{
diagnostics.AddRange(AnalyzeProperty(property));
}
// 分析方法
foreach (var method in typeSymbol.GetMembers().OfType<IMethodSymbol>())
{
diagnostics.AddRange(AnalyzeMethod(method));
}
return diagnostics;
}
private List<Diagnostic> AnalyzeClass(INamedTypeSymbol typeSymbol)
{
var diagnostics = new List<Diagnostic>();
// 检查特性
if (!HasRequiredAttribute(typeSymbol))
{
diagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.Errors.MissingAttribute,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol.Name,
"GenerateAttribute"));
}
// 检查访问修饰符
if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
{
diagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.Warnings.PerformanceIssue,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol.Name));
}
return diagnostics;
}
private List<Diagnostic> AnalyzeProperty(IPropertySymbol propertySymbol)
{
var diagnostics = new List<Diagnostic>();
// 检查属性类型
if (IsComplexType(propertySymbol.Type))
{
diagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.Warnings.PerformanceIssue,
propertySymbol.Locations.FirstOrDefault(),
propertySymbol.Name));
}
return diagnostics;
}
private List<Diagnostic> AnalyzeMethod(IMethodSymbol methodSymbol)
{
var diagnostics = new List<Diagnostic>();
// 检查方法复杂度
if (methodSymbol.Parameters.Length > 5)
{
diagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.Warnings.PerformanceIssue,
methodSymbol.Locations.FirstOrDefault(),
methodSymbol.Name));
}
return diagnostics;
}
private bool HasRequiredAttribute(INamedTypeSymbol typeSymbol)
{
return typeSymbol.GetAttributes()
.Any(a => a.AttributeClass?.Name == "GenerateAttribute");
}
private bool IsComplexType(ITypeSymbol typeSymbol)
{
return typeSymbol.TypeKind == TypeKind.Class &&
typeSymbol.SpecialType == SpecialType.None;
}
}3. 诊断报告生成器
csharp
public class DiagnosticReportGenerator
{
public string GenerateReport(List<Diagnostic> diagnostics)
{
var sb = new StringBuilder();
sb.AppendLine("# 诊断报告");
sb.AppendLine();
sb.AppendLine($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine();
// 统计信息
var errorCount = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error);
var warningCount = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Warning);
var infoCount = diagnostics.Count(d => d.Severity == DiagnosticSeverity.Info);
sb.AppendLine("## 统计");
sb.AppendLine($"- 总计: {diagnostics.Count}");
sb.AppendLine($"- 错误: {errorCount}");
sb.AppendLine($"- 警告: {warningCount}");
sb.AppendLine($"- 信息: {infoCount}");
sb.AppendLine();
// 按严重级别分组
if (errorCount > 0)
{
sb.AppendLine("## 错误");
foreach (var diagnostic in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))
{
sb.AppendLine($"- [{diagnostic.Id}] {diagnostic.GetMessage()}");
sb.AppendLine($" 位置: {diagnostic.Location}");
}
sb.AppendLine();
}
if (warningCount > 0)
{
sb.AppendLine("## 警告");
foreach (var diagnostic in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning))
{
sb.AppendLine($"- [{diagnostic.Id}] {diagnostic.GetMessage()}");
sb.AppendLine($" 位置: {diagnostic.Location}");
}
sb.AppendLine();
}
return sb.ToString();
}
}📈 诊断最佳实践总结
设计原则
- 清晰的消息: 诊断消息应该清楚地说明问题
- 准确的位置: 指向问题的确切位置
- 合适的严重级别: 根据问题的影响选择级别
- 可操作的建议: 提供如何修复问题的建议
- 一致的 ID: 使用有意义的诊断 ID
实施步骤
- 定义诊断描述符: 创建清晰的诊断规则
- 实现分析逻辑: 编写检测问题的代码
- 报告诊断: 在适当的时机报告问题
- 测试诊断: 验证诊断的准确性
- 文档化: 记录诊断规则和修复方法
性能考虑
- 避免重复分析: 缓存分析结果
- 早期退出: 在发现严重错误时停止
- 批量报告: 收集所有诊断后一次性报告
- 异步处理: 对于耗时的分析使用异步方法
🎯 总结
通过本示例,你学习了:
✅ 如何创建诊断描述符 ✅ 如何报告不同严重级别的诊断 ✅ 如何指定诊断位置 ✅ 如何设计诊断消息 ✅ 如何实现完整的诊断系统 ✅ 如何测试和验证诊断
诊断是源生成器的重要组成部分,它帮助用户理解和修复问题。一个好的诊断系统可以大大提升用户体验。
继续学习其他示例,深入掌握源生成器的各个方面!
最后更新: 2025-01-21