LoginSignup
10
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

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

Last updated at Posted at 2024-01-28

PlantUmlClassDiagramGenerator.SourceGenerator

.NET Compiler Platform "Roslyn" の機能、SourceGeneratorを利用してソースコードからPlantUMLのクラス図を生成するツールを作ってみました。

NuGetにて公開しています。とりあえず作ってみたというレベルなのでアルファテストバージョンとしています。

ソースコード

ソースコードはこちら

SyntaxTree と Symbol

PlantUmlClassDiagramGeneratorには 既に.NET Global ToolVSCode拡張としてリリースしているものがあります。
こちらも同じく "Roslyn" の機能を利用しているのですが、構文木(SyntaxTree)を SyntaxWalkerで辿りながらUMLのコードを生成する仕組みとなっています。

今回のSourceGeneratorを使った実装では、コンパイルの過程で生成されるSymbolからクラス図を生成するのに必要な情報を取得するように再設計しています。

Symbolはソースコードの解析に必要な情報をSyntaxTreeと比べ利用しやすい形で提供してくれます。また、参照している別アセンブリ内の型情報など、SyntaxTreeでは取得できない情報も含んでおり、これまで実現の難しかった機能も実装できるようになるかもしれません。

使い方と(現時点での)仕様を載せておきますので、興味がありましたら試してみてください。フィードバックも大歓迎です。

使い方

1. NuGet パッケージのインストール

PlantUmlClassDiagramGenerator.SourceGenerator パッケージを NuGet Gallery から取得し、.NET プロジェクトにインストールします。

2. 条件付きコンパイルシンボルに "GENERATE_PLANTUML" を含める

このツールは、プリプロセッサシンボルに "GENERATE_PLANTUML" が定義されている場合にのみ動作します。ツールはコーディング中に常に動作する必要がなく、必要になったタイミングで 1 度だけ実行すれば十分です。そのため、特定のビルド構成時のみ動作する仕組みとしています。

プロジェクトのビルド構成の条件付きコンパイルシンボルに "GENERATE_PLANTUML" を追加します。

リリースビルド時にツールを実行するように設定するには、.csproj ファイルに以下のセクションを追加します。

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <DefineConstants>$(DefineConstants);GENERATE_PLANTUML</DefineConstants>
</PropertyGroup>

3. クラスに"PlantUmlDiagram"属性を付与する

クラス図として出力したいクラス、構造体などに"PlantUmlDiagram"属性を付与します。

using PlantUmlClassDiagramGenerator.SourceGenerator.Attributes;

[PlantUmlDiagram]
class ClassA 
{
  //...
}

4. クラス図の出力

クラス図は、プロジェクトディレクトリ配下に作成される generated-uml フォルダへ出力されます。

  • 1クラスごとにファイルが生成されます
  • ファイル名は {クラス名}.puml です

仕様(仮)

C# sample code

InputClasses.cs に記述されているソースコードを例に、どのようにクラス図へ変換されるかを見ていきます。

型の表現

PlantUMLで利用できる型のキーワードは以下の通りです。

  • class
  • struct
  • interface
  • abstract class
  • enum

record, static,sealedなどの修飾子はステレオタイプ(<<keyword>>)で表現します。

  • class
class Item  {
    + Name : string <<get>> <<set>>
    + Value : Vector <<get>> <<set>>
    + Item() : void
}

Item.png

  • struct
struct Point <<sealed>>  {
    + Point(x : int, y : int) : void
    + X : int <<get>> <<set>>
    + Y : int <<get>> <<set>>
    + Point() : void
}

Point.png

  • interface
interface IInterface  {
    + MethodA(parameters : Paramters) : void
    + MethodB(value : int) : int
}

IInterface.png

  • abstract class
abstract class BaseClass`2<T1, T2>  {
    + {abstract} <<readonly>> Name : T1 <<get>>
    + {abstract} <<readonly>> Value : T2 <<get>>
    + {abstract} GetNameValue() : string
    # BaseClass() : void
}

BaseClass`2.png

  • enum
enum LogLevel <<sealed>>  {
    Trace = 0
    Debug = 1
    Info = 2
    Warning = 3
    Error = 4
    + LogLevel() : void
}
enum ItemFlags <<Flags>> <<sealed>>  {
    None = 0
    Alpha = 1
    Beta = 2
    Gamma = 4
    Delta = 8
    + ItemFlags() : void
}

enum.png

  • record
    レコード型は自動実装されるメソッドまでも出力されてしまってますね。
