C# + RoslynでVB.NETのコード分析ツールを作りたい

はじめに

あけましておめでとうございました。

普段メタプログラミング・リフレクション大好きと言っておきながらいつまでもRoslynを触らないのはどうなのかなぁ。 ということでC#とRoslynでVBの簡易的なコード分析ツールを作ってみました。

まぁ、コード分析と言ってもサイクロマティック複雑度だけですし、計算方法も要素の出現回数を数えるだけという恐ろしく単純なものですが。

えっ? VBで書けって? いいじゃんそんなこと。VBC#も変わらないよ。どっちもコンパイル後はMSILになっちゃうんだから。

github.com

Roslyを使えばシンタックスツリーの構築からセマンティクス解析まで5行程度で書けるので超お手軽ですね。 ただ、日本語の解説が少なかったりCTPの時の記事が残っていて混乱を引き起こす*1ので割とつらぽよポイントは高めです。

また、.NET 4.5.2以降じゃないと動かないのは一部の企業戦士にとってはつらいかもしれません。

環境

弊社では以下の環境で検証しました。

また、シンタックスツリーの解析コードを書くのに.NET Compiler Platform SDKのSyntaxVisualizerが無いとつらぽよポイントが爆アゲになるのでインストールしておくべきです。

NuGetでのパッケージ取得

必要なライブラリはNuGetからサクッとぶち込みましょう。

インストールするのは以下の2つです。

割といっぱい依存ライブラリがあり、結構な量がぶち込まれますが諦めて眺めていましょう。

コード解析

プロジェクトによくわからんファイルがゴテゴテありますが、本命のファイルはSaru.csだけです。

var workspace = MSBuildWorkspace.Create();
var project = workspace.OpenProjectAsync(projectFilePath).Result;
var compilation = project.GetCompilationAsync().Result;

上のコードで

までを行えます。

割と時間のかかる処理(っぽい?)ので非同期メソッドになっていますが、コンソールアプリケーションで非同期とか逆に使いずらいしゆとり世代なので.Resultで完了するまでブロックしてます。

そのあと、各ファイル毎に処理を行います。

foreach (var tree in compilation.SyntaxTrees)
{
    // 処理...
}

とりあえずファイル名とコンパイル結果の構文エラー(構文がおかしいとか)とセマンティクスエラー(宣言されていないシンボルへのアクセスとか)を出力させてます。 必要かどうかと言われると微妙ですが、まぁ一応って事で。

output.WriteLine(Path.GetFileName(tree.FilePath));
foreach (var diag in tree.GetDiagnostics())
{
    output.WriteLine(diag);
}

var semantic = compilation.GetSemanticModel(tree);
foreach(var diag in semantic.GetDiagnostics())
{
    output.WriteLine(diag);
}

あとはシンタックスツリーを根こそぎひっくり返しつつ型宣言されているものがあったら型名を引っこ抜きつつその型をメトリック計算メソッドに放り込んでいるだけです。

SyntaxVisitor? 知らない子ですね。

var nodes = new Queue<SyntaxNode>();
nodes.Enqueue(tree.GetRoot());

while (nodes.Count != 0)
{
    var node = nodes.Dequeue();
    if (node is TypeBlockSyntax)
    {
        var typeStatement = node.ChildNodes().First(e => e is TypeStatementSyntax);
        output.WriteLine("  " + semantic.GetDeclaredSymbol(typeStatement).ToDisplayString());
        CalcMethodMetric(node.ChildNodes(), semantic, output);
    }

    foreach (var child in node.ChildNodes())
    {
        nodes.Enqueue(child);
    }
}

CalcMethodMetricメソッドで実際のメトリック計算を行っています。

型宣言の子要素のうちMethodBlockSyntaxを見つけてはひたすらひたすらMethodBlockSyntaxの子要素をバラしてサイクロマティック複雑度を1upさせる要素の数を数えるだけの簡単なお仕事です。

foreach (var n in nodes)
{
    var method = n as MethodBlockSyntax;
    if (method != null)
    {
        var methodStatement = method.ChildNodes().First(e => e is MethodStatementSyntax);
        var name = semantic.GetDeclaredSymbol(methodStatement)
            .ToMinimalDisplayString(semantic, 0, ConsoleSymbolDisplayFormat.Format);
        output.Write($"    {name} ");

        var cyclomatic = 1;
        var ns = new Queue<SyntaxNode>();
        ns.Enqueue(method);

        while (ns.Count != 0)
        {
            var node = ns.Dequeue();

            if (CyclomaticCounterUtil.IsCyclomaticCountUpStatement(node))
            {
                cyclomatic++;
            }

            foreach (var i in node.ChildNodes())
            {
                ns.Enqueue(i);
            }
        }
        output.WriteLine(cyclomatic);
    }
}

ちなみにIsCyclomaticCountUpStatement(SyntaxNode)の中身はこうなっておりましてつらぽよポイントはかなり高めとなっています。

internal static bool IsCyclomaticCountUpStatement(SyntaxNode node)
{
    if (node.Kind() == SyntaxKind.GoToStatement) return true;
    if (node.Kind() == SyntaxKind.ContinueWhileStatement) return true;
    if (node.Kind() == SyntaxKind.ContinueDoStatement) return true;
    if (node.Kind() == SyntaxKind.ContinueForStatement) return true;
    if (node.Kind() == SyntaxKind.SingleLineIfStatement) return true;
    if (node.Kind() == SyntaxKind.IfStatement) return true;
    if (node.Kind() == SyntaxKind.ElseIfStatement) return true;
    if (node.Kind() == SyntaxKind.ErrorStatement) return true;
    if (node.Kind() == SyntaxKind.OnErrorGoToZeroStatement) return true;
    if (node.Kind() == SyntaxKind.OnErrorGoToMinusOneStatement) return true;
    if (node.Kind() == SyntaxKind.OnErrorGoToLabelStatement) return true;
    if (node.Kind() == SyntaxKind.OnErrorResumeNextStatement) return true;
    if (node.Kind() == SyntaxKind.ResumeStatement) return true;
    if (node.Kind() == SyntaxKind.ResumeLabelStatement) return true;
    if (node.Kind() == SyntaxKind.ResumeNextStatement) return true;
    if (node.Kind() == SyntaxKind.SelectStatement) return true;
    if (node.Kind() == SyntaxKind.CaseStatement) return true;
    if (node.Kind() == SyntaxKind.CaseElseStatement) return true;
    if (node.Kind() == SyntaxKind.WhileStatement) return true;
    if (node.Kind() == SyntaxKind.ForStatement) return true;
    if (node.Kind() == SyntaxKind.ForEachStatement) return true;
    if (node.Kind() == SyntaxKind.SimpleDoStatement) return true;
    if (node.Kind() == SyntaxKind.DoWhileStatement) return true;
    if (node.Kind() == SyntaxKind.DoUntilStatement) return true;
    if (node.Kind() == SyntaxKind.SimpleLoopStatement) return true;
    if (node.Kind() == SyntaxKind.LoopWhileStatement) return true;
    if (node.Kind() == SyntaxKind.LoopUntilStatement) return true;
    return false;
}

ちょっと余談なんですが、今時非構造化例外処理を使う人っているんですかね。

おわりに

いまいちよく分からない解説だとは思うのですが、正直弊社もよく分かっておりません。

とりあえず結論としては割合簡単なコードでシンタックスツリーやセマンティクスモデルにアクセスしてごにょごにょできる.NET Compiler Platform ("Roslyn")すげーって事で。

おしまい

*1:APIの変更とかがあったっぽい