働くプログラマ

Roslyn 登場、第 2 部: 診断の作成

Ted Neward
Joe Hummel

Ted Neward最近、読者の皆さんは、マイクロソフトが次世代デベロッパーツールに対して進めている戦略についての話題を耳にすることが多くなったのではないでしょうか。オープン ソース化を進めるらしい、クロスプラットフォーム対応が増えるらしい、よりオープンになり透明性が高くなるらしいといった話題です。その話題の中心になっているのが、"Roslyn" (.NET のコンパイラ プラットフォーム プロジェクトのコード ネーム) です。Roslyn はマイクロソフトがまさに全力を傾けて取り組んだ製品レベルの品質を備えたコンパイラ ツール インフラストラクチャで、初めてオープン開発モデルとなるものです。マイクロソフトの発表によれば、Microsoft .NET Framework チーム自体が現在 Roslyn をコンパイラとして .NET のビルドに使用しているということなので、Roslyn はある程度その歩みを開始したと考えられます。プラットフォームとその言語ツールは、そのプラットフォームとその言語ツール自体によってビルドされるようになっています。読者の皆さんもこのコラムを参考に、この言語ツールを使って、プラットフォームのビルドに役立つ言語ツールをビルドしてみてはいかがでしょう。

複雑に思えるかもしれませんが、実はそれほど難しくはありません。

「それぞれやり方が違う」

あるプログラマが別のプログラマと共同作業を始め、少なくともどちらかが「相手のやり方が違う」ことに気付いたとします。こうなると、チームの一体感がなくなり、コーディング方法、エラー チェックの実行度合い、オブジェクトの使用方法などに一貫性がなくなります。歴史的に見れば、これは「コーディング標準」、つまり、原則的に社内のプログラマ全員がコーディングする際に従うべき一連の規則の範疇です。時折、プログラマはコーディング標準に目を通すこともありますが、理路整然とし、一貫性を強制する類のコーディング標準でなければ、昔ながらの「コード レビュー」でかっこの位置や変数名についての口論が必ず始まり、結果的にコーディング標準が全体的なコード品質に影響することはほとんどありません。

時代が進み、言語ツールが成熟してくると、開発者は強制力のある制約を与えてくれる役割をツールに求めるようになります。結局のところ、コンピューターが得意な作業があるとすれば、見落とさず、ためらわず、失敗せずに詳しい解析を何度も繰り返すことです。そもそも、こうした作業はコンパイラの仕事の一部です。コンパイラは、コードにエラーを起こしかねない、ありがちな人的ミスを発見すると直ちにエラーとし、ユーザーに発覚する前にそのミスを修正するようプログラマに求めます。コードを分析し、エラーのパターンを探すツールを「静的解析ツール」と呼び、単体テストを実行するはるか前にバグを特定するのに役立ちます。

.NET Framework の歴史を振り返ると、このようなツールをビルドしてメンテナンスするのは難しいとされていました。静的解析ツールには大掛かりな開発作業が必要で、言語やライブラリが進化するたびに更新しなければなりません。C# と Visual Basic .NET の両方を使っていれば作業は倍になります。FxCop などのバイナリ解析ツールは、こうした言語の複雑性を避け、中間言語 (IL) レベルで機能します。ただし、ソースから IL に変換すると、少なく見積もっても構造的な情報が失われます。このため、プログラマが作業しているソースのレベルに問題点を再度関連付けるのはかなり難しくなります。バイナリ解析ツールはコンパイル後に実行され、プログラミング過程での IntelliSense のようなフィードバックは得られません。

一方、Roslyn は当初から拡張を前提にビルドされています。Roslyn は「アナライザー」という、ソースコードを解析する拡張機能を使用します。アナライザーは、開発者がプログラミング中にバックグラウンドで実行させることができます。アナライザーを作成すれば、Roslyn に高度な「規則」を追加でき、別のツールを実行しなくても、バグを修正できるようになります。

間違いの元は何か

認めるのは悲しいことですが、次のようなコードをよく目にします。

try
{
  int x = 5; int y = 0;
  // Lots of code here
  int z = x / y;
}
catch (Exception ex)
{
  // TODO: come back and figure out what to do here
}

