とある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に使い倒すというより簡単なコード解析と置換程度なので、しっかり使ってバグ報告、とかの貢献は出来なさそうですんがー。

ImplicitQueryString - 暗黙的変換を活用したC#用のクエリストリング変換ライブラリ

QueryStringかったるいですね、変換するのが。intに。boolに。それ以外に。そのままじゃどうしようもなくストレスフル。そこで、以下のように書けるライブラリを作りました。勿論NuGetでのインストールも可能です。あと、ライブラリといっても例によって.csファイル一個だけなので、導入は超お手軽。

以下はASP.NETの例ですが、NameValueCollectionを使っているものは全て対象になります。QueryStringだけじゃなくてRequest.Formなどにも使えますね。

// using Codeplex.Web;

int count;
string query;
bool? isOrdered; // support nullable

protected void Page_Load(object sender, EventArgs e)
{
    count = Request.QueryString.ParseValue("c");
    query = Request.QueryString.ParseValue("q");
    isOrdered = Request.QueryString.ParseValue("ord");
}

NameValueCollectionへの拡張メソッドとして実装しているので、using Codeplex.Webを忘れないように。ポイントは「型の明示が不要」というところです。クエリストリングを解析したい時って、通常はフィールドで既に変数自体は作っているのではないかと思います。なら、代入時に左から型を推論させてしまえばいいわけです、モダンなC#はわざわざ明示的に型なんて書かない、書きたくない。なのでこのライブラリ、ImplicitQueryStringではParseValueだけで全ての基本型(int, long, bool, string, DateTimeなど)へと型指定不要で代入可能となっています。代入先の型がNullableである場合は、キーが見つからなかったりパースに失敗した場合はnullにすることもサポートしてます。

また、よくあるクエリストリングはUrlEncodeされているのでデコードしたかったりするケースにも簡単に対応しています、UrlDecodeを渡すだけ!他にもEnumへの変換も出来ますし、キーが見つからなかったり変換不可能だったりした場合は指定したデフォルト値を返すParseValueOrDefault/ParseEnumOrDefaultも用意してあります。キーがあるかチェックするContainsKeyも。

enum Sex
{
    Unknown = 0, Male = 1, Female = 2
}

enum BloodType
{
    Unknown, A, B, AB, O
}

int age;
string name;
DateTime? requestTime;  // nullableやDateTimeもサポート
bool hasChild;
Sex sex;               // enumもいけます
BloodType bloodType;

protected void Page_Load(object sender, EventArgs e)
{
     // こんなQueryStringがあるとして
    // a=20&n=John%3dJohn+Ab&s=1&bt=AB

    // ageは左から推論してintを返します
    age = Request.QueryString.ParseValue("a"); // 20

    // UrlDecodeしたstringが欲しい時は第二引数にメソッドそのものを渡すだけ
    name = Request.QueryString.ParseValue("n", HttpUtility.UrlDecode); // John=John Ab

    // 代入先の型がnullableの場合は、もしキーが見つからなかったりパースに失敗したらnullにしてくれます
    requestTime = Request.QueryString.ParseValue("t", HttpUtility.UrlDecode); // null

    // キーが見つからなかったりパースに失敗したら指定した値を返してくれます
    hasChild = Request.QueryString.ParseValueOrDefault("cld", false); // false
    
    // Enumの変換は数字の場合でも文字列の場合でも、どちらでも変換可能です
    sex = Request.QueryString.ParseEnum<Sex>("s"); // Sex.Male
    bloodType = Request.QueryString.ParseEnumOrDefault<BloodType>("bt", BloodType.Unknown); // BloodType.AB

    // ContainsKeyはキーの有無をチェックします
    var hasFlag = qs.ContainsKey("flg"); // false
}

