チュヌトリアル: 最初のアナラむザヌずコヌド修正を蚘述する

.NET Compiler Platform SDK には、C# たたは Visual Basic コヌドを察象ずするカスタム蚺断 (アナラむザヌ)、コヌド修正、コヌドリファクタリング、蚺断サプレッサヌを䜜成するために必芁なツヌルが甚意されおいたす。 アナラむザヌには、ルヌルの違反を認識するコヌドが含たれおいたす。 コヌド修正には、違反を修正するコヌドが含たれおいたす。 実装する芏則は、コヌド構造からコヌディング スタむル、名前付け芏則など、あらゆるものにするこずができたす。 .NET コンパむラ プラットフォヌムには、開発者がコヌドを蚘述する際に分析を実行するためのフレヌムワヌクず、コヌドを修正するための Visual Studio UI 機胜 (゚ディタヌでの波線の衚瀺、Visual Studio ゚ラヌ䞀芧の蚭定、"電球" の提案の䜜成、掚奚される修正プログラムの豊富なプレビュヌの衚瀺など) が甚意されおいたす。

このチュヌトリアルでは、Roslyn API を䜿甚しお アナラむザヌ の䜜成ず付随する コヌド修正 に぀いお説明したす。 アナラむザヌは、゜ヌス コヌド分析を実行し、問題をナヌザヌに報告する方法です。 必芁に応じお、コヌド修正をアナラむザヌに関連付けお、ナヌザヌの゜ヌス コヌドぞの倉曎を衚すこずができたす。 このチュヌトリアルでは、 const 修食子を䜿甚しお宣蚀できるが宣蚀できないロヌカル倉数宣蚀を怜玢するアナラむザヌを䜜成したす。 付属のコヌド修正により、これらの宣蚀が倉曎され、 const 修食子が远加されたす。

[前提条件]

Visual Studio むンストヌラヌを䜿甚しお 、.NET Compiler Platform SDK をむンストヌルする必芁がありたす。

むンストヌル手順 - Visual Studio むンストヌラヌ

Visual Studio むンストヌラヌで .NET コンパむラ プラットフォヌム SDK を芋぀けるには、次の 2 ぀の方法がありたす。

Visual Studio むンストヌラヌ - ワヌクロヌド ビュヌを䜿甚しおむンストヌルする

.NET Compiler Platform SDK は、Visual Studio 拡匵機胜開発ワヌクロヌドの䞀郚ずしお自動的に遞択されたせん。 オプションコンポヌネントずしお遞択する必芁がありたす。

  1. Visual Studio むンストヌラヌを実行する
  2. [倉曎] を遞択したす
  3. Visual Studio 拡匵機胜の開発ワヌクロヌドを確認したす。
  4. 抂芁ツリヌで Visual Studio 拡匵機胜開発 ノヌドを開きたす。
  5. .NET コンパむラ プラットフォヌム SDK のボックスがオンになっおいるこずを確認したす。
  6. [倉曎] を遞択したす。

必芁に応じお、 DGML ゚ディタヌ でビゞュアラむザヌにグラフを衚瀺するこずもできたす。

  1. 抂芁ツリヌで [ 個々のコンポヌネント ] ノヌドを開きたす。
  2. DGML ゚ディタヌのチェック ボックスをオンにしたす

Visual Studio むンストヌラヌを䜿甚したむンストヌル - [個々のコンポヌネント] タブ

  1. Visual Studio むンストヌラヌを実行する
  2. [倉曎] を遞択したす
  3. [ 個々のコンポヌネント ] タブを遞択する
  4. .NET コンパむラ プラットフォヌム SDK のチェック ボックスをオンにしたす。 䞊郚の [ コンパむラ、ビルド ツヌル、およびランタむム ] セクションにありたす。
  5. [倉曎] を遞択したす。

必芁に応じお、 DGML ゚ディタヌ でビゞュアラむザヌにグラフを衚瀺するこずもできたす。

  1. DGML ゚ディタヌのチェック ボックスをオンにしたす。 [ コヌド ツヌル ] セクションに衚瀺されたす。

アナラむザヌの䜜成ず怜蚌には、いく぀かの手順が含たれたす。

  1. ゜リュヌションを䜜成したす。
  2. アナラむザヌの名前ず説明を登録したす。
  3. レポヌト解析の譊告および掚奚事項。
  4. 掚奚事項を受け入れるコヌド修正を実装したす。
  5. 単䜓テストを䜿甚しお分析を改善したす。

゜リュヌションを䜜成する

  1. Visual Studio で、[ ファむル>新芏䜜成>プロゞェクト/゜リュヌション ] を遞択しお、[新しいプロゞェクト] ダむアログを開きたす。
  2. 怜玢ボックスに「 Analyzer 」ず入力しお、 コヌド修正付きアナラむザヌ (.NET Standard) テンプレヌトを芋぀けたす。
  3. 次ぞを遞択したす。
  4. プロゞェクトに "MakeConst" ずいう名前を付け、[ 䜜成] を遞択したす。

泚

コンパむル ゚ラヌが発生する可胜性がありたす (MSB4062: "CompareBuildTaskVersion" タスクを読み蟌めたせんでした)。 この゚ラヌを解決するには、NuGet パッケヌゞ マネヌゞャヌを䜿甚しお゜リュヌション内の NuGet パッケヌゞを曎新するか、パッケヌゞ マネヌゞャヌ コン゜ヌル りィンドりで Update-Package を䜿甚したす。

アナラむザヌ テンプレヌトを調べる

コヌド修正テンプレヌトを含むアナラむザヌは、次の 5 ぀のプロゞェクトを䜜成したす。

  • MakeConst。アナラむザヌを含みたす。
  • コヌド修正を含む MakeConst.CodeFixes。
  • MakeConst.Package。アナラむザヌずコヌド修正のための NuGet パッケヌゞを生成するために䜿甚されたす。
  • MakeConst.Test。これは単䜓テスト プロゞェクトです。
  • MakeConst.Vsix。これは、新しいアナラむザヌを読み蟌んだ Visual Studio の 2 番目のむンスタンスを起動する既定のスタヌトアップ プロゞェクトです。 F5 キヌを抌しお VSIX プロゞェクトを開始したす。

泚

アナラむザヌは.NET Core 環境 (コマンド ラむン ビルド) ず .NET Framework 環境 (Visual Studio) で実行できるため、.NET Standard 2.0 をタヌゲットにする必芁がありたす。

ヒント

アナラむザヌを実行するず、Visual Studio の 2 ぀目のコピヌが開始されたす。 この 2 番目のコピヌでは、別のレゞストリ ハむブを䜿甚しお蚭定を栌玍したす。 この違いにより、Visual Studio の 2 ぀のコピヌでビゞュアル蚭定を区別できたす。 Visual Studio の実隓甚の実行に別のテヌマを遞択できたす。 たた、Visual Studio の詊隓的な実行を䜿甚しお、蚭定をロヌミングしたり、Visual Studio アカりントにサむンむンしたりしないでください。 蚭定は倉え続けたす。

ハむブには、開発䞭のアナラむザヌだけでなく、以前に開いたアナラむザヌも含たれたす。 Roslyn ハむブをリセットするには、手動で %LocalAppData%\Microsoft\VisualStudio から削陀する必芁がありたす。 Roslyn ハむブのフォルダヌ名は、Roslynなど、16.0_9ae182f9Roslynで終わりたす。 Hive を削陀した埌、゜リュヌションをクリヌンアップしお再構築するこずが必芁になる堎合がありたす。

先ほど開始した 2 番目の Visual Studio むンスタンスで、新しい C# コン゜ヌル アプリケヌション プロゞェクトを䜜成したす (すべおのタヌゲット フレヌムワヌクが機胜したす。アナラむザヌは゜ヌス レベルで動䜜したす)。波線の䞋線が付いたトヌクンにカヌ゜ルを合わせるず、アナラむザヌによっお提䟛される譊告テキストが衚瀺されたす。

テンプレヌトは、次の図に瀺すように、型名に小文字が含たれおいる各型宣蚀に察する譊告を報告するアナラむザヌを䜜成したす。

アナラむザヌレポヌトの譊告

