構文解析の概要

このチュートリアルでは、Syntax API について学習します。 Syntax API では、C# または Visual Basic プログラムを記述するデータ構造へのアクセスが提供されます。 これらのデータ構造には、あらゆるサイズのあらゆるプログラムを完全に表すことができる十分な情報があります。 これらの構造では、正しくコンパイルして実行される完全なプログラムを記述できます。 エディターでは、書き込み時に、不完全なプログラムを記述することもできます。

この優れた式を有効にする場合、Syntax API を構成する API とデータ構造が必然的に複雑になります。 まずは、一般的な "Hello World" プログラムのデータ構造がどのようになるかを見てみましょう。

using System;
using System.Collections.Generic;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

上のプログラムのテキストを見てください。 使い慣れた要素であることがわかります。 テキスト全体は単一のソース ファイル (コンパイル ユニット) を表しています。 そのソース ファイルの最初の 3 行は using ディレクティブです。 残りのソースは名前空間宣言に含まれています。 名前空間宣言には子クラス宣言が含まれています。 クラス宣言には 1 つのメソッド宣言が含まれています。

Syntax API では、コンパイル ユニットを表すルートを含むツリー構造が作成されます。 ツリー内のノードは、using ディレクティブ、名前空間宣言およびプログラムの他のすべての要素を表しています。 ツリー構造は最下位レベルまで続きます。文字列 "Hello World!" は、引数の子孫である文字列リテラル トークンです。 Syntax API では、プログラムの構造体へのアクセスが提供されます。 特定のコード プラクティスに対してクエリを実行し、ツリー全体をウォークしてコードを理解し、既存のツリーを変更して新しいツリーを作成することができます。

その簡単な説明では、Syntax API を使用してアクセスできる情報の種類の概要を示します。 Syntax API は、C# の使い慣れたコード コンストラクトを記述する正式な API にすぎません。 完全な機能には、改行、空白、インデントを含め、コードの書式設定方法に関する情報が含まれます。 この情報を使用して、人間のプログラマまたはコンパイラによって書き込まれ、読み取られるコードを完全に表すことができます。 この構造を使用することで、深い意味のあるレベルのソース コードと対話することができます。 テキスト文字列はもう存在しませんが、C# プログラムの構造を表すデータはあります。

まず .NET Compiler Platform SDK をインストールする必要があります。

インストール手順 - Visual Studio インストーラー

Visual Studio インストーラー.NET Compiler Platform SDK を見つけるには、以下の 2 つの異なる方法があります。

Visual Studio インストーラーを使用したインストール - ワークロード ビュー

.NET Compiler Platform SDK は、Visual Studio 拡張機能の開発ワークロードの一部として自動的に選択されません。 省略可能なコンポーネントとして選択する必要があります。

  1. Visual Studio インストーラーを実行します。
  2. [変更] を選択します
  3. Visual Studio 拡張機能の開発ワークロードを確認します。
  4. 概要ツリーの [Visual Studio 拡張機能の開発] ノードを開きます。
  5. [.NET Compiler Platform SDK] のチェック ボックスをオンにします。 省略可能なコンポーネントの最後に表示されます。

また、必要に応じて、DGML エディターのビジュアライザーでグラフを表示します。

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

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

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

また、必要に応じて、DGML エディターのビジュアライザーでグラフを表示します。

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

構文ツリーについて

C# コードの構造の分析には Syntax API を使用します。 Syntax API では、パーサー、構文ツリー、および構文ツリーを分析して構築するためのユーティリティを公開します。 これを使用して、特定の構文要素のコードの検索またはプログラムのコードの読み取りを行います。

