C# と Visual Basic

Roslyn を使用した API 向けライブ コード アナライザーの作成

Alex Turner

この 10 年間、C# と Visual Basic のアセンブリをビルドするときに、Visual Studio コード分析によってビルド時の分析が提供されてきました。このコード分析では、Microsoft .NET Framework 2.0 向けに記述された特定の FxCop ルール セットが実行されます。これらのルールによってコードに含まれる一般的な欠陥を回避できますが、ルールの対象となるのは 2005 年までに開発者が経験してきた欠陥だけです。今日の新しい言語機能と API についてはどうすればよいでしょう。

Visual Studio 2015 Preview のプロジェクトベースのライブ コード アナライザーを利用すれば、API の作成者は、特定領域を対象とするコード分析を NuGet パッケージの一部に含めてリリースできます。これらのアナライザーは、.NET コンパイラ プラットフォーム (コードネーム "Roslyn") のサポートを受けているため、コードの入力中、その行の入力が完了する前に、コード内で警告が生成されます。つまり、コードをビルドする前にミスに気付くことができます。また、アナライザーは Visual Studio の新しい電球プロンプトを通じて自動的なコード修正を表示できるため、その場でコードを修正できるようになります。

このようなライブ コード アナライザーの多くは 50 ~ 100 行のコードから成るため、これらを作成するためにコンパイラのしくみに精通する必要はありません。今回は、.NET 正規表現 (regex) を使用する開発者がコーディング時によく陥る問題に的を絞って、アナライザーの作成方法を示します。つまり、開発者が記述した regex パターンが構文的に有効かどうかを、アプリケーションの実行前に確認する方法に取り組みます。今回はその解決策として、無効な regex パターンを指摘するための診断を含むアナライザーを作成する方法を示します。シリーズの 2 回目は、アナライザーによって検出されたエラーを解決するためのコード修正を追加する方法を示す予定です。

準備作業

まず、必要な Visual Studio 2015 Preview 製品を準備します。

  • 次の 2 つの方法のいずれかを使用して、Visual Studio 2015 Preview をセットアップします。
    1.       Visual Studio 2015 Preview を aka.ms/vs2015preview からインストールする。
    2.       ビルド済みの Azure VM イメージを aka.ms/vs2015azuregallery (英語) から入手する。
  • Visual Studio 2015 Preview SDK を aka.ms/vs2015preview からインストールします。この手順は、Azure VM イメージを使用する場合でも必要です。
  • SDK Templates VSIX パッケージを aka.ms/roslynsdktemplates (英語) からインストールして、.NET コンパイラ プラットフォームのプロジェクト テンプレートを入手します。
  • Syntax Visualizer VSIX パッケージを aka.ms/roslynsyntaxvisualizer からインストールして、分析する構文ツリーの確認に役立つ Syntax Visualizer ツール ウィンドウを入手します。
  • アナライザーのデバッグに使用する実験用の Visual Studio サンドボックスにも、Syntax Visualizer をインストールする必要があります。この方法については、後ほど Syntax Visualizer の項で説明します。

アナライザー テンプレートの確認

Visual Studio 2015、Visual Studio SDK、および必要な VSIX パッケージを準備したら、アナライザーをビルドするためのプロジェクト テンプレートを確認します。

