dotnet-roslyn-analyzers について
カスタム Roslyn アナライザー、コード修正、リファクタリング、診断抑制器を作成する .NET 開発者向けのスキルです。実装、テスト、パッケージ化、複数バージョンへの対応を扱います。
主な機能
- 01アナライザープロジェクトと診断の設定
- 02分析コンテキストの登録パターン
- 03コード修正とリファクタリング
- 04診断抑制の指針
- 05複数バージョン対応とパッケージ化
- 06Microsoft.CodeAnalysis.Testing によるテスト
活用シーン
- 01カスタム C# アナライザーの作成
- 02診断への自動修正の追加
- 03診断を伴わないリファクタリングの構築
- 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 | 宣言されたシンボル | シンボルレベルのプロパティ(アクセシビリティ、型、属性)の確認 |
RegisterOperationAction | IL レベルの操作 | 構文に依存しないセマンティック操作(代入、呼び出し)の分析 |
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 | 欠陥ではなく、改善の提案である場合 |
Hidden | IDE 内だけで表示するリファクタリングの提案で、ビルド出力には表示しない場合 |
ほとんどのルールでは、既定で 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.8 | VS 16.8 / .NET 5 SDK | DiagnosticSuppressor、IOperation の改善 |
| 4.2 | VS 17.2 / .NET 6 SDK | RegisterHostObjectAction、インクリメンタル分析の改善 |
| 4.4 | VS 17.4 / .NET 7 SDK | SyntaxValueProvider.ForAttributeWithMetadataName(ジェネレーター) |
| 4.6 | VS 17.6 / .NET 8 SDK | インターセプターのプレビュー、IOperation ノードの拡充 |
| 4.8 | VS 17.8 / .NET 8 U1 | CollectionExpression 構文のサポート |
| 4.14 | VS 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 | タイトル | 検出内容 |
|---|---|---|
| RS1001 | DiagnosticAnalyzerAttribute がありません | アナライザークラスに [DiagnosticAnalyzer(LanguageNames.CSharp)] がありません |
| RS1004 | 言語サポートの追加を推奨します | アナライザーは C# をサポートしていますが、VB はサポートしていません(情報提供のみ) |
| RS1007 | ローカライズ可能な引数を指定してください | DiagnosticDescriptor の文字列には LocalizableResourceString を使用する必要があります |
| RS1008 | コンパイルごとのデータを保存しないでください | コンパイル固有のデータを保持するインスタンスフィールドは、同時実行を妨げます |
| RS1010 | 一意の EquivalenceKey を指定してコードアクションを作成してください | FixAll をサポートするには、CodeAction に等価キーが必要です |
| RS1015 | null でない helpLinkUri を指定してください | DiagnosticDescriptor のヘルプリンクが null または空です |
| RS1016 | コード修正プロバイダーは FixAll をサポートしてください | GetFixAllProvider() のオーバーライドがありません |
| RS1017 | DiagnosticId は null でない定数である必要があります | 診断 ID がコンパイル時定数ではありません |
| RS1022 | Workspaces アセンブリの型を使用しないでください | アナライザーが Microsoft.CodeAnalysis.Workspaces を参照しています(すべてのホストで使用できるわけではありません) |
| RS1024 | シンボルの比較には等価性を使用してください | シンボルの比較に SymbolEqualityComparer ではなく == を使用しています |
| RS1026 | 同時実行を有効にしてください | context.EnableConcurrentExecution() の呼び出しがありません |
| RS1029 | 予約済みの診断 ID を使用しないでください | CA、CS、IDE、またはその他のプラットフォーム予約済みのプレフィックスを使用しています |
| RS1030 | Compilation.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 を追跡します。