構文ツリーは、C# および Visual Basic プログラムを理解するために C# および Visual Basic コンパイラで使用されるデータ構造です。 構文ツリーは、プロジェクトのビルド時、または開発者が F5 キーを押したときに実行されるのと同じパーサーによって生成されます。 構文ツリーは言語に対して完全に忠実であり、コード ファイル内のすべての情報はツリーで表されます。 構文ツリーをテキストに書き込むことで、解析された元の正確なテキストが再現されます。 構文ツリーは不変でもあります。構文ツリーを作成した後で変更することはできません。 ツリーのコンシューマーは、データが変更されないことを認識したうえで、ロックやその他のコンカレンシー手段を使用せずに、複数のスレッドでツリーを分析できます。 API を使用して、新しいツリーを作成することができます。その場合、既存のツリーを変更します。

構文ツリーの 4 つの基本的な構成要素は次のとおりです。

トリビア、トークン、およびノードは、Visual Basic または C# コードのフラグメント内のすべてを完全に表すツリーを形成するために階層的に構成されます。 この構造は、Syntax Visualizer ウィンドウを使用して確認することができます。 Visual Studio で、 [ビュー]>[その他のウィンドウ]>[Syntax Visualizer](Syntax Visualizer) の順に選択します。 たとえば、Syntax Visualizer を使用して調べた上記の C# ソース ファイルは、次の図のようになります。

SyntaxNode: 青 | SyntaxToken: 緑 | SyntaxTrivia: 赤 C# コード ファイル

このツリー構造を移動することで、ステートメント、式、トークン、またはコード ファイルのわずかな空白を見つけることができます。

Syntax API を使用してコード ファイルで何でも見つけることはできますが、ほとんどのシナリオでは、コードの小さなスニペットの確認や、特定のステートメントまたはフラグメントの検索が必要です。 以下の 2 つの例では、コードの構造の参照、または単一ステートメントの検索を行う場合の一般的な使用方法を示します。

ツリーの走査

2 つの方法で構文ツリー内のノードを調べることができます。 ツリーを走査して、各ノードを調べることができます。あるいは、特定の要素やノードに対してクエリを実行することができます。

手動による走査

このサンプルの完成したコードは、GitHub のリポジトリで確認できます。

注意

構文ツリー型では継承を使用して、プログラムのさまざまな場所で有効なさまざまな構文要素を記述します。 これらの API を使用することは、多くの場合、特定の派生型にプロパティまたはコレクション メンバーをキャストすることを意味します。 次の例では、割り当てとキャストは別のステートメントであり、明示的に型指定された変数を使用します。 コードを読み取り、API の戻り値の型と返されるオブジェクトのランタイム型を確認することができます。 実際には、暗黙的に型指定された変数を使用して、API 名に依存して、調べられるオブジェクトの型を記述するのがより一般的です。