Visual Studio 2015 で、[ファイル]、[新規作成]、[プロジェクト]、[Visual C#]、[Extensibility] の順にクリックし、[Diagnostic with Code Fix (NuGet + VSIX)] テンプレートを選択します (図 1 参照)。アナライザーは Visual Basic でも作成できますが、ここでは C# を使用します。一番上のターゲット フレームワークが .NET Framework 4.5 に設定されていることを確認します。プロジェクトに「RegexAnalyzer」という名前を付け、[OK] を選択してプロジェクトを作成します。

アナライザー プロジェクトの作成
図 1 アナライザー プロジェクトの作成

次の 3 つのプロジェクトのセットがテンプレートによって生成されます。

  • RegexAnalyzer: 診断とコード修正を含むアナライザー DLL をビルドするメイン プロジェクトです。このプロジェクトをビルドすると、アナライザーを含むプロジェクト ローカルな NuGet パッケージ (.nupkg ファイル) も作成されます。
  • RegexAnalyzer.VSIX: アナライザー DLL を Visual Studio 単位の拡張機能パッケージ (.vsix ファイル) にバンドルするプロジェクトです。アナライザーでビルドに影響を与える警告を追加する必要がない場合、NuGet パッケージではなく、この .vsix ファイルを配布することもできます。いずれの場合も、.vsix プロジェクトは、開発者が F5 キーを押して、アナライザーを Visual Studio の別の (デバッグ対象) インスタンス内でテストできるようにします。
  • RegexAnalyzer.Test: これは単体テスト プロジェクトです。アナライザーによって正しい診断と修正が生成されているかどうかを、毎回 Visual Studio のデバッグ対象インスタンスを実行することなく確認できます。

メイン プロジェクトの DiagnosticAnalyzer.cs を開くと、テンプレート内の診断を生成する既定のコードが表示されます。この既定の診断では、小文字を含むすべての型名に "波線を表示する" というあまり意味のない処理が実行されます。とは言え、ほとんどのプログラムにはこのような型名が含まれるため、あまり意味のないコードでもアナライザーが動作していることは簡単に確認できるでしょう。

RegexAnalyzer.VSIX プロジェクトをスタートアップ プロジェクトにして、F5 キーを押します。この VSIX プロジェクトを実行すると、Visual Studio の実験用サンドボックスのコピーが読み込まれます。これにより、Visual Studio は Visual Studio 拡張機能の独立したセットを管理できるようになります。これは、独自の拡張機能を開発し、Visual Studio で Visual Studio をデバッグする必要があるときに便利です。デバッグ対象の Visual Studio インスタンスは新しい実験用サンドボックスになるため、Visual Studio 2015 の初回実行時と同じダイアログが表示されます。これらのダイアログを通常どおりにクリックして先に進みます。Visual Studio 自体のデバッグ シンボルをダウンロードするときに、少し時間がかかることもあります。初回以降は、Visual Studio によってこれらのシンボルが自動的にキャッシュされます。

デバッグ対象の Visual Studio インスタンスを起動したら、そのインスタンスを使用して C# コンソール アプリケーションを作成します。デバッグしているアナライザーは Visual Studio 単位の .vsix 拡張機能になるため、数秒で緑色の波線が Program クラスの定義に表示されます。この波線をマウスでポイントするか、エラー一覧で確認すると、"Type name ‘Program’ contains lowercase letters" というメッセージが表示されます (図 2 参照)。波線をクリックすると、左側に電球アイコンが表示されます。電球アイコンをクリックすると [Make uppercase] という修正が表示され、型名を大文字に変更することによって、診断は表示されなくなります。

アナライザー テンプレートから提供されるコード修正
図 2 アナライザー テンプレートから提供されるコード修正

ここからアナライザーをデバッグすることもできます。Visual Studio のメイン インスタンスで、DiagnosticAnalyzer.cs 内の Initialize メソッドの中にブレークポイントを設定します。エディター内で文字を入力すると、常にアナライザーによって診断が再実行されます。デバッグ対象の Visual Studio インスタンスに含まれる Program.cs 内で次に文字を入力したときに、デバッガーがそのブレークポイントで停止することを確認できます。

コンソール アプリケーション プロジェクトはこの後も使用するため、開いたままにしておきます。

Syntax Visualizer を使用した関連コードの調査

アナライザー テンプレートについて理解したところで、次は、分析対象コード内でどのようなコード パターンを検出したときに、波線を表示するかを考えます。

今回の目的は、開発者が無効な regex パターンを書いたときにエラーを表示することです。まず、コンソール アプリケーションの Main メソッド内に、無効な regex パターンを指定して Regex.Match を呼び出す次の行を追加します。

Regex.Match("my text", @"\pXXX");

このコードを見て、Regex と Match をマウスでポイントするとしたら、波線を生成するタイミングとしては次の条件が考えられます。

  • Regex.Match メソッドへの呼び出しが存在する。
  • 関連する Regex 型が System.Text.RegularExpressions 名前空間に属している。
  • メソッドの 2 つ目のパラメーターが、無効な正規表現パターンを表す文字列リテラルである (実際には、表現は変数や定数の参照、つまり計算される文字列になることもありますが、今回のアナライザー初期バージョンでは、文字列リテラルを使用します。多くの場合、単純なケースでアナライザーを徹底的にテストしてから、サポートするコード パターンを増やしていくのが得策です)。

では、これらの単純な制約を .NET コンパイラ プラットフォームのコードに変換することを考えましょう。これに適したツールが Syntax Visualizer です。

このビジュアライザーを、アナライザーのデバッグに使用している実験用サンドボックスにインストールします。既にこのビジュアライザーをインストールしているかもしれませんが、インストーラーは、パッケージをメインの Visual Studio にしかインストールしません。

デバッグ対象の Visual Studio インスタンスを実行している状態で、[ツール]、[拡張機能と更新プログラム]、[オンライン] の順に開き、Visual Studio ギャラリーで "syntax visualizer" を検索します。[.NET Compiler Platform Syntax Visualizer] パッケージをダウンロードしてインストールします。インストール後、[今すぐ再起動] を選択して Visual Studio を再起動します。

Visual Studio が再起動されたら、同じコンソール アプリケーション プロジェクトを開き、[表示]、[その他のウィンドウ]、[Roslyn Syntax Visualizer] の順に選択して、Syntax Visualizer を開きます。これで、エディター内でキャレットを移動させると、構文ツリーの関連部分が Syntax Visualizer に表示されるようになります。図 3 は、ここで注目する Regex.Match の呼び出し式に対するビューを示しています。

ターゲット呼び出し式に対する Syntax Visualizer の動作
図 3 ターゲット呼び出し式に対する Syntax Visualizer の動作

構文ツリーの要素: 構文ツリー内をざっと見渡すと、さまざまな要素があることがわかります。

ツリー内の青色のノードが構文ノードで、コンパイラによってファイルが解析された後の、コードの論理ツリー構造を表します。

ツリー内の緑色のノードは構文トークン、つまりソース ファイルの読み取り時にコンパイラによって検出された個々の単語、数字、および記号です。トークンは、ツリー内でそれらが属する構文ノードの下に表示されます。

ツリー内の赤色のノードは付加情報で、空白やコメントなど、トークン以外のすべての要素を表します。この情報を除外するコンパイラもありますが、.NET コンパイラ プラットフォームではこの情報が保持されるため、コード修正によってユーザーのコードを変更するときに、必要に応じて付加情報を保持します。

エディター内のコードを選択するとツリー内の関連ノードが選択され、ツリー内のノードを選択するとエディター内の関連コードが選択されます。目的のノードを視覚化するには、Syntax Visualizer ツリー内の [InvocationExpression] を右クリックし、[View Directed Syntax Graph] を選択します。これにより、選択したノードの下位ツリー構造を視覚化する .dgml ダイアグラムが生成されます (図 4 参照)。

対象とする InvocationExpression の構文グラフ
図 4 対象とする InvocationExpression の構文グラフ

今回の場合、参照している InvocationExpression は Regex.Match の呼び出しなので、ArgumentList の 2 つ目の Argument ノードに StringLiteralExpression が含まれています。文字列値が "\pXXX" などの無効な regex パターンを表す場合、ここに波線を表示することになります。これで、診断アナライザーの作成に必要な情報がほとんど揃いました。

シンボルとセマンティック モデル: 構文ツリーの一歩先へ: Syntax Visualizer に表示される構文ノード、トークン、および付加情報はファイルのすべてのテキストを表しますが、必要な情報はこれだけではありません。コード内の各識別子が実際に何を参照しているかも把握する必要があります。たとえば、この呼び出しが Regex 型の Match メソッドで、2 つのパラメーターが指定されていることはわかりますが、この Regex 型が属する名前空間や、Match のどのオーバーロードが呼び出されているかはわかりません。どの定義が参照されているかを正確に把握するには、近くにある using ディレクティブのコンテキストでコンパイラが識別子を分析する必要があります。

このような疑問を解決するには、所定の式ノードに関連付けられたシンボルを指定して、セマンティック モデルに問い合わせます。シンボルは、型やメソッドなど、コードが定義する論理エンティティを表します。特定の式が参照するシンボルを見つけ出すプロセスを、バインディングと呼びます。シンボルは、基本クラス ライブラリ (BCL) の Regex 型など、参照先のライブラリから使用するエンティティを表すこともできます。

[InvocationExpression] を右クリックし、[View Symbol] を選択すると、下のプロパティ グリッドに、呼び出されるメソッドのメソッド シンボルに関する情報が表示されます (図 5 参照)。

Syntax Visualizer に表示された Regex.Match メソッドのシンボル
図 5 Syntax Visualizer に表示された Regex.Match メソッドのシンボル

今回の場合、OriginalDefinition プロパティを調べると、この呼び出しで参照されているのが System.Text.RegularExpressions.Regex.Match メソッドであり、他の Regex 型のメソッドではないことがわかります。診断の作成における最後の作業は、この呼び出しをバインドし、返されるシンボルが文字列 System.Text.RegularExpressions.Regex.Match に一致するかどうかを確認することです。

診断のビルド

方針が決まったところで、デバッグ対象の Visual Studio インスタンスを閉じ、アナライザー プロジェクトに戻って診断のビルドに着手します。

SupportedDiagnostics プロパティ: DiagnosticAnalyzer.cs ファイルを開き、一番上あたりに表示されている 4 つの文字列定数を確認します。この部分で、診断ルールのメタデータを定義します。アナライザーによって波線が生成される前でも、ルール セット エディターやその他の Visual Studio の機能では、アナライザーによって生成される可能性がある診断の詳細を把握するために、このメタデータが使用されます。

生成する Regex の診断に対応するよう、これらの文字列を次のように更新します。

public const string DiagnosticId = "Regex";
internal const string Title = "Regex error parsing string argument";
internal const string MessageFormat = "Regex error {0}";
internal const string Category = "Syntax";

この診断が生成されると、診断 ID と代入されたメッセージ文字列がユーザーのエラー一覧に表示されます。ユーザーは診断 ID をソース コード内の #pragma ディレクティブで使用して、この診断のインスタンスが生成されないようにすることもできます。次回は、診断 ID を使用してコード修正をこのルールに関連付ける方法を示します。

Rule フィールドを宣言する行では、生成する診断の重大度が警告ではなくエラーになるように、重大度を更新することもできます。regex 文字列が解析されない場合、Match メソッドの実行時に必ず例外がスローされるため、C# のコンパイラ エラーと同様に、ビルドをブロックします。次のように、ルールの重大度を DiagnosticSeverity.Error に変更します。

internal static DiagnosticDescriptor Rule =
  new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
   Category, DiagnosticSeverity.Error, isEnabledByDefault: true);

