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 命名"问题:简单的消息不能帮助用户理解问题。
🔗 相关文档
- 上一篇: DiagnosticDescriptor 详解
- 下一篇: 严重级别
- 相关: 诊断位置和范围
其他参考
返回: 诊断 API 索引