Skip to content

诊断 API 中级指南

⏱️ 15-20 分钟 | 📚 中级 | 前置知识: 诊断 API 基础

🎯 学习目标

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

  • [ ] 使用多位置诊断显示相关代码
  • [ ] 实现符号分析器
  • [ ] 处理诊断类别和自定义标签
  • [ ] 使用诊断属性传递信息
  • [ ] 实现更复杂的分析逻辑

📖 核心概念

多位置诊断

多位置诊断可以同时标记多个相关的代码位置,帮助用户理解问题的上下文。

使用场景:

  • 显示重复的声明
  • 标记相关的代码片段
  • 显示依赖关系

符号分析

符号分析在语义分析阶段执行,可以访问类型信息和符号关系。

优势:

  • 访问类型信息
  • 检查继承关系
  • 分析成员关系

🔧 多位置诊断

基本用法

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DuplicateDeclarationAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "DESIGN001",
        title: "重复的声明",
        messageFormat: "'{0}' 已经在其他位置声明",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
    }
    
    private void AnalyzeNamedType(SymbolAnalysisContext context)
    {
        var typeSymbol = (INamedTypeSymbol)context.Symbol;
        
        // 检查重复的方法
        var methodGroups = typeSymbol.GetMembers()
            .OfType<IMethodSymbol>()
            .Where(m => m.MethodKind == MethodKind.Ordinary)
            .GroupBy(m => m.Name);
        
        foreach (var group in methodGroups)
        {
            var methods = group.ToList();
            if (methods.Count > 1)
            {
                // 检查是否有相同签名
                for (int i = 0; i < methods.Count; i++)
                {
                    for (int j = i + 1; j < methods.Count; j++)
                    {
                        if (HaveSameSignature(methods[i], methods[j]))
                        {
                            ReportDuplicate(context, methods[i], methods[j]);
                        }
                    }
                }
            }
        }
    }
    
    private void ReportDuplicate(
        SymbolAnalysisContext context,
        IMethodSymbol method1,
        IMethodSymbol method2)
    {
        // 主位置: 第二个声明
        var mainLocation = method2.Locations.FirstOrDefault();
        
        // 附加位置: 第一个声明
        var additionalLocations = ImmutableArray.Create(
            method1.Locations.FirstOrDefault());
        
        if (mainLocation != null)
        {
            var diagnostic = Diagnostic.Create(
                descriptor: Rule,
                location: mainLocation,
                additionalLocations: additionalLocations,
                messageArgs: method2.Name);
            
            context.ReportDiagnostic(diagnostic);
        }
    }
    
    private bool HaveSameSignature(IMethodSymbol method1, IMethodSymbol method2)
    {
        if (method1.Parameters.Length != method2.Parameters.Length)
            return false;
        
        for (int i = 0; i < method1.Parameters.Length; i++)
        {
            if (!SymbolEqualityComparer.Default.Equals(
                method1.Parameters[i].Type,
                method2.Parameters[i].Type))
            {
                return false;
            }
        }
        
        return true;
    }
}

显示多个相关位置

csharp
/// <summary>
/// 显示所有相关的使用位置
/// </summary>
public class UnusedVariableAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "USAGE001",
        title: "未使用的变量",
        messageFormat: "变量 '{0}' 已声明但从未使用",
        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.RegisterSemanticModelAction(AnalyzeSemanticModel);
    }
    
    private void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
    {
        var semanticModel = context.SemanticModel;
        var root = semanticModel.SyntaxTree.GetRoot(context.CancellationToken);
        
        // 查找所有局部变量声明
        var variableDeclarators = root.DescendantNodes()
            .OfType<VariableDeclaratorSyntax>();
        
        foreach (var declarator in variableDeclarators)
        {
            var symbol = semanticModel.GetDeclaredSymbol(
                declarator, 
                context.CancellationToken);
            
            if (symbol is ILocalSymbol localSymbol)
            {
                // 查找所有引用
                var references = root.DescendantNodes()
                    .OfType<IdentifierNameSyntax>()
                    .Where(id => id.Identifier.Text == localSymbol.Name)
                    .Where(id => !id.Ancestors().Contains(declarator))
                    .ToList();
                
                // 如果没有引用,报告诊断
                if (references.Count == 0)
                {
                    var diagnostic = Diagnostic.Create(
                        Rule,
                        declarator.Identifier.GetLocation(),
                        localSymbol.Name);
                    
                    context.ReportDiagnostic(diagnostic);
                }
            }
        }
    }
}

