プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

メタプログラミング入門 - 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 StudioC# のコンソール アプリケーションを作成
  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 さん。