mcpField
D
dotnet-roslyn-analyzers

dotnet-roslyn-analyzers

github.com/wshaddix開発8697 回インストール35 週間前に更新
リポジトリ

dotnet-roslyn-analyzers について

カスタム Roslyn アナライザー、コード修正、リファクタリング、診断抑制器を作成する .NET 開発者向けのスキルです。実装、テスト、パッケージ化、複数バージョンへの対応を扱います。

主な機能

  1. 01アナライザープロジェクトと診断の設定
  2. 02分析コンテキストの登録パターン
  3. 03コード修正とリファクタリング
  4. 04診断抑制の指針
  5. 05複数バージョン対応とパッケージ化
  6. 06Microsoft.CodeAnalysis.Testing によるテスト

活用シーン

  1. 01カスタム C# アナライザーの作成
  2. 02診断への自動修正の追加
  3. 03診断を伴わないリファクタリングの構築
  4. 04複数の Roslyn バージョン向けのパッケージ化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-roslyn-analyzers

インストール後、Claude Code で/dotnet-roslyn-analyzersを入力して読み込まれたか確認します。

同梱ファイルdetails.md

詳細

dotnet-roslyn-analyzers

カスタム Roslyn アナライザー、コード修正プロバイダー、コード リファクタリング プロバイダー、診断抑制機能の作成に関するガイダンスです。プロジェクトのセットアップ、DiagnosticDescriptor の規約、分析コンテキストの登録、コード修正アクション、コード リファクタリング アクション、複数の Roslyn バージョン(3.8~4.14)へのターゲット設定、Microsoft.CodeAnalysis.Testing を使ったテスト、NuGet パッケージ化、パフォーマンスに関するベストプラクティスを扱います。

対象範囲: このスキルではアナライザーの作成を扱います。既存のアナライザー(CA ルール、EditorConfig の重大度、サードパーティーパッケージ)の使用と設定については、[skill:dotnet-add-analyzers] を参照してください。ソースジェネレーターの作成(IIncrementalGenerator、構文プロバイダー、コード生成)については、[skill:dotnet-csharp-source-generators] を参照してください。アナライザーとソースジェネレーターは同じ NuGet パッケージ構成(analyzers/dotnet/cs/)と Microsoft.CodeAnalysis.CSharp 依存関係を共有しますが、目的は異なります。アナライザーは診断を報告し、ジェネレーターはコードを生成します。

相互参照:[skill:dotnet-csharp-source-generators] では、共通する Roslyn のパッケージ化の概念と IIncrementalGenerator のパターンを、[skill:dotnet-add-analyzers] では、アナライザーの使用と設定を、[skill:dotnet-testing-strategy] では、一般的なテストの構成とフレームワークの選択を、[skill:dotnet-csharp-coding-standards] では、アナライザーのコードに適用される命名規則を扱います。


プロジェクトのセットアップ

アナライザーのプロジェクトは、必ず netstandard2.0 をターゲットにする必要があります。コンパイラーは、さまざまなホストプロセス(.NET Framework/Mono 上の Visual Studio、.NET Core 上の MSBuild、dotnet build CLI)にアナライザーを読み込みます。net8.0+ をターゲットにすると、そのランタイムで動作しないホストとの互換性が失われます。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IsRoslynComponent>true</IsRoslynComponent>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
    
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
  </ItemGroup>
</Project>
  • EnforceExtendedAnalyzerRules は、アナライザーの作成でよくあるミスを検出する RS 系メタ診断を有効にします(下記の「メタ診断」セクションを参照)。
  • IsRoslynComponent は、プロジェクトの IDE ツールサポートを有効にします。
  • LangVersion>latest にすると、netstandard2.0 をターゲットにしたまま、アナライザー自体で最新の C# を記述できます。
  • Roslyn SDK パッケージはすべて PrivateAssets="all" を指定し、推移的な依存関係として配布されないようにする必要があります。

DiagnosticAnalyzer

