次の方法で共有


構文変換の概要

このチュートリアルは、「構文解析の概要」および「セマンティック解析の概要」クイック スタートで説明した概念と手法に基づいて作成されています。 これらのクイック スタートをまだ完了していない場合は、このクイック スタートを始める前に完了する必要があります。

このクイック スタートでは、構文ツリーを作成および変換する手法を学習します。 前のクイック スタートで学習した手法と組み合わせて、初めてのコマンド ライン リファクタリングを作成します。

インストール手順 - 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 コンパイラ プラットフォーム

不変性は、.NET コンパイラ プラットフォームの基本原則です。 不変データ構造は、作成後には変更できません。 不変データ構造は、複数のコンシューマーから安全かつ同時に共有、分析できます。 コンシューマーが、予期できない方法で別のコンシューマーに影響を及ぼす危険はありません。 アナライザーには、ロックやその他のコンカレンシー手段は不要です。 この規則は、構文ツリー、コンパイル、記号、セマンティック モデルなど、出現するすべてのデータ構造に当てはまります。 既存の構造体を変更するのではなく、API は古いオブジェクトに対して指定された相違点に基づいて、新しいオブジェクトを作成します。 この概念を構文ツリーに適用して、変換を使用して新しいツリーを作成します。

ツリーの作成と変換

構文の変換では、2 つの方法のうち 1 つを選択します。 ファクトリ メソッドは、置き換える特定のノードや、新しいコードを挿入する特定の場所を検索するときに最もよく使用されます。 リライターは、プロジェクト全体をスキャンして、置き換えるコード パターンを探す場合に最適です。

ファクトリ メソッドを使用してノードを作成する

最初の構文変換では、ファクトリ メソッドを使用します。 using System.Collections; ステートメントを using System.Collections.Generic; ステートメントで置き換えます。 この例は、Microsoft.CodeAnalysis.CSharp.SyntaxFactory ファクトリ メソッドを使用して Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode オブジェクトを作成する方法を示しています。 ノードトークントリビアの各種類に対して、その種類のインスタンスを作成するファクトリ メソッドが用意されています。 ボトムアップ方式でノードを階層的に構成して、構文ツリーを作成します。 次に、既存のノードを作成した新しいツリーで置き換えることで、既存のプログラムを変換します。

Visual Studio を起動し、新しい C# の Stand-Alone Code Analysis Tool プロジェクトを作成します。 Visual Studio で、 [ファイル]>[新規]>[プロジェクト] の順に選択して、[新しいプロジェクト] ダイアログを表示します。 [Visual C#]>[機能拡張] で、 [Stand-Alone Code Analysis Tool] を選択します。 このクイック スタートには 2 つのサンプル プロジェクトがあるため、ソリューションに「SyntaxTransformationQuickStart」、プロジェクトに「ConstructionCS」という名前を付けます。 [OK] をクリックします。

このプロジェクトでは、Microsoft.CodeAnalysis.CSharp.SyntaxFactory クラスのメソッドを使用して、System.Collections.Generic 名前空間を表す Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax を構築します。

Program.cs の先頭に、次の using ディレクティブを追加します。

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static System.Console;

using System.Collections.Generic; ステートメントを表すツリーをビルドするための名前構文ノードを作成します。 NameSyntax は、C# に現れる 4 つの型の基底クラスです。 これらの 4 つの型の名前を組み合わせて、C# 言語中に出現するすべての名前を作成できます。

IdentifierName(String) メソッドを使用して、NameSyntax ノードを作成します。 Program.cs で、Main メソッドに次のコードを追加します。

NameSyntax name = IdentifierName("System");
WriteLine($"\tCreated the identifier {name}");

上のコードでは、IdentifierNameSyntax オブジェクトを作成して変数 name に割り当てます。 Roslyn API の多くは、関連の型を使用しやすくするために基底クラスを返します。 NameSyntax である変数 name は、QualifiedNameSyntax をビルドするときに再利用できます。 サンプルをビルドするときに、型の推定を使用しないでください。 このプロジェクトではそのステップを自動化します。

これで名前が作成されました。 次に、QualifiedNameSyntax をビルドして、ツリー内にさらに多くのノードをビルドします。 新しいツリーでは、name を名前の左側として使用し、Collections 名前空間の新しい IdentifierNameSyntaxQualifiedNameSyntax の右側として使用します。 program.cs に次のコードを追加します。