次のようにして、新しい C# の Stand-Alone Code Analysis Tool プロジェクトを作成します。

  • Visual Studio で、 [ファイル]>[新規]>[プロジェクト] の順に選択して、[新しいプロジェクト] ダイアログを表示します。
  • [Visual C#]>[機能拡張] で、 [Stand-Alone Code Analysis Tool] を選択します。
  • プロジェクトに "SyntaxTreeManualTraversal" という名前を付けて、[OK] をクリックします。

前に示した基本的な "Hello World!" プログラムを分析します。 Hello World プログラムのテキストを Program クラスの定数として追加します。

        const string programText =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";

次に、以下のコードを追加して、programText 定数のコード テキストの構文ツリーをビルドします。 次の行を Main メソッドに追加します。

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

これら 2 行でツリーを作成し、そのツリーのルート ノードを取得します。 これでツリー内のノードを調べることができます。 以下の行を Main メソッドに追加して、ツリー内のルート ノードのプロパティをいくつか表示します。

WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

アプリケーションを実行して、このツリー内のルート ノードについて、コードで検出された内容を確認します。

通常、コードについて学習する場合、ツリーを走査します。 この例では、使い慣れたコードを分析して、API を調べます。 次のコードを追加して、root ノードの最初のメンバーを調べます。

MemberDeclarationSyntax firstMember = root.Members[0];
WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

そのメンバーは Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax です。 namespace HelloWorld 宣言のスコープ内ですべてを表します。 次のコードを追加して、HelloWorld 名前空間内で宣言されているノードを調べます。

WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");
WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");

プログラムを実行して、学習した内容を確認します。

宣言が Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax であることがわかったので、その型の新しい変数を宣言して、クラス宣言を調べます。 このクラスには 1 つのメンバー (Main メソッド) のみが含まれます。 次のコードを追加して Main メソッドを見つけ、それを Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax にキャストします。

var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
WriteLine($"There are {programDeclaration.Members.Count} members declared in the {programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];

メソッド宣言ノードには、メソッドに関するすべての構文情報が含まれています。 次は、Main メソッドの戻り値の型、引数の数と型、およびメソッドの本文を表示します。 次のコードを追加します。

WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");
WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
    WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body?.ToFullString());

var argsParameter = mainDeclaration.ParameterList.Parameters[0];

プログラムを実行して、このプログラムについて検出したすべての情報を表示します。

The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
        System
        System.Collections
        System.Linq
        System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
        {
            Console.WriteLine("Hello, World!");
        }

クエリ メソッド

ツリーの走査に加え、Microsoft.CodeAnalysis.SyntaxNode で定義されているクエリ メソッドを使用して、構文ツリーを調べることもできます。 XPath を使い慣れていれば、これらのメソッドをすぐに使いこなすことができます。 LINQ でこれらのメソッドを使用することで、ツリー内の内容をすばやく検索できます。 SyntaxNode には、DescendantNodesAncestorsAndSelfChildNodes などのクエリ メソッドがあります。

これらのクエリ メソッドを使用すれば、ツリーを移動せずに、Main メソッドに対する引数を検索することができます。 次のコードを Main メソッドの下部に追加します。

var firstParameters = from methodDeclaration in root.DescendantNodes()
                                        .OfType<MethodDeclarationSyntax>()
                      where methodDeclaration.Identifier.ValueText == "Main"
                      select methodDeclaration.ParameterList.Parameters.First();

var argsParameter2 = firstParameters.Single();

WriteLine(argsParameter == argsParameter2);

最初のステートメントでは LINQ 式と DescendantNodes メソッドを使用して、前の例と同じパラメーターを検索します。

プログラムを実行すると、ツリーを手動で移動する場合と同じパラメーターが LINQ 式で検出されたことがわかります。

このサンプルでは WriteLine ステートメントを使用して、走査された構文ツリーに関する情報を表示します。 デバッガーで完成したプログラムを実行して、さらに理解を深めることもできます。 hello world プログラム用に作成された構文ツリーの一部であるメソッドとプロパティをさらに調べることができます。

構文ウォーカー

多くの場合、構文ツリーで特定の型のノード (ファイル内のすべてのプロパティ宣言など) をすべて検索する必要があります。 Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker クラスを拡張し、VisitPropertyDeclaration(PropertyDeclarationSyntax) メソッドをオーバーライドして、構造を事前に認識せずに、構文ツリーのすべてのプロパティ宣言を処理します。 CSharpSyntaxWalker は、ノードとその各子に再帰的にアクセスする特定の種類の CSharpSyntaxVisitor です。

この例では、構文ツリーを調べる CSharpSyntaxWalker を実装します。 System 名前空間をインポートしていない using ディレクティブを収集します。

新しい C# のStand-Alone Code Analysis Tool プロジェクトを作成し、"SyntaxWalker" という名前を付けます。

このサンプルの完成したコードは、GitHub のリポジトリで確認できます。 GitHub のサンプルには、このチュートリアルで説明されている両方のプロジェクトが含まれています。

上記のサンプルと同様に、分析しようとしているプログラムのテキストを保持する文字列定数を定義できます。

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;

    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;

        class Foo { }
    }

    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;

        class Bar { }
    }
}";

このソース テキストには、4 つの異なる場所 (ファイル レベル、最上位の名前空間、2 つの入れ子になった名前空間) に分散されている using ディレクティブが含まれます。 この例では、コードに対してクエリを実行する CSharpSyntaxWalker クラスを使用する主なシナリオに焦点を当てます。 using 宣言を検索するためにルート構文ツリー内のすべてのノードにアクセスするのは面倒です。 代わりに、派生クラスを作成し、ツリー内の現在のノードが using ディレクティブである場合にのみ、呼び出されるメソッドをオーバーライドします。 ビジターは他のノード型に対して何も行いません。 この単一のメソッドで各 using ステートメントを調べ、System 名前空間にはない名前空間のコレクションをビルドします。 すべての using ステートメント (using ステートメントのみ) を調べる CSharpSyntaxWalker をビルドします。

これでプログラム テキストを定義したので、SyntaxTree を作成して、そのツリーのルートを取得する必要があります。

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

次は、新しいクラスを作成します。 Visual Studio で、 [プロジェクト]>[新しい項目の追加] の順に選択します。 [新しい項目の追加] ダイアログで、ファイル名として「UsingCollector.cs」と入力します。

UsingCollector クラスで using ビジター機能を実装します。 まず、CSharpSyntaxWalker から UsingCollector クラスを派生させます。

class UsingCollector : CSharpSyntaxWalker

収集する名前空間ノードを保持する記憶域が必要です。 UsingCollector クラスでパブリックの読み取り専用プロパティを宣言します。その場合、以下の変数を使用して、検索する UsingDirectiveSyntax ノードを格納します。

public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

基本クラスの CSharpSyntaxWalker では、構文ツリー内の各ノードにアクセスするロジックを実装します。 派生クラスは、対象となる特定のノードに対して呼び出されたメソッドをオーバーライドします。 この例では、using ディレクティブが対象となります。 つまり、VisitUsingDirective(UsingDirectiveSyntax) メソッドをオーバーライドする必要があります。 このメソッドへの 1 つの引数は Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax オブジェクトです。 これがビジターを使用する最も重要は利点です。特定のノード型に既にキャストされている引数を使用して、オーバーライドされたメソッドを呼び出します。 Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax クラスには、インポートされる名前空間の名前を格納する Name プロパティがあります。 それは Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax です。 VisitUsingDirective(UsingDirectiveSyntax) オーバーライドで次のコードを追加します。

public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
    WriteLine($"\tVisitUsingDirective called with {node.Name}.");
    if (node.Name.ToString() != "System" &&
        !node.Name.ToString().StartsWith("System."))
    {
        WriteLine($"\t\tSuccess. Adding {node.Name}.");
        this.Usings.Add(node);
    }
}

前の例と同様に、このメソッドを理解するのに役立つさまざまな WriteLine ステートメントを追加しました。 呼びされるタイミングと、毎回渡される引数を確認できます。

最後に、2 行のコードを追加して UsingCollector を作成し、ルート ノードにアクセスするようにして、using ステートメントをすべて収集する必要があります。 次に、foreach ループを追加して、コレクターが検出した using ステートメントをすべて表示します。

var collector = new UsingCollector();
collector.Visit(root);
foreach (var directive in collector.Usings)
{
    WriteLine(directive.Name);
}

プログラムをコンパイルして実行します。 次の出力が表示されます。

        VisitUsingDirective called with System.
        VisitUsingDirective called with System.Collections.Generic.
        VisitUsingDirective called with System.Linq.
        VisitUsingDirective called with System.Text.
        VisitUsingDirective called with Microsoft.CodeAnalysis.
                Success. Adding Microsoft.CodeAnalysis.
        VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
                Success. Adding Microsoft.CodeAnalysis.CSharp.
        VisitUsingDirective called with Microsoft.
                Success. Adding Microsoft.
        VisitUsingDirective called with System.ComponentModel.
        VisitUsingDirective called with Microsoft.Win32.
                Success. Adding Microsoft.Win32.
        VisitUsingDirective called with System.Runtime.InteropServices.
        VisitUsingDirective called with System.CodeDom.
        VisitUsingDirective called with Microsoft.CSharp.
                Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .

おめでとうございます! Syntax API を使用して、C# ソース コードで特定の種類の C# ステートメントと宣言を検索しました。