これで、あらゆるケースでサクッと変換することが可能なのではかと思います。ちなみに、ParseValue/ParseEnumでキーが見つからなかった場合はKeyNotFoundExceptionを返します。ParseValueで失敗したらFormatException、ParseEnumで失敗したらArgumentExceptionです。この二つが分かれているのは、int.Parseとかに渡しているだけなので、そちらの都合です。

仕組み

ImplicitQueryStringという名前がネタバレなのですが、単純に暗黙的変換をしているだけです。左から推論とか、ただのハッタリです。neue cc - dynamicとQueryString、或いは無限に不確定なオプション引数についてを書いたときに、わざわざdynamicを持ち出さなくても、暗黙的変換で十分じゃないの?そのほうが利便性も上じゃないの?と思ったのでやってみたら、やはりドンピシャでした。

暗黙的変換は、あまり使う機能じゃあないと思いますし、実際、乱用すべきでない機能であることには違いないのですが、たまに活用する分には中々刺激的で創造性に満ちています。大昔からあるけれど普段使わない機能だなんて、ロストテクノロジーっぽくて浪漫に溢れています。

さて、ただし活用するにはコード中に型を並べなければならないので、汎用的というわけではないのもそうなのですが、人力で書くのもシンドイところ。勿論人力でやる気はしないのでT4 Templateを使いました。

<#@ template language="C#" #>
<#@ output extension="cs" #>
<#@ assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Reflection" #>
<#
    var methods = typeof(Convert).GetMethods(BindingFlags.Static | BindingFlags.Public);

    var converters = methods.Where(x => x.Name.StartsWith("To"))
        .Select(x => Regex.Replace(x.Name, "^To", ""))
        .Where(x => !x.StartsWith("Base64") && x != "String")
        .Distinct()
        .ToArray();