この行は、ルールを既定で有効にするかどうかを判断する部分でもあります。アナライザーでは、既定では無効になっている大きなルール セットを定義することができます。そうすることで、ユーザーはこうしたルールの一部またはすべてを選択して適用することができます。今回はルールを既定で有効にします。

SupportedDiagnostics プロパティは、この DiagnosticDescriptor を不変配列の 1 つの要素として返します。今回の場合、アナライザーによって生成される診断の種類は 1 つのみなので、これに変更を加える必要はありません。アナライザーが複数の種類の診断を生成する場合は、複数の記述子を作成し、それらをすべて SupportedDiagnostics から返すことができます。

Initialize メソッド: 診断アナライザーのメイン エントリ ポイントは Initialize メソッドです。このメソッドでは、さまざまな構文ノードを検出したときや、新しいシンボルの宣言を検出したときなど、コードの検証時にコンパイラによって起動されるさまざまなイベントを処理するための一連のアクションを登録します。テンプレートによって提供されるあまり意味のない既定のアナライザーは、型のシンボルが変更または挿入されるタイミングを特定するために RegisterSymbolAction を呼び出します。この場合、シンボルのアクションによって、アナライザーは各型のシンボルを調べ、そのシンボルが波線を必要とする不正な名前の型を示しているかどうかを検証できるようになります。

