終了ボタンが押せない。どこからも読み込まれていないファイルが一つあった
tscもテストも通るのに、動かない。
日本進化シミュレーション第5回 / 全16回目次
ファイル分割が一段落した頃、ゲームを一通り触って確認していた。
タイトル画面へ戻る「終了」ボタンを押した。
何も起きない。
コンソールにエラーは出ていない。型チェックも通っている。テストも全部緑だった。
「動くはずのものが、静かに動かない」というのは、エラーが出るより厄介だ。
僕には、何を手がかりに調べればいいのか分からない。
ボタンのイベント登録自体が、一度も実行されていなかった
終了ボタンの処理はexit.tsというファイルにまとまっていた。
中身を読むと、コードは正しい。ボタンを取得して、クリックイベントを登録している。
ロジックとしては何も間違っていない。
おかしいのはそこじゃなかった。
このファイルを、どこからもimportしていなかった。
exit.tsは、他のファイルから関数を呼ばれることはなく、DOMのイベント登録は自分の中で完結しているタイプのモジュールだった。
だから「使われているように見えて、実は一度も読み込まれていない」という状態に気づきにくい。
importされていなければ、中身がどれだけ正しくても実行されない。当たり前のことなのに、分割作業の中では見落とした。
tscもVitestも、この種のバグは検出できない
型チェックは、書かれているコードの型が正しいかを見る。
テストは、呼び出した関数が正しく動くかを見る。
どちらも「そのファイルが、そもそも一度でも実行されるか」までは見てくれない。
コンパイルは通る。型も合う。ただ、一度も評価されないだけ。
このゲームにはreport.tsやphotomode.tsのような、同じ性質を持つファイルが他にもある。
「他ファイルから呼ばれる関数もあるが、DOMイベント登録は自分の中で完結している」という形のモジュールは、同じ穴に落ちる可能性がある。
孤児モジュールを機械的に見つける
一つ見つけて終わりにするのは怖かったので、全部のファイルを機械的にチェックすることにした。
for f in src/*.ts; do name=$(basename "$f" .ts) case "$name" in main|global|*.test) continue;; esac hits=$(grep -lE "from \"\./$name\"|^import \"\./$name\";" src/*.ts 2>/dev/null | grep -v "^src/$name.ts$" | wc -l) if [ "$hits" -eq 0 ]; then echo "ORPHAN: $name.ts"; fidoneどのファイルからも参照されていないファイル名を、ORPHANとして吐き出すだけの単純なワンライナーだ。
これを回して、exit.tsが本当に孤立していたことを確認し、main.tsへimport "./exit";を一行足して直した。
コンパイルが通ることと、動くことは別の話
この一件で覚えたのは、「型チェックとテストが両方通っている」という安心感を、そのまま「機能が動く」に読み替えてはいけないということだった。
ファイルを分割するという作業そのものが、新しい種類のバグを持ち込む。
単一ファイルの時代には存在しなかった「importし忘れ」というミスは、分割した本人にしか気づけない。
以来、新しいファイルを追加したりimportを張り替えたりした後は、この孤児チェックを機能テストとは別に必ず回すようにしている。
※運営者が個人で書いているnoteです。