読者です 読者をやめる 読者になる 読者になる

銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

jsonをクラスとして貼り付けたあとに、プロパティ名をPascal Case に変更する CodeFixProvider を作ってみた (要Json.Net)

C# CodeFixProvider Visual Studio SDK

最近のVisual Studioでは jsonの文字列をコピーして、C# のクラスとして貼り付ける機能があります。

f:id:tanaka733:20150607225117p:plain

なんですが、これ元のjsonのキーが snake_case の場合、できたC#のクラス名も snake_case になるので気持ち悪いんですね。

f:id:tanaka733:20150607230240p:plain

C# でjsonを扱う多くの場合、Json.Net (Newtonsoft.Json) を利用していると思います。このJson.Netを使う場合、JsonPropertyという属性をプロパティに指定すると、任意のプロパティ名をC#側で利用できて、PropertyNameに指定した名前をjsonのキーとして利用することができるようになります。しかし、すべてのプロパティに手動でJsonPropertyを追加するのはハイパー刺身たんぽぽ作業でつらい...

というわけで、プロパティ名をPascal Caseに変更する CodeFixProvider を作りました。Nugetから利用できます。

github.com

www.nuget.org

まだ、とりあえず作ってみた状態でいくつか既知の問題があります。他にも見つけたらぜひバグレポートをお願いします。

  • Json.Netのインストールは行わない(存在しないとコンパイルエラーになる)
  • usingディレクティブが1つもないコードには using Newtonsoft.Json の自動追加ができない
  • 繰り返し実行することは想定していない

コードとしては CodeFixProvider.cs がほぼすべてですが、3点ほど詳しく取り上げてみます。

繰り返し SyntaxNode の置き換えをやる場合、その都度置換対象のSyntaxNodeを取得しないといけない

今回、クラスのプロパティすべてに対して繰り返し操作を行いますが、SyntaxTree は immutable なのでその都度作り直します。なので、あらかじめクラスのプロパティ一覧を IEnumerable で確保しておいても、最初のプロパティを新しいプロパティで置換した Document に対して次のプロパティで置換しようとしても、そんなPropertyDeclarationSyntaxはねえ、と怒られます。なので、いったんクラス名とプロパティ名を確保しておき、構築しなしたSyntaxTreeに対し、都度置換対象となるプロパティを検索するようにしています。

var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root;
var names = typeDecl.ChildNodes()
    .OfType<PropertyDeclarationSyntax>()
    .Select(p => p.Identifier.Text);

foreach (var name in names)
{
    //最新のドキュメントルートから変更対象のプロパティを名前から探す。
    //ドキュメントルートは構築しなおされているので、SyntaxNodeの参照一致では見つけられない
    var property = newRoot.DescendantNodes()
        .OfType<TypeDeclarationSyntax>()
        .First(t => t.Identifier.Text == typeDecl.Identifier.Text)
        .ChildNodes()
        .OfType<PropertyDeclarationSyntax>()
        .First(p => p.Identifier.Text == name);
    var previousName = property.Identifier.Text;
    //以下省略
}

プロパティに属性を追加するコード

これはコードを見るのが早いでしょう。[JsonProperty(snake_case, Hoge = 1)] というプロパティを構築するコードです*1

//新しいプロパティを構築
var newProperty = property
    .WithIdentifier(SyntaxFactory.Identifier("snake_case"))
    .AddAttributeLists(
        SyntaxFactory.AttributeList(
            SyntaxFactory.SingletonSeparatedList(
                SyntaxFactory.Attribute(SyntaxFactory.ParseName("JsonProperty"))
                .AddArgumentListArguments(
                    SyntaxFactory.AttributeArgument(
                        SyntaxFactory.LiteralExpression(
                            SyntaxKind.StringLiteralExpression, 
                            SyntaxFactory.Literal(previousName))),
                        SyntaxFactory.AttributeArgument(SyntaxFactory.NameEquals("Hoge"), default(NameColonSyntax),
                            SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(1)))
                )
            )
        )
    );

usingディレクティブに Newtonsoft.Json を追加するコード

usingディレクティブをチェックして、Newtonsoft.Jsonが定義されていない場合のみ末尾に追加するコードです。TODOコメントにあるように、usingが1つもないときに挿入する方法がわかっていません...

if (newRoot.ChildNodes()
    .OfType<UsingDirectiveSyntax>()
    .All(u => (u.Name as QualifiedNameSyntax)?.ToFullString() != "Newtonsoft.Json"))
{
    //usingディレクティブの最後の要素
    var lastUsing = newRoot.DescendantNodes()
            .OfType<UsingDirectiveSyntax>()
            .LastOrDefault();
    var insertingUsing = SyntaxFactory.UsingDirective(
        SyntaxFactory.IdentifierName("Newtonsoft.Json"));
    if (lastUsing == null)
    {
        //TODO using が1つもない場合に挿入する方法がわからない
        //newRoot = newRoot.InsertNodesBefore(newRoot.ChildNodes().First(), new[] {insertingUsing});
    }
    else
    {
        newRoot = newRoot.InsertNodesAfter(lastUsing, new[] { insertingUsing });
    }
}

CodeFixProvider は面白いんですが、「こうしたいときにこう書く」という情報が探しづらいので、もっと出てくるといいですね。

*1:もっと Parse を使って簡単にかける気もしつつ