今回の場合、SyntaxNode アクションを登録して、Regex.Match の新しい呼び出しが実行されるタイミングを特定する必要があります。Syntax Visualizer でも確認したように、ここで探す特定のノードの種類は InvocationExpression なので、Initialize メソッドの Register 呼び出しを次の呼び出しに置き換えます。

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);

この regex アナライザーが登録する必要があるのは、ローカルで診断を生成する構文ノード アクションのみです。では、複数のメソッドにまたがってデータを収集する、より複雑な分析についてはどうでしょう。これについては、後ほど「その他の登録メソッドの処理」で詳しく説明します。

AnalyzeNode メソッド: テンプレートの AnalyzeSymbol メソッドは今回不要になるため削除します。代わりに AnalyzeNode メソッドを作成します。RegisterSyntaxNodeAction 呼び出し内の AnalyzeNode をクリックし、Ctrl キーを押しながらピリオド (.) キーを押して、新しい電球メニューを表示します。ここから、メソッドを生成する項目を選択して、正しいシグネチャを持つ AnalyzeNode メソッドを作成します。生成された AnalyzeNode メソッドで、パラメーターの名前を "obj" から "context" に変更します。

これでアナライザーの中核部分に達したため、次は問題の構文ノードを調べて、診断を表示すべきかどうかを確認します。

まず、Ctrl キーを押しながら F5 キーを押して、再度 Visual Studio のデバッグ対象インスタンスを起動します (今回はデバッグを実行しません)。先ほどまで使用していたコンソール アプリケーションを開き、Syntax Visualizer が使用可能であることを確認します。このビジュアライザーは、AnalyzeNode メソッドのビルド時に、関連する詳細を確認するために数回使用します。

ターゲット ノードの取得: AnalyzeNode メソッドでの最初の手順は、分析するノード オブジェクトを取得し、それを関連する型にキャストすることです。この型を確認するために、先ほど開いた Syntax Visualizer を使用します。Regex.Match 呼び出しを選択し、構文ツリー内で InvocationExpression ノードに移動します。プロパティ グリッドのすぐ上に表示されているように、InvocationExpression はノードの種類で、その型は InvocationExpressionSyntax です。

ノードの型は型チェックでテストでき、その特定の種類は IsKind メソッドでテストできますが、今回は Initialize メソッド内で特定の種類を問い合わせているため、キャストが成功することは保証されており、どちらのテストも実行する必要はありません。アクションで分析するノードは、次のように context パラメーターの Node プロパティから取得できます。

var invocationExpr = (InvocationExpressionSyntax)context.Node;

これで呼び出しノードを取得できたため、次はそのノードが波線を必要とする Regex.Match 呼び出しかどうかをチェックします。

**チェック 1: 「Match メソッドへの呼び出しかどうか」**必要な最初のチェックは、正しい Regex.Match の呼び出しかどうかを確認することです。このアナライザーはエディター内でキーストロークが発生するたびに実行されるため、最も簡単なテストを最初に実行し、それらの初期テストに合格した場合のみ、負荷の高い API のテストを実行するようにします。

最もコストのかからないテストは、呼び出し構文が Match という名前のメソッドの呼び出しかどうかを確認することです。これを確認するタイミングは、どの特定の Match メソッドが呼び出されるかを突き止めるための処理をコンパイラがまだ実行していないときです。

Syntax Visualizer に戻って確認すると、InvocationExpression には SimpleMemberAccessExpression と ArgumentList という 2 つのメインの子ノードが含まれていることがわかります。エディター内で Match 識別子を選択すると (図 6 参照)、該当するノードが SimpleMemberAccessExpression 内の 2 つ目の IdentifierName であることがわかります。

構文ツリー内での Match 識別子の確認
図 6 構文ツリー内での Match 識別子の確認

アナライザーのビルド時には、コード内で参照する必要がある関連型およびプロパティ値を見つけ出すために、このように構文やシンボルを調べることがよくあります。アナライザーのビルド時には、ターゲット プロジェクトを Syntax Visualizer ですぐ確認できるようにしておくと便利です。

アナライザーのコードに戻り、invocationExpr のメンバーを IntelliSense で表示すると、InvocationExpression の各子ノードに一致する、Expression と ArgumentList という名前のプロパティが含まれていることがわかります。今回使用するのは、Expression という名前のプロパティです。呼び出しの引数リストの前の部分にはさまざまな形式 (ローカル変数のデリゲート呼び出しなど) を指定できるため、このプロパティでは、汎用基本型である ExpressionSyntax を返します。Syntax Visualizer から、予期する具象型は MemberAccessExpressionSyntax であることがわかるため、次のように Expression をその型にキャストします。

var memberAccessExpr =
  invocationExpr.Expression as MemberAccessExpressionSyntax;

memberAccessExpr のプロパティを調べると、同様の内容が表示されます。ピリオドの前には任意の式を表す Expression プロパティ、ピリオドの後には識別子を表す Name プロパティがあります。ここでは Match メソッドを呼び出しているかどうかをチェックする必要があるため、Name プロパティの文字列値をチェックします。構文ノードの場合、文字列値を取得することが、そのノードのソース テキストを取得する最も簡単な方法です。C# の新しい "?." 演算子を使用すると、分析している式が実際にはメンバー アクセスではなかった場合、前の行の "as" 句で null 値を返すことによって、その状況に対処できます。

if (memberAccessExpr?.Name.
    ToString() != "Match") return;

