Skip to content

诊断 API 高级指南

⏱️ 20-30 分钟 | 📚 高级 | 前置知识: 诊断基础, 诊断中级

🎯 学习目标

完成本指南后,你将能够:

  • [ ] 实现高性能的诊断分析器
  • [ ] 处理复杂的分析场景
  • [ ] 优化分析器性能
  • [ ] 实现编译级别的分析
  • [ ] 处理增量分析

📖 核心概念

性能优化的重要性

诊断分析器在 IDE 中频繁运行,性能至关重要:

  • 影响用户体验
  • 影响编译速度
  • 影响 IDE 响应性

高级分析技术

  • 编译分析 - 分析整个编译单元
  • 增量分析 - 只分析变化的部分
  • 缓存策略 - 缓存分析结果
  • 并发执行 - 利用多核处理器

⚡ 性能优化

1. 启用并发执行

csharp
public override void Initialize(AnalysisContext context)
{
    // 启用并发执行 - 必须调用!
    context.EnableConcurrentExecution();
    
    // 配置生成代码分析
    context.ConfigureGeneratedCodeAnalysis(
        GeneratedCodeAnalysisFlags.None);
    
    context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
}

2. 快速检查优先

csharp
private void Analyze(SyntaxNodeAnalysisContext context)
{
    var node = context.Node;
    
    // ✅ 先进行快速的语法检查
    if (!QuickSyntaxCheck(node))
        return;
    
    // 只在必要时进行语义分析
    var semanticModel = context.SemanticModel;
    var typeInfo = semanticModel.GetTypeInfo(node, context.CancellationToken);
    
    if (DetailedCheck(typeInfo))
    {
        ReportDiagnostic(context, node);
    }
}

private bool QuickSyntaxCheck(SyntaxNode node)
{
    // 快速的语法检查,不需要语义信息
    return node.ChildNodes().Count() > 0;
}

3. 缓存重复查询

csharp
public class CachedAnalyzer : DiagnosticAnalyzer
{
    // 使用 ConditionalWeakTable 缓存结果
    private static readonly ConditionalWeakTable<Compilation, ConcurrentDictionary<ISymbol, bool>> 
        _cache = new();
    
    private void AnalyzeSymbol(SymbolAnalysisContext context)
    {
        var compilation = context.Compilation;
        var symbol = context.Symbol;
        
        // 获取或创建缓存
        var symbolCache = _cache.GetOrCreateValue(compilation);
        
        // 检查缓存
        if (symbolCache.TryGetValue(symbol, out var cachedResult))
        {
            if (cachedResult)
            {
                ReportDiagnostic(context, symbol);
            }
            return;
        }
        
        // 执行分析
        bool hasIssue = PerformAnalysis(symbol);
        
        // 缓存结果
        symbolCache.TryAdd(symbol, hasIssue);
        
        if (hasIssue)
        {
            ReportDiagnostic(context, symbol);
        }
    }
    
    private bool PerformAnalysis(ISymbol symbol)
    {
        // 执行实际的分析逻辑
        return false;
    }
    
    private void ReportDiagnostic(SymbolAnalysisContext context, ISymbol symbol) { }
}

4. 避免重复的语义查询

csharp
private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
{
    var method = (MethodDeclarationSyntax)context.Node;
    
    // ❌ 不好 - 多次查询语义模型
    foreach (var statement in method.Body.Statements)
    {
        var typeInfo = context.SemanticModel.GetTypeInfo(statement);
        // ...
    }
    
    // ✅ 好 - 批量收集后一次性查询
    var statements = method.Body.Statements.ToList();
    var typeInfos = statements
        .Select(s => context.SemanticModel.GetTypeInfo(s, context.CancellationToken))
        .ToList();
    
    for (int i = 0; i < statements.Count; i++)
    {
        var statement = statements[i];
        var typeInfo = typeInfos[i];
        // ...
    }
}

🔄 编译级别分析

RegisterCompilationAction