name = QualifiedName(name, IdentifierName("Collections"));
WriteLine(name.ToString());

コードを再度実行し、結果を確認します。 コードを表すノードのツリーをビルドします。 このパターンを繰り返して、名前空間 System.Collections.GenericQualifiedNameSyntax をビルドします。 Program.cs に次のコードを追加します。

name = QualifiedName(name, IdentifierName("Generic"));
WriteLine(name.ToString());

プログラムを再度実行して、追加するコード用のツリーがビルドされたことを確認します。

変更されたツリーを作成する

これで、1 つのステートメントを含む小さな構文ツリーがビルドされました。 新しいノードを作成するための API は、単一のステートメントや他の小規模なコード ブロックを作成するときに適した選択肢です。 しかし、より大規模なコード ブロックをビルドするときは、ノードを置き換えたり既存のツリーにノードを挿入するためのメソッドを使用する必要があります。 すでに説明したように、構文ツリーは不変です。 構文 API には、作成後に既存の構文ツリーを変更するメカニズムはありません。 その代わり、既存のツリーへの変更に基づいて新しいツリーを生成するメソッドが用意されています。 With* メソッドは、SyntaxNode から派生した具象クラス内、または SyntaxNodeExtensions クラスで宣言された拡張メソッド内で定義されます。 これらのメソッドは、既存のノードの子プロパティに変更を適用することで新しいノードを作成します。 さらに、ReplaceNode 拡張メソッドを使用すると、サブツリー内の子孫ノードを置き換えることができます。 このメソッドも、新しく作成された子ノードをポイントするように親を更新し、このプロセスをツリー全体で上位に向かって繰り返します (このプロセスを、ツリーの "再スピン" と呼びます)。

次のステップでは、(小規模な) プログラム全体を表すツリーを作成した後で、それを変更します。 Program クラスの先頭に次のコードを追加します。

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

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

注意

このコード例では、System.Collections.Generic 名前空間ではなく System.Collections 名前空間を使用します。

次に、Main メソッドの末尾に、テキストを解析してツリーを作成する次のコードを追加します。

SyntaxTree tree = CSharpSyntaxTree.ParseText(sampleCode);
var root = (CompilationUnitSyntax)tree.GetRoot();

この例では、WithName(NameSyntax) メソッドを使用して、UsingDirectiveSyntax ノード内の名前を先ほどのコードで作成した名前に置き換えます。

名前 System.Collections を先ほどのコードで作成した名前に置き換えるには、WithName(NameSyntax) メソッドを使用して新しい UsingDirectiveSyntax ノードを作成します。 Main メソッドの末尾に次のコードを追加します。

var oldUsing = root.Usings[1];
var newUsing = oldUsing.WithName(name);
WriteLine(root.ToString());

プログラムを実行し、出力を注意深く見てください。 ルート ツリーに newUsing が配置されていません。 元のツリーは変更されていません。

新しいツリーを作成するために、ReplaceNode 拡張メソッドを使用した次のコードを追加します。 新しいツリーが、既存のインポートを更新された newUsing ノードで置き換えた結果として作成されます。 この新しいツリーを既存の root に割り当てます。

root = root.ReplaceNode(oldUsing, newUsing);
WriteLine(root.ToString());

再びプログラムを実行します。 今度は、ツリーに System.Collections.Generic 名前空間が正しくインポートされます。

SyntaxRewriters を使用してツリーを変換する

With* メソッドと ReplaceNode メソッドは、構文ツリーの個々のブランチを変換するのに便利な手段です。 Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter クラスは、構文ツリー上で複数の変換を実行します。 Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter クラスは Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor<TResult> のサブクラスです。 CSharpSyntaxRewriter は、特定の型の SyntaxNode に変換を適用します。 構文ツリー内にその型が存在すれば、複数の型の SyntaxNode オブジェクトに変換を適用できます。 このクイック スタートの 2 番目のプロジェクトでは、型の推定が使用される可能性があるすべての場所のローカル変数宣言に含まれる明示的な型を削除する、コマンド ライン リファクタリングを作成します。