呼び出されるメソッドの名前が Match ではない場合、単純に処理を終了します。分析は最小コストで完了し、診断は生成されません。

**チェック 2: 「実際に Regex.Match メソッドの呼び出しかどうか」**メソッドの名前が Match の場合、少し複雑なチェックを実行し、どの Match メソッドがコード内で呼び出されるかを正確に特定するようコンパイラに依頼します。正確な Match を特定するには、この式をバインドして参照先のシンボルを取得するよう、コンテキストのセマンティック モデルに問い合わせる必要があります。

次のように、セマンティック モデルの GetSymbolInfo メソッドを呼び出し、シンボルを取得する式をこのメソッドに渡します。

var memberSymbol =
  context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;

返されるシンボル オブジェクトは、Syntax Visualizer で [SimpleMemberAccessExpression] を右クリックし、[View Symbol] を選択したときにプレビューできるものと同じです。今回は、そのシンボルを一般的な IMethodSymbol インターフェイスにキャストしています。このインターフェイスは、Syntax Visualizer 内のこのシンボルの説明に含まれる、内部型 PEMethodSymbol によって実装されます。

これでシンボルを取得できたため、次はこのシンボルを、実際の Regex.Match メソッドから返されることを想定している完全修飾名と比較します。シンボルの場合、文字列値を取得すると、その完全修飾名を取得できます。どのオーバーロードを呼び出しているかはまだ気にする必要がないため、次のように、Match という単語までチェックすれば問題ありません。

if (!memberSymbol?.ToString().
  StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return;

前のテストと同様、シンボルが想定する名前と一致するかどうかをチェックし、一致していない場合、またはシンボルが実際にはメソッド シンボルではなかった場合、処理を終了します。ここで、文字列に対して演算を実行するのは少し奇妙に思えるかもしれませんが、文字列の比較はコンパイラ内でよく実行される演算です。

残りのチェック: テストもリズムに乗ってきました。ステップごとに、ツリーを少しずつ下の階層に辿り、構文ノードまたはセマンティック モデルをチェックすることによって、まだエラーが発生するかどうかをテストします。毎回 Syntax Visualizer を使用して、想定する型とプロパティ値を確認できるため、どの状況で前に戻り、どの状況で先に進むかを把握できます。このパターンに従って、次のいくつかの条件をチェックします。

ArgumentList に少なくとも 2 つの引数が含まれていることを、次のように確認します。

var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
if ((argumentList?.Arguments.Count ?? 0) < 2) return;

次に、2 つ目の引数が LiteralExpression であることを確認します。これは文字列リテラルを想定しているためです。

var regexLiteral =
  argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
if (regexLiteral == null) return;

最後に、2 つ目の引数がリテラルであることがわかったら、次のように、その引数のコンパイル時の定数値を返し、その値が明確に文字列リテラルであることを確認するようセマンティック モデルに問い合わせることができます。

 

var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
if (!regexOpt.HasValue) return;
var regex = regexOpt.Value as string;
if (regex == null) return;

regex パターンの検証: これで必要なデータがすべて揃いました。呼び出すメソッドが Regex.Match であることがわかり、パターン表現の文字列値も取得できました。では、このパターンをどのように検証すればよいでしょう。

単純に同じ Regex.Match メソッドを呼び出し、このパターン文字列を渡します。検出するのはパターン文字列内の解析エラーのみなので、空の入力文字列を 1 つ目の引数として渡すことができます。また、try-catch ブロック内で呼び出しを行い、無効なパターン文字列が検出されたときに Regex.Match によってスローされる ArgumentException をキャッチできるようにします。

try
{
  System.Text.RegularExpressions.Regex.Match("", regex);
}
catch (ArgumentException e)
{
}

パターン文字列の解析でエラーが検出されなかった場合、AnalyzeNode メソッドは通常どおりに終了し、何も報告されません。解析エラーが検出された場合、引数の例外をキャッチします。これで診断を報告する準備が整います。

診断の報告: catch ブロック内で、先ほど Diagnostic オブジェクトを作成するために記述した Rule オブジェクトを使用します。このオブジェクトは、生成する 1 つの特定の波線を表します。各診断は、そのインスタンスに固有の 2 つのメイン要素を必要とします。これらは、波線を表示する必要があるコードの範囲と、先ほど定義したメッセージ形式内に挿入する文字列です。

var diagnostic =
    Diagnostic.Create(Rule,
    regexLiteral.GetLocation(), e.Message);

今回の場合、文字列リテラルに波線を表示する必要があるため、その場所を診断の範囲として渡します。また、パターン文字列の問題について説明する例外メッセージを取得し、それを診断メッセージに含めます。

最後のステップは、この診断を報告としてコンテキストに返し、そのコンテキストを AnalyzeNode に渡すことです。これにより、エラー一覧に行を追加し、エディター内に波線を追加する必要があることが Visual Studio によって認識されます。

context.ReportDiagnostic(diagnostic);

この時点で、DiagnosticAnalyzer.cs のコードは図 7 のようになります。

図 7 DiagnosticAnalyzer.cs の完全なコード

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace RegexAnalyzer
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class RegexAnalyzerAnalyzer : DiagnosticAnalyzer
  {
    public const string DiagnosticId = "Regex";
    internal const string Title = "Regex error parsing string argument";
    internal const string MessageFormat = "Regex error {0}";
    internal const string Category = "Syntax";
    internal static DiagnosticDescriptor Rule =
      new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
      Category, DiagnosticSeverity.Error, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor>
      SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
    public override void Initialize(AnalysisContext context)
    {
      context.RegisterSyntaxNodeAction(
        AnalyzeNode, SyntaxKind.InvocationExpression);
    }
    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
    {
      var invocationExpr = (InvocationExpressionSyntax)context.Node;
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      if (memberAccessExpr?.Name.ToString() != "Match") return;
      var memberSymbol = context.SemanticModel.
        GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      if (!memberSymbol?.ToString().StartsWith(
        "System.Text.RegularExpressions.Regex.Match") ?? true) return;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      if ((argumentList?.Arguments.Count ?? 0) < 2) return;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      if (regexLiteral == null) return;
      var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
      if (!regexOpt.HasValue) return;
      var regex = regexOpt.Value as string;
      if (regex == null) return;
      try
      {
        System.Text.RegularExpressions.Regex.Match("", regex);
      }
      catch (ArgumentException e)
      {
        var diagnostic =
          Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message);
        context.ReportDiagnostic(diagnostic);
      }
    }
  }
}