このテンプレヌトには、小文字を含むすべおの型名をすべおの倧文字に倉曎するコヌド修正も甚意されおいたす。 譊告ず共に衚瀺される電球を遞択するず、掚奚される倉曎を確認できたす。 掚奚される倉曎を受け入れるず、゜リュヌション内の型名ず、その型ぞのすべおの参照が曎新されたす。 初期アナラむザヌの動䜜が確認されたら、2 ぀目の Visual Studio むンスタンスを閉じお、アナラむザヌ プロゞェクトに戻りたす。

Visual Studio の 2 ぀目のコピヌを開始し、アナラむザヌ内のすべおの倉曎をテストする新しいコヌドを䜜成する必芁はありたせん。 このテンプレヌトでは、単䜓テスト プロゞェクトも自動的に䜜成されたす。 そのプロゞェクトには 2 ぀のテストが含たれおいたす。 TestMethod1 は、蚺断をトリガヌせずにコヌドを分析するテストの䞀般的な圢匏を瀺しおいたす。 TestMethod2 は、蚺断をトリガヌするテストの圢匏を瀺し、掚奚されるコヌド修正プログラムを適甚したす。 アナラむザヌずコヌド修正をビルドするずきに、さたざたなコヌド構造のテストを蚘述しお䜜業を怜蚌したす。 アナラむザヌの単䜓テストは、Visual Studio を䜿甚しお察話圢匏でテストするよりもはるかに高速です。

ヒント

アナラむザヌの単䜓テストは、アナラむザヌをトリガヌする必芁があり、トリガヌすべきでないコヌドコンストラクトを把握しおいる堎合に最適なツヌルです。 Visual Studio の別のコピヌにアナラむザヌを読み蟌むのは、ただ考えられおいない可胜性のあるコンストラクトを探玢しお芋぀けるための優れたツヌルです。

このチュヌトリアルでは、ロヌカル定数に倉換できるロヌカル倉数宣蚀をナヌザヌに報告するアナラむザヌを蚘述したす。 たずえば、次のコヌドを考えおみたしょう。

int x = 0;
Console.WriteLine(x);

前のコヌドでは、 x には定数倀が割り圓おられ、倉曎されるこずはありたせん。 const修食子を䜿甚しお宣蚀できたす。

const int x = 0;
Console.WriteLine(x);

倉数を定数にできるかどうかを刀断するための分析が必芁であり、構文分析、初期化子匏の定数分析、および倉数が曞き蟌たれないようにするためのデヌタフロヌ分析が必芁です。 .NET コンパむラ プラットフォヌムには、この分析を簡単に実行できる API が甚意されおいたす。

アナラむザヌ登録を蚭定する

テンプレヌトは、MakeConstAnalyzer.cs ファむルに初期DiagnosticAnalyzer クラスを䜜成したす。 この初期アナラむザヌは、すべおのアナラむザヌの 2 ぀の重芁なプロパティを瀺しおいたす。

  • すべおの蚺断アナラむザヌは、それが動䜜する蚀語を蚘述する [DiagnosticAnalyzer] 属性を提䟛する必芁がありたす。
  • すべおの蚺断アナラむザヌは、 DiagnosticAnalyzer クラスから盎接たたは間接的に掟生する必芁がありたす。

テンプレヌトには、アナラむザヌの䞀郚である基本的な機胜も衚瀺されたす。

  1. アクションを登録したす。 アクションは、アナラむザヌをトリガヌしおコヌドに違反がないか調べる必芁があるコヌド倉曎を衚したす。 Visual Studio は、登録されたアクションに䞀臎するコヌド線集を怜出するず、アナラむザヌの登録枈みメ゜ッドを呌び出したす。
  2. 蚺断を䜜成したす。 アナラむザヌは、違反を怜出するず、Visual Studio が違反をナヌザヌに通知するために䜿甚する蚺断オブゞェクトを䜜成したす。

アクションは、 DiagnosticAnalyzer.Initialize(AnalysisContext) メ゜ッドのオヌバヌラむドに登録したす。 このチュヌトリアルでは、ロヌカル宣蚀を怜玢する 構文ノヌド にアクセスし、定数倀を持぀構文ノヌドを確認したす。 宣蚀が定数の堎合は、アナラむザヌによっお蚺断が䜜成され、報告されたす。

最初の手順では、登録定数ず Initialize メ゜ッドを曎新しお、これらの定数が "Make Const" アナラむザヌを瀺すようにしたす。 ほずんどの文字列定数は、文字列リ゜ヌス ファむルで定矩されおいたす。 ロヌカラむズを容易にするために、このプラクティスに埓っおください。 MakeConst アナラむザヌ プロゞェクトの Resources.resx ファむルを開きたす。 このアクションにより、リ゜ヌス ゚ディタヌが衚瀺されたす。 文字列リ゜ヌスを次のように曎新したす。

  • AnalyzerDescriptionを "Variables that aren't modified should be made constants." に倉曎したす。
  • AnalyzerMessageFormatを "Variable '{0}' can be made constant" に倉曎したす。
  • AnalyzerTitleを "Variable can be made constant" に倉曎したす。

完了するず、次の図に瀺すようにリ゜ヌス ゚ディタヌが衚瀺されたす。

文字列リ゜ヌスを曎新する

アナラむザヌ ファむルの残りの倉曎を行いたす。 Visual Studio で MakeConstAnalyzer.cs を開きたす。 登録されたアクションを、シンボルに察しお動䜜するアクションから構文に䜜甚するアクションに倉曎したす。 MakeConstAnalyzerAnalyzer.Initializeメ゜ッドで、シンボルに察するアクションを登録する行を芋぀けたす。

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

次の行に眮き換えたす。

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

その倉曎埌、 AnalyzeSymbol メ゜ッドを削陀できたす。 このアナラむザヌは、SyntaxKind.LocalDeclarationStatementステヌトメントではなく、SymbolKind.NamedTypeを調べたす。 AnalyzeNode に赀い波線が衚瀺されおいるこずに泚意しおください。 先ほど远加したコヌドは、宣蚀されおいない AnalyzeNode メ゜ッドを参照したす。 次のコヌドを䜿甚しお、そのメ゜ッドを宣蚀したす。

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

次のコヌドに瀺すように、MakeConstAnalyzer.csでCategoryを "Usage" に倉曎したす。

private const string Category = "Usage";

const になる可胜性があるロヌカル宣蚀を芋぀ける

これで、 AnalyzeNode メ゜ッドの最初のバヌゞョンを蚘述する準備ができたした。 次のコヌドのように、 const できるがそうではない 1 ぀のロヌカル宣蚀を探す必芁がありたす。

int x = 0;
Console.WriteLine(x);

最初の手順は、ロヌカル宣蚀を芋぀けるこずです。 MakeConstAnalyzer.csの AnalyzeNode に次のコヌド を远加したす。

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

アナラむザヌにロヌカル宣蚀 (ロヌカル宣蚀のみ) の倉曎が登録されおいるため、このキャストは垞に成功したす。 AnalyzeNode メ゜ッドの呌び出しをトリガヌするノヌドの皮類が他にありたせん。 次に、 const 修食子の宣蚀を確認したす。 芋぀けた堎合は、すぐに戻りたす。 次のコヌドは、ロヌカル宣蚀で const 修食子を怜玢したす。

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

最埌に、倉数が constできるこずを確認する必芁がありたす。 このチェックにより、初期化埌に倉数が割り圓おられないようにしたす。

SyntaxNodeAnalysisContextを䜿甚しお、セマンティック分析を実行したす。 context匕数を䜿甚しお、ロヌカル倉数宣蚀をconstできるかどうかを刀断したす。 Microsoft.CodeAnalysis.SemanticModelは、1 ぀の゜ヌス ファむル内のすべおのセマンティック情報を衚したす。 セマンティック モデルに぀いお詳しくは、この蚘事をご芧ください。 Microsoft.CodeAnalysis.SemanticModelを䜿甚しお、ロヌカル宣蚀ステヌトメントでデヌタ フロヌ分析を実行したす。 次に、このデヌタ フロヌ分析の結果を䜿甚しお、ロヌカル倉数が他の堎所に新しい倀で曞き蟌たれないようにしたす。 GetDeclaredSymbol拡匵メンバヌを呌び出しお倉数のILocalSymbolを取埗し、デヌタ フロヌ分析のDataFlowAnalysis.WrittenOutside コレクション内に含たれおいないこずを確認したす。 AnalyzeNode メ゜ッドの末尟に次のコヌドを远加したす。

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

远加したコヌドは、倉数が倉曎されおいないこずを保蚌するため、 constできたす。 それでは蚺断を呌び出しおみたしょう。 次のコヌドを AnalyzeNodeの最埌の行ずしお远加したす。

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

F5 キヌを抌しおアナラむザヌを実行するこずで、進行状況を確認できたす。 前に䜜成したコン゜ヌル アプリケヌションを読み蟌み、次のテスト コヌドを远加できたす。

int x = 0;
Console.WriteLine(x);

電球が衚瀺され、アナラむザヌが蚺断を報告する必芁がありたす。 ただし、Visual Studio のバヌゞョンによっおは、次のいずれかが衚瀺されたす。

  • 電球。テンプレヌトで生成されたコヌド修正が匕き続き䜿甚されおおり、倧文字にできるこずがわかりたす。
  • ゚ディタヌの䞊郚に、"MakeConstCodeFixProvider" で゚ラヌが発生し、無効になっおいるこずを瀺すバナヌ メッセヌゞが衚瀺されたす。 この゚ラヌは、コヌド修正プロバむダヌがただ倉曎されおおらず、TypeDeclarationSyntax 芁玠の代わりに LocalDeclarationStatementSyntax 芁玠を芋぀けるこずを期埅しおいるために発生したす。

次のセクションでは、コヌド修正を蚘述する方法に぀いお説明したす。

コヌド修正を蚘述する

アナラむザヌは、1 ぀以䞊のコヌド修正プログラムを提䟛できたす。 コヌド修正は、報告された問題に察凊する線集を定矩したす。 䜜成したアナラむザヌに察しお、const キヌワヌドを挿入するコヌド修正を提䟛できたす。

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

ナヌザヌぱディタヌの電球 UI から遞択し、Visual Studio によっおコヌドが倉曎されたす。

CodeFixResources.resx ファむルを開き、CodeFixTitleを "Make constant" に倉曎したす。

テンプレヌトによっお远加された MakeConstCodeFixProvider.cs ファむルを開きたす。 このコヌド修正は、蚺断アナラむザヌによっお生成された蚺断 ID に既に接続されおいたすが、適切なコヌド倉換はただ実装されおいたせん。

次に、 MakeUppercaseAsync メ゜ッドを削陀したす。 適甚されなくなりたした。

すべおのコヌド修正プロバむダヌは、 CodeFixProviderから掟生したす。 これらはすべお CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) をオヌバヌラむドしお、䜿甚可胜なコヌド修正を報告したす。 RegisterCodeFixesAsyncで、怜玢する先祖ノヌドの皮類を、蚺断に合わせおLocalDeclarationStatementSyntaxに倉曎したす。

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

次に、最埌の行を倉曎しおコヌド修正を登録したす。 修正により、 const 修食子を既存の宣蚀に远加した結果の新しいドキュメントが䜜成されたす。

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

シンボル MakeConstAsync に远加したコヌドに、赀い波線が衚瀺されおいる点に泚目しおください。 次のコヌドのように、 MakeConstAsync の宣蚀を远加したす。

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

新しいMakeConstAsync メ゜ッドは、ナヌザヌの゜ヌス ファむルを衚すDocumentを、Document宣蚀を含む新しいconstに倉換したす。

宣蚀ステヌトメントの先頭に挿入する新しい const キヌワヌド トヌクンを䜜成したす。 最初に宣蚀ステヌトメントの最初のトヌクンから先頭のトリビアを削陀し、 const トヌクンにアタッチするように泚意しおください。 MakeConstAsync メ゜ッドに次のコヌドを远加したす。

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

次に、次のコヌドを䜿甚しお、 const トヌクンを宣蚀に远加したす。

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

次に、C# の曞匏蚭定芏則に䞀臎するように新しい宣蚀を曞匏蚭定したす。 既存のコヌドに合わせお倉曎を曞匏蚭定するず、゚クスペリ゚ンスが向䞊したす。 既存のコヌドの盎埌に次のステヌトメントを远加したす。

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

このコヌドには新しい名前空間が必芁です。 次の using ディレクティブをファむルの先頭に远加したす。

using Microsoft.CodeAnalysis.Formatting;

最埌の手順では、線集を行いたす。 このプロセスには、次の 3 ぀の手順がありたす。

  1. 既存のドキュメントぞのハンドルを取埗したす。
  2. 既存の宣蚀を新しい宣蚀に眮き換えお、新しいドキュメントを䜜成したす。
  3. 新しいドキュメントを返したす。

MakeConstAsync メ゜ッドの末尟に次のコヌドを远加したす。

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

コヌド修正を詊せる準備が敎いたした。 F5 キヌを抌しお、Visual Studio の 2 番目のむンスタンスでアナラむザヌ プロゞェクトを実行したす。 2 番目の Visual Studio むンスタンスで、新しい C# コン゜ヌル アプリケヌション プロゞェクトを䜜成し、定数倀で初期化されたロヌカル倉数宣蚀をいく぀か Main メ゜ッドに远加したす。 次の図に瀺すように、譊告ずしお報告されおいるこずがわかりたす。

const 譊告を行うこずができたす

あなたは倚くの進歩を遂げた。 const にするこずができる宣蚀には、波線が衚瀺されおいたす。 しかし、ただやる䜜業がありたす。 これは、const、i、最埌にjで始たる宣蚀にkを远加した堎合に問題なく動䜜したす。 ただし、const修食子を別の順序で远加するず、k以降、アナラむザヌによっお゚ラヌが䜜成されたす。kずconstの䞡方が既にiされおいない限り、jはconst宣蚀できたせん。 倉数を宣蚀および初期化するさたざたな方法を確実に凊理するには、さらに分析を行う必芁がありたす。

単䜓テストをビルドする

アナラむザヌずコヌド修正は、const を䜜成できる 1 ぀の宣蚀の単玔なケヌスで動䜜したす。 この実装が間違いを犯す可胜性のある宣蚀ステヌトメントが倚数存圚したす。 これらのケヌスに察凊するために、テンプレヌトによっお蚘述された単䜓テスト ラむブラリを䜿甚したす。 Visual Studio の 2 番目のコピヌを繰り返し開くよりもはるかに高速です。

単䜓テスト プロゞェクトで MakeConstUnitTests.cs ファむルを開きたす。 テンプレヌトは、アナラむザヌずコヌド修正単䜓テストの 2 ぀の䞀般的なパタヌンに埓う 2 ぀のテストを䜜成したした。 TestMethod1 は、テストのパタヌンを瀺しおいたす。そうすべきではないずきにアナラむザヌが蚺断を報告しないようにしたす。 TestMethod2 は、蚺断を報告し、コヌド修正を実行するためのパタヌンを瀺しおいたす。

このテンプレヌトでは、単䜓テスト に Microsoft.CodeAnalysis.Testing パッケヌゞを䜿甚したす。

ヒント

テスト ラむブラリでは、次のような特殊なマヌクアップ構文がサポヌトされおいたす。

  • [|text|]: textの蚺断が報告されるこずを瀺したす。 既定では、このフォヌムは、DiagnosticDescriptor によっお提䟛される DiagnosticAnalyzer.SupportedDiagnostics が 1 ぀だけのアナラむザヌのテストにのみ䜿甚できたす。
  • {|ExpectedDiagnosticId:text|}: IdExpectedDiagnosticId を含む蚺断が textに぀いお報告されるこずを瀺したす。

MakeConstUnitTest クラスのテンプレヌト テストを次のテスト メ゜ッドに眮き換えたす。

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

このテストを実行しお、合栌するこずを確認したす。 Visual Studio で、Test>> を遞択しおテスト ゚クスプロヌラヌを開きたす。 次に、[ すべお実行] を遞択したす。

有効な宣蚀のテストを䜜成する

䞀般的なルヌルずしお、アナラむザヌはできるだけ早く終了し、最小限の䜜業を行う必芁がありたす。 Visual Studio は、ナヌザヌがコヌドを線集するず、登録枈みのアナラむザヌを呌び出したす。 応答性は重芁な芁件です。 蚺断を発生させるべきではないコヌドには、いく぀かのテスト ケヌスが存圚したす。 アナラむザヌは既にいく぀かのテストを凊理しおいたす。 これらのケヌスを衚す次のテスト メ゜ッドを远加したす。

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }
        [TestMethod]
        public async Task VariableIsAlreadyConst_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }
        [TestMethod]
        public async Task NoInitializer_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i;
        i = 0;
        Console.WriteLine(i);
    }
}
");
        }

アナラむザヌが既にこれらの条件を凊理しおいるため、これらのテストは成功したす。

  • 初期化埌に割り圓おられた倉数は、デヌタ フロヌ分析によっお怜出されたす。
  • 既に const されおいる宣蚀は、 const キヌワヌドを調べお陀倖されたす。
  • 初期化子のない宣蚀は、宣蚀倖の割り圓おを怜出するデヌタ フロヌ分析によっお凊理されたす。

次に、ただ凊理しおいない条件のテスト メ゜ッドを远加したす。

  • 初期化子をコンパむル時定数にできないため、初期化子が定数ではない宣蚀:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

C# では耇数の宣蚀を 1 ぀のステヌトメントずしお䜿甚できるため、さらに耇雑になる可胜性がありたす。 次のテスト ケヌス文字列定数に぀いお考えおみたす。

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

倉数 i は定数にできたすが、倉数 j できたせん。 したがっお、このステヌトメントを const 宣蚀にするこずはできたせん。

テストをもう䞀床実行するず、これらの最埌の 2 ぀のテスト ケヌスが倱敗するこずがわかりたす。

正しい宣蚀を無芖するようにアナラむザヌを曎新する

これらの条件に䞀臎するコヌドを陀倖するには、アナラむザヌの AnalyzeNode メ゜ッドを匷化する必芁がありたす。 これらの条件はすべお関連しおいるため、同様の倉曎によっおこれらの条件がすべお修正されたす。 AnalyzeNode に次の倉曎を加えたす。

  • セマンティック分析では、1 ぀の倉数宣蚀を調べたした。 このコヌドは、同じステヌトメントで宣蚀されおいるすべおの倉数を調べる foreach ルヌプ内にある必芁がありたす。
  • 宣蚀された各倉数には初期化子が必芁です。
  • 宣蚀された各倉数の初期化子は、コンパむル時定数である必芁がありたす。

AnalyzeNodeメ゜ッドで、元のセマンティック分析を眮き換えたす。

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

次のコヌド スニペットのようにしたす。

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

最初の foreach ルヌプでは、構文分析を䜿甚しお各倉数宣蚀を調べたす。 最初のチェックでは、倉数に初期化子があるこずを保蚌したす。 2 番目のチェックでは、初期化子が定数であるこずが保蚌されたす。 2 番目のルヌプには、元のセマンティック分析がありたす。 セマンティック チェックは、パフォヌマンスに倧きな圱響を䞎えるため、個別のルヌプ内にありたす。 テストを再実行し、すべおのテストが成功するこずを確認しおください。

最埌の研磚を远加する

完了たでもう少しです。 アナラむザヌが凊理する条件は他にもいく぀かありたす。 Visual Studio は、ナヌザヌがコヌドを蚘述しおいる間にアナラむザヌを呌び出したす。 倚くの堎合、コンパむルされないコヌドに察しおアナラむザヌが呌び出されたす。 蚺断アナラむザヌの AnalyzeNode メ゜ッドでは、定数倀が倉数型に倉換できるかどうかを確認したせん。 したがっお、珟圚の実装では、 int i = "abc" などの正しくない宣蚀がロヌカル定数に倉換されたす。 この堎合のテスト メ゜ッドを远加したす。

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

たた、参照型は正しく凊理されたせん。 参照型に䜿甚できる定数倀は、文字列リテラルを蚱可する null の堎合を陀き、System.Stringのみです。 ぀たり、 const string s = "abc" は有効ですが、 const object s = "abc" は有効ではありたせん。 このコヌド スニペットは、その条件を怜蚌したす。

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

完党に行うには、別のテストを远加しお、文字列の定数宣蚀を䜜成できるこずを確認する必芁がありたす。 次のスニペットでは、蚺断を発生させるコヌドず、修正プログラムが適甚された埌のコヌドの䞡方を定矩したす。

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

最埌に、倉数が var キヌワヌドで宣蚀されおいる堎合、コヌド修正によっお間違った凊理が行われ、C# 蚀語でサポヌトされおいない const var 宣蚀が生成されたす。 このバグを修正するには、 var キヌワヌドを掚論された型の名前に眮き換える必芁がありたす。

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

幞いにも、䞊蚘のバグはすべお、孊習したのず同じ手法を䜿甚しお察凊できたす。

最初のバグを修正するには、 MakeConstAnalyzer.cs を開き、各ロヌカル宣蚀の初期化子がチェックされ、定数倀が割り圓おられおいるこずを確認する foreach ルヌプを芋぀けたす。 最初の ルヌプのforeach、context.SemanticModel.GetTypeInfo()を呌び出しお、ロヌカル宣蚀の宣蚀された型に関する詳现情報を取埗したす。

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

次に、 foreach ルヌプ内で、各初期化子が倉数型に倉換できるこずを確認したす。 初期化子が定数であるこずを確認した埌、次のチェックを远加したす。

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

次の倉曎は、最埌の倉曎に基づいおいたす。 最初の foreach ルヌプの䞭かっこの前に、定数が文字列たたは null の堎合にロヌカル宣蚀の型を怜査する次のコヌドを远加したす。

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

var キヌワヌドを正しい型名に眮き換えるには、コヌド修正プロバむダヌにもう少しコヌドを蚘述する必芁がありたす。 MakeConstCodeFixProvider.csに戻りたす。 远加するコヌドでは、次の手順を実行したす。

  • 宣蚀が var 宣蚀であるかどうかを確認し、そうであれば以䞋の手順を実行しおください。
  • 掚論された型の新しい型を䜜成したす。
  • 型宣蚀が゚むリアスではないこずを確認したす。 その堎合は、 const varを宣蚀するこずは有効です。
  • varがこのプログラムの型名ではないこずを確認したす。 (その堎合、 const var は有効です)。
  • 完党な型名を簡略化する

それは倚くのコヌドのように聞こえたす。 そうじゃありたせん。 newLocalを宣蚀しお初期化する行を次のコヌドに眮き換えたす。 これは、 newModifiersの初期化盎埌に行われたす。

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

using型を䜿甚するには、1 ぀のSimplifier ディレクティブを远加する必芁がありたす。

using Microsoft.CodeAnalysis.Simplification;

テストを実行するず、すべおが成功するはずです。 完成したアナラむザヌを実行しお、自分を耒めたしょう。 Ctrl+キヌを抌しお、Roslyn Preview 拡匵機胜が読み蟌たれた Visual Studio の 2 番目のむンスタンスでアナラむザヌ プロゞェクトを実行したす。

  • 2 番目の Visual Studio むンスタンスで、新しい C# コン゜ヌル アプリケヌション プロゞェクトを䜜成し、main メ゜ッドに int x = "abc"; を远加したす。 最初のバグ修正により、このロヌカル倉数宣蚀に察する譊告は報告されたせん (ただし、コンパむラ ゚ラヌは想定どおりに発生したす)。
  • 次に、Main メ゜ッドに object s = "abc"; を远加したす。 2 ぀目のバグ修正により、譊告は報告されたせん。
  • 最埌に、 var キヌワヌドを䜿甚する別のロヌカル倉数を远加したす。 譊告が報告され、巊䞋に提案が衚瀺されたす。
  • 波線に゚ディタヌのカレットを移動し、Ctrl+. キヌを抌したす。 掚奚されるコヌド修正を衚瀺したす。 コヌド修正プログラムを遞択するず、 var キヌワヌドが正しく凊理されるこずに泚意しおください。

最埌に、次のコヌドを远加したす。

int i = 2;
int j = 32;
int k = i + j;

これらの倉曎の埌は、最初の 2 ぀の倉数にのみ赀い波線が衚瀺されたす。 constずiの䞡方にjを远加するず、kがconstできるようになったため、新しい譊告が衚瀺されたす。

おめでずうございたす! 最初の .NET コンパむラ プラットフォヌム拡匵機胜を䜜成したした。この拡匵機胜は、問題を怜出するためのオンザフラむ コヌド分析を実行し、修正するための簡単な修正プログラムを提䟛したす。 その過皋で、.NET Compiler Platform SDK (Roslyn API) の䞀郚であるコヌド API の倚くを孊習したした。 サンプル GitHub リポゞトリの 完成したサンプル に察しお䜜業を確認できたす。

その他のリ゜ヌス