42. 競技プログラミング用のコードテンプレートジェネレータを作成した
今日は AtCoder で使うであろうユーティリティをチマチマと実装していました。
ここ一週間ほどユーティリティの充実を続けているのですが、そろそろライブラリが肥大化してきて実際に競技プログラミングで利用するにはやや厳しくなってきました。
というのも、AtCoder では 1 ファイルにまとめて提出するので、別ファイルに分かれているクラスを上手く抽出してコンパイルできるように置き直す必要があります。当然ながら、各クラス利用している名前空間が違ったり、多段参照していたりするので、この作業はライブラリが充実するほど正確に行うことが困難になります。
というわけで、コピペ用コードテンプレートを自動生成するプロジェクトを作ってみました。
ソリューション構成
ソリューションはこのようになっています。CPUtility はライブラリ本体のプロジェクトです。CPUtilityTests は CPUtility のテストセットで、出来る限り全てのメソッドに対して書いています。そして、TemplateGenerator が今回作成したコードジェネレータです。
仕組みとしては、独自記法によるテンプレートに対して、ライブラリ本体から読み込んだコードを埋め込む感じです。今回は以下のような記法を定めて、疑似 .cs ファイルをテンプレートとして用意できるようにしました。
##
から始まる行は、テンプレートに対するコメントとみなし、出力しない[[Attribute]]
と書かれた部分は、ライブラリのクラスへ付与された属性と対応付けてコードを埋め込む
具体的には以下のようなものです(独自記法なのでスクリーンショットで示します)。
Attribute はライブラリ側で定義して、埋め込みたいクラスに対して付与します。なお、名前のみに意味があるため、中身は空っぽです。
さて、これで仕様が決まったのですが、コード埋め込みのためにクラス定義部を適切に読み込む必要があります。真面目に構文解析するのは骨が折れるので、ここは自分用ということもあり、以下のような仕様としました。
[Attribute]
以下の行から埋め込み対象とする(この記述を見つけたら、読み込みモードに切り替える)。カウントを 0 とする。{
を見つけたらカウントを 1 増やし、}
を見つけたらカウントを 1 減らす- カウントを減らしたタイミングで 0 になったら、その行までを埋め込み対象として読み込みモードを終了する
これを Attribute ごとに走査します。埋め込むべき記述が確定したら、満を持してテンプレートへ埋め込みます。成果物は、実際の利用を考えてジェネレータではなくライブラリ側へ出力します。
プロジェクト設定
あとは、普段の開発を良い感じにするための csproj 設定を行います。
テンプレートおよび出力されたコード群はコピペ用なので、コンパイルに巻き込むと面倒です。そこで、該当ファイルのプロパティでビルドアクションを「C# コンパイラ」から「なし」にして、コンパイル対象から除外します。
また、ライブラリを更新するごとに手動でジェネレータを実行するのも面倒だし忘れそうです。そこで、ライブラリ側のビルド後イベントとしてジェネレータを実行させます。
初め、ここで出力されたジェネレータの exe を実行するようなイベントを設定していました。ところが、GitHub Actions は Unix 環境で動いているため、CI がコケるという状態になっていました。そのため、Windows でも Unix 環境でも動くように以下のようなコマンドに変更しました。
dotnet run --framework=netcoreapp3.1 --project=$(SolutionDir)TemplateGenerator/TemplateGenerator.csproj
なお、コマンドライン引数を渡したい場合は最後に --
のみを追加し、その後に書き連ねれば良いです。