とあるRoslynではないC# Parser、NRefactoryの紹介

ロッズリーン、はっじまらないよ~。というわけでMicrosoft “Roslyn” CTP、Compiler as a Service。NuGet - Roslynでも手に入るので、サンプル類やC# Interactiveとかはなしで、とりあえずScriptingやCompilerを触ってみたい、ということなら、お手軽です。しかし、まあ未実装も少なくなく、まだまだ先は長そうな雰囲気ではある。今すぐ欲しいのに!切実にC# Parserが!というわけで、今日はその良き代替となる(かもしれない)NRefactoryを紹介します。

NRefactoryはNuGet - NRefactoryからも入ります。verは5.0.0.4、「This is an alpha release. Expect bugs and breaking changes in the future.」とのことで、こちらもまだまだこれからのよう(MonoDevelopの新しいC#エディタで使われる予定、だそうです)。とりあえずNuGetで参照してみませう。

参照するとMono.Cecilが入ったり名前空間にMono.CSharpがあったりと、全体的にMonoのCSharp Compilerやその周辺が使われているふいんき。Mono.CSharpは単体でもついこないだNuGet - Mono.CSharpで入れられるようになりましたが、そのまんまだと、なんというかどう使っていいか分からないというか、はいEvalできた、さて、はて?みたいになってしまって。そのへん、NRefactoryはゆるふわで、結構すぐに使い方分かります。とりあえず使ってみませう。

using System;
using System.IO;
using System.Linq;
using ICSharpCode.NRefactory.CSharp;

class Program
{
    static void Main(string[] args)
    {
        var code = File.ReadAllText(@"../../Program.cs");
        var parser = new CSharpParser();

        var root = parser.Parse(code, "");

        var program = root.Descendants.OfType<ICSharpCode.NRefactory.CSharp.TypeDeclaration>().First();
        program.Name = "Hogegram";

        Console.WriteLine(root.ToSourceString());
    }
}

public static class CompilationUnitExtensions
{
    static readonly CSharpFormattingOptions DefaultOptions = new CSharpFormattingOptions()
    {
        // TODO:130のboolを自分の気にいるようなOption(というかVSのデフォ)に近づける
    };

    public static string ToSourceString(this CompilationUnit compilationUnit, int indentation = 4, string indentationString = " ")
    {
        return ToSourceString(compilationUnit, DefaultOptions, indentation, indentationString);
    }

    public static string ToSourceString(this CompilationUnit compilationUnit, CSharpFormattingOptions options, int indentation = 4, string indentationString = " ")
    {
        using (var sw = new StringWriter())
        {
            var formatter = new TextWriterOutputFormatter(sw)
            {
                Indentation = indentation,
                IndentationString = indentationString
            };

            var visitor = new CSharpOutputVisitor(formatter, options);
            compilationUnit.AcceptVisitor(visitor);
            return sw.ToString();
        }
    }
}

Program.csを読み込んで、クラス名をHogegramに変更したのを出力する、というだけのものです。今のところ整形した文字列化にはCSharpOutputVisitorを作って、AcceptVisitorしなきゃならないようで面倒ぃので、とりあえず拡張メソッドにしました。デフォルトの整形オプションが気に食わないのですが、フォーマット設定が超細かくて100個以上のboolをON/OFFしなきゃいけないのでとりあえず放置。

構文木の取得自体は超単純で、new CSharpParser()してParse。そしてツリーを辿るのはLINQ to Xmlと同じ感覚でDescendantsやAncestors、Childrenなどなどが用意されているので、一発で分かりますね!そしてOfTypeで自分の欲しいのにフィルタリング、と。この辺は超簡単。そして名前を変えたければ、プロパティに代入するだけ。非常に楽ちんです、素晴らしい……!

vs Roslyn

さて、じゃあRoslynでも同じことをやってみましょう。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Roslyn.Compilers.CSharp;

class Program
{
    static void Main(string[] args)
    {
        var code = File.ReadAllText(@"../../Program.cs");
        var tree = SyntaxTree.ParseCompilationUnit(code);

        var program = tree.Root.DescendentNodes().OfType<ClassDeclarationSyntax>().First();

        var newNode = new ClassNameRewriter(new Dictionary<ClassDeclarationSyntax, string> { { program, "Hogegram" } })
            .Visit(tree.Root);

        Console.WriteLine(newNode.ToString());
    }
}

public class ClassNameRewriter : SyntaxRewriter
{
    readonly IDictionary<ClassDeclarationSyntax, string> replaceNames;

    public ClassNameRewriter(IDictionary<ClassDeclarationSyntax, string> replaceNames)
    {
        this.replaceNames = replaceNames;
    }

    protected override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        var oldIdentifierToken = node.Identifier;

        if (replaceNames.ContainsKey(node))
        {
            var newNode = node.Update(node.Attributes,
               node.Modifiers, node.Keyword,
               Syntax.Identifier(
                   oldIdentifierToken.LeadingTrivia,
                   replaceNames[node], // ここだけ!
                   oldIdentifierToken.TrailingTrivia),
               node.TypeParameterListOpt, node.BaseListOpt,
               node.ConstraintClauses, node.OpenBraceToken,
               node.Members, node.CloseBraceToken,
               node.SemicolonTokenOpt);

            return newNode;
        }
        else
        {
            return base.VisitClassDeclaration(node);
        }
    }
}

構文木の取得はこちらも簡単です、SyntaxTree.ParseCompilationUnitだけ。ツリーの辿り方も似ていて、DescendentNodes、ChildNodes、Ancestors、と、戸惑うことなく使える感じです。OfTypeでフィルタして絞り込みも同じ。書き換えたコードの出力は、こちらはToStringだけでOK、しかもフォーマットルールは何の設定もいらずVS標準と同じ状態になっているので楽ちん。

が、しかし、こちらはクラス名の書き換えが面倒。Expression Treeと同じく基本的にイミュータブルになっているので、書き換えはプロパティに代入するだけ、とはいかず、大掛かりな仕掛けが必要です。というかこの程度のためだけにVisitorとか……。一応ReplaceNodeとかUpdateとかもあるんですが、うーん、まあ、何というか、よくわかってないので深く突っ込まれると窮します:)

まとめ

提供する機能は同じでも、使い心地とかは、両者、結構違くなるのではという印象。どちらも発展途上なのですが、ライセンス的にRoslynは今は使えないのに比べると、NRefactoryはMITライセンスで、アルファ版なのを留意しておけば使えるのではというのが、私的には大きいかなあ。割と切迫してC# Parserが必要なところなので、ここはちょっとNRefactoryを使い込んでみたいなあ、なんて思っています。いや、そこまでDeepに使い倒すというより簡単なコード解析と置換程度なので、しっかり使ってバグ報告、とかの貢献は出来なさそうですんがー。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive