[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 さん。