すべてのアナライザーは DiagnosticAnalyzer を継承し、[DiagnosticAnalyzer(LanguageNames.CSharp)] 属性を付ける必要があります。

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

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoPublicFieldsAnalyzer : DiagnosticAnalyzer
{
    // Diagnostic ID uses project prefix + sequential number
    public const string DiagnosticId = "MYLIB001";

    private static readonly DiagnosticDescriptor Rule = new(
        id: DiagnosticId,
        title: "Public fields should be properties",
        messageFormat: "Field '{0}' is public; use a property instead",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        helpLinkUri: $"https://example.com/docs/rules/{DiagnosticId}");

    // Return an ImmutableArray -- allocating a new array per call is RS1030-adjacent waste
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
        = ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        // Required: enable concurrent execution and generated code analysis config
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(
            GeneratedCodeAnalysisFlags.None);

        context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
    }

    private static void AnalyzeField(SymbolAnalysisContext context)
    {
        var field = (IFieldSymbol)context.Symbol;

        if (field.DeclaredAccessibility == Accessibility.Public
            && !field.IsConst
            && !field.IsReadOnly)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                field.Locations[0],
                field.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}
分析コンテキストの登録

分析に最も適した登録メソッドを選択してください。

メソッド粒度使用する場面
RegisterSyntaxNodeAction個々の構文ノード特定の構文(例:if 文、メソッド宣言)のパターンマッチング
RegisterSymbolAction宣言されたシンボルシンボルレベルのプロパティ(アクセシビリティ、型、属性)の確認
RegisterOperationActionIL レベルの操作構文に依存しないセマンティック操作(代入、呼び出し)の分析
RegisterSyntaxTreeAction構文ツリー全体ファイルレベルのチェック(例:ヘッダーの欠落、エンコーディング)
RegisterCompilationStartActionコンパイル開始時コンパイル中に状態を蓄積し、最後に報告する必要がある場合
RegisterCompilationActionコンパイル全体すべてのファイルのコンパイル後に行う一括分析
// RegisterSyntaxNodeAction -- analyze specific syntax nodes
context.RegisterSyntaxNodeAction(
    AnalyzeInvocation,
    SyntaxKind.InvocationExpression);

// RegisterOperationAction -- analyze semantic operations
context.RegisterOperationAction(
    AnalyzeAssignment,
    OperationKind.SimpleAssignment);

// RegisterCompilationStartAction -- accumulate state, then report at compilation end
context.RegisterCompilationStartAction(compilationContext =>
{
    // Resolve types once at compilation start
    var disposableType = compilationContext.Compilation
        .GetTypeByMetadataName("System.IDisposable");

    if (disposableType is null)
        return;

    compilationContext.RegisterSymbolAction(
        ctx => AnalyzeTypeDisposal(ctx, disposableType),
        SymbolKind.NamedType);
});

DiagnosticDescriptor の規約

カスタムアナライザーでは、次の規約に従ってください。

ID のプレフィックスパターン

プロジェクト名またはライブラリ名に由来する、短く一意なプレフィックスの後に、連番を付けます。

パターン例使用する場面
PROJ###MYLIB001単一プロジェクトのアナライザー
AREA####PERF0001カテゴリ別のアナライザー(パフォーマンス、セキュリティ)
XX####MA0042短いプレフィックスを使う規約(例:Meziantou.Analyzer)

.NET プラットフォームで予約されているプレフィックス(CA(コード分析)、CS(コンパイラー)、RS(Roslyn SDK)、IDE(コードスタイル)、IL(リンカー)、SYSLIB(ランタイム))は避けてください。複数のアナライザーパッケージをインストールしたときの衝突を防ぐため、ID 定数に名前空間を含めてください。

カテゴリ名

該当する場合は、標準の .NET 分析カテゴリを使用してください。

Design、Globalization、Interoperability、Maintainability、Naming、Performance、Reliability、Security、Style、Usage

ドメイン固有のカテゴリには、わかりやすいタイトルケースの名前を使用してください(例:EntityFramework、AspNetCore)。

重要度の選択
重要度使用する場合
Errorコードが実行時に正しく動作しない場合(null 参照、SQL インジェクション、リソースリーク)
Warningコードは動作するものの、ベストプラクティスに違反している場合や、パフォーマンス上の問題がある場合
Info欠陥ではなく、改善の提案である場合
HiddenIDE 内だけで表示するリファクタリングの提案で、ビルド出力には表示しない場合

