Skip to content

调试技巧

掌握源生成器的各种调试技术,快速定位和解决问题

📋 文档信息

难度级别: 中级
预计阅读时间: 20 分钟
前置知识:

  • C# 调试基础
  • Visual Studio 使用
  • 源生成器基础

🎯 学习目标

学完本章节后,你将能够:

  • 使用 Debugger.Launch() 进行交互式调试
  • 查看和分析生成的文件
  • 使用日志文件记录执行过程
  • 在单元测试中调试生成器
  • 使用诊断分析器输出调试信息

🔍 高级调试技术

方法 1: 使用 Debugger.Launch() 进行交互式调试

csharp
using System.Diagnostics;
using Microsoft.CodeAnalysis;

/// <summary>
/// 支持交互式调试的源生成器
/// </summary>
[Generator]
public class DebuggableGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        #if DEBUG
        // 检查是否需要启动调试器
        if (!Debugger.IsAttached)
        {
            // 方式 1: 自动启动调试器(推荐用于开发)
            Debugger.Launch();
            
            // 方式 2: 等待调试器附加(推荐用于 CI/CD)
            // while (!Debugger.IsAttached)
            // {
            //     System.Threading.Thread.Sleep(100);
            // }
        }
        
        // 设置断点在这里
        Debugger.Break();
        #endif
        
        // 生成器逻辑
        var classes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "GenerateAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) =>
                {
                    // 在这里设置断点可以检查每个类的处理
                    return GetClassInfo(ctx);
                });
        
        context.RegisterSourceOutput(classes, (spc, classInfo) =>
        {
            // 在这里设置断点可以检查代码生成
            var code = GenerateCode(classInfo);
            spc.AddSource($"{classInfo.Name}.g.cs", code);
        });
    }
    
    private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
    {
        var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
        
        // 调试技巧:使用条件断点
        // 右键断点 -> 条件 -> classSymbol.Name == "MyClass"
        
        return new ClassInfo(classSymbol.Name);
    }
    
    private string GenerateCode(ClassInfo classInfo)
    {
        // 调试技巧:使用日志点(Tracepoint)
        // 右键断点 -> 操作 -> 记录消息到输出窗口
        
        return $"public partial class {classInfo.Name} {{ }}";
    }
    
    private record ClassInfo(string Name);
}

使用步骤:

  1. 在生成器代码中添加 Debugger.Launch()
  2. 编译生成器项目
  3. 编译使用生成器的项目
  4. 选择调试器(Visual Studio 实例)
  5. 设置断点并开始调试

方法 2: 查看生成的文件

xml
<!-- 在使用生成器的项目中添加 -->
<PropertyGroup>
  <!-- 启用生成文件输出 -->
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  
  <!-- 指定输出目录(可选) -->
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

生成的文件位置:

obj/
  Debug/
    net8.0/
      generated/
        YourGenerator/
          YourGenerator.Generator/
            GeneratedFile1.g.cs
            GeneratedFile2.g.cs

查看生成文件的代码:

csharp
// 在生成器中添加文件输出日志
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    var code = GenerateCode(classInfo);
    var fileName = $"{classInfo.Name}.g.cs";
    
    #if DEBUG
    // 输出文件名和内容到调试窗口
    System.Diagnostics.Debug.WriteLine($"Generating: {fileName}");
    System.Diagnostics.Debug.WriteLine(code);
    #endif
    
    spc.AddSource(fileName, code);
});

方法 3: 使用日志文件进行调试

csharp
/// <summary>
/// 带日志功能的源生成器
/// </summary>
[Generator]
public class LoggingGenerator : IIncrementalGenerator
{
    // 日志文件路径
    private static readonly string LogPath = 
        Path.Combine(Path.GetTempPath(), "generator.log");
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 清空日志文件
        File.WriteAllText(LogPath, $"=== Generator Started at {DateTime.Now} ===\n");
        