この TODO はたいてい善意で書かれています。しかし、「地獄への道は善意で敷きつめられている」という古いことわざがあります。本来、コーディング標準ではこのような記述を不適切としていますが、それでも違反を犯さなければ他人に意図を理解してもらえません。テキストファイルをスキャンすれば確実に "TODO" が見つかります。しかし、嫌なエラーがなくなるとはいえ、コードのあちこちに TODO が散見されるようになります。もちろん、こうしたコードを目にすることになるのは、大きなデモがいつのまにかクラッシュした後だけです。おそらく、苦労しながらゆっくりと、そのコードを見つけるまで惨状をさかのぼることになります。状況を飲み込み、近くに迫る破滅も知らないままにプログラムを進めるよりも、むしろ例外を発生させて派手にエラーとなる方が良かったのかもしれません。

コーディング標準にも似たところがあります。常に例外をスローするよう規定されています。あるいは、常に例外を標準診断ストリームにログ記録するよう規定されています。両方規定される場合もあります。しかし、やはり強制力がなければ、標準は誰にも読まれない紙きれになってしまいます。

Roslyn では例外をキャッチする診断を作成することができます。さらに Visual Studio Team Foundation Server と連携させ (構成が必要)、空の catch ブロックが修正されるまでコードをチェックインできないようにすることも可能です。

Roslyn の診断

本稿執筆時点では、Roslyn プロジェクトはプレビュー リリース版で、Visual Studio 2015 Preview の一環としてインストールされます。Visual Studio 2015 Preview SDK と Roslyn SDK テンプレートをインストールしたら、提供される Extensibility テンプレートの "Diagnostic with Code Fix (NuGet + VSIX)" を使用して診断を作成できます。まずは診断テンプレートを選択し、プロジェクトに「EmptyCatchDiagnostic」という名前を付けます (図 1 参照)。

Diagnostic with Code Fix (NuGet + VSIX) プロジェクト テンプレート
図 1 Diagnostic with Code Fix (NuGet + VSIX) プロジェクト テンプレート

次に、構文ノード アナライザーを作成します。このアナライザーは、抽象構文ツリー (AST) をスキャンし、空の catch ブロックを探します。小さな AST フラグメントを図 2 に示します。さいわい、Roslyn コンパイラが自動的に AST をスキャンします。必要なのは、対象とするノードを解析するコードを用意するだけです (従来の Gof (Gang-of-Four: 四人組) のデザイン パターンをご存知であれば、これは Visitor パターンに相当します)。アナライザーは、DiagnosticAnalyzer 抽象基本クラスから継承し、以下の 2 つのメソッドを実装する必要があります。

public abstract
  ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
public abstract void Initialize(AnalysisContext context);

コード フラグメント if (score > 100) grade = "A++"; に対する Roslynの抽象構文ツリー
図 2 コード フラグメント if (score > 100) grade = "A++"; に対する Roslynの抽象構文ツリー

SupportedDiagnostics メソッドはシンプルで、Roslyn に提示している各アナライザーの説明を返します。Initialize メソッドは、アナライザー コードを Roslyn に登録します。このメソッドでの初期化中に、解析対象のノードと、コンパイル中にそのノードを見つけたときに実行するコードを Roslyn に提示します。Visual Studio はバックグラウンドでコンパイルを実行するため、この呼び出しはユーザーがコードを編集中に行われ、起こりうるエラーに関して迅速なフィードバックを提供します。

まず、事前に生成済みのテンプレート コードを変更し、空の catch ブロックの診断に必要なコードを記述します。このコードは EmptyCatchDiagnostic プロジェクトの DiagnosticAnalyzer.cs ソース コード ファイルにあります (ソリューションには他にもプロジェクトが含まれていますが、無視してもかまいません)。それに続くコードの太字で示されている部分は、事前生成済みコードに関する変更です。まず、今回の診断を表す文字列をいくつか用意します。

internal const string Title = "Catch Block is Empty";
internal const string MessageFormat =  
  "'{0}' is empty, app could be unknowingly missing exceptions";
internal const string Category = "Safety";

生成された SupportedDiagnostics メソッドは正しいので、Initialize メソッドを変更して、独自に作成した構文解析ルーチン AnalyzeSyntax を次のように登録するだけです。

public override void Initialize(AnalysisContext context)
{
  context.RegisterSyntaxNodeAction<SyntaxKind>(
    AnalyzeSyntax, SyntaxKind.CatchClause);
}

登録の一環として、対象とするのは AST 内の catch 句のみであることを Roslyn に通知しています。これによって余計なノードの数が減り、アナライザーも 1 つの目的を持ち、シンプルで無駄のない状態を保てます。