ほとんどのルールでは、既定で Warning を使用してください。Error は控えめに使用してください。ルール全体を無効にしない限り、ユーザーは EditorConfig でエラーを抑制できないためです。

helpLinkUri

null 以外の helpLinkUri を必ず指定してください(RS1015 により必須です)。安定したドキュメントを参照してください。

helpLinkUri: $"https://github.com/myorg/mylib/blob/main/docs/rules/{DiagnosticId}.md"

CodeFixProvider

コード修正プロバイダーは、診断に対する自動修正を提供します。CodeFixProvider を継承し、特定の診断 ID に対する修正を登録します。

using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NoPublicFieldsCodeFixProvider))]
[Shared]
public sealed class NoPublicFieldsCodeFixProvider : CodeFixProvider
{
    public override ImmutableArray<string> FixableDiagnosticIds { get; }
        = ImmutableArray.Create(NoPublicFieldsAnalyzer.DiagnosticId);

    // Enable FixAll support -- RS1016 flags missing FixAllProvider
    public override FixAllProvider GetFixAllProvider()
        => WellKnownFixAllProviders.BatchFixer;

    public override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document
            .GetSyntaxRootAsync(context.CancellationToken)
            .ConfigureAwait(false);

        var diagnostic = context.Diagnostics[0];
        var diagnosticSpan = diagnostic.Location.SourceSpan;

        var fieldDeclaration = root?.FindToken(diagnosticSpan.Start)
            .Parent?
            .AncestorsAndSelf()
            .OfType<FieldDeclarationSyntax>()
            .FirstOrDefault();

        if (fieldDeclaration is null)
            return;

        // EquivalenceKey enables FixAll grouping -- RS1010/RS1011 require unique keys
        context.RegisterCodeFix(
            CodeAction.Create(
                title: "Convert to property",
                createChangedDocument: ct =>
                    ConvertToPropertyAsync(context.Document, fieldDeclaration, ct),
                equivalenceKey: "ConvertToProperty"),
            diagnostic);
    }

    private static async Task<Document> ConvertToPropertyAsync(
        Document document,
        FieldDeclarationSyntax fieldDeclaration,
        CancellationToken cancellationToken)
    {
        var root = await document
            .GetSyntaxRootAsync(cancellationToken)
            .ConfigureAwait(false);

        var variable = fieldDeclaration.Declaration.Variables[0];
        var propertyName = variable.Identifier.Text;

        // Build auto-property with same type and accessibility
        var property = SyntaxFactory.PropertyDeclaration(
                fieldDeclaration.Declaration.Type,
                propertyName)
            .WithModifiers(fieldDeclaration.Modifiers)
            .WithAccessorList(
                SyntaxFactory.AccessorList(
                    SyntaxFactory.List(new[]
                    {
                        SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
                            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
                        SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
                            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                    })))
            .WithLeadingTrivia(fieldDeclaration.GetLeadingTrivia())
            .WithTrailingTrivia(fieldDeclaration.GetTrailingTrivia());

        var newRoot = root!.ReplaceNode(fieldDeclaration, property);
        return document.WithSyntaxRoot(newRoot);
    }
}
CodeFixProvider の主なパターン
  • EquivalenceKey: FixAll をサポートするため、すべての CodeAction に一意の equivalenceKey を設定する必要があります(RS1010、RS1011)。修正の説明または診断 ID をキーとして使用してください。
  • Document と Solution の変更: 修正が単一のファイルを変更する場合は createChangedDocument を使用してください。ファイル間でシンボルの名前を変更する場合や、新しいファイルを追加する場合は createChangedSolution を使用してください。
  • トリビアの保持: 書式やコメントを維持するため、置き換えるノードの先頭および末尾のトリビアを必ず引き継いでください。
  • FixAllProvider: 一括適用可能な修正では WellKnownFixAllProviders.BatchFixer を返してください。ユーザーの操作が必要な修正、または修正間の依存関係がある修正に限り、省略してください。

