次の方法で共有


コンパイラ

マイクロソフトの次世代コンパイラ プロジェクトによるコードの強化方法

Jason Bock

コード サンプルのダウンロード

開発者であれば、優れたコードを記述したいと考えるのは当然です。絶えず機能の追加や問題の解決が求められ、バグが多く、メンテナンスが難しいシステムを作成したいとはだれも考えません。常に混沌とした状態にあるプロジェクトに携わっていても楽しくありません。コード ベースへのアプローチに一貫性がないため、ほとんど理解不可能になり、長い時間が浪費されます。階層がしっかりと定義され、単体テストが数多く行われ、ビルド サーバーが絶えず稼働していて、すべてが機能するようなプロジェクトに携わりたいものです。このようなプロジェクトには、通常、開発者が従う一連のガイドラインや基準がきちんと存在しています。

いくつかのチームがこのようなガイドラインを用意しています。たとえば、あるメソッドには問題になりそうな箇所があるため、そのメソッドを呼び出さないよう開発者に求めるガイドラインがあるかもしれません。あるいは、特定の状況のコードでは同じパターンを使用するよう求めるガイドラインもあるでしょう。たとえば、次のような基準であれば、プロジェクトの開発者の合意が得られる可能性が高くなります。

  • 現地の DateTime 値は使用しないで、常に、協定世界時 (UTC) を使用する。
  • 値型の Parse メソッド (int.Parse など) の使用は避け、int.TryParse を使用する。
  • 作成するエンティティ クラスはすべて等値演算をサポートする。つまり、エンティティ クラスでは、Equals と GetHashCode をオーバーライドし、== 演算子、!= 演算子、および IEquatable<T> インターフェイスを実装する。

基準を掲載したドキュメントには、このような規則が含まれています。一貫性があるのはすばらしいことで、全員が同じ慣行に従えば、コードのメンテナンスがより簡単になります。必要なのは、効率的かつ再利用可能な方法で、チームの開発者全員にこうした規則をすばやく伝えることです。

コード レビューは、潜在的な問題を見つける方法の 1 つです。ある実装について、元の作成者は気付かない問題を、新しい視点を持つ別の担当者が見つけることはよくあります。自分以外の目によるレビューを受けることにはメリットがあります。そのレビュー担当者が仕事に詳しくなければなおさらです。それでも、開発中に問題が見逃されることはよくあります。さらに、コード レビューには時間がかかります。開発者は、コードのレビューや、見つかった問題についての他の開発者とのミーティングに時間を使わなければなりません。このような時間は短縮したいものです。また、ミスした場合にはできるだけ早く把握したいものです。エラーの早期発見は、長期的には時間と費用の節約になります。

Visual Studio には、コード分析など、コードを分析して潜在的な問題を通知できるツールがあります。コード分析には、事前定義済みの多くの規則があり、オブジェクトを破棄しなかった状況や、メソッドに未使用の引数がある状況を検出できます。残念ながら、コード分析では、コンパイル完了時まで規則を適用できないため、早期発見とは言えません。規則に従っていないミスが新しいコードにある場合、コード入力中にわかればありがたいものです。エラーの早期発見はすばらしいことです。時間 (ひいては費用) が節約され、将来数多くの問題を引き起こす可能性があるコードをコミットしなくて済みます。そのためには、入力時に実行されるように規則をコード化できる必要があります。ここで役に立つのが Microsoft "Roslyn" CTP です。

Microsoft "Roslyn" とは

.NET 開発者がコード分析に使用できる最高のツールの 1 つはコンパイラです。コンパイラは、コードをトークンに解析する方法と、解析したトークンをコード内の位置を基準に意味のある情報に変換する方法を把握しています。この解析と変換は、出力としてアセンブリをディスクに生成することによって行われます。コンパイルのパイプラインには、コンパイルの過程で苦労して得られる情報がたくさんあります。このような情報を利用できればありがたいのですが、残念なことに、C# と Visual Basic のコンパイラにはこのような情報にアクセスする API が用意されていないため、.NET 環境では利用できません。Roslyn により、この状況が変わります。Roslyn とは、コンパイル過程のすべての段階に開発者が完全にアクセスできる一連のコンパイラ API です。図 1 は、Roslyn で利用できるようになる、コンパイラ プロセスのさまざまな段階を示しています。