コンパイル中に catch 句ノードが AST 内に見つかると、解析メソッド AnalyzeSyntax が呼び出されます。このメソッドでは catch ブロック内のステートメントの数を確認します。ステートメント数が 0 であれば、ブロックが空なので診断の警告を表示します。アナライザーは、空の catch ブロックを見つけると、新しく診断の警告を作成し、catch キーワードの場所に移動して、その場所をレポートします (図 3 参照)。

図 3 Catch 句の検出

// Called when Roslyn encounters a catch clause.
private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
  // Type cast to what we know.
  var catchBlock = context.Node as CatchClauseSyntax;
  // If catch is present we must have a block, so check if block empty?
  if (catchBlock?.Block.Statements.Count == 0)
  {
    // Block is empty, create and report diagnostic warning.
    var diagnostic = Diagnostic.Create(Rule,
      catchBlock.CatchKeyword.GetLocation(), "Catch block");
    context.ReportDiagnostic(diagnostic);
  }
}

3 つ目の手順として、この診断を作成して実行します。この手順で行うことは、分かってしまえば当然のことですが、非常に興味深いものです。コンパイラが起動する診断を作成しましたが、どのようにテストすればよいでしょう。なんと、Visual Studio を起動すれば、作成した診断がインストールされ、空の catch ブロックのあるプロジェクトを開き、動作の確認まで行われます。その状態を示しているのが図 4 です。既定のプロジェクトの種類は VSIX インストーラーなので、プロジェクトを "実行" すると、Visual Studio はそれ自体の別のインスタンスを立ち上げ、そのインスタンスに対してインストーラーを実行します。2 つ目のインスタンスが起動されると、テストできるようになります。残念ながら、診断の自動テストは今回のプロジェクトの範囲をやや超えています。しかし、診断がシンプルで目的が 1 つであれば、手動テストもそれほど難しくはありません。

別のインスタンスで空の Catch ブロック診断を実行する Visual Studio
図 4 別のインスタンスで空の Catch ブロック診断を実行する Visual Studio

放置しないで修正を

残念ながら、エラーが指摘され、その修正が簡単なのに修正してくれないツールは本当に腹立たしいものです。例えるなら、ドアを開けようとしばらく奮闘しているのを眺めて「鍵がかかってますよ」と話しかけられ、それなら別の入り口を探そうとうろうろしていると「鍵ならありますよ」と言われた気分です。

Roslyn はそんな奴ではありません。

コードを修正しようとすると、アナライザーが見つけた問題の修正案がいくつか開発者に提案されます。catch ブロックが空の場合、簡単なコードの修正は throw ステートメントを追加し、キャッチされた例外がすぐに再スローされるようにすることです。図 5 に示しているのは、Visual Studio で見慣れたツールヒントとして提案されるコードの修正方法です。

空の Catch ブロック内でスローすることを提案するコード修正
図 5 空の Catch ブロック内でスローすることを提案するコード修正

コード修正で注目するのは、プロジェクトで事前生成済みの別のソース ファイル、CodeFixProvider.cs です。必要な作業は抽象基本クラス CodeFixProvider から継承し、3 つのメソッドを実装することです。重要なメソッドは、開発者へ提案を行う ComputeFixesAsync です。

public sealed override async Task ComputeFixesAsync(CodeFixContext context)

アナライザーが問題を報告するときに、Visual Studio IDE からこのメソッドが呼び出され、コード修正案があるかどうか調べます。修正案があれば、IDE が修正案を含むツールヒントを表示し、開発者が選択できるようにします。どれか 1 つが選択されると、ソース ファイルの AST を記載する特定のドキュメントが、提案された修正案を受けて更新されます。

つまり、コード修正とは AST への変更を提案していることに他なりません。AST を変更することによって、開発者がそのコードを書いたかのように、変更内容がコンパイラの残りのフェーズに渡されます。今回の例では、提案は throw ステートメントを追加することです。図 6 は、大まかな動きを説明したものです。

抽象構文ツリーの更新
図 6 抽象構文ツリーの更新

ここではメソッドが新しいサブツリーを作成し、AST にある既存の catch ブロック サブツリーに置き換えます。新しいサブツリーはボトムアップ方式で構築されます。つまり、新しい throw ステートメント、そのステートメントを含むリスト、リストのスコープを設定するブロック、そのブロックをアンカーする catch の順に作成されます。