DiagnosticSuppressor

DiagnosticSuppressor は、他のアナライザーから報告された診断を条件付きで抑制します。サードパーティ製アナライザーが誤って指摘するパターンがコードベースにあり、EditorConfig では抑制条件を表現できない場合に使用してください。

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

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CustomNullCheckSuppressor : DiagnosticSuppressor
{
    private static readonly SuppressionDescriptor SuppressCA1062 = new(
        id: "MYLIB_SUP001",
        suppressedDiagnosticId: "CA1062",
        justification: "Parameter is validated by custom Guard.NotNull helper");

    public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; }
        = ImmutableArray.Create(SuppressCA1062);

    public override void ReportSuppressions(SuppressionAnalysisContext context)
    {
        foreach (var diagnostic in context.ReportedDiagnostics)
        {
            var tree = diagnostic.Location.SourceTree;
            if (tree is null)
                continue;

            var root = tree.GetRoot(context.CancellationToken);
            var node = root.FindNode(diagnostic.Location.SourceSpan);

            // Walk up to the containing method and check for Guard.NotNull call
            var method = node.FirstAncestorOrSelf<Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax>();
            if (method is null)
                continue;

            var methodText = method.ToFullString();

            // Simplified check -- production code should use semantic analysis
            if (methodText.Contains("Guard.NotNull"))
            {
                context.ReportSuppression(
                    Suppression.Create(SuppressCA1062, diagnostic));
            }
        }
    }
}
DiagnosticSuppressor と EditorConfig の使い分け
方法使用する場合
EditorConfig の重要度オーバーライドルールのすべてのインスタンスを無条件に抑制する場合
[SuppressMessage] 属性正当な理由を添えて、特定のコード位置で抑制する場合
DiagnosticSuppressorコードの構造やパターン(例:独自の検証、コード生成マーカー)に応じて抑制する場合

抑制機能は新しい診断を報告できません。既存の診断を抑制することのみ可能です。抑制機能は同じアナライザーパイプラインに参加し、同じ netstandard2.0 ターゲット要件に従います。

バージョン要件: DiagnosticSuppressor には Roslyn 3.8 以降が必要です。アナライザーパッケージが複数バージョンのパッケージングを通じて古い Roslyn バージョンを対象とする場合は、抑制機能の登録を #if ROSLYN_3_8_OR_GREATER の背後で条件付きにしてください(下記の「複数 Roslyn バージョンのターゲット設定」を参照)。


CodeRefactoringProvider

CodeRefactoringProvider は、診断を必要とせず、ユーザーの操作(電球メニュー)によって実行されるコード変換を提供します。欠陥ではない構造的な改善、パターンの適用、コード生成に使用してください。

using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = nameof(ExtractInterfaceRefactoring))]
[Shared]
public sealed class ExtractInterfaceRefactoring : CodeRefactoringProvider
{
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var root = await context.Document
            .GetSyntaxRootAsync(context.CancellationToken)
            .ConfigureAwait(false);

        var node = root?.FindNode(context.Span);
        var classDecl = node?.FirstAncestorOrSelf<ClassDeclarationSyntax>();

        if (classDecl is null)
            return;

        // Only offer when cursor is on the class identifier
        if (!classDecl.Identifier.Span.IntersectsWith(context.Span))
            return;

        context.RegisterRefactoring(
            CodeAction.Create(
                title: $"Extract interface I{classDecl.Identifier.Text}",
                createChangedSolution: ct =>
                    ExtractInterfaceAsync(context.Document, classDecl, ct),
                equivalenceKey: "ExtractInterface"));
    }

    private static async Task<Solution> ExtractInterfaceAsync(
        Document document,
        ClassDeclarationSyntax classDecl,
        CancellationToken cancellationToken)
    {
        // ... build interface from public method signatures, add base type
        // See details.md for complete implementation
        throw new NotImplementedException();
    }
}

完全な ExtractInterfaceAsync の実装(抽出したメソッドの一覧を指定する methods パラメーターを追加します)と、CSharpCodeRefactoringVerifier<T> のテスト例については、details.md を参照してください。

