诊断 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)
{
// 报告诊断
}🔗 相关资源
进阶学习
- 诊断 API 高级指南 - 复杂分析器、性能优化
基础回顾
- 诊断 API 基础 - 基础概念
完整参考
- 诊断 API 完整参考 - 完整文档
⏭️ 下一步
完成本指南后,建议:
- 实践 - 创建一个使用符号分析的分析器
- 学习高级 - 了解性能优化和复杂场景
- 探索代码修复 - 学习如何提供自动修复
最后更新: 2025-01-21