🎯 符号分析

分析类型符号

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class PublicTypeAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "DESIGN002",
        title: "公共类型应该有文档注释",
        messageFormat: "公共类型 '{0}' 应该添加 XML 文档注释",
        category: "Documentation",
        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.RegisterSymbolAction(
            AnalyzeNamedType,
            SymbolKind.NamedType);
    }
    
    private void AnalyzeNamedType(SymbolAnalysisContext context)
    {
        var typeSymbol = (INamedTypeSymbol)context.Symbol;
        
        // 只检查公共类型
        if (typeSymbol.DeclaredAccessibility != Accessibility.Public)
            return;
        
        // 检查是否有文档注释
        var xmlComment = typeSymbol.GetDocumentationCommentXml();
        
        if (string.IsNullOrWhiteSpace(xmlComment))
        {
            var location = typeSymbol.Locations.FirstOrDefault();
            if (location != null && !location.IsInMetadata)
            {
                var diagnostic = Diagnostic.Create(
                    Rule,
                    location,
                    typeSymbol.Name);
                
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
}

分析方法符号

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MethodComplexityAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "MAINT001",
        title: "方法过于复杂",
        messageFormat: "方法 '{0}' 的圈复杂度为 {1},超过了阈值 {2}",
        category: "Maintainability",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    private const int ComplexityThreshold = 10;
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
    }
    
    private void AnalyzeMethod(SymbolAnalysisContext context)
    {
        var methodSymbol = (IMethodSymbol)context.Symbol;
        
        // 跳过编译器生成的方法
        if (methodSymbol.IsImplicitlyDeclared)
            return;
        
        // 获取方法的语法节点
        var syntaxReference = methodSymbol.DeclaringSyntaxReferences.FirstOrDefault();
        if (syntaxReference == null)
            return;
        
        var methodSyntax = syntaxReference.GetSyntax(context.CancellationToken);
        
        // 计算圈复杂度
        int complexity = CalculateComplexity(methodSyntax);
        
        if (complexity > ComplexityThreshold)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                methodSymbol.Locations.First(),
                methodSymbol.Name,
                complexity,
                ComplexityThreshold);
            
            context.ReportDiagnostic(diagnostic);
        }
    }
    
    private int CalculateComplexity(SyntaxNode node)
    {
        // 基础复杂度为 1
        int complexity = 1;
        
        // 计算决策点
        var decisionPoints = node.DescendantNodes()
            .Where(n => 
                n.IsKind(SyntaxKind.IfStatement) ||
                n.IsKind(SyntaxKind.WhileStatement) ||
                n.IsKind(SyntaxKind.ForStatement) ||
                n.IsKind(SyntaxKind.ForEachStatement) ||
                n.IsKind(SyntaxKind.CaseSwitchLabel) ||
                n.IsKind(SyntaxKind.LogicalAndExpression) ||
                n.IsKind(SyntaxKind.LogicalOrExpression))
            .Count();
        
        return complexity + decisionPoints;
    }
}

🏷️ 诊断类别和标签

标准类别

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

自定义标签

csharp
// 使用内置标签
var descriptor = new DiagnosticDescriptor(
    id: "PERF001",
    title: "性能问题",
    messageFormat: "此处有性能问题",
    category: "Performance",
    defaultSeverity: DiagnosticSeverity.Warning,
    isEnabledByDefault: true,
    customTags: new[] { 
        WellKnownDiagnosticTags.Telemetry,      // 收集遥测数据
        WellKnownDiagnosticTags.Unnecessary     // 标记不必要的代码
    });

📦 诊断属性