        var classes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "GenerateAttribute",
                predicate: (node, _) =>
                {
                    var result = node is ClassDeclarationSyntax;
                    Log($"Predicate: {node.GetType().Name} -> {result}");
                    return result;
                },
                transform: (ctx, _) =>
                {
                    var classSymbol = (INamedTypeSymbol)ctx.TargetSymbol;
                    Log($"Transform: Processing class {classSymbol.Name}");
                    
                    try
                    {
                        var info = GetClassInfo(ctx);
                        Log($"Transform: Successfully processed {classSymbol.Name}");
                        return info;
                    }
                    catch (Exception ex)
                    {
                        Log($"Transform: Error processing {classSymbol.Name}: {ex}");
                        throw;
                    }
                });
        
        context.RegisterSourceOutput(classes, (spc, classInfo) =>
        {
            Log($"RegisterSourceOutput: Generating code for {classInfo.Name}");
            
            try
            {
                var code = GenerateCode(classInfo);
                spc.AddSource($"{classInfo.Name}.g.cs", code);
                Log($"RegisterSourceOutput: Successfully generated {classInfo.Name}.g.cs");
            }
            catch (Exception ex)
            {
                Log($"RegisterSourceOutput: Error generating {classInfo.Name}: {ex}");
                throw;
            }
        });
        
        Log("=== Generator Initialization Complete ===\n");
    }
    
    private static void Log(string message)
    {
        try
        {
            var logMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}\n";
            File.AppendAllText(LogPath, logMessage);
        }
        catch
        {
            // 忽略日志错误
        }
    }
    
    private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
    {
        var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
        
        Log($"  - Class: {classSymbol.Name}");
        Log($"  - Namespace: {classSymbol.ContainingNamespace}");
        Log($"  - Properties: {classSymbol.GetMembers().OfType<IPropertySymbol>().Count()}");
        
        return new ClassInfo(classSymbol.Name);
    }
    
    private string GenerateCode(ClassInfo classInfo)
    {
        var code = $"public partial class {classInfo.Name} {{ }}";
        Log($"  - Generated code length: {code.Length} characters");
        return code;
    }
    
    private record ClassInfo(string Name);
}

查看日志:

powershell
# Windows
notepad $env:TEMP\generator.log

# 或使用 PowerShell 实时监控
Get-Content $env:TEMP\generator.log -Wait

日志输出示例:

=== Generator Started at 2025-01-21 10:30:45 ===
[10:30:45.123] Predicate: ClassDeclarationSyntax -> True
[10:30:45.125] Transform: Processing class MyClass
[10:30:45.126]   - Class: MyClass
[10:30:45.126]   - Namespace: MyNamespace
[10:30:45.127]   - Properties: 3
[10:30:45.128] Transform: Successfully processed MyClass
[10:30:45.130] RegisterSourceOutput: Generating code for MyClass
[10:30:45.131]   - Generated code length: 35 characters
[10:30:45.132] RegisterSourceOutput: Successfully generated MyClass.g.cs
=== Generator Initialization Complete ===

方法 4: 使用单元测试调试

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;

/// <summary>
/// 源生成器单元测试
/// 可以在测试中设置断点进行调试
/// </summary>
public class GeneratorTests
{
    [Fact]
    public void Generator_GeneratesExpectedCode()
    {
        // 准备输入代码
        var source = @"
using System;

[Generate]
public partial class MyClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}";
        
        // 创建编译
        var compilation = CreateCompilation(source);
        
        // 创建生成器实例
        var generator = new MyGenerator();
        
        // 运行生成器(可以在这里设置断点)
        var driver = CSharpGeneratorDriver.Create(generator);
        driver = driver.RunGeneratorsAndUpdateCompilation(
            compilation, 
            out var outputCompilation, 
            out var diagnostics);
        
        // 验证没有错误
        Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
        
        // 获取生成的代码
        var runResult = driver.GetRunResult();
        var generatedTrees = runResult.GeneratedTrees;
        
        // 在这里设置断点可以检查生成的代码
        Assert.Single(generatedTrees);
        
        var generatedCode = generatedTrees[0].ToString();
        
