これは、Visual Basic Advent Calendar 2013 12/24日の記事。
今回は、Roslynについてもう少し踏み込んだことをしてみます。
丁度よく、C# Advent Calendar 2013の12日にて、翔ソフトウェア (Sho's)の@FujiwoさんがRoslynを使用した「メタプログラミング入門 - Roslyn による C# ソースコードの解析と変更」をブログを投稿されていたので、今回はこれの「ReplaceNode メソッドによるコードの変更」までをVB版に移植してみることにします。
といっても、RoslynのVisualBasicのサンプルがネット上でほとんど見つからない状態で、C#の移植するにもRoslynの理解が足りないため四苦八苦状態です。
前回は、再帰処理を使ってソースコードの構成要素であるノードの種類と値を出力しましたが、今回はRoslyn.Compilers.VisualBasic 名前空間の SyntaxWalker クラスを用います。このクラスは、Visitor パターンになっており、これを継承し各種メソッドをオーバーライドすることで、様々な種類のノードやトークンを辿ることができます。
下記のように SyntaxWalker の派生クラスを用意し、各ノードを Visit するメソッドをオーバーライドすると、ソースコードの構成要素であるノードを全部辿ることができる。
Class Walker
Inherits SyntaxWalker
' Visitor パターンでソースコードを解析
Public Overrides Sub Visit(node As SyntaxNode)
' 各ノードを Visit
If node IsNot Nothing Then
Console.WriteLine("[Node - Type: {0}, Kind: {1}]" & vbLf & "{2}" & vbLf, node.[GetType]().Name, node.Kind, node)
End If
MyBase.Visit(node)
End Sub
End Class
このWalkerクラスを使用して、文字列変数のソースコードを解析してみる。
Imports Roslyn.Compilers
Imports Roslyn.Compilers.VisualBasic
Sub Main()
Dim sourceCode As String =
<string>
Imports System
Module Program
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End Module
</string>.Value
Dim syntaxTree_ = SyntaxTree.ParseText(sourceCode) ' ソースコードをパースしてシンタックス ツリーに
Dim rootNode = syntaxTree_.GetRoot() ' ルートのノードを取得
Dim walker_ As Walker = New Walker
End Sub
【出力結果】
[Node - Type: CompilationUnitSyntax, Kind: CompilationUnit]
Imports System
Module Program
Sub Main()
Console.WriteLine("")
End Sub
End Module
[Node - Type: ImportsStatementSyntax, Kind: ImportsStatement]
Imports System
[Node - Type: MembersImportsClauseSyntax, Kind: MembersImportsClause]
System
[Node - Type: IdentifierNameSyntax, Kind: IdentifierName]
System
[Node - Type: ModuleBlockSyntax, Kind: ModuleBlock]
Module Program
Sub Main()
Console.WriteLine("")
End Sub
End Module
[Node - Type: ModuleStatementSyntax, Kind: ModuleStatement]
Module Program
[Node - Type: MethodBlockSyntax, Kind: SubBlock]
Sub Main()
Console.WriteLine("")
End Sub
[Node - Type: MethodStatementSyntax, Kind: SubStatement]
Sub Main()
[Node - Type: ParameterListSyntax, Kind: ParameterList]
()
[Node - Type: CallStatementSyntax, Kind: CallStatement]
Console.WriteLine("")
[Node - Type: InvocationExpressionSyntax, Kind: InvocationExpression]
Console.WriteLine("")
[Node - Type: MemberAccessExpressionSyntax, Kind: MemberAccessExpression]
Console.WriteLine
[Node - Type: IdentifierNameSyntax, Kind: IdentifierName]
Console
[Node - Type: IdentifierNameSyntax, Kind: IdentifierName]
WriteLine
[Node - Type: ArgumentListSyntax, Kind: ArgumentList]
("")
[Node - Type: SimpleArgumentSyntax, Kind: SimpleArgument]
""
[Node - Type: LiteralExpressionSyntax, Kind: StringLiteralExpression]
""
[Node - Type: EndBlockStatementSyntax, Kind: EndSubStatement]
End Sub
[Node - Type: EndBlockStatementSyntax, Kind: EndModuleStatement]
End Module
前回と同じ各ノードの情報が表示されました。
次に、Walkerクラスを少し変更して、ノードではなく、より細かいソースコードの構成要素であるトークンを表示してみます。
今度は、各トークンを Visit する VisitToken メソッドをオーバーライドして、全トークンを辿ってみる。
Class Walker
Public Sub New()
MyBase.New(depth:=Common.SyntaxWalkerDepth.Token)
' トークンの深さまで Visit
End Sub
Public Overrides Sub VisitToken(token As SyntaxToken)
' 各トークンを Visit
If token <> Nothing Then
Console.WriteLine("[Token - Type: {0}, Kind: {1}]" & vbLf & "{2}" & vbLf, token.[GetType]().Name, token.Kind, token)
End If
MyBase.VisitToken(token)
End Sub
End Class
実行してみると、今度は、より細かく "Import"、"System"、"Module" 等の各トークンの情報が表示される。
【出力結果】
[Token - Type: SyntaxToken, Kind: ImportsKeyword]
Imports
[Token - Type: SyntaxToken, Kind: IdentifierToken]
System
[Token - Type: SyntaxToken, Kind: StatementTerminatorToken]
[Token - Type: SyntaxToken, Kind: ModuleKeyword]
Module
[Token - Type: SyntaxToken, Kind: IdentifierToken]
Program
[Token - Type: SyntaxToken, Kind: StatementTerminatorToken]
[Token - Type: SyntaxToken, Kind: SubKeyword]
Sub
[Token - Type: SyntaxToken, Kind: IdentifierToken]
Main
[Token - Type: SyntaxToken, Kind: OpenParenToken]
(
[Token - Type: SyntaxToken, Kind: CloseParenToken]
)
[Token - Type: SyntaxToken, Kind: StatementTerminatorToken]
[Token - Type: SyntaxToken, Kind: EndKeyword]
End
[Token - Type: SyntaxToken, Kind: SubKeyword]
Sub
[Token - Type: SyntaxToken, Kind: StatementTerminatorToken]
[Token - Type: SyntaxToken, Kind: EndKeyword]
End
[Token - Type: SyntaxToken, Kind: ModuleKeyword]
Module
[Token - Type: SyntaxToken, Kind: StatementTerminatorToken]
[Token - Type: SyntaxToken, Kind: EndOfFileToken]
■Roslyn によるコードの変更
ReplaceNode メソッドによるコードの変更。
Roslynでは、コードを単に解析するだけでなく、改変することも出来ます。
文字列変数のソースリストのConsole.ReadLine()からConsole.WriteLine("Hello, World!")のコードに変更してみます。
移植元のC#では、空のブロックを置換対象先としていましたが、Visual Basicは文法的に空のブロックが出来ません。
その変わりとしてConsole.ReadLine()のノードである「CallStatement」を置換対象とします。
※Mainの内部が空だと「CallStatement」も無くなってしまうため、あえてConsole.ReadLine()を入れてあります。
【処理手順】
1.Roslyn.Compilers.CSharp.Syntax クラスを用い、Console.WriteLine("Hello, World!")が入ったブロックをノードとして作成する「CreateHelloWorldBlock メソッド」を用意
2.元の単純な Visual Basicのソースコードのソースコードをパースしてシンタックス ツリーにする
3.Main メソッドからCallStatementのConsole.ReadLine()を取り出す
4.Roslyn.Compilers.CommonSyntaxNodeExtensions クラスにある ReplaceNode 拡張メソッドを使って、Console.ReadLine()をConsole.WriteLine("Hello, World!")が入ったブロックに置き換える
実装してみると、次のようになる。
Imports Roslyn.Compilers
Imports Roslyn.Compilers.VisualBasic
Sub Main()
Dim sourceCode As String =
<string>
Imports System
Module Program
Sub Main()
Console.ReadLine()
End Sub
End Module
</string>.Value
Dim syntaxTree_ = SyntaxTree.ParseText(sourceCode)
' ソースコードをパースしてシンタックス ツリーに
Dim rootNode = syntaxTree_.GetRoot()
' ルートのノードを取得
' Main メソッドのステートメントを取得
Dim statement_ = rootNode.DescendantNodes().First(Function(node) node.Kind = SyntaxKind.CallStatement)
' ノードの置き換え
' 元のConsole.ReadLine()のステートメント
' Console.WriteLine("Hello world!") が入ったステートメント
Dim newNode = rootNode.ReplaceNode(oldNode:=statement_, newNode:=CreateHelloWorldBlock())
Console.WriteLine(newNode.NormalizeWhitespace()) ' 整形して表示
Console.ReadLine()
End Sub
Private Function CreateHelloWorldBlock() As CallStatementSyntax
' Console.WriteLine("Hello world!")
' Console.WriteLine というメンバー アクセス
' 引数リスト
' "Hello world!"
Dim expression_ As Roslyn.Compilers.VisualBasic.ExpressionSyntax
Dim argumentList_ As Roslyn.Compilers.VisualBasic.ArgumentListSyntax
expression_ = Syntax.MemberAccessExpression(Syntax.IdentifierName("Console"),
Syntax.Token(SyntaxKind.DotToken),
Syntax.IdentifierName("WriteLine"))
Dim paramItem As LiteralExpressionSyntax = Syntax.LiteralExpression(SyntaxKind.StringLiteralExpression,
Syntax.Literal("Hello world!"))
argumentList_ = Syntax.ArgumentList(Syntax.SeparatedList(Of ArgumentSyntax)(node:=Syntax.SimpleArgument(paramItem)))
Dim invocationExpression_ = Syntax.InvocationExpression(expression_, argumentList_)
Return Syntax.CallStatement(invocationExpression_)
End Function
End Module
実行してみると、Main メソッドのConsole.ReadLine()が、Console.WriteLine("Hello, World!")のブロックに変更されたのが分かる。
【出力結果】
Imports System
Module Program
Sub Main()
Console.WriteLine("Hello world!")
End Sub
End Module
今回、C#からVisualBasicに移植する上で下記の違いがありました。
・C#のSyntax.MemberAccessExpressionの引数「kind」はなく「operatorToken」となっているので、Syntax.Token(SyntaxKind.DotToken)を指定しました。
・C#のSyntax.Argumentは存在しないため、Syntax.SimpleArgumentに変更しました。
・C#のSyntax.Blockは存在しないため、Syntax.CallStatementに変更しました。
C#とVisualBasicは言語の文法形式が違いがあるため、メソッド自体もノードに対応したものに変更する必要があります。
■RoslynのVBコードがある参照先
MicrosoftR “Roslyn” CTP
http://msdn.microsoft.com/en-us/vstudio/roslyn.aspx
Semantic Information ? Part 1
http://blogs.msdn.com/b/prakashb/archive/2012/02/06/semantic-information-part-1.aspx
Less than Dot
http://blogs.lessthandot.com/index.php/DesktopDev/?s=Roslyn&advm=&advy=&author=
■Roslynの活用
MicrosoftではRoslynの使用方法として、C#からVisualBasicに変換するツールのデモを公開してました。
それを見た方が、C#をPowerShellに変換するアプローチをブログに書いております。
Using Roslyn and PowerShell 3 to Convert C# to PowerShell
他にも「Roslyn」でネット検索すると、Roslynを使ってC#をC++に変換し、開発は書きやすいマネージド言語で、実際の動作は高速なネイティブ言語として開発しているのが見られます。
10章にRoslynの説明があるため購入。Roslynの先はメタプログラミング!