Skip to content

诊断和错误报告

项目位置

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

错误消息设计

设计原则

  1. 清晰明确:直接说明问题
  2. 提供上下文:包含相关的名称和位置
  3. 给出解决方案:告诉用户如何修复
  4. 使用用户语言:避免技术术语
  5. 保持一致:使用统一的格式和风格

好的错误消息示例

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.NoneIDE 可以直接跳转
严重级别根据影响选择全部使用 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
        └─ 否 → Hidden

2. 诊断 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:诊断分析器

创建分析器统计项目中的诊断分布。


🔗 相关资源

深入学习

API 参考

官方文档

相关文档


总结

诊断功能是源生成器的重要组成部分,通过提供清晰的错误消息和精确的位置信息,可以大大改善开发体验。关键要点:

  1. 选择合适的严重级别
  2. 提供清晰的错误消息
  3. 包含精确的位置信息
  4. 提供帮助链接
  5. 支持本地化

文档字数统计:约 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);

🎓 学习建议

初学者

  1. 从简单的错误检测开始
  2. 学习如何创建 DiagnosticDescriptor
  3. 理解不同的严重级别
  4. 练习报告诊断

中级

  1. 实现自定义验证规则
  2. 学习位置和范围的使用
  3. 添加代码修复建议
  4. 处理复杂的验证场景

高级

  1. 实现多语言支持
  2. 创建诊断分析工具
  3. 优化诊断性能
  4. 设计诊断架构

📚 相关资源


❓ 常见问题解答 (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();
    }
}

📈 诊断最佳实践总结

设计原则

  1. 清晰的消息: 诊断消息应该清楚地说明问题
  2. 准确的位置: 指向问题的确切位置
  3. 合适的严重级别: 根据问题的影响选择级别
  4. 可操作的建议: 提供如何修复问题的建议
  5. 一致的 ID: 使用有意义的诊断 ID

实施步骤

  1. 定义诊断描述符: 创建清晰的诊断规则
  2. 实现分析逻辑: 编写检测问题的代码
  3. 报告诊断: 在适当的时机报告问题
  4. 测试诊断: 验证诊断的准确性
  5. 文档化: 记录诊断规则和修复方法

性能考虑

  1. 避免重复分析: 缓存分析结果
  2. 早期退出: 在发现严重错误时停止
  3. 批量报告: 收集所有诊断后一次性报告
  4. 异步处理: 对于耗时的分析使用异步方法

🎯 总结

通过本示例,你学习了:

✅ 如何创建诊断描述符 ✅ 如何报告不同严重级别的诊断 ✅ 如何指定诊断位置 ✅ 如何设计诊断消息 ✅ 如何实现完整的诊断系统 ✅ 如何测试和验证诊断

诊断是源生成器的重要组成部分,它帮助用户理解和修复问题。一个好的诊断系统可以大大提升用户体验。

继续学习其他示例,深入掌握源生成器的各个方面!


最后更新: 2025-01-21

基于 MIT 许可发布