public sealed override async Task ComputeFixesAsync(
  CodeFixContext context)
{
  // Create a new block with a list that contains a throw statement.
  var throwStmt = SyntaxFactory.ThrowStatement();
  var stmtList = new SyntaxList<StatementSyntax>().Add(throwStmt);
  var newBlock = SyntaxFactory.Block().WithStatements(stmtList);
  // Create a new, replacement catch block with our throw statement.
  var newCatchBlock = SyntaxFactory.CatchClause().WithBlock(newBlock).
    WithAdditionalAnnotations(
    Microsoft.CodeAnalysis.Formatting.Formatter.Annotation);

次に、ソース ファイルの AST のルートを取得し、アナライザーが特定した catch ブロックを探してから新しい AST を作成します。たとえば newRoot は、今回のソース ファイルで新しくルートに置き換えられた AST を表します。

var root = await context.Document.GetSyntaxRootAsync(
    context.CancellationToken).ConfigureAwait(false);
  var diagnostic = context.Diagnostics.First();
  var diagnosticSpan = diagnostic.Location.SourceSpan;
  var token = root.FindToken(diagnosticSpan.Start); // This is catch keyword.
  var catchBlock = token.Parent as CatchClauseSyntax; // This is catch block.
  var newRoot = root.ReplaceNode(catchBlock, newCatchBlock); // Create new AST.

最後に修正を呼び出し AST を更新するコードのアクションを登録します。

var codeAction =
    CodeAction.Create("throw", context.Document.WithSyntaxRoot(newRoot));
  context.RegisterFix(codeAction, diagnostic);
}

多くの正当な理由から、Roslyn のデータ構造の大部分は、AST も含めて不変です。開発者が実際にコードの修正を選択しない限り AST を更新することはないため、今回の例では特に適切な選択肢となります。既存の AST は不変なので、メソッドは新しい AST を返し、コード修正が選択されたときに IDE が新しい AST に置き換えられるようにしています。

AST が不変だとメモリ使用量が多くなりそうと思うかもしれません。AST が不変なら、変更の際は必ず完全なコピーが必要になるのでしょうか。心配はいりません。不変性を確保するために行われるコピーの量を最小限に抑えるために、AST には差分のみが格納されます (AST 全体を可変にすることで生じる同時実行や一貫性の問題を扱うよりも、差分を格納する方が容易になるためです)。

新天地を切り開く

このように、Roslyn がコンパイラ (および IDE) をオープンにしていることで、いくつか新天地が切り開かれます。何年もの間 C# は「厳密な型指定」言語であることを売りにし、事前コンパイルによってエラーが減ると主張していました。実際、C# はわずかなステップ数で、他の言語にありがちなミス (整数とブール値の比較の扱いや、C++ 開発者を悩ませる悪名高い「if (x = 0)」バグなど) を回避しようとしています。しかし、コンパイラは適用する (できる) 規則に関して、常に選択の幅を広く持たなければなりません。選択されるルールは業界ごとに異なったり、規則が「厳格すぎる」または「あいまいすぎる」など企業によってさまざまな考えがあるためです。マイクロソフトがコンパイラの内部を開発者にオープンにしたことで、自身でコンパイラのエキスパートになる必要なく、「独自の規則」を適用できるようになります。

Roslyn への着手方法の詳細については、roslyn.codeplex.com (英語) のプロジェクト ページをご覧ください。構文解析や語彙分析については多くの書籍が出版されています。「Dragon Book」として知られる『Compilers: Principles, Techniques & Tools』 (Addison Wesley、2006 年) が公式に出版されています。さらに .NET 中心のアプローチに関心がある方は、『Compiling for the .NET Common Language Runtime (CLR)』(John Gough 著、Prentice Hall、2001 年) や『Writing Compilers and Interpeters: A Software Engineering Approach』(Ronald Mak 著、Wiley、2009年) をご検討ください。

それでは、コーディングを楽しんでください。


Ted Newardは、コンサルティング サービス会社の iTrellis で CTO を務めています。これまでに 100 本を超える記事を執筆している Ted は、さまざまな書籍を執筆していて、『Professional F# 2.0』 (Wrox、2010 年) もその 1 つです。F# MVP であり、世界中で講演を行っています。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを blogs.tedneward.com (英語) に公開しています。

Joe Hummel は、Pluralsight.com のコンテンツ作成者、Visual C++ MVP 兼個人コンサルタントとして活躍するシカゴのイリノイ大学の研究准教授で、博士号も取得しています。彼は、カリフォルニア大学アーバイン校で高性能コンピューティングの分野で博士号を取得しており、あらゆるものを並列処理することに関心があります。シカゴ在住で、海に出ていなければ、電子メール (joe@joehummel.net (英語のみ)) で連絡を取ることができます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Kevin Pilch-Bisson に心より感謝いたします。