Skip to content

最佳实践

本文档总结使用诊断 API 时的最佳实践。

文档信息

  • 难度级别: 中级
  • 预计阅读时间: 15 分钟

🎯 学习目标

  • ✅ 掌握 DiagnosticDescriptor 最佳实践
  • ✅ 学会 Diagnostic 报告最佳实践
  • ✅ 了解 DiagnosticAnalyzer 最佳实践

DiagnosticDescriptor 最佳实践

  1. 使用一致的 ID 命名约定
  2. 提供清晰的标题和消息
  3. 选择合适的严重级别
  4. 提供详细的描述和帮助链接
  5. 使用标准类别
  6. 合理设置默认启用
  7. 使用参数化消息
  8. 集中管理描述符
  9. 使用常量定义
  10. 添加遥测标签

Diagnostic 报告最佳实践

  1. 使用精确的位置
  2. 提供有用的消息参数
  3. 使用附加位置显示相关代码
  4. 验证位置有效性
  5. 避免重复报告
  6. 使用合适的分析上下文
  7. 考虑性能影响
  8. 提供诊断属性
  9. 处理生成的代码
  10. 使用描述性的 ID

DiagnosticAnalyzer 最佳实践

  1. 启用并发执行
  2. 配置生成代码分析
  3. 使用合适的分析动作
  4. 避免耗时操作
  5. 使用取消令牌
  6. 缓存语义信息
  7. 使用不可变集合
  8. 处理边界情况
  9. 编写单元测试
  10. 提供代码修复

示例:完整的最佳实践

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

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BestPracticeAnalyzer : DiagnosticAnalyzer
{
    // 1. 使用常量定义
    private const string DiagnosticId = "BP001";
    private const string Category = "Naming";
    
    // 2. 集中管理描述符
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: DiagnosticId,
        title: "类名应该使用 PascalCase",
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名",
        category: Category,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "类名应该遵循 PascalCase 命名约定。",
        helpLinkUri: "https://docs.example.com/naming");
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        // 3. 配置生成代码分析
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        
        // 4. 启用并发执行
        context.EnableConcurrentExecution();
        
        // 5. 使用合适的分析动作
        context.RegisterSyntaxNodeAction(
            AnalyzeClass,
            SyntaxKind.ClassDeclaration);
    }
    
    private void AnalyzeClass(SyntaxNodeAnalysisContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        var className = classDeclaration.Identifier.Text;
        
        // 6. 快速检查
        if (IsPascalCase(className))
            return;
        
        // 7. 使用精确的位置
        var location = classDeclaration.Identifier.GetLocation();
        
        // 8. 验证位置
        if (location == null || location.IsInMetadata)
            return;
        
        // 9. 创建诊断
        var diagnostic = Diagnostic.Create(
            Rule,
            location,
            className);
        
        // 10. 报告诊断
        context.ReportDiagnostic(diagnostic);
    }
    
    private bool IsPascalCase(string name)
    {
        return !string.IsNullOrEmpty(name) && char.IsUpper(name[0]);
    }
}

反模式总结

DiagnosticDescriptor 反模式

  1. 使用不一致的 ID
  2. 消息过于简单或模糊
  3. 滥用 Error 级别
  4. 没有提供帮助信息
  5. 使用不明确的类别

Diagnostic 报告反模式

  1. 使用过大的位置范围
  2. 消息参数不匹配
  3. 在元数据位置报告
  4. 重复报告同一问题
  5. 不提供有用的消息

DiagnosticAnalyzer 反模式

  1. 不启用并发执行
  2. 不配置生成代码分析
  3. 使用错误的分析动作
  4. 执行耗时操作
  5. 不使用取消令牌

相关文档

消息格式化最佳实践

  1. 使用清晰的语言 - 避免技术术语
  2. 提供具体信息 - 包含相关的名称、类型等
  3. 使用参数占位符 - 提高灵活性
  4. 处理复数形式 - 根据语言选择合适的方式
  5. 考虑本地化 - 如果需要支持多语言,使用资源文件
  6. 保持消息简洁 - 不要过长
  7. 提供建议 - 告诉用户如何修复
  8. 使用一致的格式 - 所有消息使用相同的风格
  9. 避免重复信息 - 不要在消息中重复标题
  10. 测试消息显示 - 确保消息在 IDE 中正确显示

