9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QualiArtsAdvent Calendar 2024

Day 2

UnityでDocumentation Comment Idを生成・表示したい

Last updated at Posted at 2024-12-02

結論

demo.gif

manifest.json
{
  "scopedRegistries": [
    {
      "name": "package.openupm.com",
      "url": "https://package.openupm.com",
      "scopes": [
        "com.openupm",
        "org.nuget"
      ]
    }
  ],
  "dependencies": {
    "documentation-comment-id-viewer": "git@github.com:satanabe1/documentation-comment-id-viewer.git"
  }
}

Documentation Comment Id

Documentation Comment Idとは?

公式なドキュメントについては以下のリンク周辺にあります。

リンク先の例を元に一部抜粋すると、以下の T:Acme.WidgetM:Acme.Widget.M0 のようにクラスやメソッドを表記する方法です。詳しいフォーマットについては上記参考リンクを見てください。

namespace Acme
{
    // "T:Acme.Widget"
    class Widget
    {
        // "M:Acme.Widget.M0"
        public static void M0() { ... }
    }
}

Documentation Comment Idの使い道

Documentation Comment Idなんて、手書きしたことある人は多くないと思います。それどころか見たことすらないかも知れません。
主な用途はコード中のXMLドキュメントコメント( <summary>...</summray> のようなやつ)からドキュメントを自動生成する過程で使われるものであり、本来、人間が意識し手書きするものではありません。
しかしコードアナライザーやソースジェネレーター(UnityならILPostprocessorなんかも)の登場で、稀に手書きを強いられる状況が出てきました。

わかりやすい利用例としてBannedApiAnalyzersがあります。
https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md
BannedApiAnalyzersでは、BannedSymbols.txt にDocumentation Comment Idを記述することで、そのクラス(あるいは、メソッド・プロパティ・フィールドetc)を利用した際にコンパイルエラーや警告を発生させる、ということができます。
私が関わった実際のプロジェクトではBannedSymbols.txtには以下のような記述がありました。(一部抜粋)

M:System.DateTimeOffset.#ctor(System.DateTime);DateTimeからDateTimeOffsetを作成するコンストラクタは結果が環境依存なので使用しないでください
M:System.DateTimeOffset.op_Implicit(System.DateTime);DateTimeからDateTimeOffsetへの暗黙的な変換は結果が環境依存なので使用しないでください
P:System.DateTime.Now;TimeUtility.NowJSTDateTimeを使用してください
P:System.DateTime.UtcNow;TimeUtility.NowUnixTimeを使用してください
M:System.DateTime.ToUniversalTime();意図した変換は行えないので使用しないでください
P:System.DateTimeOffset.Now;TimeUtility.NowJSTDateTimeを使用してください
P:System.DateTimeOffset.UtcNow;TimeUtility.NowUnixTimeを使用してください
M:System.DateTimeOffset.ToUniversalTime();意図した変換は行えないので使用しないでください

Documentation Comment Idの問題点

前述のように外部テキストでアナライザーやジェネレーターの挙動を変えようと思うと、Documentation Comment Idで指定するという方法が選択肢に入ってきますし、何かもっと面白そうなことができそうな気もしてきます。
しかしこのDocumentation Comment Id、手書きするのが意外と面倒くさい、という問題点があります。
記法自体はそこまで複雑なものではないのですが、対象のソースコードの定義次第ではとんでもない長さを手書きする羽目になります。
実際のプロジェクトで指定した(今のところ)最長のDocumentation Comment Idを以下に示します。

UniTask.WaitUntil.cs
namespace Cysharp.Threading.Tasks
{
    public partial struct UniTask
    {
        // WaitUntilValueChangedを指定したかった
        public static UniTask<U> WaitUntilValueChanged<T, U>(T target, Func<T, U> monitorFunction, PlayerLoopTiming monitorTiming = PlayerLoopTiming.Update, IEqualityComparer<U> equalityComparer = null, CancellationToken cancellationToken = default(CancellationToken), bool cancelImmediately = false);
    }
}
Documentation Comment Id
// Documentation Comment Id形式
M:Cysharp.Threading.Tasks.UniTask.WaitUntilValueChanged``2(``0,System.Func{``0,``1},Cysharp.Threading.Tasks.PlayerLoopTiming,System.Collections.Generic.IEqualityComparer{``1},System.Threading.CancellationToken,System.Boolean)

これを正確に手書きするのは、なかなか難しいです。
そこで今回、Documentation Comment IdをUnity上で確認できるようなEditor拡張を作ってみます。

実装してみる

作るもの

主に次のような仕様を実現します。

  • GUIでの簡単な検索機能
  • クリップボードへDocumentation Comment Idをコピー
  • 対象assemblyの絞り込み
  • Property,Fieldなどの種類毎の絞り込み
  • ユーザー定義のコードor自動生成(BackingFieldなど)での絞り込み

完成形としては、最初の結論のところに貼ったgifのツールになりました。

実装方針と採用ライブラリ

Documentation Comment Idを生成する方法

フォーマット自体は https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/documentation-comments.md#d42-id-string-format ここに定義されている通りで、Reflectionなどでdllからクラス・メンバ情報を取得し、粛々とフォーマット通りに文字列を作るのもありだと思います。しかし、完全に対応しようとすると、ジェネリクス関係など、意外と面倒な実装が必要になってきますし、(そう無いと思いますが)今後新たな文法が登場した際にこちら側の実装を修正するのも面倒です。
そこで今回は横着して、RoslynAnalyzer(microsoft.codeanalysis.csharp)を利用することにします。RoslynAnalyzerはopenupmorg.nuget.microsoft.codeanalysis.csharp としてpublishされているので、以下のような scopedRegistries の設定を記述しておくことでUnityのPackage Managerから簡単に利用可能です。

manifest.json
{
  "scopedRegistries": [
    {
      "name": "package.openupm.com",
      "url": "https://package.openupm.com",
      "scopes": [
        "org.nuget"
      ]
    }
  ],
  "dependencies": {
    "org.nuget.microsoft.codeanalysis.csharp": "4.11.0"
  }
}

尚、RoslynAnalyzerを利用すると書くとsln/csproj/csの解析を行うようなイメージが湧いてきますが、今回はそこまでソースコードには興味が無いので、Unityがコンパイルしてくれたdllを AppDomain.CurrentDomain.GetAssemblies() で取得してRoslynAnalyzerに読み込ませて、Symbol情報だけを利用するという少々トリッキーな使い方をします。
以下がその実装になります。
https://github.com/satanabe1/documentation-comment-id-viewer/blob/a94bb3e78653a23422819104d2976d39f95ac2fe/Editor/DocumentationCommentIdUtility.cs#L17-L58

        public static IEnumerable<AssemblyDocumentationCommentIdData> GetDocumentationCommentIdData(
            IEnumerable<Assembly> assemblies)
        {
            // AppDomainから取得したassembliesを参照するCSharpCompilationを生成
            var refs = assemblies
                .Where(x => !x.IsDynamic && !string.IsNullOrEmpty(x.Location))
                // MetadataReferenceとしてassemblyを読み込ませる
                .Select(x => (asm: x, metadata: MetadataReference.CreateFromFile(x.Location)))
                .ToDictionary(x => x.asm.GetName().Name, x => x);
            var compilation = CSharpCompilation
                .Create("tmp-compilation", references: refs.Values.Select(x => x.metadata));
            // CSharpCompilation内からSymbol情報を取得する
            return compilation.Assembly.Modules
                .SelectMany(x => x.ReferencedAssemblySymbols)
                .Distinct<IAssemblySymbol>(SymbolEqualityComparer.Default)
                .Select(x => GetAssemblyDocumentationCommentIdData(x, refs.GetValueOrDefault(x.Name)))
                .Where(x => x != null)!;
        }

Symbol情報さえ取得できれば、Sybmbolから ISymbol.GetDocumentationCommentId() を通して、Documentation Comment Idを取得することができます。

GUIの実装方針

今回のツール作成は、Documentation Comment Idの手書きの面倒さや、面倒ゆえの書き損じを減らしたい、と言うところにモチベーションがあります。従って、それなりに検索しやすいGUIやコピペサポートによるタイポの抑制を目指しています。そのためにはそれなりのGUIが必要です。
UnityEditor上で動くツールを作る以上、IMGUIかUIToolkitの二択になりますが、TreeViewやSplitViewが(相対的に見ると)手軽に扱えるUIToolkitを採用することにしました。

UIToolkitのTreeViewの扱い方については、世に沢山の記事(例えば コレとか)があるので、そちらを参照してください。

SplitViewについては標準ではUIBuilderは対応していないのですが、以下のようなクラスを定義することでUIBuilder上でも扱うことができるようになります。

SplitView.cs
using UnityEngine.UIElements;

#if UNITY_2023_2_OR_NEWER
[UxmlElement]
#endif
internal partial class SplitView : TwoPaneSplitView
{
#if !UNITY_2023_2_OR_NEWER
    public new class UxmlFactory : UxmlFactory<SplitView, UxmlTraits> { }
#endif
}

splitview.png

これらを組み合わせてSplitViewで、上部にTreeViewでクラスやメソッドを表示し、下部にDocumentation Comment Idを表示してみると以下のようになりました。

スクリーンショット 2024-12-02 午後12.00.48.png

また、assemblyの解析やTreeの構築などは比較的重い処理になりやすいので、今回はThreadPoolに投げ込むことでUnityをブロックしないように配慮しました。

        private void RebuildTreeView(List<AssemblyTreeViewItem> treeData, Action? onRebuild = null)
        {
            ThreadPool.QueueUserWorkItem(_ =>
            {
                try
                {
                    // 物量次第ではここが比較的重くなるかも
                    var items = BuildTreeItems(treeData);
                    // バックグラウンドスレッドからUIToolkitを操作することはできないので、
                    // EditorApplication.delayCallでUnityスレッドに戻してRebuild
                    EditorApplication.delayCall += () =>
                    {
                        RebuildTreeView(items);
                        onRebuild?.Invoke();
                    };
                }
                catch (Exception e)
                {
                    Debug.LogException(e);
                    onRebuild?.Invoke();
                }
            });
        }

詳細な実装については
https://github.com/satanabe1/documentation-comment-id-viewer/blob/main/Editor/DocumentationCommentIdWindow.cs
を見ていただければと思います。

まとめ

  • Documentation Comment Idを書く機会が増えてきたが、手書きではダルすぎる
  • なのでDocumentation Comment IdをコピペするためのUnity用Editor拡張を作った
  • 検索性の高いGUIを実装したことでDocumentation Comment Idを扱いやすくなった
    • openupmを使えばお手軽にRoslyn Analyzerを使えて幸せだった
    • UIToolkitがやっと使いやすい感じになってきたかも
9
0
0

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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?