[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] Shos.UndoRedoList (List and ObservableCollection which support undo/redo)
Click here for Japanese version (日本語版はこち ...
[C#][.NET] メタプログラミング入門 – はじめに
数回に渡って、C#/.NET によるメタプログラミングを紹介して行きたい。 先ず ...
[C#][Design Pattern] C# による Observer パターンの実装 その1 – 古典的な実装
「Expression を使ってラムダ式のメンバー名を取得する」と云う記事で、ラ ...
[C#] Tips: interface と partial class で横断的関心事を分離
※ C# Advent Calendar 2016 の12月23日の記事。 前の ...
モンティ・ホール問題
モンティ・ホール問題 というのがある。 アメリカのゲームショー番組の中で行われた ...
ディスカッション
コメント一覧
まだ、コメントがありません