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

# PlantUmlClassDiagramGenerator.SourceGenerator
.NET Compiler Platform "Roslyn" の機能、[SourceGenerator](https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/source-generators-overview)を利用してソースコードからPlantUMLのクラス図を生成するツールを作ってみました。

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

https://www.nuget.org/packages/PlantUmlClassDiagramGenerator.SourceGenerator

## ソースコード
ソースコードはこちら

https://github.com/pierre3/PlantUmlClassDiagramGenerator/tree/feature/source-generator/src/PlantUmlClassDiagramGenerator.SourceGenerator

:::note
### SyntaxTree と Symbol
PlantUmlClassDiagramGeneratorには 既に[.NET Global Tool](https://www.nuget.org/packages/PlantUmlClassDiagramGenerator) と [VSCode拡張](https://marketplace.visualstudio.com/items?itemName=pierre3.csharp-to-plantuml)としてリリースしているものがあります。
こちらも同じく "Roslyn" の機能を利用しているのですが、構文木(SyntaxTree)を　[SyntaxWalker](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.codeanalysis.syntaxwalker?view=roslyn-dotnet-4.7.0)で辿りながらUMLのコードを生成する仕組みとなっています。

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

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

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

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

[PlantUmlClassDiagramGenerator.SourceGenerator](https://www.nuget.org/packages/PlantUmlClassDiagramGenerator.SourceGenerator) パッケージを NuGet Gallery から取得し、.NET プロジェクトにインストールします。

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

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

リリースビルド時にツールを実行するように設定するには、.csproj ファイルに以下のセクションを追加します。
```xml
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
    <DefineConstants>$(DefineConstants);GENERATE_PLANTUML</DefineConstants>
</PropertyGroup>
```

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

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

```cs
using PlantUmlClassDiagramGenerator.SourceGenerator.Attributes;

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

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

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

## 仕様(仮)

### C# sample code
[InputClasses.cs](https://github.com/pierre3/PlantUmlClassDiagramGenerator/blob/feature/source-generator/test/SourceGeneratorTest/Classes/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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/157dd411-218f-de05-1d31-58b6b9b543c5.png)


- struct

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

![Point.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/8002222b-ab7a-730f-fb20-3d9987a80f85.png)

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

![IInterface.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/4e284a37-de7f-042a-37cd-0120e28c6494.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/1385c80c-ceae-cefc-3d53-efed10b04f26.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/f0eb8943-e517-23ae-48a2-47fc72d9481a.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/63857089-1224-44e2-3ef6-bfc2ef75e528.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
```

#### 出力例　

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

- 関連クラスの`!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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28805/f45aa1c8-43c2-1d82-5e1d-e14b2b22de05.png)