诊断属性可以传递额外信息给代码修复提供程序。

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class StringConcatenationAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "PERF002",
        title: "避免在循环中使用字符串拼接",
        messageFormat: "在循环中使用字符串拼接会导致性能问题",
        category: "Performance",
        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(
            AnalyzeLoop,
            SyntaxKind.ForStatement,
            SyntaxKind.ForEachStatement,
            SyntaxKind.WhileStatement);
    }
    
    private void AnalyzeLoop(SyntaxNodeAnalysisContext context)
    {
        var loopStatement = context.Node;
        var loopBody = GetLoopBody(loopStatement);
        
        if (loopBody == null)
            return;
        
        // 查找字符串拼接
        var concatenations = loopBody.DescendantNodes()
            .OfType<BinaryExpressionSyntax>()
            .Where(b => b.IsKind(SyntaxKind.AddExpression));
        
        foreach (var concat in concatenations)
        {
            var typeInfo = context.SemanticModel.GetTypeInfo(
                concat.Left,
                context.CancellationToken);
            
            if (typeInfo.Type?.SpecialType == SpecialType.System_String)
            {
                // 创建属性字典
                var properties = ImmutableDictionary.CreateBuilder<string, string>();
                properties.Add("FixType", "UseStringBuilder");
                properties.Add("LoopType", loopStatement.Kind().ToString());
                
                var diagnostic = Diagnostic.Create(
                    Rule,
                    concat.GetLocation(),
                    properties.ToImmutable(),
                    null);
                
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
    
    private StatementSyntax GetLoopBody(SyntaxNode loopStatement)
    {
        return loopStatement switch
        {
            ForStatementSyntax forStatement => forStatement.Statement,
            ForEachStatementSyntax forEachStatement => forEachStatement.Statement,
            WhileStatementSyntax whileStatement => whileStatement.Statement,
            _ => null
        };
    }
}

💡 实践示例

示例: 检测循环中的对象创建

csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ObjectCreationInLoopAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "PERF003",
        title: "避免在循环中创建对象",
        messageFormat: "在循环中创建 '{0}' 对象可能导致性能问题",
        category: "Performance",
        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(
            AnalyzeLoop,
            SyntaxKind.ForStatement,
            SyntaxKind.ForEachStatement,
            SyntaxKind.WhileStatement,
            SyntaxKind.DoStatement);
    }
    
    private void AnalyzeLoop(SyntaxNodeAnalysisContext context)
    {
        var loopStatement = context.Node;
        var loopBody = GetLoopBody(loopStatement);
        
        if (loopBody == null)
            return;
        
        // 查找对象创建表达式
        var objectCreations = loopBody.DescendantNodes()
            .OfType<ObjectCreationExpressionSyntax>();
        
        foreach (var objectCreation in objectCreations)
        {
            var typeInfo = context.SemanticModel.GetTypeInfo(
                objectCreation,
                context.CancellationToken);
            
            if (typeInfo.Type != null && ShouldReport(typeInfo.Type))
            {
                var diagnostic = Diagnostic.Create(
                    Rule,
                    objectCreation.GetLocation(),
                    typeInfo.Type.Name);
                
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
    
    private StatementSyntax GetLoopBody(SyntaxNode loopStatement)
    {
        return loopStatement switch
        {
            ForStatementSyntax forStatement => forStatement.Statement,
            ForEachStatementSyntax forEachStatement => forEachStatement.Statement,
            WhileStatementSyntax whileStatement => whileStatement.Statement,
            DoStatementSyntax doStatement => doStatement.Statement,
            _ => null
        };
    }
    
    private bool ShouldReport(ITypeSymbol type)
    {
        // 不报告值类型
        if (type.IsValueType)
            return false;
        
        // 不报告字符串
        if (type.SpecialType == SpecialType.System_String)
            return false;
        
        // 不报告委托
        if (type.TypeKind == TypeKind.Delegate)
            return false;
        
        return true;
    }
}

✅ 最佳实践

1. 使用符号分析检查类型信息

csharp
// ✅ 好的做法 - 使用符号分析
context.RegisterSymbolAction(AnalyzeType, SymbolKind.NamedType);

// ❌ 不好的做法 - 使用语法分析然后查询语义信息
context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration);

2. 提供附加位置帮助理解问题

csharp
// ✅ 好的做法 - 显示相关位置
var diagnostic = Diagnostic.Create(
    Rule,
    mainLocation,
    additionalLocations,
    null,
    name);

3. 使用属性传递信息给代码修复

csharp
// ✅ 好的做法 - 添加属性
var properties = ImmutableDictionary.CreateBuilder<string, string>();
properties.Add("FixType", "UseStringBuilder");

var diagnostic = Diagnostic.Create(
    Rule,
    location,
    properties.ToImmutable(),
    null);

4. 过滤元数据位置

csharp
// ✅ 好的做法 - 检查位置
var location = symbol.Locations.FirstOrDefault();
if (location != null && !location.IsInMetadata)
{
    // 报告诊断
}

🔗 相关资源

进阶学习

基础回顾

完整参考


⏭️ 下一步

完成本指南后,建议:

  1. 实践 - 创建一个使用符号分析的分析器
  2. 学习高级 - 了解性能优化和复杂场景
  3. 探索代码修复 - 学习如何提供自动修复

最后更新: 2025-01-21

基于 MIT 许可发布