妄想プログラマのらくがき帳 : 9月 2014

2014年9月21日日曜日

[Roslyn]SyntaxNodeの派生クラスでソースコードを変更してみる。

今回は SyntaxNode の派生クラスを使ってソースコードを変更してみます。

MethodDeclarationSyntax を使ってメソッドを変更する

まずは MethodDeclarationSyntax を使ってソースコードを変更してみました。
string sourceCode =
@"using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RoslynSample
{
class Program
{
/// <summary>
/// エントリポイントです。
/// </summary>
/// <param name=""args"">コマンドライン引数</param>
static void Main(string[] args)
{
HelloRoslyn();
}
static HelloRoslyn()
{
System.Console.WriteLine(""Hello Roslyn!"");
}
}
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceCode);
var methodDeclarations = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>();
// HelloRoslynという名前のメソッドを抽出
var targetMethod = methodDeclarations.
Where(md => md.Identifier.ValueText == "HelloRoslyn").
FirstOrDefault();
if (targetMethod != null)
{
// メソッドの名前を変更し、修飾子を追加
var newMethod = targetMethod.
WithIdentifier(SyntaxFactory.Identifier("NewHelloRoslyn")).
AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword).
WithTrailingTrivia(SyntaxFactory.Whitespace(" "))
);
var newTree = tree.GetRoot().ReplaceNode(targetMethod, newMethod);
// コードをフォーマット
var workspace = MSBuildWorkspace.Create();
var formattedTree = Formatter.Format(newTree, workspace);
System.Diagnostics.Debug.WriteLine(formattedTree.ToFullString());
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RoslynSample
{
class Program
{
/// <summary>
/// エントリポイントです。
/// </summary>
/// <param name="args">コマンドライン引数</param>
static void Main(string[] args)
{
HelloRoslyn();
}
static private NewHelloRoslyn()
{
System.Console.WriteLine("Hello Roslyn!");
}
}
}
上側の RoslynSample_ModifyMethod.cs が MethodDeclarationSyntax を使ってソースコードを変更するサンプルコード、下側の RoslynSample_ModifyMethod_Output.cs が RoslynSample_ModifyMethod.cs で変更したソースコードを出力した結果です。 メソッド static HelloRoslyn() が static private NewHelloRoslyn() に変わっているのがわかると思います。 見てのとおり変更内容は
  • 名前を HelloRoslyn から NewHelloRoslyn に変更
  • private 修飾子の追加
の2点です。

サンプルコードの解説

上側の RoslynSample_ModifyMethod.cs について1行目から順に見ていきましょう。

1行目~25行目:
変更の対象となるソースコードです。

27行目:
ソースコードをパースして SyntaxTree を作成しています。サンプルでは文字列で定義したソースコードをパースするので ParseText() を呼び出しています。ファイルに記述されたソースコードをパースする場合、代わりに ParseFile() を呼び出します。

28行目:
SyntaxTree から MethodDeclarationSyntax 型のノードを抽出しています。

30行目~32行目:
MethodDeclarationSyntax 型のノードの中から識別子が "HelloRoslyn" のノードを取得しています。

34行目:
識別子が "HelloRoslyn" のノードが見つからなかった場合 targetMethod は null になるためガードを入れています。今回はパース対象のソースコードに識別子 "HelloRoslyn" のメソッドが必ず存在するので if 条件が false になることはありませんが、未知のソースコードをパースするような場合はこのようなガードが必要になります。

37行目~41行目:
ここで名前の変更と private 修飾子の追加を行っています。
まず WithIdentifier() で名前の変更をしています。 WithIdentifier() の引数に変更後の識別子 "NewHelloRoslyn" を渡すことで、識別子が "NewHelloRoslyn" の MethodDeclarationSyntax を戻り値として取得できます。
次に AddModifiers() で private 修飾子を追加しています。 AddModifiers() の引数には private 修飾子を表す SyntaxToken を渡していますが、 WithTrailingTrivia() で private の後ろに半角スペースを付加するようにしています。何故半角スペースを付加しているのかは後程解説します。

