次の方法で共有


チュートリアル: 最初のアナライザーとコード修正を記述する

.NET Compiler Platform SDK には、C# または Visual Basic コードを対象とするカスタム診断 (アナライザー)、コード修正、コードリファクタリング、診断サプレッサーを作成するために必要なツールが用意されています。 アナライザーには、ルールの違反を認識するコードが含まれています。 コード修正には、違反を修正するコードが含まれています。 実装する規則は、コード構造からコーディング スタイル、名前付け規則など、あらゆるものにすることができます。 .NET コンパイラ プラットフォームには、開発者がコードを記述する際に分析を実行するためのフレームワークと、コードを修正するための Visual Studio UI のすべての機能 (エディターでの波線の表示、Visual Studio エラー一覧の設定、"電球" の提案の作成、推奨される修正プログラムの豊富なプレビューの表示など) が用意されています。

このチュートリアルでは、Roslyn API を使用して アナライザー の作成と付随する コード修正 について説明します。 アナライザーは、ソース コード分析を実行し、問題をユーザーに報告する方法です。 必要に応じて、コード修正をアナライザーに関連付けて、ユーザーのソース コードへの変更を表すことができます。 このチュートリアルでは、 const 修飾子を使用して宣言できるが、宣言できないローカル変数宣言を検索するアナライザーを作成します。 付属のコード修正により、これらの宣言が変更され、 const 修飾子が追加されます。

[前提条件]

Visual Studio インストーラーを使用して 、.NET コンパイラ プラットフォーム 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 のチェック ボックスをオンにします。 これは、オプションのコンポーネントの下に最後に表示されます。

必要に応じて、 DGML エディター でビジュアライザーにグラフを表示することもできます。

  1. 概要ツリーで [ 個々のコンポーネント ] ノードを開きます。
  2. DGML エディターのチェック ボックスをオンにします

Visual Studio インストーラーを使用したインストール - [個々のコンポーネント] タブ

  1. Visual Studio インストーラーを実行する
  2. [変更] を選択します
  3. [ 個々のコンポーネント ] タブを選択する
  4. .NET コンパイラ プラットフォーム SDK のチェック ボックスをオンにします。 上部の [ コンパイラ、ビルド ツール、およびランタイム ] セクションにあります。

必要に応じて、 DGML エディター でビジュアライザーにグラフを表示することもできます。

  1. DGML エディターのチェック ボックスをオンにします。 [ コード ツール ] セクションに表示されます。

アナライザーを作成して検証するには、いくつかの手順があります。

  1. ソリューションを作成します。
  2. アナライザーの名前と説明を登録します。
  3. レポート解析の警告および推奨事項。
  4. 推奨事項を受け入れるコード修正を実装します。
  5. 単体テストを使用して分析を改善します。

ソリューションを作成する

  • Visual Studio で、[ ファイル] > [新規 > プロジェクト... ] を選択して、[新しいプロジェクト] ダイアログを表示します。
  • [Visual C# >機能拡張] で、[コード修正付きアナライザー (.NET Standard)]を選択します。
  • プロジェクトに "MakeConst" という名前を付け、[OK] をクリックします。

コンパイル エラーが発生する可能性があります (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 ハイブのフォルダー名は、16.0_9ae182f9Roslynなど、Roslynで終わることになります。 場合によっては、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 are not 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 メソッドを削除できます。 このアナライザーは、SymbolKind.NamedTypeステートメントではなく、SyntaxKind.LocalDeclarationStatementを調べます。 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" でエラーが発生し、無効になっていることを示すバナー メッセージが表示されます。 これは、コード修正プロバイダーがまだ変更されておらず、LocalDeclarationStatementSyntax要素ではなくTypeDeclarationSyntax要素を検索することが想定されているためです。

次のセクションでは、コード修正を記述する方法について説明します。

コード修正を記述する

アナライザーは、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を、const宣言を含む新しいDocumentに変換します。

宣言ステートメントの先頭に挿入する新しい 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 にすることができる宣言には、波線が表示されています。 しかし、まだやる作業があります。 これは、i以降の宣言にconstを追加してから、jし、最後にkした場合に正常に動作します。 ただし、const修飾子を別の順序で追加すると、k以降、アナライザーによってエラーが作成されます。ijの両方が既にconstされていない限り、kconst宣言できません。 変数を宣言および初期化するさまざまな方法を確実に処理するために、より多くの分析を行う必要があります。

単体テストをビルドする

アナライザーとコード修正は、const にすることができる単一の宣言の単純なケースで動作します。 この実装が間違いを犯す可能性のある宣言ステートメントは多数あります。 これらのケースに対処するために、テンプレートによって記述された単体テスト ライブラリを使用します。 Visual Studio の 2 番目のコピーを繰り返し開くよりもはるかに高速です。

単体テスト プロジェクトで MakeConstUnitTests.cs ファイルを開きます。 テンプレートは、アナライザーとコード修正単体テストの 2 つの一般的なパターンに従う 2 つのテストを作成しました。 TestMethod1 は、テストのパターンを示しています。そうすべきではないときにアナライザーが診断を報告しないようにします。 TestMethod2 は、診断を報告し、コード修正を実行するためのパターンを示しています。

このテンプレートでは、単体テスト に Microsoft.CodeAnalysis.Testing パッケージを使用します。

ヒント

テスト ライブラリでは、次のような特殊なマークアップ構文がサポートされています。

  • [|text|]: textの診断が報告されることを示します。 既定では、このフォームは、DiagnosticAnalyzer.SupportedDiagnostics によって提供される DiagnosticDescriptor が 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>Windows>Test Explorer を選択してテスト エクスプローラーを開きます。 次に、[ すべて実行] を選択します。

有効な宣言のテストを作成する

一般的なルールとして、アナライザーはできるだけ早く終了し、最小限の作業を行う必要があります。 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""|};
    }
}
");
        }

また、参照型は正しく処理されません。 参照型に使用できる定数値は、文字列リテラルを許可する System.String の場合を除き、nullのみです。 つまり、 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);

Simplifier型を使用するには、1 つのusing ディレクティブを追加する必要があります。

using Microsoft.CodeAnalysis.Simplification;

テストを実行すると、すべてが成功するはずです。 完成したアナライザーを実行して、自分を褒めましょう。 Ctrl+F5キーを押して、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 つの変数にのみ赤い波線が表示されます。 ijの両方にconstを追加すると、kconstできるようになったため、新しい警告が表示されます。

おめでとうございます! 問題を検出するためのオンザフライコード分析を実行し、それを修正するための迅速な修正を提供する最初の .NET コンパイラ プラットフォーム拡張機能を作成しました。 その過程で、.NET Compiler Platform SDK (Roslyn API) の一部であるコード API の多くを学習しました。 サンプルの GitHub リポジトリで 、完成したサンプル に対して作業を確認できます。

その他のリソース