编译级别分析可以访问整个编译单元的信息。

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AssemblyLevelAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "ASSEMBLY001",
        title: "程序集级别检查",
        messageFormat: "程序集包含 {0} 个公共类型",
        category: "Info",
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        // 注册编译开始和结束动作
        context.RegisterCompilationStartAction(OnCompilationStart);
    }
    
    private void OnCompilationStart(CompilationStartAnalysisContext context)
    {
        // 创建编译级别的状态
        var analyzer = new CompilationAnalyzer();
        
        // 注册符号动作
        context.RegisterSymbolAction(
            analyzer.AnalyzeSymbol,
            SymbolKind.NamedType);
        
        // 注册编译结束动作
        context.RegisterCompilationEndAction(analyzer.OnCompilationEnd);
    }
    
    private class CompilationAnalyzer
    {
        private int _publicTypeCount = 0;
        
        public void AnalyzeSymbol(SymbolAnalysisContext context)
        {
            var typeSymbol = (INamedTypeSymbol)context.Symbol;
            
            if (typeSymbol.DeclaredAccessibility == Accessibility.Public)
            {
                Interlocked.Increment(ref _publicTypeCount);
            }
        }
        
        public void OnCompilationEnd(CompilationAnalysisContext context)
        {
            // 在编译结束时报告统计信息
            if (_publicTypeCount > 0)
            {
                var diagnostic = Diagnostic.Create(
                    Rule,
                    Location.None,
                    _publicTypeCount);
                
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

跨文件分析

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CrossFileAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "CROSS001",
        title: "跨文件依赖检查",
        messageFormat: "类型 '{0}' 依赖于 '{1}'",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Info,
        isEnabledByDefault: true);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        context.RegisterCompilationStartAction(OnCompilationStart);
    }
    
    private void OnCompilationStart(CompilationStartAnalysisContext context)
    {
        var analyzer = new DependencyAnalyzer();
        
        // 收集所有类型
        context.RegisterSymbolAction(
            analyzer.CollectType,
            SymbolKind.NamedType);
        
        // 分析依赖关系
        context.RegisterCompilationEndAction(analyzer.AnalyzeDependencies);
    }
    
    private class DependencyAnalyzer
    {
        private readonly ConcurrentBag<INamedTypeSymbol> _types = new();
        
        public void CollectType(SymbolAnalysisContext context)
        {
            var typeSymbol = (INamedTypeSymbol)context.Symbol;
            _types.Add(typeSymbol);
        }
        
        public void AnalyzeDependencies(CompilationAnalysisContext context)
        {
            foreach (var type in _types)
            {
                // 分析类型的依赖
                var dependencies = GetDependencies(type);
                
                foreach (var dependency in dependencies)
                {
                    var location = type.Locations.FirstOrDefault();
                    if (location != null && !location.IsInMetadata)
                    {
                        var diagnostic = Diagnostic.Create(
                            Rule,
                            location,
                            type.Name,
                            dependency.Name);
                        
                        context.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
        
        private IEnumerable<INamedTypeSymbol> GetDependencies(INamedTypeSymbol type)
        {
            var dependencies = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
            
            // 基类
            if (type.BaseType != null && 
                type.BaseType.SpecialType == SpecialType.None)
            {
                dependencies.Add(type.BaseType);
            }
            
            // 接口
            foreach (var @interface in type.Interfaces)
            {
                dependencies.Add(@interface);
            }
            
            // 字段和属性类型
            foreach (var member in type.GetMembers())
            {
                if (member is IFieldSymbol field)
                {
                    if (field.Type is INamedTypeSymbol fieldType)
                        dependencies.Add(fieldType);
                }
                else if (member is IPropertySymbol property)
                {
                    if (property.Type is INamedTypeSymbol propertyType)
                        dependencies.Add(propertyType);
                }
            }
            
            return dependencies;
        }
    }
}

🎯 复杂分析场景

数据流分析

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NullReferenceAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "NULL001",
        title: "可能的空引用",
        messageFormat: "变量 '{0}' 可能为 null",
        category: "Safety",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        context.RegisterSyntaxNodeAction(
            AnalyzeMethod,
            SyntaxKind.MethodDeclaration);
    }
    
    private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
    {
        var method = (MethodDeclarationSyntax)context.Node;
        
        if (method.Body == null)
            return;
        
        // 跟踪可能为 null 的变量
        var nullableVariables = new HashSet<string>();
        
        foreach (var statement in method.Body.Statements)
        {
            AnalyzeStatement(
                context,
                statement,
                nullableVariables);
        }
    }
    
    private void AnalyzeStatement(
        SyntaxNodeAnalysisContext context,
        StatementSyntax statement,
        HashSet<string> nullableVariables)
    {
        // 变量声明
        if (statement is LocalDeclarationStatementSyntax localDecl)
        {
            foreach (var variable in localDecl.Declaration.Variables)
            {
                if (variable.Initializer == null)
                {
                    // 未初始化的变量可能为 null
                    nullableVariables.Add(variable.Identifier.Text);
                }
            }
        }
        // 赋值语句
        else if (statement is ExpressionStatementSyntax exprStatement)
        {
            if (exprStatement.Expression is AssignmentExpressionSyntax assignment)
            {
                if (assignment.Left is IdentifierNameSyntax identifier)
                {
                    var rightType = context.SemanticModel.GetTypeInfo(
                        assignment.Right,
                        context.CancellationToken);
                    
                    if (rightType.Type != null && !rightType.Type.IsValueType)
                    {
                        // 引用类型赋值,可能为 null
                        nullableVariables.Add(identifier.Identifier.Text);
                    }
                }
            }
        }
        // 成员访问
        else if (statement.DescendantNodes().OfType<MemberAccessExpressionSyntax>().Any())
        {
            foreach (var memberAccess in statement.DescendantNodes()
                .OfType<MemberAccessExpressionSyntax>())
            {
                if (memberAccess.Expression is IdentifierNameSyntax identifier)
                {
                    if (nullableVariables.Contains(identifier.Identifier.Text))
                    {
                        // 报告可能的空引用
                        var diagnostic = Diagnostic.Create(
                            Rule,
                            identifier.GetLocation(),
                            identifier.Identifier.Text);
                        
                        context.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
    }
}

控制流分析

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UnreachableCodeAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "FLOW001",
        title: "不可达代码",
        messageFormat: "此代码永远不会执行",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        context.RegisterSyntaxNodeAction(
            AnalyzeMethod,
            SyntaxKind.MethodDeclaration);
    }
    
    private void AnalyzeMethod(SyntaxNodeAnalysisContext context)
    {
        var method = (MethodDeclarationSyntax)context.Node;
        
        if (method.Body == null)
            return;
        
        bool hasReturn = false;
        
        foreach (var statement in method.Body.Statements)
        {
            if (hasReturn)
            {
                // return 之后的代码不可达
                var diagnostic = Diagnostic.Create(
                    Rule,
                    statement.GetLocation());
                
                context.ReportDiagnostic(diagnostic);
            }
            
            if (statement is ReturnStatementSyntax)
            {
                hasReturn = true;
            }
        }
    }
}

🧪 测试和调试

单元测试

csharp
using Microsoft.CodeAnalysis.Testing;
using Xunit;

public class DiagnosticAnalyzerTests
{
    [Fact]
    public async Task TestClassNameAnalyzer()
    {
        var test = @"
            class myClass { }  // 应该报告诊断
        ";
        
        var expected = DiagnosticResult
            .CompilerWarning("NAMING001")
            .WithSpan(2, 19, 2, 26)
            .WithArguments("myClass");
        
        await VerifyCS.VerifyAnalyzerAsync(test, expected);
    }
    
    [Fact]
    public async Task TestNoWarningForCorrectName()
    {
        var test = @"
            class MyClass { }  // 不应该报告诊断
        ";
        
        await VerifyCS.VerifyAnalyzerAsync(test);
    }
}

性能测试

csharp
using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class AnalyzerBenchmarks
{
    private Compilation _compilation;
    private DiagnosticAnalyzer _analyzer;
    
    [GlobalSetup]
    public void Setup()
    {
        // 创建测试编译
        var code = GenerateTestCode(1000); // 1000 个类
        _compilation = CreateCompilation(code);
        _analyzer = new MyAnalyzer();
    }
    
    [Benchmark]
    public void RunAnalyzer()
    {
        var compilationWithAnalyzers = _compilation
            .WithAnalyzers(ImmutableArray.Create(_analyzer));
        
        var diagnostics = compilationWithAnalyzers
            .GetAllDiagnosticsAsync()
            .Result;
    }
    
    private string GenerateTestCode(int classCount)
    {
        var sb = new StringBuilder();
        for (int i = 0; i < classCount; i++)
        {
            sb.AppendLine($"class Class{i} {{ }}");
        }
        return sb.ToString();
    }
    
    private Compilation CreateCompilation(string code)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(code);
        return CSharpCompilation.Create(
            "TestAssembly",
            new[] { syntaxTree },
            new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) });
    }
}

💡 高级技巧

1. 使用 SymbolEqualityComparer

csharp
// ✅ 正确 - 使用 SymbolEqualityComparer
var set = new HashSet<ISymbol>(SymbolEqualityComparer.Default);

// ❌ 错误 - 使用默认比较器
var set = new HashSet<ISymbol>();

2. 处理 Partial 类

csharp
private void AnalyzeType(SymbolAnalysisContext context)
{
    var typeSymbol = (INamedTypeSymbol)context.Symbol;
    
    // Partial 类可能有多个位置
    foreach (var location in typeSymbol.Locations)
    {
        if (!location.IsInMetadata)
        {
            // 处理每个位置
        }
    }
}

3. 使用取消令牌

csharp
private void Analyze(SyntaxNodeAnalysisContext context)
{
    // 定期检查取消
    context.CancellationToken.ThrowIfCancellationRequested();
    
    // 在长时间运行的操作中使用
    var typeInfo = context.SemanticModel.GetTypeInfo(
        node,
        context.CancellationToken);
}

✅ 最佳实践总结

  1. 性能优先

    • 启用并发执行
    • 快速检查优先
    • 缓存重复查询
  2. 正确使用分析动作

    • 语法分析用于简单检查
    • 符号分析用于类型检查
    • 编译分析用于全局检查
  3. 测试覆盖

    • 编写单元测试
    • 进行性能测试
    • 测试边界情况
  4. 用户体验

    • 提供清晰的消息
    • 使用精确的位置
    • 提供附加位置

🔗 相关资源

基础回顾

完整参考


最后更新: 2025-01-21

基于 MIT 许可发布