次の方法で共有


構文解析を始めましょう

このチュートリアルでは、 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!");
        }
    }
}

前のプログラムのテキストを確認します。 使い慣れた要素を認識します。 テキスト全体は、1 つのソース ファイルまたは コンパイル単位を表します。 そのソース ファイルの最初の 3 行は ディレクティブを使用しています。 残りのソースは 名前空間宣言に含まれています。 名前空間宣言には、子 クラス宣言が含まれています。 クラス宣言には、1 つの メソッド宣言が含まれています。

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

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

開始するには、 .NET Compiler Platform 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 のボックスがオンになっていることを確認します。
  6. [変更] を選択します。

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

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

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

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

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

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

構文ツリーについて

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

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

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

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

SyntaxNode: Blue | SyntaxToken: Green | SyntaxTrivia: Red C# コード ファイル

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

構文 API を使用してコード ファイル内で何かを見つけることができますが、ほとんどのシナリオでは、コードの小さなスニペットを調べたり、特定のステートメントやフラグメントを検索したりする必要があります。 次の 2 つの例は、コードの構造を参照したり、単一のステートメントを検索したりする一般的な用途を示しています。

ツリーの走査

構文ツリー内のノードは、2 つの方法で調べることができます。 ツリーを走査して各ノードを調べたり、特定の要素またはノードを照会したりできます。

手動トラバーサル

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

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

新しい C# Stand-Alone Code Analysis Tool プロジェクトを作成します。

  • Visual Studio で、[ ファイル>新規作成>プロジェクト ] を選択して、[新しいプロジェクト] ダイアログを表示します。
  • Visual C#>Extensibility で、Stand-Alone コード分析ツールを選択します。
  • プロジェクトに "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 directives. 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であることがわかったら、その型の新しい変数を宣言して、クラス宣言を調べます。 このクラスには、 Main メソッドという 1 つのメンバーのみが含まれます。 次のコードを追加して、 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 directives. 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ディレクティブを収集します。

新しい 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 { }
    }
}";

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

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

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

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

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

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 ステートメントを追加しました。 呼び出されるタイミングと、毎回渡される引数を確認できます。

最後に、 UsingCollector を作成し、ルート ノードにアクセスして、すべての using ディレクティブを収集するために、2 行のコードを追加する必要があります。 次に、コレクターが見つけたすべての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 . . .

おめでとうございます! 構文 API を使用して、C# のソース コードで特定の種類のディレクティブと宣言を検索しました。