読者です 読者をやめる 読者になる 読者になる

C#のソースコードからPlantUMLのクラス図を生成するアプリ(改)をリリースしました

Roslyn PlantUML C#

サンプルコードの棚卸

以前、以下の記事で作成したC#ソースコードからPlantUMLを生成するサンプルプログラムですが、(長らく放置状態でしたが)
少し手直しをして、それなりに使えるようにしました。

pierre3.hatenablog.com

pierre3.hatenablog.com

 PlantUmlClassDiagramGenerator

リポジトリはこちら

github.com

こちらからバイナリ(.zip)をダウンロードできます。ぜひお試しください。

Release v0.5.0.0-beta

使い方

PlantUmlClassDiagramGeneratorはコンソールアプリケーションです。 以下の様にパラメータを指定して実行します。

C:\> PlantUmlClassDiagramGenerator.exe InputPath [OutputPath] [-dir] ^
 [-public | -ignore IgnoreAccessibilities] [-excludePaths ExcludePathList]
  • InputPath (必須)
    入力するソースコードのファイル名またはディレクトリ名を指定します。
    ディレクトリを指定した場合、サブフォルダ以下を含めた全ての.csファイルが変換の対象となります。
  • OutputPath (省略可)
    出力先のファイル名またはディレクトリ名を指定します。
    省略した場合、変換元の.csファイルと同じディレクトリに変換後の.pumlファイルが出力されます。
  • -dir (省略可)
    InputPath および OutputPath がディレクトリ名の場合にこのオプションを指定します。
  • -public (省略可)
    クラス、構造体のパブリックメンバーのみを出力する場合に指定します。
  • -ignore (省略可)
    出力対象外とするアクセシビリティをカンマ区切りで指定します。
    (例): -ignore private,protected
  • -excludePaths (省略可)
    除外するファイル名またはディレクトリ名を指定します。-dir オプションを指定した場合のみ有効
    (例): -excludePaths obj,Properties\AssemblyInfo.cs

使用例

例として、こちらのソースコードを変換してみます。

github.com

ソリューションディレクトリをC:\Source\CsvEditSharpとして、プロジェクトCsvEditSharp以下の.csファイルをまとめて変換する場合を例にします。
出力先はC:\Source\CsvEditSharp\Documents\umlとします。

C:\> PlantUmlClassDiagramGenerator.exe C:\Source\CsvEditSharp\CsvEditSharp ^
 C:\Source\CsvEditSharp\Documents\uml -dir -public -excludePaths obj,Properties 

今回は、パブリックメンバーのみを出力対象としました。
また、objフォルダ内に自動生成される.cs ファイルと、Propertiesフォルダ内のAssemblyInfo.csファイルは変換対象から除外するようにしました。

変換結果は以下で確認できます。

CsvEditSharp/Documents/uml at master · pierre3/CsvEditSharp · GitHub

ちなみに、ディレクトリ単位で変換を行った場合、全ての出力ファイルを !include で参照したinclude.pumlをInputPath内に出力するようにしています。

@startuml
!include .\\App.xaml.puml
!include .\\Models\ColumnValidation.puml
!include .\\Models\CompletionData.puml
!include .\\Models\CsvConfigFileManager.puml
!include .\\Models\CsvEditSharpConfigurationHost.puml
!include .\\Models\CsvEditSharpWorkspace.puml
!include .\\Models\CustomBooleanConverter.puml
!include .\\Models\EnumerableExt.puml
!include .\\Models\GenerateConfigSettings.puml
!include .\\Models\ICsvEditSharpConfigurationHost.puml
!include .\\Models\RegisterClassMapTarget.puml
!include .\\Models\SaveConfigSettings.puml
//...(中略)...
!include .\\ViewModels\GenerateConfigDialogViewModel.puml
!include .\\ViewModels\MainWindowViewModel.puml
!include .\\ViewModels\SaveConfigDialogViewModel.puml
!include .\\Views\GenerateConfigDialog.xaml.puml
!include .\\Views\MainWindow.xaml.puml
!include .\\Views\SaveConfigDialog.xaml.puml
@enduml

これをそのままPlantUMLで画像に変換すると、こんな感じになります↓

http://pierre3net.azurewebsites.net/content/image/include.svg

主な変更点

以前の記事に書いた仕様からの主な変更点は以下の通りです。

ネストクラスの扱い

ネストされたクラスは、ネストのまま変換されてPlantUMLでエラーとなってしまっていましたが、
今回、ネストクラスは外に展開して、+-- で関連付けするように修正しました。

class OuterClass 
{
  class InnerClass 
  {
    struct InnerStruct 
    {

    }
  }
  • PlantUML
class OuterClass{

}
class InnerClass{

}
<<struct>> class InnerStruct {

}
OuterClass +- InnerClass
InnerClass +- InnerStruct

f:id:pierre3:20161217211654p:plain

ジェネリクス型の変換

PlantUMLでは、ジェネリクス型は型引数の数が異なっても同じクラスとして認識されてしまうようです。

class GenericsType {
}
class GenericsType<T1> {
}
class GenericsType<T1,T2> {
}

PlantUMLでは、上記は全て GenericsType クラスとして認識され、ダイアグラムは最後に記述した定義で上書きされてしまいます。

そこで今回は、以下の様に"クラス名に型引数の数を付加したもの"を変換後のクラス名とするようにしました。

class "GenericsType`1"<T1>{
}
class "GenericsType`2"<T1,T2>{
}

f:id:pierre3:20161217211751p:plain

継承関係

クラス、インターフェースを継承しているクラスは、ベースクラスと<|--で結んで継承関係を表すようにしました。

abstract class BaseClass
{
    public abstract void AbstractMethod();
    protected virtual int VirtualMethod(string s) => 0;
}
class SubClass : BaseClass
{
    public override void AbstractMethod() { }
    protected override int VirtualMethod(string s) => 1;
}

interface IInterfaceA {}
interface IInterfaceA<T>:IInterfaceA
{
    T Value { get; }
}
class ImplementClass : IInterfaceA<int>
{
    public int Value { get; }
}
  • PlantUML
abstract class BaseClass {
    + {abstract} AbstractMethod() : void
    # <<virtual>> VirtualMethod(s:string) : int
}
class SubClass {
    + <<override>> AbstractMethod() : void
    # <<override>> VirtualMethod(s:string) : int
}
interface IInterfaceA {
}
interface "IInterfaceA`1"<T> {
    Value : T <<get>>
}
class ImplementClass {
    + Value : int <<get>>
}
BaseClass <|-- SubClass
IInterfaceA <|-- "IInterfaceA`1"
"IInterfaceA`1" "<int>" <|-- ImplementClass

f:id:pierre3:20161217211830p:plain