Skip to content

Diagnostic 报告

本文档详细介绍如何创建和报告诊断,包括指定位置、添加参数和使用附加位置。

📚 文档信息

  • 难度级别: 中级
  • 预计阅读时间: 15 分钟
  • 前置知识:
    • DiagnosticDescriptor 基础
    • C# 基础语法

🎯 学习目标

通过本文档,你将学会:

  • ✅ 创建 Diagnostic 实例
  • ✅ 指定诊断位置
  • ✅ 添加消息参数
  • ✅ 使用附加位置
  • ✅ 报告诊断到 IDE

📖 快速导航

主题描述
创建诊断基本用法
指定诊断位置位置指定方式
附加位置多位置诊断
完整示例实际应用
最佳实践推荐做法
反模式避免陷阱

什么是 Diagnostic?

Diagnostic 类表示一个具体的诊断实例,包含诊断的位置、消息参数等信息。创建诊断后,需要通过分析上下文报告给编译器。

创建诊断

使用 Diagnostic.Create 方法创建诊断实例:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

public class BasicDiagnosticReporting
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "DEMO001",
        title: "示例诊断",
        messageFormat: "发现问题:'{0}'",
        category: "Demo",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public void ReportDiagnostic(
        SyntaxNodeAnalysisContext context,
        SyntaxNode node,
        string problemDescription)
    {
        // 创建诊断
        var diagnostic = Diagnostic.Create(
            descriptor: Rule,
            location: node.GetLocation(),
            messageArgs: problemDescription);
        
        // 报告诊断
        context.ReportDiagnostic(diagnostic);
    }
}

指定诊断位置

诊断位置决定了在 IDE 中哪里显示波浪线和错误信息。有多种方式指定位置:

方式 1:使用语法节点的位置

csharp
public class NodeLocationExample
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "LOC001",
        title: "节点位置示例",
        messageFormat: "在此处发现问题",
        category: "Demo",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public void AnalyzeClass(SyntaxNodeAnalysisContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        
        // 使用整个类声明的位置
        var diagnostic = Diagnostic.Create(
            Rule,
            classDeclaration.GetLocation());
        
        context.ReportDiagnostic(diagnostic);
    }
}

方式 2:使用 Token 的位置

csharp
public class TokenLocationExample
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "LOC002",
        title: "Token 位置示例",
        messageFormat: "标识符 '{0}' 有问题",
        category: "Demo",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public void AnalyzeClass(SyntaxNodeAnalysisContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        
        // 只在类名标识符处显示诊断
        var diagnostic = Diagnostic.Create(
            Rule,
            classDeclaration.Identifier.GetLocation(),
            classDeclaration.Identifier.Text);
        
        context.ReportDiagnostic(diagnostic);
    }
}

方式 3:使用 TextSpan 指定精确范围

csharp
using Microsoft.CodeAnalysis.Text;

public class TextSpanLocationExample
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "LOC003",
        title: "TextSpan 位置示例",
        messageFormat: "此范围有问题",
        category: "Demo",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public void AnalyzeMethod(SyntaxNodeAnalysisContext context)
    {
        var methodDeclaration = (MethodDeclarationSyntax)context.Node;
        
        // 创建自定义的 TextSpan
        var start = methodDeclaration.Identifier.SpanStart;
        var length = methodDeclaration.ParameterList.Span.End - start;
        var span = new TextSpan(start, length);
        
        // 创建 Location
        var location = Location.Create(
            methodDeclaration.SyntaxTree,
            span);
        
        var diagnostic = Diagnostic.Create(Rule, location);
        
        context.ReportDiagnostic(diagnostic);
    }
}

方式 4:使用符号的位置

csharp
public class SymbolLocationExample
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "LOC004",
        title: "符号位置示例",
        messageFormat: "符号 '{0}' 有问题",
        category: "Demo",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);
    
    public void AnalyzeSymbol(SymbolAnalysisContext context)
    {
        var symbol = context.Symbol;
        
        // 使用符号的第一个位置
        var location = symbol.Locations.FirstOrDefault();
        
        if (location != null)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                location,
                symbol.Name);
            
            context.ReportDiagnostic(diagnostic);
        }
    }
}

附加位置

诊断可以包含多个位置,用于显示相关的代码位置:

csharp
using System.Collections.Immutable;

public class MultiLocationDiagnosticExample
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "MULTI001",
        title: "重复的声明",
        messageFormat: "'{0}' 已经在其他位置声明",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);
    
    public void ReportDuplicateDeclaration(
        SyntaxNodeAnalysisContext context,
        SyntaxNode currentDeclaration,
        SyntaxNode previousDeclaration,
        string name)
    {
        // 主位置:当前声明
        var mainLocation = currentDeclaration.GetLocation();
        
        // 附加位置:之前的声明
        var additionalLocations = ImmutableArray.Create(
            previousDeclaration.GetLocation());
        
        // 创建带附加位置的诊断
        var diagnostic = Diagnostic.Create(
            descriptor: Rule,
            location: mainLocation,
            additionalLocations: additionalLocations,
            messageArgs: name);
        
        context.ReportDiagnostic(diagnostic);
    }
}

完整的使用示例

示例 1:类名检查分析器

以下是完整的诊断报告示例,展示如何在实际分析器中使用:

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

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ClassNameAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: "NAMING001",
        title: "类名应该使用 PascalCase",
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名",
        category: "Naming",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "类名应该遵循 PascalCase 命名约定。");
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSyntaxNodeAction(
            AnalyzeClassDeclaration,
            SyntaxKind.ClassDeclaration);
    }
    
    private void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        var className = classDeclaration.Identifier.Text;
        
        if (!IsPascalCase(className))
        {
            var diagnostic = Diagnostic.Create(
                descriptor: Rule,
                location: classDeclaration.Identifier.GetLocation(),
                messageArgs: className);
            
            context.ReportDiagnostic(diagnostic);
        }
    }
    
    private bool IsPascalCase(string name)
    {
        if (string.IsNullOrEmpty(name))
            return false;
        
        if (!char.IsUpper(name[0]))
            return false;
        
        if (name.Contains('_'))
            return false;
        
        return true;
    }
}

示例 2:重复方法检测

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

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DuplicateMethodAnalyzer : 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;
    }
}

最佳实践

1. 使用精确的位置

csharp
// ✅ 正确:使用标识符的位置
var diagnostic = Diagnostic.Create(
    Rule,
    classDeclaration.Identifier.GetLocation(),
    className);

// ❌ 错误:使用整个节点的位置(波浪线太长)
var diagnostic = Diagnostic.Create(
    Rule,
    classDeclaration.GetLocation(),
    className);

原因:精确的位置让用户更容易定位问题。

2. 提供有用的消息参数

csharp
// ✅ 正确:提供具体的信息
var diagnostic = Diagnostic.Create(
    Rule,
    location,
    methodName,
    expectedReturnType,
    actualReturnType);

// ❌ 错误:消息参数不足
var diagnostic = Diagnostic.Create(
    Rule,
    location);

原因:详细的参数帮助用户理解问题。

3. 使用附加位置显示相关代码

csharp
// ✅ 正确:使用附加位置
var additionalLocations = ImmutableArray.Create(
    previousDeclaration.GetLocation());

var diagnostic = Diagnostic.Create(
    Rule,
    currentLocation,
    additionalLocations,
    name);

// ❌ 错误:不提供相关位置
var diagnostic = Diagnostic.Create(
    Rule,
    currentLocation,
    name);

原因:附加位置帮助用户理解问题的上下文。

4. 在报告前验证位置

csharp
// ✅ 正确:验证位置是否有效
var location = symbol.Locations.FirstOrDefault();
if (location != null && !location.IsInMetadata)
{
    var diagnostic = Diagnostic.Create(Rule, location);
    context.ReportDiagnostic(diagnostic);
}

// ❌ 错误:不验证位置
var diagnostic = Diagnostic.Create(Rule, symbol.Locations[0]);
context.ReportDiagnostic(diagnostic);

原因:避免在元数据或无效位置报告诊断。

5. 避免重复报告

csharp
// ✅ 正确:使用集合跟踪已报告的问题
private HashSet<string> _reportedIssues = new HashSet<string>();

public void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    var key = GetUniqueKey(context.Node);
    
    if (!_reportedIssues.Contains(key))
    {
        _reportedIssues.Add(key);
        var diagnostic = Diagnostic.Create(Rule, location);
        context.ReportDiagnostic(diagnostic);
    }
}