        // 验证生成的代码
        Assert.Contains("public partial class MyClass", generatedCode);
    }
    
    private static Compilation CreateCompilation(string source)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(source);
        
        var references = new[]
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
        };
        
        return CSharpCompilation.Create(
            "TestAssembly",
            new[] { syntaxTree },
            references,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
    }
}

调试步骤:

  1. 在测试方法中设置断点
  2. 右键测试 -> 调试测试
  3. 可以单步进入生成器代码
  4. 检查中间变量和生成结果

方法 5: 使用诊断分析器

csharp
/// <summary>
/// 带诊断输出的源生成器
/// </summary>
[Generator]
public class DiagnosticGenerator : IIncrementalGenerator
{
    // 定义诊断描述符
    private static readonly DiagnosticDescriptor DebugInfo = new(
        id: "SG9999",
        title: "Generator Debug Info",
        messageFormat: "Debug: {0}",
        category: "Generator",
        DiagnosticSeverity.Info,
        isEnabledByDefault: true);
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "GenerateAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) => GetClassInfo(ctx));
        
        context.RegisterSourceOutput(classes, (spc, classInfo) =>
        {
            // 报告调试信息
            spc.ReportDiagnostic(Diagnostic.Create(
                DebugInfo,
                Location.None,
                $"Processing class: {classInfo.Name}"));
            
            // 报告属性数量
            spc.ReportDiagnostic(Diagnostic.Create(
                DebugInfo,
                Location.None,
                $"Properties found: {classInfo.PropertyCount}"));
            
            var code = GenerateCode(classInfo);
            spc.AddSource($"{classInfo.Name}.g.cs", code);
            
            // 报告生成完成
            spc.ReportDiagnostic(Diagnostic.Create(
                DebugInfo,
                Location.None,
                $"Generated code for: {classInfo.Name}"));
        });
    }
    
    private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
    {
        var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
        var propertyCount = classSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Count();
        
        return new ClassInfo(classSymbol.Name, propertyCount);
    }
    
    private string GenerateCode(ClassInfo classInfo)
    {
        return $"public partial class {classInfo.Name} {{ }}";
    }
    
    private record ClassInfo(string Name, int PropertyCount);
}

查看诊断输出:

在 Visual Studio 的"错误列表"窗口中,选择"消息"级别,可以看到所有诊断信息。

💡 调试技巧总结

使用条件断点

在复杂的生成器中,使用条件断点可以快速定位问题:

csharp
public void Execute(GeneratorExecutionContext context)
{
    foreach (var syntaxTree in context.Compilation.SyntaxTrees)
    {
        var root = syntaxTree.GetRoot();
        var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
        
        foreach (var classDecl in classes)
        {
            // 在这里设置条件断点:classDecl.Identifier.Text == "MyClass"
            var code = GenerateCode(classDecl);
            context.AddSource($"{classDecl.Identifier.Text}.g.cs", code);
        }
    }
}

使用数据断点

监视特定变量的值变化:

csharp
public class GeneratorState
{
    private int _generatedCount = 0;
    
    public void IncrementCount()
    {
        _generatedCount++;  // 在这里设置数据断点
    }
}

使用跟踪点(Tracepoints)

不中断执行,只输出日志:

csharp
public void ProcessClass(ClassDeclarationSyntax classDecl)
{
    // 设置跟踪点:输出 "Processing class: {classDecl.Identifier.Text}"
    var properties = GetProperties(classDecl);
    
    // 设置跟踪点:输出 "Found {properties.Count} properties"
    GenerateCode(properties);
}

🔗 相关资源

💡 关键要点

  1. 使用 Debugger.Launch() 进行交互式调试
  2. 启用 EmitCompilerGeneratedFiles 查看生成的代码
  3. 使用日志文件 记录详细的执行过程
  4. 编写单元测试 在测试环境中调试
  5. 使用诊断分析器 输出调试信息
  6. 使用条件断点 快速定位特定问题
  7. 使用跟踪点 不中断执行地输出日志

最后更新: 2025-01-21

基于 MIT 许可发布