Roslyn で C#のソースコードからPlantUMLのクラス図を生成する

前回 に引き続き、PlantUMLの話になります。

ReactiveProperty の記事もまだ途中なのですが、もうちょっと寄り道します。

今回は、Roslynで C#ソースコードから PlantUMLのクラス図を生成するプログラムを作ってみよう!というお話です。

Roslyn を使って C#ソースコードから PlantUML のクラス図を生成してみる

Roslyn でメタプログラミング

Roslynの導入方法、Roslynを使ったプログラミングについては、Build Insiderの記事が参考になります。

www.buildinsider.net

www.buildinsider.net

サンプルコード

今回も、サンプルコードをGitHubに上げておきました。

github.com

CSharpSyntaxTree および CSharpSyntaxWalker

今回のプログラムですが、基本的な仕組みは以下のようになります。

  1. ソースコード構文木(CSharpSyntaxTree)にパース
  2. CSharpSyntaxWalker を継承したクラスを用意して、必要な要素をVisitorパターンで訪問
  3. 訪問先で、取得した情報を加工して出力

といった流れで、いたってシンプルです。

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        //ソースコード
        var code = @"class Hoge{ ...(略)"; 
        //SyntaxTree にパース
        var tree = CSharpSyntaxTree.ParseText(code);
        var root = tree.GetRoot();
        
        //出力先。今回はStringWriterで書き込みます
        var output = new StringBuilder();
        using (var writer = new StringWriter(output))
        {
            //CSharpSyntaxWalker を継承したクラス
            //この中で変換処理やら出力やらをやります
            var gen = new ClassDiagramGenerator(writer, indent:"    ");
            gen.Visit(root);
        }
        Console.WriteLine(output);
    }
}

ClassDiagramGenerator

上記のコードで重要なのが、ClassDiagramGenerator で、このプログラムの心臓部になります。
CSharpSyntaxWalker を継承しています。

では、ClassDiagramGenerator をクラス図で見てみましょう。

http://pierre3net.azurewebsites.net/Content/image/ClassDiagramGenerator.svg

Visit~ と名前の付いたメソッドがいくつかあります。
これらはCSharpSyntaxWalker からオーバーライドしたものです。構文木を辿り、対応するノード(ここではクラス、インターフェース、プロパティ、メソッド等の定義部)を訪問した際に実行されます。
このメソッド内に、必要な情報を取得し、適切な文字列に変換して出力する処理を記述します。

細かな実装については、割愛させていただきます。 Roslynのおかげでとてもシンプルなコードになっていると思いますので、詳細は以下のソースコードで確認してみてください。

ClassDiagramGenerator.cs

変換例

実際に、C#のクラス定義がどのように変換されるかの一例を示します。

C# (ClassA.cs)

public class ClassA
{
    private readonly int intField = 100;
    private static string strField;
    protected double X = 0, Y = 1, Z = 2;

    protected int PropA { get; private set; }
    protected internal string PropB { get; protected set; }
    internal double PropC { get; } = 3.141592;

    public ClassA() { }
    static ClassA() { strField = "static field"; }

    protected virtual void VirtualMethod() { }
    public override string ToString()
    {
        return intField.ToString();
    }

    public static string StaticMethod() { return strField; }
}

PlantUML (ClassA.plantuml)

class ClassA {
    - <<readonly>> intField : int = 100
    - {static} strField : string
    # X : double = 0
    # Y : double = 1
    # Z : double = 2
    # PropA : int <<get>>
    # <<internal>> PropB : string <<get>> <<protected set>>
    <<internal>> PropC : double <<get>> = 3.141592
    + ClassA()
    {static} ClassA()
    # <<virtual>> VirtualMethod() : void
    + <<override>> ToString() : string
    + {static} StaticMethod() : string
}

クラス図 (ClassA.svg)
http://pierre3net.azurewebsites.net/Content/image/ClassA.svg

オブジェクト間のリンク

複数オブジェクトの関連付けなどには対応していません。
これは、別途手書きで行います。

@startuml
!include ClassA.plantuml
!include ClassB.plantuml

IDisposable ()-- ClassA
ClassBBase <|-- ClassB
ClassA o- ClassB : aggregation
@enduml

PlantUMLでは、!include [ファイル名] で別のPlanuUMLファイルを読み込んで使用することが出来ます。
このように、クラス単位で自動生成したファイルをIncludeして、関連のみを別ファイルに記載するとスッキリして良いです。

http://pierre3net.azurewebsites.net/Content/image/ClassDiagram.svg

まとめ

今回の様に、C#のコードから他の言語(DSL)などを生成するといった事が、 Roslyn の力で いとも簡単に出来てしまうことが分かりました。

むしろ生成処理そのものよりも、C#の型定義とUMLとの対応付けをどうするかを決める作業の方が難しいのではないでしょうか。

C#に固有の、UMLでは定義されていない(もしくは微妙に意味が異なる)要素をどう扱うかが非常に悩ましいところでした。
今回は、なるべく変換処理が複雑にならないように、というのも加味して、今の実装に落ち着いたのですが...

この辺りの話は、また別の記事でまとめたいと思います