主要な CodeRefactoringProvider のパターン
  • スパンの確認: カーソルまたは選択範囲が関連するノードと交差する場合にのみ、リファクタリングを提示します。スパンを広く一致させると、電球メニューが煩雑になります。
  • 診断は不要: CodeFixProvider とは異なり、リファクタリングプロバイダーは診断を修正するのではなく、任意の変換を提示します。
  • ソリューションレベルの変更: リファクタリングでファイルを追加したり、シンボルの名前を変更したり、複数のドキュメントを変更したりする場合は、createChangedSolution を使用します。
  • 属性の要件: [ExportCodeRefactoringProvider(LanguageNames.CSharp)] と [Shared](MEF)を付与します。このプロバイダーは SupportedDiagnostics では登録しません。
  • テスト: Microsoft.CodeAnalysis.CSharp.CodeRefactoring.Testing の CSharpCodeRefactoringVerifier<T> を使用します(details.md を参照)。

複数の Roslyn バージョンを対象にする

アナライザーパッケージは、異なる Roslyn バージョンを対象とする複数の DLL を NuGet パッケージに含めることができます。これにより、古いコンパイラーとの互換性を保ちながら、利用可能な場合はアナライザーで新しい API を使用できます。

バージョンの境界

Roslyn SDK では、複数のバージョンを対象にする際に以下の境界が使用されます。

Roslyn バージョン同梱先追加された主な API
3.8VS 16.8 / .NET 5 SDKDiagnosticSuppressor、IOperation の改善
4.2VS 17.2 / .NET 6 SDKRegisterHostObjectAction、インクリメンタル分析の改善
4.4VS 17.4 / .NET 7 SDKSyntaxValueProvider.ForAttributeWithMetadataName(ジェネレーター)
4.6VS 17.6 / .NET 8 SDKインターセプターのプレビュー、IOperation ノードの拡充
4.8VS 17.8 / .NET 8 U1CollectionExpression 構文のサポート
4.14VS 17.14 / .NET 10 SDK最新の API サーフェス
プロジェクトの構成(Meziantou.Analyzer パターン)

$(RoslynVersion) MSBuild プロパティを定義し、Version="$(RoslynVersion).0" を使用して Microsoft.CodeAnalysis.CSharp を参照します。複数バージョンのビルドでは、Directory.Build.targets を使用して、ビルド構成全体で Roslyn バージョンをパラメーター化します(プロジェクト構成全体については details.md を参照)。

条件付きコンパイル定数

Directory.Build.targets で、ROSLYN_X_Y および ROSLYN_X_Y_OR_GREATER のパターンに従って定数を定義します。

<PropertyGroup Condition="'$(RoslynVersion)' >= '3.8'">
  <DefineConstants>$(DefineConstants);ROSLYN_3_8;ROSLYN_3_8_OR_GREATER</DefineConstants>
</PropertyGroup>

これらの定数を使用して、バージョン固有のコードをガードします。

#if ROSLYN_4_8_OR_GREATER
    // CollectionExpression operation kind available in Roslyn 4.8+
    context.RegisterOperationAction(AnalyzeCollectionExpression,
        OperationKind.CollectionExpression);
#endif
NuGet パッケージの配置パス

複数バージョンのアナライザーでは、バージョンごとに analyzers/dotnet/roslyn{version}/cs/ という NuGet パスを使用し、3.8 未満のホスト向けのフォールバックとして analyzers/dotnet/cs/ も使用します。ホストは、最も一致度の高い roslyn{version} ディレクトリから DLL を選択します。各ビルドを正しいパスに配置するには、<None Include="..." Pack="true" PackagePath="analyzers/dotnet/roslyn{version}/cs" /> 項目を使用します。完全なパッケージング用 .csproj とパック検証コマンドについては、details.md を参照してください。

複数バージョンのテストマトリックス

テストプロジェクトで $(RoslynVersion) をパラメーター化し、各 Roslyn バージョンのビルドを個別にテストします。テストランナーには、Microsoft.Testing.Platform v2(MTP2)を使用する xUnit v3 を使用します。

