構文ツリーは、コンパイラ API によって公開される基本的な変更できないデータ構造です。 これらのツリーは、ソース コードの字句構造と構文構造を表します。 これらは、次の 2 つの重要な目的に役立ちます。
- IDE、アドイン、コード分析ツール、リファクタリングなどのツールが、ユーザーのプロジェクト内のソース コードの構文構造を表示および処理できるようにするため。
- リファクタリングや IDE などのツールを有効にして、直接テキスト編集を使用することなく、自然な方法でソース コードを作成、変更、再配置できるようにします。 ツリーを作成および操作することで、ツールはソース コードを簡単に作成および再配置できます。
構文ツリー
構文ツリーは、コンパイル、コード分析、バインド、リファクタリング、IDE 機能、およびコード生成に使用される主な構造です。 ソース コードの一部は、最初に識別され、多くの既知の構造言語要素の 1 つに分類されないと理解されません。
注
RoslynQuoter は、プログラムの構文ツリーを構築するために使用される構文ファクトリ API 呼び出しを示すオープンソース ツールです。 ライブで試すには、 http://roslynquoter.azurewebsites.netを参照してください。
構文ツリーには、次の 3 つの主要な属性があります。
- すべてのソース情報を完全に忠実に保持します。 完全な忠実性とは、構文ツリーには、ソース テキストで見つかったすべての情報、すべての文法構造、すべての字句トークン、および空白、コメント、プリプロセッサ ディレクティブなど、その間のすべての情報が含まれていることを意味します。 たとえば、ソースに記載されている各リテラルは、入力されたとおりに表されます。 また、構文ツリーは、スキップされたトークンまたは不足しているトークンを表すことによって、プログラムが不完全または形式が正しくない場合に、ソース コードのエラーをキャプチャします。
- 解析された正確なテキストを生成できます。 任意の構文ノードから、そのノードをルートとするサブツリーのテキスト表現を取得できます。 この機能は、ソース テキストを構築および編集する方法として構文ツリーを使用できることを意味します。 ツリーを作成し、意味を持って同等のテキストを作成し、既存のツリーに対する変更から新しいツリーを作成することで、テキストを効果的に編集しました。
- これらは不変であり、スレッド セーフです。 ツリーが取得されると、コードの現在の状態のスナップショットになり、変更されることはありません。 これにより、複数のユーザーが異なるスレッドで同じ構文ツリーを同時に操作でき、ロックや重複はありません。 ツリーは不変であり、ツリーに直接変更を加えないため、ファクトリ メソッドは、ツリーの追加のスナップショットを作成して構文ツリーを作成および変更するのに役立ちます。 ツリーは基になるノードを再利用する方法が効率的であるため、新しいバージョンを迅速に再構築でき、追加のメモリをほとんど使用できません。
構文ツリーは、文字通り、非末端構造要素が他の要素を親とするツリー データ構造です。 各構文ツリーは、ノード、トークン、トリビアで構成されます。
構文ノード
構文ノードは、構文ツリーの主要な要素の 1 つです。 これらのノードは、宣言、ステートメント、句、式などの構文構造を表します。 構文ノードの各カテゴリは、 Microsoft.CodeAnalysis.SyntaxNodeから派生した個別のクラスによって表されます。 ノード クラスのセットは拡張可能ではありません。
すべての構文ノードは、構文ツリー内の非ターミナル ノードです。つまり、常に他のノードとトークンが子として存在します。 別のノードの子として、各ノードには、 SyntaxNode.Parent プロパティを介してアクセスできる親ノードがあります。 ノードとツリーは不変であるため、ノードの親は変更されません。 ツリーのルートには Null 親があります。
各ノードには SyntaxNode.ChildNodes() メソッドがあり、ソース テキスト内の位置に基づいて子ノードの一覧を順番に返します。 このリストにはトークンは含まれません。 各ノードには、 DescendantNodes、 DescendantTokens、 DescendantTrivia など、そのノードによってルート化されたサブツリーに存在するすべてのノード、トークン、トリビアの一覧を表す子孫を調べるメソッドもあります。
さらに、各構文ノード サブクラスは、厳密に型指定されたプロパティを使用して、同じ子をすべて公開します。 たとえば、 BinaryExpressionSyntax ノード クラスには、 Left、 OperatorToken、 Rightという 2 項演算子に固有の 3 つのプロパティがあります。 LeftとRightの型がExpressionSyntaxされ、OperatorTokenの型がSyntaxToken。
一部の構文ノードには、省略可能な子があります。 たとえば、 IfStatementSyntax には省略可能な ElseClauseSyntaxがあります。 子が存在しない場合、プロパティは null を返します。
構文トークン
構文トークンは、コードの最小構文フラグメントを表す言語文法の終端です。 他のノードやトークンの親になることはありません。 構文トークンは、キーワード、識別子、リテラル、句読点で構成されます。
効率のために、 SyntaxToken 型は CLR 値型です。 したがって、構文ノードとは異なり、表されるトークンの種類に応じて意味を持つプロパティが混在する、すべての種類のトークンに対して 1 つの構造しかありません。
たとえば、整数リテラル トークンは数値を表します。 トークンがまたがる生のソース テキストに加えて、リテラル トークンには、正確にデコードされた整数値を示す Value プロパティがあります。 このプロパティは、多くのプリミティブ型の 1 つである可能性があるため、 Object として型指定されます。
ValueText プロパティは、Value プロパティと同じ情報を示しますが、このプロパティは常にStringとして型指定されます。 C# ソース テキストの識別子には Unicode エスケープ文字を含めることができますが、エスケープ シーケンス自体の構文は識別子名の一部とは見なされません。 そのため、トークンによってスパンされる生テキストにはエスケープ シーケンスが含まれますが、 ValueText プロパティには含まれません。 代わりに、エスケープによって識別される Unicode 文字が含まれます。 たとえば、ソース テキストに \u03C0
として書き込まれた識別子が含まれている場合、このトークンの ValueText プロパティは π
を返します。
構文トリビア
構文トリビアは、空白、コメント、プリプロセッサ ディレクティブなど、コードを通常理解するためにほとんど重要でないソース テキストの部分を表します。 構文トークンと同様に、トリビアは値型です。 単一の Microsoft.CodeAnalysis.SyntaxTrivia 型は、あらゆる種類のトリビアを記述するために使用されます。
トリビアは通常の言語構文の一部ではなく、2 つのトークン間の任意の場所に出現できるため、ノードの子として構文ツリーに含まれません。 ただし、リファクタリングなどの機能を実装し、ソース テキストを完全に忠実に保つ場合は重要であるため、構文ツリーの一部として存在します。
トークンの SyntaxToken.LeadingTrivia または SyntaxToken.TrailingTrivia コレクションを調べることで、トリビアにアクセスできます。 ソース テキストが解析されると、トリビアのシーケンスがトークンに関連付けられます。 一般に、トークンは、次のトークンまでの同じ行の後に任意のトリビアを所有します。 その行の後のトリビアは、次のトークンに関連付けられます。 ソース ファイルの最初のトークンは最初のすべてのトリビアを取得し、ファイル内の最後のトリビアのシーケンスはファイルの終わりのトークンに取り付けられます。それ以外の場合は幅が 0 です。
構文ノードとトークンとは異なり、構文トリビアには親がありません。 ただし、これらはツリーの一部であり、それぞれが 1 つのトークンに関連付けられているため、 SyntaxTrivia.Token プロパティを使用して関連付けられているトークンにアクセスできます。
スパン
各ノード、トークン、またはトリビアは、ソース テキスト内での位置と、それが構成される文字数を認識します。 テキスト位置は、0 から始まる char
インデックスである 32 ビット整数として表されます。
TextSpan オブジェクトは、開始位置と文字数であり、どちらも整数として表されます。
TextSpanの長さが 0 の場合は、2 文字の間の位置を参照します。
各ノードには、TextSpanとSpanの 2 つのFullSpanプロパティがあります。
Span プロパティは、ノードのサブツリー内の最初のトークンの先頭から最後のトークンの末尾までのテキスト スパンです。 このスパンには、先頭または末尾のトリビアは含まれません。
FullSpan プロパティは、ノードの通常のスパンに加えて、先頭または末尾のトリビアのスパンを含むテキスト スパンです。
例えば次が挙げられます。
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}
ブロック内のステートメント ノードには、1 つの垂直バー (|) で示されるスパンがあります。
throw new Exception("Not right.");
文字が含まれます。 完全スパンは、2 本の垂直バー (||) で示されます。 これには、範囲と同じ文字と先頭および末尾のトリビアに関連付けられている文字が含まれます。
種類
各ノード、トークン、またはトリビアには、SyntaxNode.RawKind型のSystem.Int32 プロパティがあり、表される正確な構文要素を識別します。 この値は、言語固有の列挙型にキャストできます。 C# または Visual Basic の各言語には、文法で使用可能なすべてのノード、トークン、トリビア要素を一覧表示する 1 つの SyntaxKind
列挙 (それぞれMicrosoft.CodeAnalysis.CSharp.SyntaxKind と Microsoft.CodeAnalysis.VisualBasic.SyntaxKind) があります。 この変換は、 CSharpExtensions.Kind または VisualBasicExtensions.Kind 拡張メソッドにアクセスすることで自動的に行うことができます。
RawKind プロパティを使用すると、同じノード クラスを共有する構文ノードの種類を簡単にあいまいに区別できます。 トークンとトリビアの場合、このプロパティは、ある種類の要素を別の要素と区別する唯一の方法です。
たとえば、1 つの BinaryExpressionSyntax クラスに子として Left、 OperatorToken、 Right があります。 Kind プロパティは、AddExpression、SubtractExpression、またはMultiplyExpressionの種類の構文ノードのいずれであるかを区別します。
エラー
ソース テキストに構文エラーが含まれている場合でも、ソースに対してラウンドトリップ可能な完全な構文ツリーが公開されます。 パーサーは、言語の定義された構文に準拠していないコードを検出すると、次の 2 つの手法のいずれかを使用して構文ツリーを作成します。
パーサーが特定の種類のトークンを予期していても見つからない場合は、トークンが予期された場所の構文ツリーに不足しているトークンを挿入する可能性があります。 不足しているトークンは、予期されていた実際のトークンを表しますが、空のスパンがあり、その SyntaxNode.IsMissing プロパティは
true
を返します。パーサーは、解析を続行できるトークンが見つかるまでトークンをスキップできます。 この場合、スキップされたトークンは、 SkippedTokensTriviaの種類を持つトリビア ノードとしてアタッチされます。
.NET