43行目:
ReplaceNode() を用いて変更後のノードで SyntaxTree 内の変更前ノードを置換しています。

44行目~46行目:
コードを整形しています。このサンプルのケースでは整形不要ですが、 SyntaxTree に新たにノードを追加した場合等はインデントが崩れるため、明示的にコードフォーマットを行う必要があります。

48行目:
変更後のソースコードを出力しています。

***DeclarationSyntax の Add***() と With***()

***DeclarationSyntax には、引数、属性、ステートメント等のノードを構成する要素を新規追加する Add***() という名前のメソッド群と、引数、属性、ステートメント等を変更する With***() という名前のメソッド群が定義されています。これらのメソッドでノードを構成する要素を変更(=ソースコードを変更)することができます。

これらのメソッドを使う時に注意しなければならないのが ***DeclarationSyntax が不変型であるという点です(というより、Roslynのクラスは基本的に不変型です)。すなわち、各メソッドはノード自身の値は変更せず、戻り値として変更適用後のノードを返すということです。 System.String 型と同じですね。そのためノードに対し複数の変更を行う場合、サンプルのようにメソッドチェーンで記述するかまたは逐次戻り値を変数に代入し、代入後の変数でメソッド呼び出しを行う必要があります。

Add***() の注意点

Add***() のメソッドを使う時に注意しなければならないのが、 Add***() は追加対象の部分しか追加してくれないということです。例えばサンプルにある AddModifiers() の場合、修飾子は追加してくれますが前後のスペースは追加してくれません。そのため、サンプルでは WithTrailingTrivia() で半角スペースを付加しています。 WithTrailingTrivia() を使わないと該当箇所は次のようなソースコードになってしまいます。

static privateNewHelloRoslyn()  // privateの後ろにスペースが無い!

WithTrailingTrivia() でスペースを追加する代わりに SyntaxNode.NormalizeWhitespace() を使うことで適切な位置(上記例の場合は private の後ろ)にスペースを追加することも出来ますが、メソッド前後の空行やインデントを削除しちゃったりする場合があるので使い勝手があまり良くないです。なので WithTrailingTrivia() を使うことの方が多くなると思います。


ClassDeclarationSyntax を使ってメソッドを追加する