# Build and test each Roslyn version (xUnit v3 + MTP2)
for version in 3.8 4.2 4.4; do
  dotnet test -p:RoslynVersion=$version
done

複数バージョンに対応するプロジェクト構成全体(Directory.Build.props、Directory.Build.targets、パッケージング用 .csproj、xUnit v3 を使用する GitHub Actions の CI マトリックス)については、details.md を参照してください。


アナライザーのテスト

手軽に利用できる高水準のアナライザーテストには、Microsoft.CodeAnalysis.Testing インフラストラクチャを使用します。生の CSharpCompilation を使ったテストよりも、この方法を推奨します。

必須の NuGet パッケージ
<PropertyGroup>
  
  <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
</PropertyGroup>

<ItemGroup>
  
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.3" />
  
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.3" />
  
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" />
  
  <PackageReference Include="xunit.v3" Version="3.2.2" />
</ItemGroup>

移行に関する注意: フレームワーク固有のパッケージ(例:Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit)は非推奨です。代わりに、DefaultVerifier と汎用パッケージを使用してください。これにより Roslyn テストインフラストラクチャをテストフレームワークから切り離し、xUnit v3 および Microsoft.Testing.Platform v2(MTP2)と互換性を持たせることができます。

アナライザーのみのテスト

CSharpAnalyzerVerifier<T> を使用して、診断が正しく報告される(または報告されない)ことをテストします。

using Microsoft.CodeAnalysis.Testing;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
    NoPublicFieldsAnalyzer,
    Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

public class NoPublicFieldsAnalyzerTests
{
    [Fact]
    public async Task PublicField_ReportsDiagnostic()
    {
        var test = """
            public class MyClass
            {
                public int {|MYLIB001:Value|};
            }
            """;

        await Verify.VerifyAnalyzerAsync(test);
    }

    [Fact]
    public async Task PrivateField_NoDiagnostic()
    {
        var test = """
            public class MyClass
            {
                private int _value;
            }
            """;

        await Verify.VerifyAnalyzerAsync(test);
    }

    // Also test: public const fields (no diagnostic), public readonly fields (no diagnostic)
}
診断マークアップ構文

テストフレームワークでは、期待する診断位置を示すためにマークアップを使用します。

マークアップ意味
`[text
`{DIAG_ID:text
`{DIAG_ID:text{
アナライザーとコード修正のテスト

アナライザーから修正までのパイプライン全体をテストするには、CSharpCodeFixVerifier<TAnalyzer, TCodeFix> を使用します。

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<
    NoPublicFieldsAnalyzer,
    NoPublicFieldsCodeFixProvider,
    Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

public class NoPublicFieldsCodeFixTests
{
    [Fact]
    public async Task PublicField_FixConvertsToProperty()
    {
        var test = """
            public class MyClass
            {
                public int {|MYLIB001:Value|};
            }
            """;

        var fixedCode = """
            public class MyClass
            {
                public int Value { get; set; }
            }
            """;

        await Verify.VerifyCodeFixAsync(test, fixedCode);
    }
}
複数ファイルのテストシナリオ

複数ファイルのテストでは、Verify.Test クラスと TestState.Sources を使用して、名前付きソースファイルを複数追加します。各ファイルで、標準のマークアップ構文を使用して期待する診断を設定します。静的な VerifyAnalyzerAsync の簡略記法ではなく、test.RunAsync() を使用してください。


NuGet パッケージ化

アナライザーは、特定のディレクトリレイアウトを持つ NuGet パッケージとして配布されます。アセンブリは lib/ ではなく analyzers/dotnet/cs/ の下に配置します。

パッケージのレイアウト
MyAnalyzers.nupkg
  analyzers/
    dotnet/
      cs/
        MyAnalyzers.dll           # Analyzer assembly
        MyAnalyzers.CodeFixes.dll # Code fix assembly (optional, separate)
  lib/
    netstandard2.0/
      _._                        # Empty placeholder (no runtime dependency)
プロジェクトの構成

<PropertyGroup>
  <IncludeBuildOutput>false</IncludeBuildOutput>
  <DevelopmentDependency>true</DevelopmentDependency>
  <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>

<ItemGroup>
  <None Include="$(OutputPath)\$(AssemblyName).dll"
        Pack="true"
        PackagePath="analyzers/dotnet/cs" />
</ItemGroup>
アナライザーとコード修正アセンブリの分離

複数のアナライザーを含む NuGet パッケージでは、アナライザーとコード修正を分離すると IDE の読み込み時間を短縮できます。IDE はコード修正アセンブリを遅延読み込みします(ユーザーが修正を要求したときのみ読み込みます)が、アナライザーアセンブリはすぐに読み込みます。2 つのプロジェクト(MyAnalyzers と MyAnalyzers.CodeFixes)を作成し、<None Include="..." Pack="true" PackagePath="analyzers/dotnet/cs" /> 項目を使用して両方の DLL を analyzers/dotnet/cs/ にパッケージ化します。

パッケージの検証

パッケージ化した後、unzip -l ./bin/Release/MyAnalyzers.1.0.0.nupkg | grep analyzers/ を実行してレイアウトを確認します(nupkg ファイルは zip アーカイブです)。パッケージ化に関する共通の概念については、[skill:dotnet-csharp-source-generators] を参照してください(analyzers/dotnet/cs/ のレイアウトはアナライザーとソースジェネレーターの両方で同じです)。


パフォーマンスのベストプラクティス

アナライザーは編集中にリアルタイムで実行されます。パフォーマンスが低いと、そのアナライザーを使用するすべてのユーザーの IDE 体験が損なわれます。

コールバックでの割り当てを回避する

頻繁に実行されるコールバックでの割り当ては避けてください。各 RegisterSyntaxNodeAction コールバックはノードごとに実行され、キー入力 1 回につき数千回実行される可能性があります。

// BAD: resolves type on every symbol callback (per-symbol overhead)
context.RegisterSymbolAction(symbolCtx =>
{
    // GetTypeByMetadataName is called for EVERY symbol in the compilation
    var disposableType = symbolCtx.Compilation
        .GetTypeByMetadataName("System.IDisposable");
    if (disposableType is null) return;

    Analyze(symbolCtx, disposableType);
}, SymbolKind.NamedType);

// GOOD: resolve state once per compilation, closure allocated once
context.RegisterCompilationStartAction(compilationCtx =>
{
    var importantType = compilationCtx.Compilation
        .GetTypeByMetadataName("System.IDisposable");
    if (importantType is null) return;

    // One closure per compilation -- acceptable cost
    compilationCtx.RegisterSymbolAction(
        symbolCtx => Analyze(symbolCtx, importantType),
        SymbolKind.NamedType);
});

注: RegisterCompilationStartAction 内でコンパイルごとに 1 回だけ割り当てられるクロージャーは許容されます。アンチパターンは、呼び出し回数が数千回に及ぶノードごとまたはシンボルごとのコールバック内で、型を解決したりクロージャーを割り当てたりすることです。

シンボルベースのフィルタリング

シンボルベースと構文ベースの分析のどちらも可能な場合は、シンボルベースの分析を優先してください。シンボル分析はコンパイラーが解決したモデルを使用し、再解析を回避します。

// Prefer: RegisterSymbolAction for declared-symbol checks
context.RegisterSymbolAction(ctx =>
{
    var method = (IMethodSymbol)ctx.Symbol;
    if (method.ReturnsVoid && method.Parameters.Length == 0) { /* ... */ }
}, SymbolKind.Method);

// Avoid: RegisterSyntaxNodeAction when symbol analysis suffices
// Syntax analysis requires manually resolving types and handling aliases
SupportedDiagnostics での ImmutableArray

SupportedDiagnostics は ImmutableArray<DiagnosticDescriptor> フィールドとしてキャッシュしてください。ランタイムはこのプロパティを頻繁に呼び出します。

// GOOD: single allocation, cached
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
    = ImmutableArray.Create(Rule1, Rule2, Rule3);

// BAD: allocates a new array on every access
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
    => ImmutableArray.Create(Rule1, Rule2, Rule3);
その他のパフォーマンスガイドライン
  • 並行実行を有効にする: アナライザーに共有の可変状態がある場合(本来あってはなりません)を除き、必ず context.EnableConcurrentExecution() を呼び出してください。
  • フィールドにコンパイルごとの状態を保持しない: コンパイルごとのデータをアナライザーのインスタンスフィールドに保存しないでください(RS1008)。RegisterCompilationStartAction を使用して、状態を単一のコンパイルに限定してください。
  • 構文述語で早期にフィルタリングする: RegisterSyntaxNodeAction を使用する場合は、コールバックの頻度を最小限にするため、可能な限り具体的な SyntaxKind を登録してください。
  • Compilation.GetSemanticModel() を使用しない: 代わりに、分析コンテキストから提供される SemanticModel を使用してください(RS1030)。

よくあるメタ診断(RS シリーズ)

Microsoft.CodeAnalysis.Analyzers パッケージは、アナライザーコード自体に対して RS シリーズの診断を報告します。これらは、作成時のミスをコンパイル時に見つけるうえで非常に役立ちます。プロジェクトファイルで EnforceExtendedAnalyzerRules を有効にしてください。

よく発生する RS 診断
IDタイトル検出内容
RS1001DiagnosticAnalyzerAttribute がありませんアナライザークラスに [DiagnosticAnalyzer(LanguageNames.CSharp)] がありません
RS1004言語サポートの追加を推奨しますアナライザーは C# をサポートしていますが、VB はサポートしていません(情報提供のみ)
RS1007ローカライズ可能な引数を指定してくださいDiagnosticDescriptor の文字列には LocalizableResourceString を使用する必要があります
RS1008コンパイルごとのデータを保存しないでくださいコンパイル固有のデータを保持するインスタンスフィールドは、同時実行を妨げます
RS1010一意の EquivalenceKey を指定してコードアクションを作成してくださいFixAll をサポートするには、CodeAction に等価キーが必要です
RS1015null でない helpLinkUri を指定してくださいDiagnosticDescriptor のヘルプリンクが null または空です
RS1016コード修正プロバイダーは FixAll をサポートしてくださいGetFixAllProvider() のオーバーライドがありません
RS1017DiagnosticId は null でない定数である必要があります診断 ID がコンパイル時定数ではありません
RS1022Workspaces アセンブリの型を使用しないでくださいアナライザーが Microsoft.CodeAnalysis.Workspaces を参照しています(すべてのホストで使用できるわけではありません)
RS1024シンボルの比較には等価性を使用してくださいシンボルの比較に SymbolEqualityComparer ではなく == を使用しています
RS1026同時実行を有効にしてくださいcontext.EnableConcurrentExecution() の呼び出しがありません
RS1029予約済みの診断 ID を使用しないでくださいCA、CS、IDE、またはその他のプラットフォーム予約済みのプレフィックスを使用しています
RS1030Compilation.GetSemanticModel() を呼び出さないでください代わりに分析コンテキストから SemanticModel を使用してください
RS1035アナライザーで禁止されている API を使用しないでくださいすべてのアナライザーホスト環境で使用できるわけではない API を使用しています
RS1041コンパイラー拡張機能のターゲットは netstandard2.0 にしてくださいプロジェクトのターゲットが netstandard2.0 以外のフレームワークです
リリース追跡(RS2000 シリーズ)

成熟したアナライザーでは、診断 ID のライフサイクルを管理するためにリリース追跡を有効にしてください。

IDタイトル検出内容
RS2000アナライザーのリリースに診断 ID を追加してください新しい診断 ID がリリースファイルで追跡されていません
RS2001アナライザーの診断 ID に最新のエントリがあることを確認してくださいリリース追跡ファイルが最新の状態と一致していません
RS2008アナライザーのリリース追跡を有効にしてくださいプロジェクトでリリース追跡を有効にする必要があります

リリース追跡では、AnalyzerReleases.Shipped.md と AnalyzerReleases.Unshipped.md のファイルを使用して、公開済みの診断 ID を追跡します。


参考資料

コメント