🔀

Roslyn を使って独自のディレクティブを実装する

に公開
前日譚

Unity 環境において、.NET 10 の単一ファイル実行で使われる #: ディレクティブはコンパイルエラーを起こします。(ファイル先頭の #! は地味に大丈夫)

テストスクリプト自体は Tests~ 等の Unity に取り込まれないフォルダーに退避したとしても、Unity プロジェクトは .csproj を全部ルートに書き出すという構造なので参照が面倒という問題が残ります。そもそも Unity の生成した .csproj は VS Code だとビルド出来ないかもしれません。

そして Visual Studio Insiders だと単一ファイル実行の .cs ファイルは IntelliSense が効きません。しかしビルド出来ない(かもしれない)VS Code は効きます。

要するに過渡期で色々と面倒なので、Unity への依存が無いクラスであれば取り込んでしまえばいい! という事で、独自のプリプロセッサーディレクティブ風の構文をソースジェネレーターを使って作りました。

つかいかた

#:package FUnit@*
#:package FUnit.Directives@*

// 👇 Directives を参照すると //:funit: が使えるようになる
//:funit:include ../Runtime/MyClass.cs

return await FUnit.RunAsync(args, describe =>
{
    describe("Test", it =>
    {
        it("should work", () =>
        {
            // ソースジェネレーターが普通にファイルを読み込んで生成した体で出力するので、
            // internal メンバーまで見える状態でテストが可能!
            Must.BeEqual(310, MyClass.Method());
            //                ~~~~~~~~~~~~~~ MyClass.cs からインクルードした型とメソッド
        })
    });
});

include ディレクティブ

//:funit:include <対象ファイル.cs>  ※必ず行頭に置く

引数(?)を .cs ファイルからの相対パスとして扱い、単純に System.IO.File で読み込んでそのままソースジェネレーターから「生成したソースコード」として出力します。

プロジェクトが循環参照になっちゃうとか、そもそも参照していないとか、その他あらゆる垣根を飛び越えて指定したファイルをサクッと取り込むことが出来ます。

注意点

Roslyn には RS1035 というエラー診断が存在しますが無かった事にしてビルドしています。

ディレクティブ実装にあたって、IIncrementalGenerator はコード削除の検出が難しい都合から ISourceGenerator として実装していて、「単一ファイル実行に限れば」一度しか実行されないので問題にはならなと思います。

IIncrementalGenerator 限定のエラーっぽい。つまり C# でも include は合法!

それ以外にも、構文チェックやファイルの存在確認はしていますが、読み込むファイルが適切かどうかは一切確認していないので、自分自身を読み込むと全てが曖昧になります。(同一ファイルの複数回の include は問題なし)

👇 元のソース(すべてが曖昧に)

👇 生成されたソース(名前の重複等々)

おわりに

#:item というディレクティブを追加してくれ、という要望が GitHub に上がっていたので、そのうちインクルードに対応する気がします。今のままだとあと一歩たりない! って感じですからね。

公式が対応するまでの繋ぎにどうぞ。

以上です。お疲れ様でした。

Discussion