@ledsun blog

Hのキーがhellで、Sのキーがslaveだ、と彼は思った。そしてYのキーがyouだ。

Roslynを使ってC#のソースコードを編集する

自分で自分用のC#ソースコード編集ツールをつくったら捗りそうなことに気がつきました。 ググってみたら GitHub - dotnet/roslyn: The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs. というのでC#ソースコードのパースとコード生成が出来そうです。

c# - Edit loop in Roslyn - Stack Overflow で、それっぽいサンプルコードを見つけました。 試しにうごかしてみます。

まずは簡単そうなほうから試します。

// https://stackoverflow.com/questions/25568802/edit-loop-in-roslyn
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

SyntaxTree tree = CSharpSyntaxTree.ParseText(
    @"using System;
using System.Collections.Generic;
using System.Text;

static void Main(string[] args)
{
    for(int i=0; i<10; i++)
        int a = i;
}");
var root = tree.GetRoot();

var forStmt = root.DescendantNodes().OfType<ForStatementSyntax>().Single();
var rewritten = root.ReplaceNode(forStmt,
    forStmt.WithCondition(
        SyntaxFactory.ParseExpression("i>=0")
    ).WithStatement(
        SyntaxFactory.ParseStatement(@"    {
        Console.WriteLine(i);
        Console.WriteLine(i*2);
    }
")
    ));

Console.WriteLine(rewritten);

動かすと次のように編集されたソースコードが表示されます。

実行結果

勉強のために説明をしてみます。

SyntaxTree tree = CSharpSyntaxTree.ParseText(
    @"using System;
using System.Collections.Generic;
using System.Text;

static void Main(string[] args)
{
    for(int i=0; i<10; i++)
        int a = i;
}");

で、C#をパースして構文木に変換します。

var forStmt = root.DescendantNodes().OfType<ForStatementSyntax>().Single();

構文木からfor文を表すノードを取得します。

var rewritten = root.ReplaceNode(forStmt,
    forStmt.WithCondition(
        SyntaxFactory.ParseExpression("i>=0")
    ).WithStatement(
        SyntaxFactory.ParseStatement(@"    {
        Console.WriteLine(i);
        Console.WriteLine(i*2);
    }
")
    ));

で、for文を編集したものに置き換えます。 ここがややこしいです。

  1. 破壊的な変更ではない。もとのrootを変更するのではなくて、変更された新しい構文木が生成されます。
  2. 作りたい文にあわせて、良い感じのノードを作る
Console.WriteLine(rewritten);

で、構文木からコード生成します。 文字列化するだけで、コードが帰って来ます。

もう一つのサンプルも動作は同じです。

// https://stackoverflow.com/questions/25568802/edit-loop-in-roslyn
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

SyntaxTree tree = CSharpSyntaxTree.ParseText(
    @"using System;
using System.Collections.Generic;
using System.Text;

static void Main(string[] args)
{
    for(int i=0; i<10; i++)
        int a = i;
}");
var root = tree.GetRoot();
var rewritten = new Rewriter().Visit(root);
Console.WriteLine(rewritten);

class Rewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitForStatement(ForStatementSyntax node)
    {
        // update the current node with the new condition and statement
        return node.WithCondition(
            SyntaxFactory.ParseExpression("i>=0")
        ).WithStatement(
            SyntaxFactory.ParseStatement(@"{
              Console.WriteLine(i);
              Console.WriteLine(i*2);
            }")
        );
    }
}

構文木を変更するややこしい部分を Rewriter クラスに分離しています。