Mermaidでasmdefを可視化するエディタ拡張をつくってあそぼう


お世話になっております。クラスター株式会社でUnityエンジニアをしているRDAGでございます。
本記事はクラスター Advent Calendar 2022(2ページ目)の7日目の記事です!

前回は @yoan さんの「httpプロトコルでもgit認証情報を保持する (nixos設定も)」でした。
git認証周りは私も定期的にトラブったりしているのでありがたい記事です……🙏


asmdefってなに

Unityではプロジェクトがどのようにコンパイルされるかを設定し、コンパイル時間を減らしたりできるAssembly Definitionというファイルが用意されています。
clusterでもこれらを活用して層関係をクリーンに保っています。

このAssembly Definitionアセット(asmdef)ですが、数が増えてくると「これがこれ参照してるのはどうなんだ?」になったり、関係する実装が整理されてくると「ここの参照もう不要になってない?」になったりします。
こういう時にプロジェクト全体のasmdefをよい感じに可視化できれば良いのでは……?と思い至ったのがこの記事の始まりです。

可視化ツールとしてはAsmdefHelperというUPMが公開されています。色々便利機能もあってオススメです……が今回はあえて自作します!)

どうやって可視化するの

プロジェクトの構造を可視化するといえばUML図。UMLを描くためのツールといえば少し前まではPlantUMLがイチオシでしたが、何やら最近はMermaid記法なるものが流行っているそうです。
今年の春にgithubが記法をサポートするなど、よい感じの波が来ているので今回はこれを試します。

手軽にMermaidで図を描くならVSCodeのプラグインがオススメです(これとか)。markdownファイルを作って下記を入力してみます。

```mermaid
graph LR
  Hoge --> Fuga & Piyo
```
VSCodeプラグインならすぐプレビューが出る!

でた〜!キャッキャッ
記述ルールが鬼のようにシンプルで、よい感じに図の並び替えをしてくれるのがMermaidの強みらしいです。

asmdefからMermaidフォーマットを吐き出す