診断のテスト: これで診断の完成です。この診断をテストするには、単純に F5 キーを押し (RegexAnalyzer.VSIX がスタートアップ プロジェクトになっていることを確認)、再度コンソール アプリケーションを Visual Studio のデバッグ対象インスタンス内で開きます。まもなく、パターン表現の上に赤色の波線が表示され、さらに解析に失敗した理由が表示されます (図 8 参照)。

診断アナライザーのテスト
図 8 診断アナライザーのテスト

波線が表示されたら成功です。波線が表示されなかった場合、AnalyzeNode メソッド内にブレークポイントを設定し、パターン文字列内に文字を入力して再分析をトリガーした後、アナライザーのコードをステップ実行することによって、アナライザーの処理がどの部分で早期終了したかを確認します。また、作成したコードを、DiagnosticAnalyzer.cs の完全なコードを示す図 7 に照らして確認してもかまいません。

診断アナライザーのユース ケース

要点をまとめると、今回は、アナライザー テンプレートに基づいて約 30 行の新しいコードを記述することによって、ユーザーのコードに含まれる実際の問題を特定し、その問題に対して波線を表示できるようにしました。最も重要なのは、この作業が C# コンパイラの演算に関して細かい専門知識を必要としないことです。正規表現という対象領域に注目したまま、Syntax Visualizer を使用して、分析に関連する小規模な一連の構文ノードとシンボルを確認できます。

毎日のコーディングにおいて、診断アナライザーを手軽に作成して役立てることができる領域はたくさんあります。

  • チームの開発者または開発リーダーであれば、コード レビューを行うときに、他の開発者が同じミスを犯すのを目にすることがあります。このような場合、パターンに従っていないコードに波線を表示する単純なアナライザーを作成し、そのアナライザーをソース管理内にチェックインすることによって、すべての開発者が入力中に、そのようなバグの発生に気付くことができるようにします。
  • 組織のビジネス オブジェクトを定義する共有レイヤーの保守担当者であれば、型システム内にエンコードするのが難しいビジネス オブジェクト (特に、オブジェクトが数値を含む場合や、オブジェクトのプロセスに含まれるステップで特定の演算を常に他の演算よりも先に実行する必要がある場合) を正しく使用するためのビジネス ルールが存在する場合があります。このような場合、型システムで対応できない部分に対して、レイヤーの使用を管理する柔軟性の高いルールを適用できます。
  • オープン ソースまたは商用 API パッケージの所有者であれば、フォーラムで同じ質問に繰り返し回答するのが面倒になるかもしれません。ホワイト ペーパーやドキュメントを執筆したにもかかわらず、ほとんどだれも読まないために、同じ問題が絶えず発生することもあります。このような場合、API とそれに関連するコード分析を 1 つの NuGet パッケージ内にまとめると、その API を使用するすべての開発者が、最初から同じガイダンスを参照できるようになります。

