過去のバグをLintルールにして再発を止める
バグを直したあと、振り返りに「今後は気をつける」と書いて終わっていませんか。気をつけるだけの対策は、メンバーが入れ替わったり忙しくなったりすると効かなくなります。同じ書き方をした瞬間に機械が止めてくれる仕組みにすれば、人の注意力に頼らずに再発を防げます。この記事は実装工程の話で、テストを足すより手前の「コードを書いた時点」で止める方法を扱います。
ルール化に向くバグ、向かないバグ
すべてのバグがLintルールになるわけではありません。コードの見た目だけで「これは危ない」と判定できるものが向いています。
- 向く: 特定の関数を特定の使い方で呼ぶ(引数なしの
toLocaleString()、parseIntの基数省略など)、禁止したいAPIやimportを使う、必須の処理が抜ける(await忘れなど) - 向かない: 業務ロジックの誤り、データの組み合わせで初めて起きるバグ、画面の見た目の崩れ。これらはテストや受入条件で防ぎます
見分ける質問は一つです。「そのバグの原因になった1行を、別のメンバーがコードだけ見て危ないと指摘できるか」。できるならルールにできます。
既存ルールで足りるか、カスタムルールが要るか
最初からカスタムルールを書く必要はありません。次の順で確認します。
- 既存のルールやプラグインに同じ検査がないか探す(
no-floating-promisesなど) - ESLint標準の
no-restricted-syntaxやno-restricted-importsで書けないか考える - それでも足りないときだけ、カスタムルールを書く
例1: 設定だけで止める
過去のバグ: 引数なしの toLocaleString() が実行環境の言語設定で表示を変え、画面ごとに日付の形式がばらついた。これは設定だけで止められます。
// eslint.config.js
{
rules: {
'no-restricted-syntax': ['error', {
selector: "CallExpression[callee.property.name='toLocaleString'][arguments.length=0]",
message: 'toLocaleString にはロケールを渡す(過去の表示ずれ対策)',
}],
},
}メッセージに「なぜ禁止か」を一言入れるのがコツです。ルールに引っかかった人が、理由を調べに行かなくて済みます。
例2: カスタムルールを最小で書く
設定で表現できない検査は、ルールを1つ書きます。次は、自社の legacyFetch を呼んだらエラーにする最小のルールです。
// rules/no-legacy-fetch.js
export default {
meta: {
type: 'problem',
messages: { used: 'legacyFetch は廃止予定。apiClient を使う' },
schema: [],
},
create(context) {
return {
CallExpression(node) {
if (node.callee.type === 'Identifier' && node.callee.name === 'legacyFetch') {
context.report({ node, messageId: 'used' });
}
},
};
},
};読み込みは、設定ファイルの plugins にローカルのプラグインとして登録するだけです。ルールのテストには ESLint の RuleTester を使い、「引っかかる例」と「引っかからない例」を1つずつ書きます。
運用で失敗しないために
- 過去のバグ1件につき1ルールにし、ルールの説明に元のIssue番号を書く
- 既存コードが大量に引っかかるときは、いきなりエラーにせず警告から始め、段階的にエラーへ上げる
- 誤検知が出たら、無効化コメントを乱用せず、ルール側の条件を直す
- 半年に一度、一度も引っかからないルールを見直して削除する
Bugoon での実践
ルール化の出発点は「どのバグが、どのコードの書き方から生まれたか」を特定することです。Bugoon のウィジェットでは、報告時にスクリーンショットと操作ステップ、コンソールエラーが一緒に記録され、GitHub Issueとして残ります。再発しやすいバグを探すときは、Issueの原因欄から同じ書き方が繰り返されていないかを見直すと、ルール化の候補が見つかります。MCPサーバー経由でClaude CodeやCursorにIssueを読ませ、「この原因を検出するLintルールを書いて」と頼む使い方もできます。