記法はわかったので、これを出力してくれるエディタ拡張を考えます。
asmdefはAssemblyDefinitionAssetという型で扱うことができますが、実はAssemblyDefinitionAssetはTextAssetのシンプルな派生クラスになっています。
asmdefの中身はjsonなので、まずは扱いやすい型に変換する所から始めます(ここなど利用すると早いです

public class ExportAsmdefReferences : EditorWindow
{
    // 扱いやすい型(本当はもっとフィールドがあるけど、今回は最低限で)
    class AsmDef
    {
        [SerializeField] string name;
        [SerializeField] List<string> references;

        public string Name => name;
        public IReadOnlyList<string> References => references;
    }

    // Unityエディタのメニューで呼ぶようにする
    [MenuItem("Tools/Export Asmdef References")]
    public static void ShowWindow()
    {
        var window = GetWindow<ExportAsmdefReferences>();
        window.titleContent = new GUIContent("Export Asmdef References");
        window.Show();
    }

    void OnGUI()
    {
        if (GUILayout.Button("Export Select"))
        {
            // 選択しているアセットからasmdefだけを取得
            var selectedPathList = Selection.objects.OfType<AssemblyDefinitionAsset>();
            Export(selectedPathList);
        }
    }

    // 試しでasmdefの中身をログに出す
    void Export(IEnumerable<AssemblyDefinitionAsset> assemblies)
    {
        foreach (var assemblyDefinition in assemblies)
        {
            // asmdefのjsonから扱いやすい型に変換する
            var assembly = JsonUtility.FromJson<AsmDef>(assemblyDefinition.text);

            // 出力してみる
            Debug.Log(assembly.Name);
            Debug.Log(string.Join(", ", assembly.References));
        }
    }
}

よい感じです!……と言いたい所ですが、実はasmdefのReferencesは2種類の記法があって、それを吸収してあげないと表示がよい感じになりません。

よい感じの名前を出してくれるかどうかのcheckbox(offだと嬉しい)

Use GUIDsにチェックが入っている時、Referencesの中はGUID:123456789のような文字列が入っています。GUIDからasmdefを取得して、その名前を出力するように修正してみます。

// GUIDからAssemblyDefinitionAssetを読み込む
static AssemblyDefinitionAsset LoadWithGUI(string guid) =>
    AssetDatabase.LoadAssetAtPath<AssemblyDefinitionAsset>(AssetDatabase.GUIDToAssetPath(guid));

void Expor(IEnumerable<AssemblyDefinitionAsset> assemblies)
{
    foreach (var assemblyDefinition in assemblies)
    {
        // asmdefのjsonから扱いやすい型に変換する
        var assembly = JsonUtility.FromJson<AsmDef>(assemblyDefinition.text);

        if (assembly.References.Count == 0)
        {
            // Referencesが空の時は一旦考えない
            continue;
        }

        // Use GUIDsがonなら文字列は"GUID:"から始まる
        // 設定はファイル単位なので最初の記述だけ見れば十分
        const string guidHeader = "GUID:";
        if (assembly.References[0].StartsWith(guidHeader))
        {
            // GUIDで設定されてる場合はasmdefをLoadして名前を持ってくる
            // missingになっている時の対応とかが必要だけど省略
            var asmdefList = assembly.References
                .Select(guidStr => guidStr.Remove(0, guidHeader.Length))
                .Select(LoadWithGUID)
                .Select(asmdef => asmdef.name);
            Debug.Log(string.Join(", ", asmdefList));
        }
        else
        {
            Debug.Log(string.Join(", ", assembly.References));
        }
    }
}

今度こそよい感じになりました!
asmdefから情報が取れるようになったので、MermaidのフォーマットであるHoge --> Fuga & Piyoのような形で出すようにします。

// Exportのforeachの中を抜き出し
string ToMermaid(AssemblyDefinitionAsset asset)
{
    // asmdefのjsonから扱いやすい型に変換する
    var assembly = JsonUtility.FromJson<AsmDef>(asset.text);
    if (assembly.References.Count == 0)
    {
        // 空のasmdefは名前だけ控える
        return assembly.Name;
    }

    string refStr;

    // Use GUIDsがonなら文字列は"GUID:"から始まる
    const string guidHeader = "GUID:";
    if (assembly.References[0].StartsWith(guidHeader))
    {
        // GUIDで設定されてる場合は名前を持ってくる
        var asmdefList = assembly.References
            .Select(guidStr => guidStr.Remove(0, guidHeader.Length))
            .Select(LoadWithGUID)
            .Select(asmdef => asmdef.name);
        refStr = string.Join(" & ", asmdefList);
    }
    else
    {
        refStr = string.Join(" & ", assembly.References);
    }

    return $"{assembly.Name}-->{refStr}";
}

あとはエディタ拡張でasmdefを選択した状態でボタンを押すとmarkdownファイルを書き出すように調整すれば完成です!

できたエディタ拡張

最終的にできたコードがコチラです。

using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using UnityEditorInternal;

public class ExportAsmdefReferences : EditorWindow
{
    class AsmDef
    {
        [SerializeField] string name;
        [SerializeField] List<string> references;

        public string Name => name;
        public IReadOnlyList<string> References => references;
    }

    static readonly string MermaidFilePath = "AsmdefReferences.md";

    [MenuItem("Tools/Export Asmdef References")]
    public static void ShowWindow()
    {
        var window = GetWindow<ExportAsmdefReferences>();
        window.titleContent = new GUIContent("Export Asmdef References");
        window.Show();
    }

    void OnGUI()
    {
        if (GUILayout.Button("Export Select"))
        {
            // 選択asmdefを取得
            var selectedPathList = Selection.objects.OfType<AssemblyDefinitionAsset>();
            Export(selectedPathList);
        }
    }

    static AssemblyDefinitionAsset LoadWithGUID(string guid) =>
        AssetDatabase.LoadAssetAtPath<AssemblyDefinitionAsset>(AssetDatabase.GUIDToAssetPath(guid));

    void Export(IEnumerable<AssemblyDefinitionAsset> assemblies)
    {
        var references = new List<string>();

        // はじめの句
        references.Add("```mermaid");
        references.Add("graph LR");

        // 本文
        references.AddRange(assemblies.Select(ToMermaid));

        // 結びの句
        references.Add("```");

        // ファイルを保存
        var mermaid = string.Join("\n", references);
        File.WriteAllText(MermaidFilePath, mermaid);
    }

    string ToMermaid(AssemblyDefinitionAsset asset)
    {
        var assembly = JsonUtility.FromJson<AsmDef>(asset.text);
        if (assembly.References.Count == 0)
        {
            // 空のasmdefは名前だけ控える
            return assembly.Name;
        }

        string refStr;

        // Use GUIDsがonなら文字列は"GUID:"から始まる
        const string guidHeader = "GUID:";
        if (assembly.References[0].StartsWith(guidHeader))
        {
            // GUIDで設定されてる場合は名前を持ってくる
            var asmdefList = assembly.References
                .Select(guidStr => guidStr.Remove(0, guidHeader.Length))
                .Select(LoadWithGUID)
                .Select(asmdef => asmdef.name);
            refStr = string.Join(" & ", asmdefList);
        }
        else
        {
            refStr = string.Join(" & ", assembly.References);
        }

        return $"{assembly.Name}-->{refStr}";
    }
}

当たり障りのなさそうなpackageにあるasmdefで試してみます。asmdefファイルを複数選択してExportボタンを押すと……。

```mermaid
graph LR
Unity.TextMeshPro
Unity.TextMeshPro.Editor-->Unity.TextMeshPro
Unity.TextMeshPro.Editor.Tests-->Unity.TextMeshPro & Unity.TextMeshPro.Editor
Unity.TextMeshPro.Tests-->Unity.TextMeshPro
```
よさそうなのが出ている!

Mermaidの図を含むmarkdownファイルが出力されるようになりました!
「asmdef更新して参照増やしました」PRを投げる時の説明にも便利そうです。
ではお待ちかね、果たしてcluster全体のプロジェクトの参照関係はどうなっているのか?t:AssemblyDefinitionAssetでアセットにフィルタを掛けて全選択、Exportしてみます。

Maximum text size in diagram exceeded

ア!!!
(maxTextSizeを変更する手段もあるらしいので、興味のある方はトライしてみてください)

この記事が気に入ったらサポートをしてみませんか?