新しい C# の Stand-Alone Code Analysis Tool プロジェクトを作成します。 Visual Studio で、SyntaxTransformationQuickStart ソリューション ノードを右クリックします。 [追加]>[新しいプロジェクト] を選択して、 [新しいプロジェクト] ダイアログを表示します。 [Visual C#]>[機能拡張] で、 [Stand-Alone Code Analysis Tool] を選択します。 プロジェクトに「TransformationCS」という名前を付けて、[OK] をクリックします。

最初のステップは、変換を実行するための CSharpSyntaxRewriter から派生したクラスを作成することです。 新しいクラスのファイルをプロジェクトに追加します。 Visual Studio で、 [プロジェクト]>[クラスの追加...] を選択します。 [新しい項目の追加] ダイアログで、ファイル名として「TypeInferenceRewriter.cs」を入力します。

TypeInferenceRewriter.cs ファイルに次の using ディレクティブを追加します。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

次に、TypeInferenceRewriter クラスで CSharpSyntaxRewriter クラスを拡張します。

public class TypeInferenceRewriter : CSharpSyntaxRewriter

次のコードを追加して、SemanticModel を保持する読み取り専用の private フィールドを宣言し、コンストラクター内で初期化します。 このフィールドは、後で型の推定を使用できる場所を特定するために必要になります。

private readonly SemanticModel SemanticModel;

public TypeInferenceRewriter(SemanticModel semanticModel) => SemanticModel = semanticModel;

VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) メソッドをオーバーライドします。

public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
{

}

注意

Roslyn API の多くは、返される実際の ランタイム型の基底クラスである戻り値の型を宣言します。 多くのシナリオでは、ノードが別の種類のノードに完全に置き換えられることがあり、削除される場合もあります。 この例では、VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) メソッドは LocalDeclarationStatementSyntax の派生型ではなく SyntaxNode を返します。 このリライターは、既存のノードに基づいて新しい LocalDeclarationStatementSyntax ノードを返します。

このクイック スタートでは、ローカル変数宣言を処理します。 これは foreach ループ、for ループ、LINQ 式、ラムダ式などの他の宣言にも拡張できます。 さらに、このリライターは、次のような最も単純な形式の宣言しか変換しません。

Type variable = expression;

自分で探究したい場合は、次のような変数宣言の型の完成したサンプルを拡張することを検討してください。

// Multiple variables in a single declaration.
Type variable1 = expression1,
     variable2 = expression2;
// No initializer.
Type variable;

次のコードを VisitLocalDeclarationStatement メソッドの本体に追加して、これらの形式の宣言の書き換えをスキップするようにします。

if (node.Declaration.Variables.Count > 1)
{
    return node;
}
if (node.Declaration.Variables[0].Initializer == null)
{
    return node;
}

このメソッドは、node パラメーターを変更せずに返すことにより、書き換えが行われないことを示します。 いずれの if 式も true でない場合、ノードは初期化で可能な宣言を表します。 以下のステートメントを追加して、宣言で指定されている型の名前を抽出し、それを SemanticModel フィールドを使用してバインドして、型のシンボルを取得できるようにします。

var declarator = node.Declaration.Variables.First();
var variableTypeName = node.Declaration.Type;

var variableType = (ITypeSymbol)SemanticModel
    .GetSymbolInfo(variableTypeName)
    .Symbol;

さらに、次のステートメントを追加して、初期化子式をバインドします。

var initializerInfo = SemanticModel.GetTypeInfo(declarator.Initializer.Value);

最後に、次の if ステートメントを追加して、初期化子式の型が指定の型と一致した場合に既存の型名を var キーワードで置き換えるようにします。

if (SymbolEqualityComparer.Default.Equals(variableType, initializerInfo.Type))
{
    TypeSyntax varTypeName = SyntaxFactory.IdentifierName("var")
        .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
        .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

    return node.ReplaceNode(variableTypeName, varTypeName);
}
else
{
    return node;
}

宣言では初期化子式が基底クラスまたはインターフェイスにキャストされる場合があるので、この条件が必要です。 必要な場合は、割り当ての左側の型と右側の型が一致しません。 このようなケースで明示的な型を削除すると、プログラムのセマンティクスが変わってしまいます。 var はコンテキスト キーワードであるため、var はキーワードではなく識別子として指定されます。 垂直方向の空白とインデントを維持するために、先頭および末尾のトリビア (空白) が古い型名から var キーワードへと転送されています。 型名は実際には宣言ステートメントの孫であるため、With* よりも ReplaceNode を使用して LocalDeclarationStatementSyntax を変換するほうが簡単です。