原因:避免用户看到重复的诊断。

6. 使用合适的分析上下文

csharp
// ✅ 正确:根据分析类型使用正确的上下文
public void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
    context.ReportDiagnostic(diagnostic);
}

public void AnalyzeSymbol(SymbolAnalysisContext context)
{
    context.ReportDiagnostic(diagnostic);
}

原因:不同的分析阶段使用不同的上下文。

7. 考虑性能影响

csharp
// ✅ 正确:只在必要时创建诊断
if (HasProblem(node))
{
    var diagnostic = Diagnostic.Create(Rule, location);
    context.ReportDiagnostic(diagnostic);
}

// ❌ 错误:总是创建诊断对象
var diagnostic = Diagnostic.Create(Rule, location);
if (HasProblem(node))
{
    context.ReportDiagnostic(diagnostic);
}

原因:避免不必要的对象创建。

8. 提供诊断属性(可选)

csharp
// ✅ 正确:添加自定义属性
var properties = ImmutableDictionary.CreateBuilder<string, string>();
properties.Add("SuggestedFix", "UsePascalCase");
properties.Add("Severity", "High");

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

原因:自定义属性可以传递额外信息给代码修复。

9. 处理生成的代码

csharp
// ✅ 正确:检查是否为生成的代码
public override void Initialize(AnalysisContext context)
{
    context.ConfigureGeneratedCodeAnalysis(
        GeneratedCodeAnalysisFlags.None);
    
    context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
}

原因:通常不需要分析生成的代码。

10. 使用描述性的诊断 ID

csharp
// ✅ 正确:使用有意义的 ID
private const string DiagnosticId = "NAMING001";

// ❌ 错误:使用无意义的 ID
private const string DiagnosticId = "ABC123";

原因:有意义的 ID 更容易理解和管理。

反模式和常见错误

反模式 1:使用过大的位置范围

csharp
// ❌ 反模式:波浪线覆盖整个类
var diagnostic = Diagnostic.Create(
    Rule,
    classDeclaration.GetLocation(),
    className);

// ✅ 正确做法:只标记类名
var diagnostic = Diagnostic.Create(
    Rule,
    classDeclaration.Identifier.GetLocation(),
    className);

问题:过大的范围让用户难以定位具体问题。

反模式 2:消息参数不匹配

csharp
// ❌ 反模式:参数数量不匹配
messageFormat: "类名 '{0}' 应该改为 '{1}'"

var diagnostic = Diagnostic.Create(
    Rule,
    location,
    className);  // 只提供一个参数,但格式需要两个

// ✅ 正确做法:参数数量匹配
var diagnostic = Diagnostic.Create(
    Rule,
    location,
    className,
    suggestedName);

问题:参数不匹配会导致消息显示错误。

反模式 3:在元数据位置报告诊断

csharp
// ❌ 反模式:不检查位置类型
var diagnostic = Diagnostic.Create(
    Rule,
    symbol.Locations[0]);

// ✅ 正确做法:过滤元数据位置
var location = symbol.Locations.FirstOrDefault(
    loc => !loc.IsInMetadata);

if (location != null)
{
    var diagnostic = Diagnostic.Create(Rule, location);
    context.ReportDiagnostic(diagnostic);
}

问题:在元数据位置报告诊断没有意义。

反模式 4:重复报告同一问题

csharp
// ❌ 反模式:在多个分析阶段报告同一问题
public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration);
    context.RegisterSymbolAction(Analyze, SymbolKind.NamedType);
}

// ✅ 正确做法:只在一个阶段报告
public override void Initialize(AnalysisContext context)
{
    context.RegisterSymbolAction(Analyze, SymbolKind.NamedType);
}

问题:重复的诊断让用户困惑。

反模式 5:不提供有用的消息

csharp
// ❌ 反模式:消息过于简单
messageFormat: "错误"

// ✅ 正确做法:提供具体信息
messageFormat: "类名 '{0}' 应该使用 PascalCase 命名"

问题:简单的消息不能帮助用户理解问题。

🔗 相关文档

其他参考


返回: 诊断 API 索引

基于 MIT 许可发布