図 1 Roslyn コンパイラ パイプライン

Roslyn はまだ CTP の段階ですが (今回は 2012 年 9 月版を使用)、このアセンブリで利用できる機能を調べ、Roslyn でできることを理解することに時間を費やす価値はあります。手始めに、スクリプト機能を見てみましょう。Roslyn では、C# と Visual Basic コードがスクリプト可能になります。つまり、Roslyn にはスクリプト エンジンがあり、開発者はこのエンジンにコードのスニペットを入力できます。コード入力は、ScriptEngine クラスで処理されます。以下は、このエンジンから現在の DateTime 値を返す方法を示したサンプルです。

class Program
{
  static void Main(string[] args)
  {
    var engine = new ScriptEngine();
    engine.ImportNamespace("System");
    var session = engine.CreateSession();
    Console.Out.WriteLine(session.Execute<string>(
      "DateTime.Now.ToString();"));
  }
}

このコードでは、エンジンを作成して、System 名前空間をインポートしているため、Roslyn によって DateTime の意味を解決できるようになります。セッションを作成したら、後は Execute を呼び出すだけで、指定したコードが Roslyn によって解析されます。正しく解析できれば、そのコードが実行され、結果が返されます。

C# をスクリプト言語にするというのは強力な考え方です。Roslyn はまだ CTP 段階ですが、そのコンポーネントを使用して驚くべきプロジェクトやフレームワークが作られています。たとえば、scriptcs ( scriptcs.net、英語) などです。ただし、Roslyn の本領が発揮されるのは、コードの記述中に問題がある場合に警告を表示する Visual Studio 拡張機能を作成するときです。前述のスニペットでは、DateTime.Now を使用しました。この記事の冒頭で触れた規則が適用されているプロジェクトに携わっているならば、その基準に従っていないことになります。そこで、Roslyn を使用して、規則を適用する方法を見ていきます。しかし、その前に、コードを解析してトークンを取得するという、コンパイルの最初の段階について説明します。

構文ツリー

Roslyn によってコードが解析されると、構文ツリーが変更不可の状態で返されます。このツリーには、空白やタブなどの細かい部分も含めて、指定されたコードについてのすべての情報が保持されます。コードにエラーがある場合でも、最大限の情報を提供するようになっています。

それはそれで良いのですが、どのようにしたら、このツリーの中で適切な情報がある場所を見つけることができるでしょう。現時点では、Roslyn のドキュメントはかなり少ない状態です (まだ CTP 段階であることを考えれば仕方ありません)。Roslyn フォーラム ( bit.ly/16qNf7w、英語) に質問を投稿するか、Twitter の投稿で #RoslynCTP タグを使用します。また、コンポーネントをインストールすると、SyntaxVisualizerExtension というサンプルもインストールされます。このサンプルは、Visual Studio の拡張機能です。IDE にコードを入力すると、ビジュアライザーによって現在バージョンのツリーに自動更新されます。

このツールは、探している情報や、ツリーのナビゲート方法を把握するのに不可欠です。DateTime クラスの .Now を使用する場合、MemberAccessExpression (正確には、MemberAccessExpressionSyntax ベース オブジェクト) で、最後の IdentifierName の値が Now であるものを見つける必要があることがわかりました。もちろん、これは "var now = DateTime.Now;" と入力する簡単な場合です。DateTime の前に "System." を付けることもあれば、"using DT = System.DateTime;" を使用することもあります。さらに、System の別のクラスのプロパティに Now があるかもしれません。どのような場合も、正しく処理される必要があります。

