2026-09-05 · 設計 · typescript · cli · claudecode · 個人開発
linter を作るとき、何を検査しないかで品質が決まる
設定ファイルの検査ツールを作りました。作る過程で、いちばん時間を使ったのは ルールを足すことではなく、足さないと決めることでした。
検出できる項目は、いくらでも思いつきます。問題はそこではありません。
誤検出が1件あると、ツール全体が捨てられる
検査ツールには非対称性があります。
- 見逃しが1件あっても、利用者は気づきません。損失は「気づけたはずのものに気づけなかった」だけ
- 誤検出が1件あると、利用者はその場で判断を迫られます。「これは本当に間違いか?」
そして誤検出を1度でも踏むと、次からは全部の指摘を疑って読むようになります。 2度踏んだら、CI から外されます。
つまり、正しい設定を「壊れている」と言うツールは、無いほうがマシです。 検出項目を10個から30個に増やして誤検出が1件混ざるくらいなら、10個のままがいい。
これは頭では分かっていても、実装中は逆に振れます。「ここも検査できるな」と 思いつくたびに、足したくなるからです。
根拠を書けないルールは足さない
そこで、ルールを足す条件を1つに決めました。
公式ドキュメントに「エラーになる」「スキップされる」「無視される」と 書かれているものだけを検査する。
書かれていなければ、どれだけ「たぶん間違い」に見えても足しません。
この条件が効くのは、判断が自分の記憶から切り離される点です。 「たしかこの書き方は駄目だったはず」は、根拠になりません。 自分の記憶は、対象のバージョンが上がった瞬間に古くなります。
実装では、指摘ごとに根拠の文字列を持たせました。
export interface Finding {
severity: "error" | "warn";
file: string;
line?: number;
/** 何が問題か。1文 */
message: string;
/** なぜそう言えるか。出典 URL を含める */
because: string;
}
because を必須の項目にしたのが要点です。根拠を書けないルールは、
型の上で書けません。 出典を思いつかないものは、そもそも実装できない。
テストにもこれを入れました。
for (const f of findings) {
if (!f.because.includes("https://")) fail(`出典なし: ${f.message}`);
}
出典 URL が無い指摘が1つでもあれば、テストが落ちます。
検査しないと決めた4つ
実際に「できるけどやらない」と判断したものを書きます。 どれも、検出そのものは簡単です。
1. 未知のキー
設定ファイルの JSON スキーマは公開されています。それと突き合わせれば、 知らないキーを「タイポでは?」と指摘できます。
やりませんでした。公式ドキュメントにこう書いてあったからです。
スキーマは最新の CLI リリースに遅れることがあるため、最近文書化された キーに検証警告が出ても、その設定が無効であることを意味しません
つまり、新機能を使った人ほど誤検出を踏みます。 一番熱心な利用者から 順にツールを捨てていく設計になります。
2. モデル名の値
model に指定できる値を検査したくなります。しかし、有効な値の網羅リストが
ドキュメントに存在しませんでした。例としていくつか挙がっているだけです。
例は網羅ではありません。 例に載っていない値を「無効」と判定する根拠には ならない。ここも落としました。
3. ファイルパスのパターン
権限設定のパスは、先頭の記号でアンカーが4通りに変わります。
| 書き方 | 意味 |
|---|---|
//path |
ファイルシステムのルートから |
~/path |
ホームディレクトリから |
/path |
設定ファイルの位置から(絶対パスではない) |
path |
カレントディレクトリから |
3番目が特に紛らわしく、/Users/alice/file と書いても絶対パスになりません。
これに gitignore 式のグロブが重なります。
正しく実装できる自信が持てなかったので、検査対象から外しました。 自信が持てないものを検査すると、誤検出はそこから出ます。
4. 真偽値を true / false に限ること
frontmatter の真偽値です。true と false だけを許して、それ以外を
エラーにしたくなります。
ドキュメントを読むと、yes / no / on / off / 1 / 0 も有効でした。
YAML の仕様としては当然ですが、確認せずに実装していたら誤検出になっていました。
「たぶん違反」を出さない
判断がつかないものは、警告も出さずに黙って通します。
「念のため情報として出しておく」は、一見親切ですが、利用者から見ると 判断を押しつけられているのと同じです。出力が長くなり、本当に見るべき error が埋もれます。
深刻度も2段階だけにしました。
error— 根拠が取れた違反。直さないと動かないwarn— 動くが、非推奨・将来壊れる書き方
info を作らなかったのは、作ると「判断がつかないもの」の逃げ場になるからです。
逃げ場があると、迷ったものが全部そこに積まれます。
検証は「壊れた入力」より「正しい入力」を重く見る
テストは2種類の入力で行いました。
| 入力 | 期待 | 結果 |
|---|---|---|
| わざと壊した設定 | 検出すること | 22件 |
| 正しい設定 | 1件も出さないこと | 0件 |
重要なのは下です。しかも「正しい設定」には、わざと引っかかりそうなものを 混ぜました。
- 次のリリースで追加されそうな、知らないキー
- エディタ補完用の
$schema - 入れ子の中に書かれた設定(トップレベルに書くと誤りになるもの)
このどれかで1件でも出たら、設計が間違っているという合図です。
テストの失敗メッセージも、この非対称性に合わせました。
FALSE POSITIVES on a valid tree: 1
見逃しは「検出されず」と淡々と出しますが、誤検出は目立つ文言にしています。 どちらを先に直すべきかを、出力の側で決めてしまう。
まとめ
- 検査ツールの価値は、検出できる項目数ではなく誤検出しないこと
- 誤検出は1件で信頼を失い、2件で CI から外される
- ルールを足す条件を「根拠を書けること」に固定する。
型に
becauseを持たせると、根拠の無いルールは実装できなくなる - 正しく実装できる自信が持てないものは、検査しない
infoを作らない。判断がつかないものの逃げ場になる- テストは「正しい入力で0件」を最重要にする。引っかかりそうなものを混ぜておく
検出力を上げるより、黙って通す範囲を決めるほうが難しいというのが、 作ってみての実感です。