このコラムをきっかけに、皆さんがアナライザーを構築して独自のプロジェクトを拡張することを検討してくださればさいわいです。.NET コンパイラ プラットフォームによって、マイクロソフトは C# と Visual Basic に関する深い言語知識と豊富なコード分析を提供するという、大きな仕事をやり遂げました。あとは皆さん次第です。

今後の展望

これで Visual Studio において、無効な regex パターンの下にエラー波線を表示できるようになりました。他に何ができるでしょう。

正規表現の分野に知識があり、パターン文字列の問題だけでなく、その修正方法も把握しているのであれば、テンプレートの既定のアナライザーと同様に電球内に修正案を提示できます。

次回は、このコード修正の作成方法と、構文ツリーに変更を加える方法について説明します。お見逃しなく。

その他の登録メソッドの処理

Initialize メソッドの context パラメーターを調べると、呼び出し可能な登録メソッド一式が含まれていることがわかります。図 A のメソッドを使用すると、さまざまなイベントをコンパイラのパイプラインにフックできます。

注意すべき重要なポイントは、どの登録メソッドによって登録された最上位のアクションでも、アナライザー型のインスタンス フィールドになんらかの状態を格納すべきでないことです。Visual Studio では、アナライザー型の 1 つのインスタンスを Visual Studio セッション全体で再利用することによって、割り当ての繰り返しを回避します。どの状態を格納して再利用しても、以降のコンパイルの分析時に、その状態は古くなる可能性が高くなります。また、古い構文ノードやシンボルがガベージ コレクションの対象から除外された場合、メモリ リークが発生することもあります。

アクション間で状態を保持する必要がある場合、RegisterCodeBlockStartAction または RegisterCompilationStartAction を呼び出し、状態をそのアクション メソッド内でローカルに格納します。このようなアクションに渡されるコンテキスト オブジェクトを使用すると、入れ子になったアクションをラムダ式として登録し、それらの入れ子になったアクションを、外側のアクション内のローカルで同じ状態が保持されるように閉じることができます。

図 A さまざまなイベントをフックするための登録メソッド

RegisterSyntaxNodeAction 特定の種類の構文ノードが解析されているときにトリガーされます。
RegisterSymbolAction 特定の種類のシンボルが分析されているときにトリガーされます。
RegisterSyntaxTreeAction ファイルの構文ツリー全体が解析されているときにトリガーされます。
RegisterSemanticModelAction セマンティック モデルがファイル全体で使用可能なときにトリガーされます。

RegisterCodeBlockStartAction

RegisterCodeBlockEndAction

メソッド本体または他のコード ブロックの分析前後にトリガーされます。

RegisterCompilationStartAction

RegisterCompilationEndAction

プロジェクト全体の分析前後にトリガーされます。

Alex Turner * は、マイクロソフトのマネージ言語チームでシニア プログラム マネージャーを務め、.NET コンパイラ プラットフォーム ("Roslyn") プロジェクトで C# と Visual Basic の強みを考えています。ストーニー ブルック大学を卒業し、コンピューター サイエンスの修士号を取得しています。また、Build、PDC、TechEd、TechDays、および MIX カンファレンスでの講演経験があります。*

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Bill Chiles および Lucian Wischik に心より感謝いたします。
Bill Chiles は、言語 (CMU Common Lisp、Dylan、IronPython、および C#) と開発者ツールにそのキャリアの大半を費やしてきました。マイクロソフトの開発部門で過ごした最後の 17 年間は、Visual Studio の中核機能から動的言語ランタイムや C# まであらゆるものに携わりました。

Lucian Wischik は、マイクロソフトの Visual Basic/C# 言語設計チームに所属し、特に Visual Basic を担当しています。マイクロソフトに入社する以前は、大学で同時実行の理論と非同期について学んでいました。熱心なセーリング愛好家で、長距離の水泳選手でもあります。