コードの問題点の発見と解決

何を見つける必要があるのかがわかったので、Roslyn ベースの Visual Studio 拡張機能を作成して、DateTime.Now プロパティの使用を突き止めます。そのためには、単純に Visual Studio の [Roslyn] オプションをクリックして、[Code Issue] (コードの問題) テンプレートをクリックします。

クリックすると、CodeIssueProvider というクラスを 1 つ含むプロジェクトが作成されます。このクラスには、ICodeIssueProvider インターフェイスが実装されています。ただし、4 つのメンバーそれぞれを実装する必要はありません。この場合、SyntaxNode 型を操作するメンバーのみを使用します。他のメンバーを使用すると、NotImplementedException がスローされます。対応する GetIssues メソッドを処理する構文ノードの種類を指定することによって、SyntaxNodeTypes プロパティを実装します。前の例で触れたように、MemberAccessExpressionSyntax 型が重要です。次のコード スニペットは、SyntaxNodeTypes の実装方法を示しています。

public IEnumerable<Type> SyntaxNodeTypes
{
  get
  {
    return new[] { typeof(MemberAccessExpressionSyntax) };
  }
}

これは、Roslyn 向けに最適化されています。調査する型を詳細に指定すると、Roslyn では、構文の型ごとに GetIssues メソッドを呼び出す必要がなくなります。Roslyn にこのフィルター メカニズムが備えられておらず、ツリーのすべてのノードに対してコード プロバイダーを呼び出すとしたら、パフォーマンスは恐ろしいことになります。

後は、GetIssues を実装して、Now プロパティの使用がレポートされるようにするだけです。前に触れたように、必要なのは、DateTime の Now が使用されている場合を見つけることだけです。トークンを使用しているときは、テキスト以外の情報は多くありません。ただし、Roslyn では、セマンティック モデルと呼ばれるモデルが提供されます。このモデルにより、調査するコードに関する多くの情報が得られます。図 2 のコードは、DateTime.Now の使用を発見する方法を示しています。

図 2 DateTime.Now の使用を発見する

public IEnumerable<CodeIssue> GetIssues(
  IDocument document, CommonSyntaxNode node, 
  CancellationToken cancellationToken)
{
  var memberNode = node as MemberAccessExpressionSyntax;
  if (memberNode.OperatorToken.Kind == SyntaxKind.DotToken &&
    memberNode.Name.Identifier.ValueText == "Now")
  {
    var symbol = document.GetSemanticModel()
        .GetSymbolInfo(memberNode.Name).Symbol;
    if (symbol != null &&
      symbol.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeTypeDisplayString &&
      symbol.ContainingAssembly.ToDisplayString().Contains(
        Values.ExpectedContainingAssemblyDisplayString))
    {
      return new [] { new CodeIssue(CodeIssueKind.Error,
        memberNode.Name.Span,
        "Do not use DateTime.Now",
        new ChangeNowToUtcNowCodeAction(document, memberNode))};
    }
  }
  return null;
}

cancellationToken 引数を使用していないこと、さらに言えば、この記事付属のサンプル プロジェクトのどこでも使用していないことがわかります。トークンを定期的にチェックして、処理が停止するかどうかを把握するコードをサンプルに置くのは紛らわしいので、わざとこのようにしてあります。しかし、実運用可能な Roslyn ベースの拡張機能を作成する場合、トークンを頻繁にチェックして、トークンがキャンセル状態の場合には停止するようにします。

メンバー アクセス式により Now というプロパティの取得が試みられていると判断したら、そのトークンのシンボル情報を取得します。これは、ツリーのセマンティック モデルを取得することによって行います。その後、Symbol プロパティを通じて、ISymbol ベースのオブジェクトへの参照を取得します。後は、含まれる型を取得して、その名前が System.DateTime かどうか、含まれるアセンブリの名前に mscorlib が含まれるかどうかを確認するだけです。そのような名前の場合、それが探していた問題であるため、CodeIssue オブジェクトに戻って、エラーのフラグを設定します。