class Paramters <<record>>  {
    + Paramters(ParamA : string, ParamB : string, ParamC : int, ParamD : int) : void
    # <<readonly>> <<virtual>> EqualityContract : Type <<get>>
    + ParamA : string <<get>> <<set>>
    + ParamB : string <<get>> <<set>>
    + ParamC : int <<get>> <<set>>
    + ParamD : int <<get>> <<set>>
    + <<override>> ToString() : string
    # <<virtual>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Paramters?, right : Paramters?) : bool
    + {static} operator ==(left : Paramters?, right : Paramters?) : bool
    + <<override>> GetHashCode() : int
    + <<override>> Equals(obj : object?) : bool
    + <<virtual>> Equals(other : Paramters?) : bool
    # Paramters(original : Paramters) : void
    + Deconstruct(ParamA : string, ParamB : string, ParamC : int, ParamD : int) : void
}
struct Vector <<sealed>> <<readonly>> <<record>>  {
    + Vector(X : double, Y : double, Z : double) : void
    + <<readonly>> X : double <<get>>
    + <<readonly>> Y : double <<get>>
    + <<readonly>> Z : double <<get>>
    + <<readonly>> Norm() : double
    + <<readonly>> <<override>> ToString() : string
    - <<readonly>> PrintMembers(builder : StringBuilder) : bool
    + {static} operator !=(left : Vector, right : Vector) : bool
    + {static} operator ==(left : Vector, right : Vector) : bool
    + <<readonly>> <<override>> GetHashCode() : int
    + <<readonly>> <<override>> Equals(obj : object) : bool
    + <<readonly>> Equals(other : Vector) : bool
    + <<readonly>> Deconstruct(X : double, Y : double, Z : double) : void
    + Vector() : void
}

record.png

関連(Association)

ある型と他の型との関連付けは、個々の型定義の下に追加されます。関連付けを行う条件と付与する関連の種類は以下の通りです。

継承

対象の型がObject、ValueType、Struct以外の型を継承している場合、Inheritance (<|--)の関連を追加します。

"BaseClass`2" "<String, Int32>" <|-- DerivedClass

インターフェイスの実装

対象の型がインターフェイスを実装している場合、Realization (<|..)の関連を追加します。

IInterface <|.. DerivedClass

プロパティまたはフィールドのメンバー

対象の型で定義されているプロパティ、フィールドの型がPlantUmlDiagram属性でマークされた型の場合に Aggrigation (o--)の関連を追加します。

DerivedClass o-- "ILogger`1" : Logger

配列型またはIEnumerable<T>を実装している型の場合はプロパティやフィールドの型との関連ではなく、要素の型との関連を追加します。その際、要素の型側に多重度"*"を付与します。

DerivedClass o-- "*" Item : Item1  //Item1: IList<Item>
DerivedClass o-- "*" Item : Item2  //Item2: Item[]

メソッドのパラメータ

対象の型で定義されているメソッドのパラメータの型がPlantUmlDiagram属性でマークされた型の場合に Dependency (..>)の関連を追加します。

DerivedClass ..> Paramters

ファイル参照

各関連を追加すると同時に、!includeディレクティブを追加して、関連する相手方の型定義を参照するように設定します。

!include Parameters.puml
...
DerivedClass ..> Paramters

出力例 

生成される1ファイルの出力例を示します。ファイルは以下のような構成になっています。

  • 関連クラスの!include
  • クラスの定義
  • 関連の定義
@startuml DerivedClass
!include BaseClass`2.puml
!include IInterface.puml
!include ILogger`1.puml
!include Item.puml
!include Paramters.puml
class DerivedClass  {
    - <<readonly>> Logger : ILogger<DerivedClass> <<get>>
    + <<readonly>> <<override>> Name : string <<get>>
    + <<readonly>> <<override>> Value : int <<get>>
    + Item1 : IList<Item> <<get>> <<set>>
    + Item2 : Item[] <<get>> <<set>>
    + DerivedClass(logger : ILogger<DerivedClass>) : void
    + <<override>> GetNameValue() : string
    + MethodA(parameters : Paramters) : void
    + MethodB(value : int) : int
    + Process(parameters : Paramters) : void
}
"BaseClass`2" "<String, Int32>" <|-- DerivedClass
IInterface <|.. DerivedClass
DerivedClass o-- "ILogger`1" : Logger
DerivedClass o-- "*" Item : Item1
DerivedClass o-- "*" Item : Item2
DerivedClass ..> "<DerivedClass>" "ILogger`1"
DerivedClass ..> Paramters
@enduml

associations.png

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10