次は ClassDeclarationSyntax を使ってクラスにメソッドを追加してみました。
string sourceCode =
@"using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RoslynSample
{
class Program
{
/// <summary>
/// エントリポイントです。
/// </summary>
/// <param name=""args"">コマンドライン引数</param>
static void Main(string[] args)
{
HelloRoslyn();
}
static HelloRoslyn()
{
System.Console.WriteLine(""Hello Roslyn!"");
}
}
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(sourceCode);
// SyntaxTreeからProgramという名前のクラスを取得
var classDeclarations = tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>();
var targetClass = classDeclarations.
Where(cd => cd.Identifier.ValueText == "Program").
FirstOrDefault();
if (targetClass != null)
{
var newClass = targetClass.
AddMembers(
SyntaxFactory.MethodDeclaration(
attributeLists: new SyntaxList<AttributeListSyntax>(),
modifiers: new SyntaxTokenList() { SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PrivateKeyword) },
returnType: SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)),
explicitInterfaceSpecifier: null,
identifier: SyntaxFactory.Identifier(
leading: new SyntaxTriviaList(),
text: "NewMethod",
trailing: new SyntaxTriviaList()
),
typeParameterList: null,
parameterList: SyntaxFactory.ParameterList(
new SeparatedSyntaxList<ParameterSyntax>().AddRange(
new[]{
SyntaxFactory.Parameter(
attributeLists: new SyntaxList<AttributeListSyntax>(),
modifiers: new SyntaxTokenList(),
type: SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)),
identifier: SyntaxFactory.Identifier("param1"),
@default: null),
SyntaxFactory.Parameter(
attributeLists: new SyntaxList<AttributeListSyntax>(),
modifiers: new SyntaxTokenList(),
type: SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.IntKeyword)),
identifier: SyntaxFactory.Identifier("param2"),
@default: null)
}
)
).NormalizeWhitespace(),
constraintClauses: new SyntaxList<TypeParameterConstraintClauseSyntax>(),
body: SyntaxFactory.Block(
SyntaxFactory.ReturnStatement(
SyntaxFactory.ParseExpression("param1 + param2")
).NormalizeWhitespace()
).NormalizeWhitespace()
)
);
var newTree = tree.GetRoot().ReplaceNode(targetClass, newClass);
// コードをフォーマット
var workspace = MSBuildWorkspace.Create();
var formattedTree = Formatter.Format(newTree, workspace);
System.Diagnostics.Debug.WriteLine(formattedTree.ToFullString());
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RoslynSample
{
class Program
{
/// <summary>
/// エントリポイントです。
/// </summary>
/// <param name="args">コマンドライン引数</param>
static void Main(string[] args)
{
HelloRoslyn();
}
static HelloRoslyn()
{
System.Console.WriteLine("Hello Roslyn!");
}
int NewMethod(int param1, int param2)
{
return param1 + param2;
}
}
}
上側の RoslynSample_AddMethod.cs が ClassDeclarationSyntax を使ってメソッドを追加するサンプルコード、下側の RoslynSample_AddMethod_Output.cs が RoslynSample_AddMethod.cs で変更したソースコードを出力した結果です。メソッド NewMethod() が追加されているのがわかると思います。

サンプルコードの解説

RoslynSample_AddMethod.cs について見ていきましょう。

1行目~27行目:
RoslynSample_ModifyMethod.cs と同じです。

29行目~30行目:
SyntaxTree から ClassDeclarationSyntax 型のノードを抽出し、その中から識別子が "Program" のノードを取得しています。

36行目~74行目:
ここでクラス Program に NewMethod() を追加しています。ご覧のとおりもの凄く大変です(^_^;)
Roslyn はこういうメソッドをまるまる追加といった用途に使うにはあまり向いてないですね。もしそれでも Roslyn でコードジェネレータのようなものを作りたいのならば、ラッパーメソッドを用意するかまたは文字列でコードを生成してから ParseText() や ParseExpression() 等でパースした方が良いでしょう。サンプルのような方法だとコンパイルが通るようにするのにも一苦労です。実際このサンプルも苦労しました(;^_^A

処理の内容は SyntaxFactory.MethodDeclaration() で MethodDeclarationSyntax を作成し、それを ClassDeclarationSyntax.AddMembers() に渡しているだけです。
特筆すべき箇所を挙げるとすれば NewMethod() の処理部分を作成している68行目~72行目でしょうか、 SyntaxNode.NormalizeWhitespace() を呼び出して適宜スペースを追加しています。このように Roslyn で新たにコードを追加する場合、スペースを必要に応じて追加してやる必要があるのが結構メンドイです(´Д`)

75行目~:
RoslynSample_ModifyMethod.cs と同じですが、こっちのサンプルではコードフォーマットの部分が必要です。コードフォーマットを行わないと追加したメソッド部分だけインデントが無い状態になってしまいます。

SyntaxNode の派生クラスでソースコードを変更してみてわかったこと

ソースコードの一部を変更するのは容易ですが新規にクラスやメソッドを追加するのはすごく大変ですね。特にコメントやスペースといった Roslyn で Trivia と表現される要素の扱いが難しい。 NormalizeWhitespace() の動作もリファレンスが無いため良くわからず、色々と試行錯誤する羽目になりました。


次回はソースコードを変更するもう1つの方法である SyntaxRewriter を使う方法でソースコードを変更してみます。