IDE の Now というテキストの下にエラーを示す赤の波線が表示されるため、ここまでは順調に進んでいます。しかし、これだけでは十分ではありません。コードにセミコロンや中かっこがないことがコンパイラから通知されたら便利です。エラー情報を得ることが何よりも重要で、単純なエラーなら、通常、エラー メッセージに基づいて非常に簡単に解決できます。しかし、ツール自体がエラーを解決するとしたらどうでしょう。問題があることを知るのはすばらしいことです。問題の解決方法について、エラー メッセージから詳細情報が得られるとしたらさらにすばらしいことです。ツールによってその情報が使用され、問題が自動的に解決されれば、問題に費やす時間は短縮されます。時間が短縮されればされるほど、優れています。

これが、前のコード スニペットに ChangeNowToUtcNowCodeAction というクラスへの参照があった理由です。このクラスには、ICodeAction インターフェイスが実装されており、このインターフェイスでは Now を UtcNow に変更する処理が行われます。実装する必要のあるのは GetEdit というメソッドです。この場合、MemberAccessExpressionSyntax オブジェクトの Name トークンは、新しいトークンに変更する必要があります。次のコードに示すように、この置換は非常に簡単です。

public CodeActionEdit GetEdit(CancellationToken cancellationToken)
{
  var nameNode = this.nowNode.Name;
  var utcNowNode =
    Syntax.IdentifierName("UtcNow");
  var rootNode = this.document.
    GetSyntaxRoot(cancellationToken);
  var newRootNode =
    rootNode.ReplaceNode(nameNode, utcNowNode);
  return new CodeActionEdit(
    document.UpdateSyntaxRoot(newRootNode));
}

必要なのは、UtcNow テキストの新しい識別子を作成し、ReplaceNode を使用して Now トークンをこの新しい識別子と置き換えることだけです。構文ツリーは変更不可なので、現在のドキュメント ツリーは変更しません。新しいツリーを作成して、メソッド呼び出しからそのツリーを返します。

すべてのコードを適切に配置したら、単純に F5 キーを押して、Visual Studio でそのコードをテストします。拡張機能が自動的にインストールされて、Visual Studio の新しいインスタンスが起動します。

DateTime コンストラクターの分析

出発点としては上出来ですが、対処する必要のある場合が他にもあります。DateTime には、問題を引き起こす可能性のあるコンストラクターが多数定義されています。特に意識するのは次の 2 つです。

  1. DateTimeKind 列挙型をパラメーターの 1 つとして受け取らないコンストラクターでは、結果の DateTime が Unspecified 状態になる。
  2. DateTimeKind 値をパラメーターの 1 つとして受け取らないコンストラクターでは、Utc 以外の列挙値を指定できる。

どちらの場合でも、それを検出するコードを記述できます。ただし、ここでは 2 つ目のコード アクションのみを作成します。

図 3 は、不適切な DateTime コンストラクター呼び出しを発見する、ICodeIssue ベースのクラスの GetIssues メソッドを示しています。

図 3 不適切な DateTime コンストラクター呼び出しの検出