これで TypeInferenceRewriter が完成しました。 Program.cs ファイルに戻って例を完成させましょう。 テスト用の Compilation を作成し、そこから SemanticModel を取得します。 その SemanticModel を使用して、TypeInferenceRewriter を試します。 このステップは最後に実行します。 それまでの間、テスト用のコンパイルを表すプレースホルダー変数を宣言しておきます。

Compilation test = CreateTestCompilation();

しばらくすると、CreateTestCompilation メソッドが存在しないことを通知するエラーの波線が表示されます。 Ctrl + . キーを押して電球を開き、Enter キーを押して [メソッド スタブの生成] コマンドを呼び出します。 このコマンドにより、Program クラス内に CreateTestCompilation メソッドのメソッド スタブが生成されます。 このメソッドについては後で説明します。

C# Generate method from usage

テスト用の Compilation 内の各 SyntaxTree を反復処理する次のコードを記述します。 処理ごとに、そのツリーの SemanticModel を持った 新しい TypeInferenceRewriter が初期化されます。

foreach (SyntaxTree sourceTree in test.SyntaxTrees)
{
    SemanticModel model = test.GetSemanticModel(sourceTree);

    TypeInferenceRewriter rewriter = new TypeInferenceRewriter(model);

    SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

    if (newSource != sourceTree.GetRoot())
    {
        File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
    }
}

作成した foreach ステートメント内に次のコードを追加して、各ソース ツリーで変換が実行されるようにします。 このコードは、何らかの編集が行われた場合に、変換された新しいツリーを条件付きで書き出します。 リライターは、型の推定を使用して単純化される可能性があるローカル変数宣言が 1 つ以上見つかった場合にのみ、ツリーを変更します。

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

if (newSource != sourceTree.GetRoot())
{
    File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}

File.WriteAllText コードの下に波線が表示されるはずです。 電球を選択し、必要な using System.IO; ステートメントを追加します。

完了までもう少しです。 最後の 1 ステップは、Compilation の作成です。 このクイック スタートでは型の推定を一度も使用していないので、テスト ケースとするには完璧です。 残念ながら、C# プロジェクト ファイルからコンパイルを作成する方法については、このチュートリアルの対象外です。 しかし幸いなことに、これまでの手順に慎重に従ってきたならば希望が持てます。 CreateTestCompilation メソッドの内容を次のコードに置き換えます。 このクイック スタートで説明したプロジェクトに偶然にも一致するテスト用コンパイルが作成されます。

String programPath = @"..\..\..\Program.cs";
String programText = File.ReadAllText(programPath);
SyntaxTree programTree =
               CSharpSyntaxTree.ParseText(programText)
                               .WithFilePath(programPath);

String rewriterPath = @"..\..\..\TypeInferenceRewriter.cs";
String rewriterText = File.ReadAllText(rewriterPath);
SyntaxTree rewriterTree =
               CSharpSyntaxTree.ParseText(rewriterText)
                               .WithFilePath(rewriterPath);

SyntaxTree[] sourceTrees = { programTree, rewriterTree };

MetadataReference mscorlib =
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
        MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
        MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);

MetadataReference[] references = { mscorlib, codeAnalysis, csharpCodeAnalysis };

return CSharpCompilation.Create("TransformationCS",
    sourceTrees,
    references,
    new CSharpCompilationOptions(OutputKind.ConsoleApplication));

幸運を祈ってプロジェクトを実行しましょう。 Visual Studio で、 [デバッグ]>[デバッグの開始] を選択します。 Visual Studio で、プロジェクト内のファイルが変更されたという通知が表示されるはずです。 [すべてに適用] をクリックして、変更されたファイルをリロードします。 それらを調べて成果を確認しましょう。 明示的で冗長な型指定子がすべてなくなるとどれほどコードがすっきり見えるかに注目してください。

おめでとうございます! コンパイラ API を使用して、C# プロジェクト内のすべてのファイルで特定の構文パターンを検索し、それらのパターンに一致するソース コードのセマンティクスを分析して変換する独自のリファクタリングを作成できました。 これであなたも正式なリファクタリングの作成者です!