次の方法で共有


セマンティック解析の概要

このチュートリアルでは、構文 API の知識を前提としています。 「構文解析の概要」という記事が入門編になっています。

このチュートリアルでは、シンボル APIバインドの API について学習します。 これらの API は、プログラムの意味論的意味に関する情報を提供します。 プログラムのシンボルが表す型について質問したり、回答したりできます。

.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 エディター] チェック ボックスをオンにします。 [コード ツール] セクションに表示されます。

コンパイルとシンボルについて

.NET コンパイラ SDK での作業が増えると、構文 API とセマンティック API の違いに詳しくなります。 構文 API では、プログラムの構造を見ることができます。 ただし、多くの場合、プログラムの意味論または意味に関する豊富な情報が必要になります。 Visual Basic または C# の緩いコード ファイルまたはスニペットは分離して構文的に解析できますが、孤立状態では、"この変数の型は何ですか" のような質問を問うことに意味がありません。 型名の意味は、アセンブリ参照、名前空間インポート、その他のコード ファイルに依存することがあります。 このような問いには、セマンティック API で、具体的には Microsoft.CodeAnalysis.Compilation クラスで答えられます。

Compilation のインスタンスはコンパイラで見られるように 1 つのプロジェクトに類似し、Visual Basic または C# のプログラムをコンパイルするために必要なすべてを表します。 コンパイルには、コンパイルするソース ファイルのセット、アセンブリ参照、コンパイラ オプションが含まれます。 この文脈のその他すべての情報を利用し、コードの意味を推論できます。 Compilation では、型、名前空間、メンバー、名前やその他の式が参照する変数などのエンティティであるシンボルを見つけることができます。 名前や式をシンボルと関連付けるプロセスをバインドと呼んでいます。

Microsoft.CodeAnalysis.SyntaxTree と同様に、Compilation は言語固有の派生物を持つ抽象クラスです。 コンパイルのインスタンスを作成するとき、Microsoft.CodeAnalysis.CSharp.CSharpCompilation (または Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation) クラスでファクトリ メソッドを呼び出す必要があります。

シンボルにクエリを実行する

このチュートリアルでは、"Hello World" プログラムをもう一度見てみます。 今回、プログラムの中のシンボルにクエリを実行し、そのシンボルが表す型を理解します。 名前空間の型について問い、型で利用できるメソッドを確認します。

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

注意

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

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

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

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

        const string programText =
@"using System;
using System.Collections.Generic;
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();

次に、作成済みのツリーから CSharpCompilation をビルドします。 "Hello World" サンプルは、String 型と Console 型に基づきます。 コンパイルでこの 2 つの型を宣言するアセンブリを参照する必要があります。 Main メソッドに次の行を追加し、適切なアセンブリの参照を含む、構文ツリーのコンパイルを作成します。

var compilation = CSharpCompilation.Create("HelloWorld")
    .AddReferences(MetadataReference.CreateFromFile(
        typeof(string).Assembly.Location))
    .AddSyntaxTrees(tree);

CSharpCompilation.AddReferences メソッドはコンパイルに参照を追加します。 MetadataReference.CreateFromFile メソッドは参照としてアセンブリを読み込みます。

セマンティック モデルにクエリを実行する

Compilation が与えられたら、その Compilation に含まれている SyntaxTree について SemanticModel を求めることができます。 セマンティック モデルは、通常は IntelliSense から得られるすべての情報源としてとらえることができます。 SemanticModel は "この場所のスコープ内にはどのような名前があるか"、"このメソッドからアクセスできるメンバーはどれか"、"このテキストのブロックではどのような変数が使用されているか"、"この名前/式は何を参照しているか" のような質問に答えることができます。このステートメントを追加し、セマンティック モデルを作成します。

SemanticModel model = compilation.GetSemanticModel(tree);

名前をバインドする

CompilationSyntaxTree から SemanticModel を作成します。 モデルを作成したら、それにクエリを実行し、最初の using ディレクティブを見つけ、System 名前空間のシンボル情報を取得できます。 この 2 つの行を Main メソッドに追加し、セマンティック モデルを作成し、最初の using ステートメントのシンボルを取得します。

// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;

// Use the semantic model for symbol information:
SymbolInfo nameInfo = model.GetSymbolInfo(systemName);

先のコードは、最初の using ディレクティブの名前をバインドして、System 名前空間の Microsoft.CodeAnalysis.SymbolInfo を取得する方法を示しています。 先のコードでは、構文モデルを利用してコードの構造を見つけることも確認できます。セマンティック モデルを使用し、その意味を理解します。 構文モデルは、using ステートメントの文字列 System を見つけます。 セマンティック モデルには、System 名前空間に定義されている型に関するすべての情報があります。

SymbolInfo オブジェクトから、SymbolInfo.Symbol プロパティを利用して Microsoft.CodeAnalysis.ISymbol を取得できます。 このプロパティは、この式が参照するシンボルを返します。 何も参照しない式の場合 (数値リテラルなど)、このプロパティは null です。 SymbolInfo.Symbol が null ではないとき、ISymbol.Kind はシンボルの型を示します。 この例では、ISymbol.Kind プロパティは SymbolKind.Namespace です。 次のコードを Main メソッドに追加します。 System 名前空間のシンボルを取得し、System 名前空間で宣言されているすべての子名前空間を表示します。

var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
    foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
    {
        Console.WriteLine(ns);
    }
}

プログラムを実行します。次の出力が表示されるはずです。

System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .

注意

この出力には、System 名前空間の子名前空間の一部が含まれていません。 このコンパイルに存在し、System.String が宣言されているアセンブリのみを参照するすべての名前空間が表示されます。 他のアセンブリで宣言されている名前空間は、このコンパイルでは認識されません。

式をバインドする

先のコードでは、名前にバインドしてシンボルを見つける方法を確認できます。 C# プログラムには、バウンドできて名前ではない式が他にあります。 この機能を見るために、単純な文字列リテラルのバインドにアクセスしましょう。

"Hello World" プログラムには、Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax、つまりコンソールに表示される "Hello, World!" 文字列が含まれます。

"Hello, World!" 文字列を見つけるには、プログラム内の 1 つの文字列リテラルを探します。 構文ノードが見つかったら、セマンティック モデルからそのノードの型情報を取得します。 次のコードを Main メソッドに追加します。

// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();

// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);

Microsoft.CodeAnalysis.TypeInfo 構造体には TypeInfo.Type プロパティが含まれます。このプロパティによって、リテラルの型に関するセマンティック情報にアクセスできます。 この例では、string 型です。 このプロパティをローカル変数に割り当てる宣言を追加します。

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

このチュートリアルの終わりとして、string を返す string 型で宣言されているすべてのパブリック メソッドのシーケンスを作成する LINQ クエリをビルドしましょう。 このクエリは複雑になります。そのため、1 行ずつビルドしてから 1 つのクエリとして再構築します。 このクエリのソースは、string 型で宣言されているすべてのメンバーのシーケンスです。

var allMembers = stringTypeSymbol?.GetMembers();

そのソース シーケンスには、プロパティやフィールドなど、すべてのメンバーが含まれています。そのため、ImmutableArray<T>.OfType メソッドでフィルター処理し、Microsoft.CodeAnalysis.IMethodSymbol オブジェクトである要素を見つけます。

var methods = allMembers?.OfType<IMethodSymbol>();

次に、パブリック メソッドのみを返す別のフィルターを追加し、string を返します。

var publicStringReturningMethods = methods?
    .Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
    m.DeclaredAccessibility == Accessibility.Public);

名前プロパティのみを選択します。オーバーロードを削除し、別個の名前のみを選択します。

var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();

LINQ クエリ構文で完全クエリを構築し、コンソールにすべてのメソッド名を表示することもできます。

foreach (string name in (from method in stringTypeSymbol?
                         .GetMembers().OfType<IMethodSymbol>()
                         where SymbolEqualityComparer.Default.Equals(method.ReturnType, stringTypeSymbol) &&
                         method.DeclaredAccessibility == Accessibility.Public
                         select method.Name).Distinct())
{
    Console.WriteLine(name);
}

プログラムをビルドして実行します。 次の出力が表示されます。

Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .

セマンティック API を使用し、このプログラムの含まれるシンボルに関する情報を見つけ、表示しました。