public IEnumerable<CodeIssue> GetIssues(
  IDocument document, CommonSyntaxNode node, 
  CancellationToken cancellationToken)
{
  var creationNode = node as ObjectCreationExpressionSyntax;
  var creationNameNode = creationNode.Type as IdentifierNameSyntax;
  if (creationNameNode != null && 
    creationNameNode.Identifier.ValueText == "DateTime")
  {
    var model = document.GetSemanticModel();
    var creationSymbol = model.GetSymbolInfo(creationNode).Symbol;
    if (creationSymbol != null &&
      creationSymbol.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeTypeDisplayString &&
      creationSymbol.ContainingAssembly.ToDisplayString().Contains(
        Values.ExpectedContainingAssemblyDisplayString))
    {
      var argument = FindingNewDateTimeCodeIssueProvider
        .GetInvalidArgument(creationNode, model);
      if (argument != null)
      {
        if (argument.Item2.Name == "Local" ||
          argument.Item2.Name == "Unspecified")
        {
          return new [] { new CodeIssue(CodeIssueKind.Error,
            argument.Item1.Span,
            "Do not use DateTimeKind.Local or DateTimeKind.Unspecified",
            new ChangeDateTimeKindToUtcCodeAction(document, 
              argument.Item1)) };
        }
      }
      else
      {
        return new [] { new CodeIssue(CodeIssueKind.Error,
          creationNode.Span,
          "You must use a DateTime constuctor that takes a DateTimeKind") };
      }
    }
  }
  return null;
}

これは、他の問題の場合と非常によく似ています。DateTime のコンストラクターだとわかったら、引数を評価します (GetInvalidArgument の働きについてはこの後説明します)。DateTimeKind 型の引数を検出し、Utc が指定されてない場合は問題があります。それ以外の場合は、Utc の DateTime がないコンストラクターを使用しているとわかっているので、別の問題としてレポートします。図 4 は、GetInvalidArgument を示しています。

図 4 GetInvalidArgument メソッド

private static Tuple<ArgumentSyntax, ISymbol> GetInvalidArgument(
  ObjectCreationExpressionSyntax creationToken, ISemanticModel model)
{
  foreach (var argument in creationToken.ArgumentList.Arguments)
  {
    if (argument.Expression is MemberAccessExpressionSyntax)
    {
      var argumentSymbolNode = model
        .GetSymbolInfo(argument.Expression).Symbol;
      if (argumentSymbolNode.ContainingType.ToDisplayString() ==
        Values.ExpectedContainingDateTimeKindTypeDisplayString)
      {
        return new Tuple<ArgumentSyntax,ISymbol>(argument, 
            argumentSymbolNode);
      }
    }
  }
  return null;
}

この検索もよく似ています。引数の型が DateTimeKind の場合、引数の値が無効である可能性があります。引数を解決するコードは最初に見たコード アクションと実質的に同じなので、ここでは繰り返しません。これで、他の開発者が DateTime.Now の制約の回避を試みる場合でも、作業中に不適切な引数の発見や、コンストラクター呼び出しの解決も可能です。

将来

Roslyn で作成するツールについて考察するのはすばらしいことですが、まだ課題があります。Roslyn に関する現在の大きな不満の 1 つは、ドキュメントが不足していることです。インターネットやインストールしたコンポーネントには優れたサンプルがありますが、Roslyn は大規模な API セットであるため、特定のタスクを達成するにはどこから開始して、何を使用すれば良いのかを正確に知ることが難しくなります。使用する適切な呼び出しを見つけるためにしばらく調査する必要が生じることも珍しくありません。メリットとしては、通常、Roslyn で何かをするとき、最初は非常に複雑に思えたものが、最終的にはコード 100 ~ 200 行と短くできることです。

Roslyn のリリースが近づくと、Roslyn に関するすべてが強化されていくでしょう。また、Roslyn には .NET エコシステムのさまざまなフレームワークとツールの基盤となる可能性があると確信しています。.NET 開発者全員が Roslyn API を日常的に直接使用するかどうかはわかりませんが、おそらくある程度は Roslyn 利用したコンポーネントを使用することになるでしょう。これが、Roslyn に取り組み、そのしくみについて学習することをお勧めしている理由です。手法をコード化して、チームの開発者全員が利用できる再利用可能な規則にできれば、より優れたコードを迅速に作成できるようになります。


Jason Bock は、Magenic ( magenic.com、英語) のプラクティス リードで、最近出版された 『Metaprogramming in .NET』 (Manning Publications、2013 年) の共著者でもあります。連絡先は、 jasonb@magenic.com (英語のみ) です。

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