性能优化最佳实践

  1. 启用并发执行 - context.EnableConcurrentExecution()
  2. 使用合适的分析动作 - 选择最合适的分析阶段
  3. 快速检查后再详细分析 - 避免不必要的耗时操作
  4. 缓存语义查询结果 - 避免重复查询
  5. 使用取消令牌 - 允许 IDE 取消长时间运行的分析
  6. 避免在循环中进行语义查询 - 性能影响大
  7. 使用不可变集合 - 线程安全
  8. 处理边界情况 - 检查 null、空集合等
  9. 编写单元测试 - 确保分析器正确工作
  10. 监控性能指标 - 确保分析器不影响 IDE 性能

完整的最佳实践示例

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 CompleteBestPracticeAnalyzer : DiagnosticAnalyzer
{
    // 1. 使用常量定义
    private const string DiagnosticId = "BP001";
    private const string Category = "Naming";
    
    // 2. 集中管理描述符
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        id: DiagnosticId,
        title: "类名应该使用 PascalCase",
        messageFormat: "类名 '{0}' 应该使用 PascalCase 命名",
        category: Category,
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "类名应该遵循 PascalCase 命名约定。",
        helpLinkUri: "https://docs.example.com/naming");
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        // 3. 配置生成代码分析
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        
        // 4. 启用并发执行
        context.EnableConcurrentExecution();
        
        // 5. 使用合适的分析动作
        context.RegisterSyntaxNodeAction(
            AnalyzeClass,
            SyntaxKind.ClassDeclaration);
    }
    
    private void AnalyzeClass(SyntaxNodeAnalysisContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        var className = classDeclaration.Identifier.Text;
        
        // 6. 快速检查
        if (IsPascalCase(className))
            return;
        
        // 7. 使用精确的位置
        var location = classDeclaration.Identifier.GetLocation();
        
        // 8. 验证位置
        if (location == null || location.IsInMetadata)
            return;
        
        // 9. 创建诊断
        var diagnostic = Diagnostic.Create(
            Rule,
            location,
            className);
        
        // 10. 报告诊断
        context.ReportDiagnostic(diagnostic);
    }
    
    private bool IsPascalCase(string name)
    {
        return !string.IsNullOrEmpty(name) && char.IsUpper(name[0]);
    }
}

测试最佳实践

  1. 使用 Microsoft.CodeAnalysis.Testing 包
  2. 测试正常情况和边界情况
  3. 验证诊断位置的准确性
  4. 测试消息参数的正确性
  5. 测试性能影响
csharp
using Microsoft.CodeAnalysis.Testing;
using Xunit;

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

文档最佳实践

  1. 提供清晰的文档 - 说明诊断的目的和修复方法
  2. 添加帮助链接 - 提供更多信息的链接
  3. 提供代码示例 - 展示正确和错误的用法
  4. 说明配置选项 - 如何启用/禁用诊断
  5. 维护更新日志 - 记录诊断规则的变更

常见问题

Q: 如何确保分析器性能?

A:

  1. 启用并发执行
  2. 使用合适的分析动作
  3. 快速检查后再详细分析
  4. 避免耗时操作
  5. 使用取消令牌

Q: 如何测试分析器?

A: 使用 Microsoft.CodeAnalysis.Testing 包:

csharp
[Fact]
public async Task TestAnalyzer()
{
    var test = "...";
    var expected = DiagnosticResult.CompilerWarning("ID001");
    await VerifyCS.VerifyAnalyzerAsync(test, expected);
}

Q: 如何处理生成的代码?

A: 使用 ConfigureGeneratedCodeAnalysis:

csharp
context.ConfigureGeneratedCodeAnalysis(
    GeneratedCodeAnalysisFlags.None);

Q: 如何提供代码修复?

A: 实现 CodeFixProvider:

csharp
[ExportCodeFixProvider(LanguageNames.CSharp)]
public class MyCodeFixProvider : CodeFixProvider
{
    // 实现代码修复逻辑
}

总结

遵循这些最佳实践可以帮助你创建高质量的诊断分析器:

  1. 清晰的诊断定义 - 使用一致的 ID 和清晰的消息
  2. 精确的位置指定 - 只标记有问题的部分
  3. 良好的性能 - 启用并发执行,避免耗时操作
  4. 完善的测试 - 确保分析器正确工作
  5. 详细的文档 - 帮助用户理解和使用诊断

相关资源

基于 MIT 许可发布