PlantUmlClassDiagramGenerator.SourceGenerator
.NET Compiler Platform "Roslyn" の機能、SourceGeneratorを利用してソースコードからPlantUMLのクラス図を生成するツールを作ってみました。
NuGetにて公開しています。とりあえず作ってみたというレベルなのでアルファテストバージョンとしています。
ソースコード
ソースコードはこちら
SyntaxTree と Symbol
PlantUmlClassDiagramGeneratorには 既に.NET Global Tool と VSCode拡張としてリリースしているものがあります。
こちらも同じく "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
}
- struct
struct Point <<sealed>> {
+ Point(x : int, y : int) : void
+ X : int <<get>> <<set>>
+ Y : int <<get>> <<set>>
+ Point() : void
}
- interface
interface IInterface {
+ MethodA(parameters : Paramters) : void
+ MethodB(value : int) : int
}
- abstract class
abstract class BaseClass`2<T1, T2> {
+ {abstract} <<readonly>> Name : T1 <<get>>
+ {abstract} <<readonly>> Value : T2 <<get>>
+ {abstract} GetNameValue() : string
# BaseClass() : void
}
- 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
}
- 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
}
関連(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