« 2013年11月 | メイン | 2014年01月 »

2013年12月 アーカイブ

2013年12月03日

[C#][.NET][CodeDOM] メタプログラミング入門 - CodeDOM による Hello world!

Metasequoia

※ 「[C#][.NET] メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプログラミング (パフォーマンスのテスト)」の続き。

CodeDOM による動的コード生成

これまで、Reflection.Emit、式木、Roslyn による動的コード生成を試してきた。これらは、比較的新しい方法だが、実は .NET Framework には初期の頃から動的コード生成を行う仕組みが備わっていた。

それが、CodeDOM (Code Document Object Model) だ。

System.CodeDom 名前空間System.CodeDom.Compiler 名前空間にあるクラスを使うことで、C# や Visual Basic.NET 等のコードを生成することができる。

今回は、CodeDOM を使って Hello world! を表示する簡単なプログラムを生成してみよう。

CodeDOM による Hello world!

CodeDOM を使って、次の手順でプログラムを組み立てていく。

  1. 名前空間の生成
  2. 名前空間への System 名前空間のインポート
  3. Program クラスの生成
  4. Program クラスの CodeDomHelloWorldDemo 名前空間への追加
  5. Main メソッドの生成
  6. Main メソッドの中身に文を追加
  7. Program クラスに Main メソッドを追加

では、やってみよう。 System.CodeDom 名前空間を使用する。

using System.CodeDom;

class Program
{
    // Hello world プログラムの CodeDOM
    static CodeNamespace HelloWorldCodeDom()
    {
        // CodeDomHelloWorldDemo 名前空間
        var nameSpace = new CodeNamespace(name: "CodeDomHelloWorldDemo");
        // System 名前空間のインポート
        nameSpace.Imports.Add(new CodeNamespaceImport(nameSpace: "System"));
        // Program クラス
        var programClass = new CodeTypeDeclaration(name: "Program");
        // Program クラスの CodeDomHelloWorldDemo 名前空間への追加
        nameSpace.Types.Add(programClass);
        // Main メソッド
        var mainMethod   = new CodeMemberMethod { Attributes = MemberAttributes.Static, Name = "Main" };
        // Main メソッドの中身に文を追加
        mainMethod.Statements.Add(
            new CodeMethodInvokeExpression( // 関数呼び出し式
                targetObject: new CodeSnippetExpression("Console")       , // オブジェクト名: Console.
                methodName  : "WriteLine"                                , // メソッド名    : WriteLine
                parameters  : new CodePrimitiveExpression("Hello world!")  // 引数          : ("Hello world!")
            )
        );
        // Main メソッドの中身に文を追加
        mainMethod.Statements.Add(
            new CodeMethodInvokeExpression( // 関数呼び出し式
                targetObject: new CodeSnippetExpression("Console"), // オブジェクト名: Console.
                methodName  : "ReadKey"                             // メソッド名    : ReadKey()
            )
        );
        // Program クラスに Main メソッドを追加
        programClass.Members.Add(mainMethod);

        return nameSpace;
    }

    static void Main()
    {
        // Hello world プログラムの名前空間を生成
        var helloWorldCodeDom = HelloWorldCodeDom();
    }
}

この HelloWorldCodeDom メソッドで、Hello world! を表示する Main メソッドを含む名前空間を生成したことになる。

CodeDOM によるソースコードの生成

次に、上で作った名前空間から CodeDOM を使って C# のソースコードを生成してみる。 System.CodeDom.Compiler 名前空間を使用する。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;

class Program
{
    // Hello world プログラムの CodeDOM
    static CodeNamespace HelloWorldCodeDom()
    {
        …… 同じなので省略 ……
    }

    // 名前空間からソースコードを生成
    static string GenerateCode(CodeNamespace codeNamespace)
    {
        // コンパイル オプション
        var compilerOptions = new CodeGeneratorOptions { IndentString = "    ", BracingStyle = "C" };

        var codeText = new StringBuilder();
        using (var codeWriter = new StringWriter(codeText)) {
            // 名前空間からソースコードを生成
            CodeDomProvider.CreateProvider("C#").GenerateCodeFromNamespace(codeNamespace, codeWriter, compilerOptions);
        }
        return codeText.ToString(); // 生成されたソースコード
    }

    static void Main()
    {
        // Hello world プログラムの名前空間を生成
        var helloWorldCodeDom = HelloWorldCodeDom();

        // Hello world プログラムのソースコードを生成
        var code = GenerateCode(helloWorldCodeDom);
        Console.WriteLine(code); // 表示
    }
}

実行してみると、次のように C# のソースコードが表示される。

namespace CodeDomHelloWorldDemo
{
    using System;


    public class Program
    {

        static void Main()
        {
            Console.WriteLine("Hello world!");
            Console.ReadKey();
        }
    }
}

ちなみに、上記 GenerateCode メソッド中の "C#" とある部分を "VB" と置き換えて実行してみると、次のように Visual Basic.NET のソースコードとなる。

Imports System

Namespace CodeDomHelloWorldDemo

    Public Class Program

        Shared Sub Main()
            Console.WriteLine("Hello world!")
            Console.ReadKey
        End Sub
    End Class
End Namespace

"C#" を "JScript" に置き換えた場合は、次のようになる。

//@cc_on
//@set @debug(off)

import System;

package CodeDomHelloWorldDemo
{

    public class Program
    {

        private static function Main()
        {
            Console.WriteLine("Hello world!");
            Console.ReadKey();
        }
    }
}

CodeDOM によるアセンブリの生成

では次に、CodeDOM で生成した名前空間をコンパイルしてアセンブリを生成してみよう。

先程までのプログラムに、更に書き足して、次のようにする。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Text;

class Program
{
    // Hello world プログラムの CodeDOM
    static CodeNamespace HelloWorldCodeDom()
    {
        …… 同じなので省略 ……
    }

    // 名前空間からソースコードを生成
    static string GenerateCode(CodeNamespace codeNamespace)
    {
        …… 同じなので省略 ……
    }

    // 名前空間を実行可能アセンブリへコンパイル
    static void CompileExecutableAssembly(CodeNamespace codeNamespace, string outputAssemblyName)
    {
        var codeCompileUnit = new CodeCompileUnit();     // コンパイル単位
        codeCompileUnit.Namespaces.Add(codeNamespace);   // コンパイル単位に名前空間を追加
        // 実行可能アセンブリへコンパイル
        CodeDomProvider.CreateProvider("C#").CompileAssemblyFromDom(
            options         : new CompilerParameters {   // コンパイル オプション
                OutputAssembly     = outputAssemblyName, // 出力アセンブリのファイル名
                GenerateExecutable = true                // 実行可能なアセンブリを生成
            },
            compilationUnits: codeCompileUnit                     // コンパイル単位
        );
    }

    static void Main()
    {
        // Hello world プログラムの名前空間を生成
        var helloWorldCodeDom = HelloWorldCodeDom();

        // Hello world プログラムのソースコードを生成
        var code = GenerateCode(helloWorldCodeDom);
        Console.WriteLine(code); // 表示

        // Hello world プログラムを、実行可能アセンブリとしてコンパイル
        const string outputAssemblyName = "CodeDomHelloWorldDemo.exe";
        CompileExecutableAssembly(codeNamespace: HelloWorldCodeDom(), outputAssemblyName: outputAssemblyName);
        Process.Start(fileName: outputAssemblyName); // 生成された実行可能アセンブリを実行
    }
}

実行してみると、"CodeDomHelloWorldDemo.exe" が立ち上がり、Hello world! と表示される。

Hello world!

まとめ

今回は、CodeDOM を使った動的コード生成を行ってみた。 次回も CodeDOM を使ってみよう。

2013年12月04日

[C#][.NET][CodeDOM] メタプログラミング入門 - CodeDOM によるクラスの生成

Metasequoia

※ 「[C#][.NET][CodeDOM] メタプログラミング入門 - CodeDOM による Hello world!」の続き。

CodeDOM によるクラスの動的生成

前回は、CodeDOM を使って Hello world! を表示するプログラムを動的に生成した。

もう少し CodeDOM を使ってみよう。 今回は、CodeDOM を使ってクラスを作ってみる。

次のような Item クラスを動的に作ることにする。

namespace CodeDomClassDemo
{
    public class Item
    {
        private int price;

        public int Price
        {
            get
            {
                return this.price;
            }
            set
            {
                this.price = value;
            }
        }

        public override string ToString()
        {
            return (this.Price + "円");
        }
    }
}

次のような手順だ。

  1. CodeDomClassDemo 名前空間の生成
    ― CodeNamespace を new
  2. Item クラスの生成
    ― CodeTypeDeclaration を new
  3. Item クラスを CodeDomClassDemo 名前空間へ追加
  4. price フィールドを生成
    ― CodeMemberField を new
  5. price フィールドを Item クラスに追加
  6. Price プロパティを生成
    ― CodeMemberProperty を new
  7. Price プロパティの Get を追加
    ― price フィールドへの参照を return する文を生成して追加
  8. Price プロパティの Set を追加
    ― price フィールドへの参照に value を代入する文を生成して追加
  9. Price プロパティを Item クラスに追加
  10. ToString メソッドを生成
    ― CodeMemberMethod を new
  11. ToString メソッドの中身を追加
    ― Price プロパティへの参照に &qout;円&qout; を + して return する文を生成して追加
  12. ToString メソッドを Item クラスに追加

では、やってみよう。 前回同様、System.CodeDom 名前空間を使用する。

using System.CodeDom;

class Program
{
    // Item クラスの Code DOM
    static CodeNamespace ItemClassCodeDom()
    {
        // CodeDomHelloWorldDemo 名前空間
        var nameSpace              = new CodeNamespace(name: "CodeDomClassDemo");
        //// System 名前空間のインポート
        //nameSpace.Imports.Add(new CodeNamespaceImport(nameSpace: "System"));

        // Item クラス
        var itemClass              = new CodeTypeDeclaration(name: "Item");
        // CodeDomHelloWorldDemo 名前空間に Item クラスを追加
        nameSpace.Types.Add(itemClass);

        // price フィールド
        var priceField             = new CodeMemberField(type: typeof(int), name: "price") {
            Attributes = MemberAttributes.Private // private
        };
        // Item クラスに price フィールドを追加
        itemClass.Members.Add(priceField);

        // Price プロパティ
        var priceProperty          = new CodeMemberProperty {
            Name       = "Price"                                         , // 名前は、Price                 
            Attributes = MemberAttributes.Public | MemberAttributes.Final, // public で virtual じゃない
            Type       = new CodeTypeReference(typeof(int))                // 型は int
        };
        // price フィールドへの参照
        var priceFieldReference    = new CodeFieldReferenceExpression(
            targetObject: new CodeThisReferenceExpression(), // this.
            fieldName   : "price"                            // price
        );
        // Price プロパティの Get を追加
        priceProperty.GetStatements.Add(
            new CodeMethodReturnStatement(priceFieldReference) // price フィールドへの参照を return する文
        );
        // Price プロパティの Set を追加
        priceProperty.SetStatements.Add(
            new CodeAssignStatement(                                  // 代入文
                left : priceFieldReference                          , // 左辺は、price フィールドへの参照
                right: new CodePropertySetValueReferenceExpression()  // 右辺は、value
            )
        );
        // Item クラスに Price プロパティを追加
        itemClass.Members.Add(priceProperty);

        // ToString メソッド
        var toStringMethod         = new CodeMemberMethod {
            Name       = "ToString"                                         , // 名前は、ToString
            Attributes = MemberAttributes.Public | MemberAttributes.Override, // public で override
            ReturnType = new CodeTypeReference(typeof(string))                // 戻り値の型は、string
        };
        // Price プロパティへの参照
        var pricePropertyReference = new CodePropertyReferenceExpression(
            targetObject: new CodeThisReferenceExpression(), // this.
            propertyName: "Price"                            // Price
        );
        // ToString メソッドの中身
        toStringMethod.Statements.Add(
            new CodeMethodReturnStatement(                    // return 文
                new CodeBinaryOperatorExpression(             // 二項演算子の式
                    left : pricePropertyReference           , // Price プロパティへの参照
                    op   : CodeBinaryOperatorType.Add       , // +
                    right: new CodePrimitiveExpression("円")  // "円"
                )
            )
        );
        // Item クラスに ToString メソッドを追加
        itemClass.Members.Add(toStringMethod);

        return nameSpace;
    }

    static void Main()
    {
        // Item クラスを含む名前空間を CodeDOM で生成
        var itemClassCodeDom = ItemClassCodeDom();
    }
}

この ItemClassCodeDom メソッドで、Item クラスを含む名前空間を生成したことになる。

CodeDOM によるソースコードの生成

前回同様、上で作った名前空間から CodeDOM を使って C# のソースコードを生成してみる。 手順は前回と全く同じで、System.CodeDom.Compiler 名前空間を使用する。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;

class Program
{
    // Item クラスの Code DOM
    static CodeNamespace ItemClassCodeDom()
    {
        …… 同じなので省略 ……
    }

    // 名前空間からソースコードを生成
    static string GenerateCode(CodeNamespace codeNamespace)
    {
        // コンパイル オプション
        var compilerOptions = new CodeGeneratorOptions { IndentString = "    ", BracingStyle = "C" };

        var codeText        = new StringBuilder();
        using (var codeWriter = new StringWriter(codeText)) {
            // 名前空間からソースコードを生成
            CodeDomProvider.CreateProvider("C#").GenerateCodeFromNamespace(codeNamespace, codeWriter, compilerOptions);
        }
        return codeText.ToString(); // 生成されたソースコード
    }

    static void Main()
    {
        // Item クラスを含む名前空間を CodeDOM で生成
        var itemClassCodeDom = ItemClassCodeDom();

        // Item クラスのソースコードを生成
        var code             = GenerateCode(itemClassCodeDom);
        Console.WriteLine(code);
    }
}

実行してみると、次のように C# のソースコードが表示される。

namespace CodeDomClassDemo
{


    public class Item
    {

        private int price;

        public int Price
        {
            get
            {
                return this.price;
            }
            set
            {
                this.price = value;
            }
        }

        public override string ToString()
        {
            return (this.Price + "円");
        }
    }
}

GenerateCode メソッド中の "C#" の部分を "VB" に置き換えて実行した場合は次の通り。

Namespace CodeDomClassDemo

    Public Class Item

        Private price As Integer

        Public Property Price() As Integer
            Get
                Return Me.price
            End Get
            Set
                Me.price = value
            End Set
        End Property

        Public Overrides Function ToString() As String
            Return (Me.Price + "円")
        End Function
    End Class
End Namespace

CodeDOM によるアセンブリの生成

次に、CodeDOM で生成した名前空間をコンパイルしてアセンブリを生成してみよう。

前回は、アセンブリをファイルに出力したが、今回はオンメモリで生成してみる。

生成したアセンブリの中から Item クラスを取り出してインスタンスを生成し、Price プロパティや ToString メソッドを使ってみよう。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

class Program
{
    // Item クラスの Code DOM
    static CodeNamespace ItemClassCodeDom()
    {
        …… 同じなので省略 ……
    }
    
    // 名前空間からソースコードを生成
    static string GenerateCode(CodeNamespace codeNamespace)
    {
        …… 同じなので省略 ……
    }

    // 名前空間をアセンブリへコンパイル
    static Assembly CompileAssembly(CodeNamespace codeNamespace)
    {
        var codeCompileUnit = new CodeCompileUnit();   // コンパイル単位
        codeCompileUnit.Namespaces.Add(codeNamespace); // コンパイル単位に名前空間を追加
        // アセンブリへコンパイル
        var compilerResults = CodeDomProvider.CreateProvider("C#").CompileAssemblyFromDom(
            options         : new CompilerParameters { // コンパイル オプション
                GenerateInMemory = true                // アセンブリをメモリ内で生成
            },
            compilationUnits: codeCompileUnit          // コンパイル単位
        );
        return compilerResults.CompiledAssembly; // コンパイルされたアセンブリを返す
    }

    static void Main()
    {
        // Item クラスを含む名前空間を CodeDOM で生成
        var itemClassCodeDom = ItemClassCodeDom();

        // Item クラスのソースコードを生成
        var code             = GenerateCode(itemClassCodeDom);
        Console.WriteLine(code);

        // Item クラスを、アセンブリとしてコンパイル
        var itemAssembly     = CompileAssembly(codeNamespace: itemClassCodeDom);
        // Item クラス アセンブリのテスト
        TestItemAssembly(itemAssembly);
    }

    // Item クラス アセンブリのテスト
    static void TestItemAssembly(Assembly itemAssembly)
    {
        // Item クラスを、アセンブリから取得
        var     itemType = itemAssembly.GetTypes().First();
        // Item クラスのインスタンスを動的に生成
        dynamic item     = Activator.CreateInstance(itemType);

        // Item クラスの Price プロパティのテスト
        item.Price       = 2980;
        Console.WriteLine(item.Price);
        // Item クラスの ToString メソッドのテスト
        Console.WriteLine(item);
    }
}

実行してみると、次のように表示され、Price プロパティとToString メソッドが正常に使えるのが分かる。

2980
2980円

まとめ

今回は、CodeDOM を使った動的にクラスを生成した。CodeDOM を使うことで、クラスを実行時に作ることができる。

2013年12月12日

[C#][.NET][Roslyn] メタプログラミング入門 - Roslyn による C# ソースコードの解析と変更

Metasequoia

この記事は、「C# Advent Calendar 2013」の 12 月 12 日分。

※ 「[C#][.NET][CodeDOM] メタプログラミング入門 - CodeDOM によるクラスの生成」の続き。

Roslyn

Roslyn は、C# や Visual Basic のコンパイラーの内部の API 等を公開したものだ。"Compiler as a Service" と表現されている。

Roslyn に関しては、以前、次にあげる記事で扱った。参考にしてほしい。

Roslyn によるコード解析

先ず、「Roslyn による Visual Studio のアドイン」で行った Roslyn を使った C# のソースコードの解析を、もっと簡単な例でやってみたい。

次の手順でコード解析のサンプルを準備する。

  1. Visual Studio で C# のコンソール アプリケーションを作成
  2. メタプログラミング入門 - Roslyn による Add メソッドの動的生成」のときと同様の手順で Roslyn をインストール
    1. Visual Studio の「ソリューション エクスプローラー」でプロジェクト名を右クリックし、「NuGet パッケージの管理...」を開く
    2. "Roslyn" を検索し、インストール

このプロジェクトの Main メソッド内に、単純な C# のソースコードを文字列として準備する。 「Program クラスの中に空の Main メソッドがあるだけ」のソースコードだ。

class Program
{
    static void Main()
    {
        // 解析する C# のソースコード
        var sourceCode = @"
            using System;

            class Program
            {
                static void Main()
                {}
            }
        ";
    }
}

この単純な C# のソースコードの文字列を、Roslyn でパースしてシンタックス ツリーに変換し、簡単な解析をしてみよう。

それには、Roslyn.Compilers.CSharp 名前空間の SyntaxWalker クラスを用いる。

このクラスは、Visitor パターンになっていて、これを継承し、各種メソッドをオーバーライドすることで、様々な種類のノードやトークンを辿ることができるようになっている。

例えば、次のような SyntaxWalker の派生クラスを用意し、各ノードを Visit するメソッドをオーバーライドすると、ソースコードの構成要素であるノードを全部辿ることができる。

using Roslyn.Compilers.CSharp;
using System;

class Walker : SyntaxWalker // Visitor パターンでソースコードを解析
{
    public override void Visit(SyntaxNode node) // 各ノードを Visit
    {
        if (node != null)
            Console.WriteLine("[Node  - Type: {0}, Kind: {1}]\n{2}\n", node.GetType().Name, node.Kind, node);

        base.Visit(node);
    }
}

この Walker クラスで、先程の単純な C# のソースコードを解析してみる。

using Roslyn.Compilers.CSharp;

class Program
{
    static void Main()
    {
        // 解析する C# のソースコード
        var sourceCode = @"
            using System;

            class Program
            {
                static void Main()
                {}
            }
        ";

        var syntaxTree = SyntaxTree.ParseText(sourceCode); // ソースコードをパースしてシンタックス ツリーに
        var rootNode   = syntaxTree.GetRoot();             // ルートのノードを取得

        new Walker().Visit(rootNode);                      // 解析
    }
}

実行してみよう。

[Node  - Type: CompilationUnitSyntax, Kind: CompilationUnit]
using System;

    class Program
    {
        static void Main()
        {}
    }


[Node  - Type: UsingDirectiveSyntax, Kind: UsingDirective]
using System;

[Node  - Type: IdentifierNameSyntax, Kind: IdentifierName]
System

[Node  - Type: ClassDeclarationSyntax, Kind: ClassDeclaration]
class Program
    {
        static void Main()
        {}
    }

[Node  - Type: MethodDeclarationSyntax, Kind: MethodDeclaration]
static void Main()
        {}

[Node  - Type: PredefinedTypeSyntax, Kind: PredefinedType]
void

[Node  - Type: ParameterListSyntax, Kind: ParameterList]
()

[Node  - Type: BlockSyntax, Kind: Block]
{}

各ノードの情報が表示される。 ノードは入れ子になっているのが分かる。

次に、Walker クラスを少し変更して、ノードでなく、より細かいソースコードの構成要素であるトークンを表示してみる。 今度は、各トークンを Visit する VisitToken メソッドをオーバーライドして、全トークンを辿ってみる。

class Walker : SyntaxWalker // Visitor パターンでソースコードを解析
{
    public Walker() : base(depth: SyntaxWalkerDepth.Token) // トークンの深さまで Visit
    {}

    public override void VisitToken(SyntaxToken token) // 各トークンを Visit
    {
        if (token != null)
            Console.WriteLine("[Token - Type: {0}, Kind: {1}]\n{2}\n", token.GetType().Name, token.Kind, token);

        base.VisitToken(token);
    }
}

実行してみよう。

[Token - Type: SyntaxToken, Kind: UsingKeyword]
using

[Token - Type: SyntaxToken, Kind: IdentifierToken]
System

[Token - Type: SyntaxToken, Kind: SemicolonToken]
;

[Token - Type: SyntaxToken, Kind: ClassKeyword]
class

[Token - Type: SyntaxToken, Kind: IdentifierToken]
Program

[Token - Type: SyntaxToken, Kind: OpenBraceToken]
{

[Token - Type: SyntaxToken, Kind: StaticKeyword]
static

[Token - Type: SyntaxToken, Kind: VoidKeyword]
void

[Token - Type: SyntaxToken, Kind: IdentifierToken]
Main

[Token - Type: SyntaxToken, Kind: OpenParenToken]
(

[Token - Type: SyntaxToken, Kind: CloseParenToken]
)

[Token - Type: SyntaxToken, Kind: OpenBraceToken]
{

[Token - Type: SyntaxToken, Kind: CloseBraceToken]
}

[Token - Type: SyntaxToken, Kind: CloseBraceToken]
}

[Token - Type: SyntaxToken, Kind: EndOfFileToken]

今度は、より細かく "using"、"System"、";"、"class" 等の各トークンの情報が表示される。 ノードと異なり入れ子にはなっていない。

Roslyn によるコードの変更

ReplaceNode メソッドによるコードの変更

Roslyn では、コードを単に解析するだけでなく、改変することも可能だ。

試しに先程の Program クラスの中に空の Main メソッドがあるだけの C# のソースコードの Main の中を、"Hello world!" を表示するコードに変更してみよう。 こんな感じだ。

  1. Roslyn.Compilers.CSharp.Syntax クラスを用い、Console.WriteLine("Hello world!"); が入ったブロックをノードとして作成する「CreateHelloWorldBlock メソッド」を用意
  2. 元の単純な C# のソースコードのソースコードをパースしてシンタックス ツリーにする
  3. Main メソッドからブロック ("{" と "}" で囲まれた部分) を取り出す
  4. Roslyn.Compilers.CommonSyntaxNodeExtensions クラスにある ReplaceNode 拡張メソッドを使って、空のブロックをConsole.WriteLine("Hello world!"); が入ったブロックに置き換える

実装してみると、次のようになる。

using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using System;
using System.Linq;

class Program
{
    // Roslyn.Compilers.CSharp.Syntax クラスを用いた Console.WriteLine("Hello world!"); が入ったブロックの作成
    static BlockSyntax CreateHelloWorldBlock()
    {
        var invocationExpression = Syntax.InvocationExpression(       // Console.WriteLine("Hello world!");
            expression: Syntax.MemberAccessExpression(                // Console.WriteLine というメンバー アクセス
                kind      : SyntaxKind.MemberAccessExpression,
                expression: Syntax.IdentifierName("Console"  ),
                name      : Syntax.IdentifierName("WriteLine")
            ),
            argumentList: Syntax.ArgumentList(                        // 引数リスト
                arguments: Syntax.SeparatedList<ArgumentSyntax>(
                    node: Syntax.Argument(                            // "Hello world!"
                        expression: Syntax.LiteralExpression(
                            kind : SyntaxKind.StringLiteralExpression,
                            token: Syntax.Literal("Hello world!")
                        )
                    )
                )
            )
        );

        var statement            = Syntax.ExpressionStatement(expression: invocationExpression);
        return Syntax.Block(statement);
    }

    static void Main()
    {
        // 改変する C# のソースコード
        var sourceCode = @"
            using System;

            class Program
            {
                static void Main()
                {}
            }
        ";

        var syntaxTree           = SyntaxTree.ParseText(sourceCode); // ソースコードをパースしてシンタックス ツリーに
        var rootNode             = syntaxTree.GetRoot();             // ルートのノードを取得

        // Main メソッドのブロックを取得
        var block                = rootNode.DescendantNodes().First(node => node.Kind == SyntaxKind.Block);

        var newNode              = rootNode.ReplaceNode(                 // ノードの置き換え
                                        oldNode: block                  , // 元の空のブロック
                                        newNode: CreateHelloWorldBlock()  // Console.WriteLine("Hello world!"); が入ったブロック
                                   );

        Console.WriteLine(newNode.NormalizeWhitespace()); // 整形して表示
    }
}

実行してみよう。

using System;

class Program
{
    static void Main()
    {
        Console.WriteLine(@"Hello world!");
    }
}

Main メソッドの空だったブロックが、Console.WriteLine(@"Hello world!"); 入りのブロックに変更されたのが分かる。

SyntaxRewriter クラスによるコードの変更

上では ReplaceNode 拡張メソッドを使ったが、Roslyn.Compilers.CSharp.SyntaxRewriter を使ってもコードを変更することができる。

こちらの方は、ノード内に一斉に同じ変更を行うのに向いている。

SyntaxRewriter クラスは、上の方でソースコードの解析に用いた SyntaxWalker クラスと同様に、継承することで Visitor パターンによって、様々な種類のノードやトークンを辿ることができるクラスだ。

SyntaxRewriter クラスでは、適宜メソッドをオーバーライドすることで、ノードやトークンを書き換えることができる。

今回は、SyntaxRewriter を使い、ソースコード中の邪魔な #region と #endregion を消してみよう。

先ず、C# のソースコードの文字列として、次のように #region と #endregion が入ったものを用意する。

class Program
{
    static void Main()
    {
        // 改変する #region と #endregion 入の C# のソースコード
        var sourceCode = @"
            public class MyViewModel : INotifyPropertyChanged
            {
            #region INotifyPropertyChanged メンバー
                public event PropertyChangedEventHandler PropertyChanged;

                protected void OnPropertyChanged(string name)
                {
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs(name));
                }
            #endregion // INotifyPropertyChanged メンバー
            }
        ";
    }
}

次に #region と #endregion を除去するためのクラス RemoveRegionRewriter を用意する。 SyntaxRewriter クラスからの派生クラスだ。

using Roslyn.Compilers.CSharp;

// #region と #endregion を除去するクラス
class RemoveRegionRewriter : SyntaxRewriter
{
    public RemoveRegionRewriter() : base(visitIntoStructuredTrivia: true) // true にすることで #region や #endregion まで辿れる
    {}

    // #region を Visit
    public override SyntaxNode VisitRegionDirectiveTrivia(RegionDirectiveTriviaSyntax node)
    {
        return Syntax.SkippedTokensTrivia(); // スキップする
    }

    // #endregion を Visit
    public override SyntaxNode VisitEndRegionDirectiveTrivia(EndRegionDirectiveTriviaSyntax node)
    {
        return Syntax.SkippedTokensTrivia(); // スキップする
    }
}

では、このクラスを使って先程の #region と #endregion 入の C# のソースコードから #region と #endregion を除いてみよう。

using Roslyn.Compilers.CSharp;
using System;

class Program
{
    static void Main()
    {

        // 改変する C# のソースコード
        var sourceCode = @"
            public class MyViewModel : INotifyPropertyChanged
            {
            #region INotifyPropertyChanged メンバー
                public event PropertyChangedEventHandler PropertyChanged;

                protected void OnPropertyChanged(string name)
                {
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs(name));
                }
            #endregion // INotifyPropertyChanged メンバー
            }
        ";

        var syntaxTree = SyntaxTree.ParseText(sourceCode);                 // ソースコードをパースしてシンタックス ツリーに
        var rootNode   = syntaxTree.GetRoot();                             // ルートのノードを取得
        var newNode    = new RemoveRegionRewriter().Visit(node: rootNode); // #region と #endregion の除去
        Console.WriteLine(newNode.NormalizeWhitespace());                  // 整形して表示
    }
}

実行してみよう。

public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
}

#region と #endregion の行が除去されているのが分かるだろう。

まとめ

今回は、Roslyn を使い、簡単な C# ソースコードの解析と変更を行った。

C# Advent Calendar 2013」の明日は yone64 さん。

About 2013年12月

2013年12月にブログ「プログラミング C# - 翔ソフトウェア (Sho's)」に投稿されたすべてのエントリーです。過去のものから新しいものへ順番に並んでいます。

前のアーカイブは2013年11月です。

次のアーカイブは2014年01月です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。

Powered by
Movable Type 3.35