「Movable Type Advent Calendar 2025」の19日目です。
MTを使っていると、
- 再構築がなぜか遅い
- テンプレートを見ても「どこが悪いのか分からない」
- インデントや書き方がバラバラで、触るのが億劫
といった状態になりがちです。
特に MT のテンプレートは、
動いている限り問題にならないため、気づいたら手を入れづらい状態になっている
というケースがとても多いと感じています。
そこで今回は、
- MTテンプレートの書き方を静的解析するLintツール
- 再構築パフォーマンスと保守性の両面を意識
- Rustで実装し、MTプラグインからバイナリを呼び出す構成
- テンプレート編集画面に警告を出す
というところまでをやってみた話を書きます。
Rustはほぼ分からない状態でしたが、仕様設計はAIと会話しながら進め、実装はほぼAIに書いてもらい、自分は動作確認と調整を担当という進め方にしました。
なぜLintを作ろうと思ったか
MTのテンプレートは、書き方次第で再構築パフォーマンスが大きく変わる
例えば:
-
Entriesにlastnを付けていない(管理画面で9999とかが指定されている) - ソート指定しているのに件数制限がない
- 大文字小文字が混じっている
これらは動作的には正しいので気づきにくいですが、
規模が大きくなると一気に再構築時間が伸びます。
とはいえ、
- 毎回レビューで指摘するのも大変
- 管理画面でテンプレートを書いている人が多く、VSCodeのLint前提は厳しい
そこで、
テンプレート保存時に「その書き方、将来遅くなりますよ」と教えてくれる仕組み
が欲しくなりました。
パフォーマンス以外にも、放置されがちな問題がある
MTテンプレートで厄介なのは、「動くからそのままにされがち」な点です。
例えば:
- インデントが崩れていて構造が分かりづらい
- タグの書き方(大文字小文字、コロン有無)がバラバラ
- include がどこで何をしているのか追いづらい
- レビュー時に「直したいが、影響範囲が読めない」
こうした問題は、
- 今すぐバグになるわけではない
- でも、保守や改修のたびに確実にコストを上げる
という地味だが効き続ける負債になりがちです。
本当はフォーマットもやりたかった
今回のツールはLintのみとなってしまいました。
正直なところ、
フォーマッタも一緒にやりたかった
という気持ちはあります。
- インデントを揃える
- タグ表記を統一する
- 人が読んで理解しやすい形に整える
こうしたことができれば、
- レビューが楽になる
- 「触るのが怖いテンプレート」が減る
という効果が期待できます。
ただ、
- フォーマットは影響範囲が大きい
- 「正解」がプロジェクトごとに違う
という理由から、
今回はLintに割り切ることにしました。
フォーマットについては、
今後の課題として時間があるときに検討したいと考えています。
なぜRust?
最初は Perl / JavaScript も考えましたが、
- テンプレート全体をパースする
- ASTを作ってルールを複数評価する
- 保存時に毎回動く
という性質上、とにかく軽くしたい。
Biome(Rust製のJSツール)が ESLint より速いのを思い出し、
- LintコアはRust
- MT側はPerlプラグイン
- 外部バイナリとして呼び出す
という構成にしました。
Rust自体はほぼ分からないので、
仕様を固めてからAIにコードを書かせる 方式です。
仕様はAIと会話しながら詰めた
やったことはだいたいこんな流れです。
- 「MTテンプレートで遅くなりがちな書き方」を洗い出す
- Lintルールとして成立する粒度に落とす
- AST(抽象構文木)をどう持つか決める
- v0では「完全なMT構文対応」を目指さないと割り切る
- 出力はJSONで、MT側はそれを表示するだけにする
この辺は ChatGPT とかなり細かく仕様相談しました。
「Rustでルールを書き切ると拡張できなくならない?」
「ESLintみたいな独自ルール追加はどうする?」
「includeはどこまで追う?」
みたいな話も含めて、
まずはv0として割り切った仕様にしています。
Rust製LintをCLIとして作る
まずは標準入力でテンプレートを受け取るCLIを作りました。
試したコマンドはこんな感じです。
% echo '<mt:Entries sort_by="authored_on"><$mt:BlogName$></mt:Entries>' \
| docker-compose run --rm dev cargo run -- --stdin --format=json --locale=en
結果はJSONで返ってきます。
{
"summary": {
"errors": 0,
"warnings": 2
},
"violations": [
{
"ruleId": "mt/no-unlimited-entries",
"level": "warn",
"message": "Entries tag does not have lastn or limit attribute"
},
{
"ruleId": "mt/sort-without-limit",
"level": "warn",
"message": "Sort specified but no limit set"
}
]
}
-
lastnが無い - ソート指定+件数制限なし
という「将来重くなる典型パターン」について、警告を返すようになっています。
結果をJSONで返せば、それをプラグインから呼び出すだけでも使えるだろうと思ったので、JSONで返すようにしました。
MTプラグインから呼び出す
Rust側は「テンプレート文字列 → JSON結果」を返すだけにして、
- テンプレート保存時に外部バイナリを実行
- 返却されたJSONをパースして、警告やエラーがあればテンプレート編集画面に表示
という構成にしました。
mt環境にインストールして、テンプレートに下記のコードを追加して保存してみました。
<mt:Entries sort_by="authored_on">
<$mt:BlogName$>
</mt:Entries>
保存後に画面に警告が表示されるようになりました。
AIでどこまで実装できるかの検証として、あえて「AIのみ」で作ってみた
今回の取り組みには、もう一つテーマがあります。
仕様をきちんと詰めれば、AIはどこまで実装を担えるのか?
あえて自分ではRustを書かない
今回、RustコードはすべてAIに書かせました。
自分は以下のみを担当しています。
- 仕様設計
- 方針の言語化
- 動作確認
- 想定と違う点のフィードバック
使用したAI
- Claude Code
- Sonnet 4.5
パーサ設計、AST構造、Lintルール設計まで含めて、かなり安定して実装を進められました。
実装されたソースコード
自分が理解していない言語だと、この実装が正しいのかどうか、というのが判断できません。
できるのは動作確認して、ちゃんと動いているかのみになります。
このあたりは自分がプログラマだからなのか、結構な不安要素です。
もし本格的にプロジェクトで使うとしても、自分が理解できている言語でレビューができるくらいでないと、
動作検証に抜けがあったり、セキュリティーリスクはすごく大きなものになりそうです。
ただ、これはこれで使い道があるなと思ったのは、知らない言語の例文が書かれるということです。
前はネットで調べて、HelloWorld!と言わせるまでに実行環境などを用意しなければならなかったのですが、
今は下記をAIに投げるだけです。
「Dockerで動作する簡単なRustの環境を作成してください。サンプルスクリプトとしてHelloWorld!を表示して」
こうするだけで、あっというまに実行環境ができるので、あとは出来上がったソースを読み込んで理解するだけです。
自分はPerlも他の人のソースを読み込んでこう書くのかと学んだので、学習するうえでもこうした使い方もできるのかもと思いました。
実装してみての全体的な感想
- 仕様が曖昧だと(良かれと思って別の方向に実装してしまい)破綻する
- 仕様が固まっていれば、実装はかなりAIに任せられる
- 動作確認やレビューは必須
「AIで実装できた」というより、
設計をちゃんと文章に落とし込めるかが、そのまま実装品質に直結する
という感触でした。
今後やりたいこと
- 日本語メッセージ対応
- ルール追加
- プロジェクトごとの警告レベル切り替え
- フォーマッタ(インデント・表記統一)の検討
まとめ
- MTテンプレートは「動くが遅くなりやすい」書き方が多い
- パフォーマンスだけでなく、保守性の問題も放置されがち
- Lintは「気づきを与える」だけでも十分価値がある
- AIと仕様を詰めれば、知らない言語でも実装はかなり任せられる(ただし注意が必要)
同じように MT のテンプレート保守や再構築速度で悩んでいる方の参考になれば嬉しいです。
