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

この記事は、「C# Advent Calendar 2013」の 12 月 12 日分。
※ 「[C#][.NET][CodeDOM] メタプログラミング入門 – CodeDOM によるクラスの生成」の続き。
Roslyn
Roslyn は、C# や Visual Basic のコンパイラーの内部の API 等を公開したものだ。"Compiler as a Service" と表現されている。
Roslyn に関しては、以前、次にあげる記事で扱った。参考にしてほしい。
- [C#][Roslyn] Roslyn による Visual Studio のアドイン
- [C#][.NET][Roslyn] メタプログラミング入門 – Roslyn による Add メソッドの動的生成
- [C#][.NET] メタプログラミング入門 – メソッド呼び出しのパフォーマンスの比較
- [C#][.NET][Roslyn] メタプログラミング入門 – 応用編 – オブジェクトの文字列変換のメタプログラミング (Roslyn 編)
Roslyn によるコード解析
先ず、「Roslyn による Visual Studio のアドイン」で行った Roslyn を使った C# のソースコードの解析を、もっと簡単な例でやってみたい。
次の手順でコード解析のサンプルを準備する。
- Visual Studio で C# のコンソール アプリケーションを作成
- 「メタプログラミング入門 – Roslyn による Add メソッドの動的生成」のときと同様の手順で Roslyn をインストール
- Visual Studio の「ソリューション エクスプローラー」でプロジェクト名を右クリックし、「NuGet パッケージの管理…」を開く
- "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!" を表示するコードに変更してみよう。
こんな感じだ。
- Roslyn.Compilers.CSharp.Syntax クラスを用い、Console.WriteLine("Hello world!"); が入ったブロックをノードとして作成する「CreateHelloWorldBlock メソッド」を用意
- 元の単純な C# のソースコードのソースコードをパースしてシンタックス ツリーにする
- Main メソッドからブロック ("{" と "}" で囲まれた部分) を取り出す
- 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 さん。
関連
関連記事
[C#][.NET][Roslyn] Build 2014 でオープンソースになったと発表された Roslyn のソースコードを弄ってみた
Build 2014 での Roslyn 関連の発表 先日、マイクロソフトの開発 ...
[C#][.NET][式木] メタプログラミング入門 – 応用編 – オブジェクトの文字列変換のメタプログラミング (式木編)
※ 「 メタプログラミング入門 - 応用編 - オブジェクトの文字列変換のメタプ ...
[C#][.NET] .NET アプリケーション (WPF/Windows フォーム) で多重起動を禁止し、単一のプロセスで動作させる
■ 概要 .NET アプリケーション (WPF/Windows フォーム) で多 ...
[C#] Windows 及び Internet Explorer のバージョンを調べる
■ 概要 C# で Windows 及び Internet Explorer の ...
[C#][.NET][Roslyn][式木] Room metro #23 大阪「メタプログラミング C#」の資料公開
Room metro #23 大阪 (3月1日) でやったセッションの資料を公開 ...
ディスカッション
コメント一覧
まだ、コメントがありません