#>
using System;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace Codeplex.Web
{
    public static class NameValueCollectionExtensions
    {
        public static ConvertableString ParseValue(this NameValueCollection source, string key)
        {
            return ParseValue(source, key, null);
        }

        public static ConvertableString ParseValue(this NameValueCollection source, string key, Func<string, string> converter)
        {
            var values = source.GetValues(key);
            if (values == null) return new ConvertableString(null);

            var value = values[0];
            return new ConvertableString(converter == null ? value : converter(value));
        }
        
        // 中略
    }

    public struct ConvertableString
    {
        public readonly string Value;

        public ConvertableString(string value)
        {
            this.Value = value;
        }

<# foreach (var converter in converters) { #>
        public static implicit operator <#= converter #>(ConvertableString self)
        {
            if (self.Value == null) throw new KeyNotFoundException();
            return <#= converter #>.Parse(self.Value);
        }

        public static implicit operator <#= converter #>?(ConvertableString self)
        {
            <#= converter #> value;
            return (self.Value != null && <#= converter #>.TryParse(self.Value, out value))
                ? new Nullable<<#= converter #>>(value)
                : null;
        }

<# } #>
        public static implicit operator String(ConvertableString self)
        {
            return self.Value;
        }

        public override string ToString()
        {
            return Value;
        }
    }
}

以上のコードから、T4生成部分を取り出すと

public static implicit operator Boolean(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Boolean.Parse(self.Value);
}

public static implicit operator Boolean?(ConvertableString self)
{
    Boolean value;
    return (self.Value != null && Boolean.TryParse(self.Value, out value))
        ? new Nullable<Boolean>(value)
        : null;
}

public static implicit operator Char(ConvertableString self)
{
    if (self.Value == null) throw new KeyNotFoundException();
    return Char.Parse(self.Value);
}

public static implicit operator Char?(ConvertableString self)
{
    Char value;
    return (self.Value != null && Char.TryParse(self.Value, out value))
        ? new Nullable<Char>(value)
        : null;
}

// 以下SByte, Byte, Int16, Int32, Int64, ..., DateTimeと繰り返し

といったコードがジェネレートされます。清々しいまでのゴリ押しっぷり。一周回ってむしろエレガント。ConvertableStringのほうでKeyNotFoundExceptionを出すのがイケてないのですが、まあそこはShoganaiかなー、ということで。さて、それはそれとして、やっぱVisual Studioに組み込みで、こういった自動生成のテンプレートエンジンが用意されているというのは非常に嬉しいところです。

まとめ

特にASP.NETを使っている方々にどうぞ。ASP.NET MVCの人はしらにゃい。なのでいつもの私だったら.NET 4.0専用にするところが、今回は.NET 3.5でも大丈夫!Visual Studio 2008でも動作確認取りました!

それにしても実際ASP.NETを使いこなしてる人はクエリストリングの取得はどんな風にやっていたものなのでしょうかー。何かしらの手法がないとシンドすぎやしませんか?そう思って検索してみたんですが、どうにも見つからず。さすがに ParseValueAsInt() とかって拡張メソッドぐらい作ってやり繰りは私も大昔していたし、それでintに対応しておけばほぼほぼOKではありますが(拡張メソッドがなかった時代はさすがに想像したくない!)。そう考えると、ちょっとこれはやりすぎ感も若干。でも、一度できあがればずっと使えますしね。というわけでImplicitQueryString、是非是非使ってみてください。本当に楽になれると思います。

dynamicとQueryString、或いは無限に不確定なオプション引数について

どうもこんばんわ、30時間も寝てないわー。まあ、お陰で真昼間の就業中にうつらうつらしてましたが!ちなみにGRAVITY DAZEにはまって延々とプレイしてたから寝てないだけです。PS陣営にくだるなんて!さて、それはそれとしてBlog更新頻度の低下っぷりはお察し下さい。そんなこんなで最近の仕事(仕事ですって!仕事してるんですって!?)はASP.NETなんですが、色々アレですね。ふふふ。ソウルジェムは真っ黒と真っ白を行ったり来たりしてて楽しいです。まあ、ようするに楽しいです。楽しいのはいいとしてBlogが停滞するのはいくないので、電車でGRAVITY DAZEやりながらQueryStringうぜえええええ、とか悶々としたので帰り道に殺害する算段を整えていました。

QueryStringって、qとかnumとかiとかsとか、それそのものだけじゃ何を指してるかイミフなので、C#的にはちゃんと名前をつけてやりたいよね。だから、ただシンプルに触れるというだけじゃダメで。あと、コンバートもしたいよね。数字はintにしたいしboolは当然boolなわけで。さて、そんな時に私たちが持つツールと言ったら、dynamicです。C#とスキーマレスな世界を繋ぐ素敵な道具。今日も活躍してもらうことにしましょう。たまにしか出番ないですからね。

private int id;
private int? count;
private string keyword;
private bool isOrdered;

protected void Page_Load(object sender, EventArgs e)
{
    // id=10&c=100&q=hogehoge&od=true というクエリストリングが来る場合

    // dynamicに変換!
    var query = this.Request.QueryString.AsDynamic();

    id = query.id; // キャストは不要、書くの楽ですね!(キーが存在しないと例外)
    count = query.c; // 型がnullableならば、存在しなければnull
    keyword = query.q; // stringの場合も存在しない場合はnull
    isOrdered = query.od(false); // メソッドのように値を渡すと、キーが存在しない場合のデフォルト値になる
}

ASP.NETの例ですが、どうでしょう、まぁまぁ良い感じじゃあなくて?ちなみに最初はnull合体演算子(??)を使いたかったのですが、DynamicObjectでラップした時点で、そもそも存在がnullじゃないということで、何をどうやってもうまく活用できなくて泣いた。しょうがないのでメソッド形式でデフォルト値を渡すことでそれっぽいような雰囲気に誤魔化すことにしました、とほほほ。

dynamicとオプション引数

クエリストリングを生成することも出来ます。

// 戻り値はdynamicで空のDynamicQueryStringを生成
var query = DynamicQueryString.Create();

// オプション引数の形式で書くと……?
query(id: 100, c: 100, q: "hogehoge", od: true);

Console.WriteLine(query.ToString()); // id=10&c=100&q=hogehoge&od=true

といった感じで、面白いのがオプション引数の使い方。匿名型渡しのように、スマートにKey:Valueを渡すことを実現しています。dynamicなので引数名は完全自由。個数も完全自由。非常にC#らしくなくC#らしいところが最高にCOOL。匿名型とどちらがいいの?というと何とも言えないところですが、面白いには違いないし、面白いは正義。C#も工夫次第でまだまだ色々なやり方が模索できるんですよー、ってところです。

実装など

眠いのでコードの解説はしません(ぉ。DynamicObjectの実装方法はneue cc - C# DynamicObjectの基本と細かい部分についてでどーぞ。overrideしていくだけなので、別に難しくもなんともないので是非是非やってみてください。ちなみに無限のオプション引数を実現している箇所は binder.CallInfo.ArgumentNames.Zip(args, (key, value) => new { key, value }) です。

public static class NameValueCollectionExtensions
{
    public static dynamic AsDynamic(this NameValueCollection queryString)
    {
        return new DynamicQueryString(queryString);
    }
}

public class DynamicQueryString : DynamicObject
{
    NameValueCollection source;

    public DynamicQueryString()
    {
        this.source = new NameValueCollection();
    }

    public DynamicQueryString(NameValueCollection queryString)
    {
        this.source = queryString;
    }

    public static dynamic Create()
    {
        return new DynamicQueryString();
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var value = source[binder.Name];
        result = new StringMember((value == null) ? value : value.Split(',').FirstOrDefault());
        return true;
    }

    public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
    {
        foreach (var item in binder.CallInfo.ArgumentNames.Zip(args, (key, value) => new { key, value }))
        {
            source.Add(item.key, item.value.ToString());
        }

        result = this.ToString();
        return true;
    }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
        if (binder.Type != typeof(string))
        {
            result = null;
            return false;
        }
        else
        {
            result = this.ToString();
            return true;
        }
    }

    public override string ToString()
    {
        return string.Join("&", source.Cast<string>().Select(key => key + "=" + source[key]));
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return source.Cast<string>();
    }

    class StringMember : DynamicObject
    {
        readonly string value;

        public StringMember(string value)
        {
            this.value = value;
        }

        public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
        {
            var defaultValue = args.First();

            try
            {
                result = (value == null)
                    ? defaultValue
                    : Convert.ChangeType(value, defaultValue.GetType());
            }
            catch (FormatException) // 真面目にやるならType.GetTypeCodeでTypeを分けて、例外キャッチじゃなくてTryParseのほうがいいかな?
            {
                result = defaultValue;
            }

            return true;
        }

        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            try
            {
                var type = (binder.Type.IsGenericType && binder.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
                    ? binder.Type.GetGenericArguments().First()
                    : binder.Type;

                result = (value == null)
                    ? null
                    : Convert.ChangeType(value, binder.Type);
            }
            catch (FormatException)
            {
                result = null;
            }

            return true;
        }

        public override string ToString()
        {
            return value ?? "";
        }
    }
}

コンバートに対応させるために、要素一つだけのDynamicObjectを中で用意しちゃってます。そこがポイント、といえばポイント、かしら。GetDynamicMemberNamesはデバッガの「動的ビュー」に表示されるので、Visual Studioに優しいコードを書くなら必須ですね。

まとめ

dynamicはC#と外の世界を繋ぐためのもの。今日もまた一つ繋いでしまった。それはともかくとして、一番最初、DynamicJsonを実装した頃にも言ったのですが、dynamicはDSL的な側面もあって、普通に楽しめますので、ちょっと頭をひねって活用してみると、また一つ、素敵な世界が待っています。

今回はコード書くのに2時間、この記事を書くのに1時間、でしたー。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive