第2章 AI向けガチガチ lint — Vue/React + Express の ESLint 設定
この章では、AI に大量のコードを書かせる前提で、遠慮なく厳しくした ESLint 設定を作ります。題材は、実際に AI 主体で数百 PR を回しているプロジェクト MulmoClaude の eslint.config.mjs(Vue + Express + TypeScript)です。
そもそも lint ってなに?
lint(リント)は、コードの「校正ツール」です。文章を書くときの校正 — 誤字脱字や不自然な言い回しに赤線を引いてくれるあれ — のプログラム版だと思ってください。
名前の由来は、乾燥機のフィルタに溜まる糸くず(lint)。「コードに付いた小さなゴミを取る道具」として名付けられました(元祖は1978年の C言語用ツールです)。
実際に何をしてくれるのか見てみましょう。こんなコードがあったとします:
if (user = null) {
console.log(count);
}
一見それっぽいですが、問題が2つ隠れています。lint にかけると:
error 条件式の中で比較(==)のつもりが代入(=)になっています no-cond-assign
error 'count' はどこにも定義されていません no-undef
(実際の出力は英語です)。このように、実行する前に「これバグでは?」を機械が指摘してくれます。押さえておくポイントは4つ:
- エディタ(VS Code + 拡張機能)なら打った瞬間に赤線が出ます。コマンド(
yarn lint)でまとめてチェックもできます - 指摘には error(違反。CI で落とす) と warn(注意) の2段階があります
- 指摘の多くは
--fixオプションで自動修正できます - どのルールを有効にするかは設定ファイルで自由に選べます — この章の主題は「AI 相手なら何を選ぶべきか」です
登場する道具たち — 名前と正体
この章にはカタカナの道具がたくさん出てきます。先に正体を押さえておきましょう。全部「ESLint 本体 + その拡張(プラグイン)+ 相棒たち」という関係です。
| 道具 | 正体 | 担当 |
|---|---|---|
| ESLint | JavaScript/TypeScript 用 lint の事実上の標準ツール | ルール違反の検出。プラグインで拡張できる本体 |
| Prettier | コード整形ツール(フォーマッタ) | 見た目(インデント・改行・クォート)を機械的に統一 |
| typescript-eslint | ESLint の TypeScript 対応プラグイン | 型がらみのルール(any 禁止など) |
| eslint-plugin-sonarjs | コード品質分析の老舗 SonarSource 社のバグ検出ルール集の ESLint 版 | バグパターン・「読んで疲れる度」の検出 |
| eslint-plugin-security | セキュリティ特化のプラグイン | インジェクション等の危険パターン検出 |
| eslint-plugin-import | import 文まわりのプラグイン | 循環参照などファイル間の秩序 |
| eslint-plugin-vue / eslint-plugin-react | フレームワーク向けプラグイン | Vue テンプレート / React(JSX)固有の問題 |
| eslint-config-prettier | 調停役 | ESLint と Prettier の縄張り争い(整形系ルールの衝突)を解消 |
| tsc(typecheck) | TypeScript 本体の型チェック | データの形の食い違い検出。lint と並走させる別枠の番人 |
Prettier の隠れた大仕事 — git のコンフリクトを減らす
Prettier は「見た目を揃えるお化粧の道具」と思われがちですが、チーム開発ではもっと実利的な仕事をしています。
整形が人それぞれ(AI それぞれ)だと、中身が同じでも空白や改行の違いで差分(diff)が生まれます。すると:
- レビューで「本当の変更」が、どうでもいい整形差分に埋もれる
- 同じファイルを2人が触ったとき、意味のない整形の違いがコンフリクトになる(Git入門 第4章で学んだあのコンフリクトが、無意味な理由で発生する)
全員が同じ Prettier を通せば、コードは誰が書いても同じ形に整形されるので、diff は常に「意味のある変更」だけになり、コンフリクトの種も減ります。AI は生成のたびに整形の癖が揺れるので、この効果は人間チーム以上に大きく効きます。
Prettier 自体の設定はほぼ不要です。「整形の議論の余地をなくす」が思想のツールなので、選べるオプションが意図的に少なくなっています。
インストールと動かし方
セットアップは3ステップです。
① インストール(開発時だけ使う道具なので -D を付けます):
yarn add -D eslint @eslint/js globals typescript-eslint \
eslint-plugin-sonarjs eslint-plugin-security eslint-plugin-import \
eslint-plugin-vue vue-eslint-parser \
prettier eslint-plugin-prettier eslint-config-prettier
② 設定ファイルを置く: リポジトリの一番上に eslint.config.mjs を作ります。中身は後述のサンプルをそのままコピーすればOKです。
③ 実行コマンドを package.json に登録:
{
"scripts": {
"lint": "eslint src server test --cache",
"lint:fix": "eslint src server test --fix",
"format": "prettier --write '{src,server,test}/**/*.{ts,vue,json}'",
"typecheck": "vue-tsc --noEmit && tsc -p server/tsconfig.json --noEmit"
}
}
これで yarn lint(チェック)、yarn lint:fix(自動修正)、yarn format(整形)、yarn typecheck(型)が使えます。VS Code なら ESLint 拡張と Prettier 拡張を入れると、打った瞬間に赤線が出て、保存のたびに自動整形されます。
💡 このセットアップ自体、Claude Code に「この章のサンプル設定で ESLint と Prettier をセットアップして」と頼めば全部やってくれます。人間の仕事は、道具の操作ではなく何を強制するか(どのルールを選ぶか)を決めることです。
なぜ「ガチガチ」にするのか
人間のチームでは、厳しい lint は嫌われます。「この程度いいじゃん」という摩擦のコストが、ルールの利益を上回るからです。AI ではこの前提が逆転します。
- AIは文句を言わない — lint エラーを見せれば黙って直します。摩擦コストがほぼゼロ
- AIは自分で直せる — 「エラーを検出 → AI が修正 → 再チェック」のループが自動で回る。厳しいルール = 自動品質改善の燃料
- AIは書く量が多い — 人間レビューの前に、機械が9割の問題を止めてくれないとレビューが破綻する
- AIは一貫性を機械に頼る — セッションをまたいだ「書き方の記憶」がないので、ルールがコードの一貫性を担保する唯一の手段
そして根本の事実として、CLAUDE.md に書いた指示は、必ずしも全部守られるわけではありません。指示はあくまで「助言」であって、強制力はない — 公式ドキュメントもそう明言しています。だからこそ、機械的に強制できる lint・型・テスト・CI の重要度が跳ね上がるのです。第1章の仕分け(機械で判定できるルールは lint へ)は、この事実の裏返しでした。
「きれいなコード」の原則は、AI時代にむしろ得になった
「AI が書くんだから、人間向けの『きれいなコード』のルールはもう古いのでは?」— 逆です。3つの理由で、価値がむしろ上がりました。
-
テストの自動化は、テストしやすいコードを要求する ハーネスの心臓はテストです(壊してもすぐ分かるから、AI に大胆に書かせられる)。そしてテストしやすいのは「入力→出力が決まる、小さな純粋関数」— 外部依存(DB・API・時刻)は中で直接呼ばず引数で受け取る形です。つまり小さく分割された、疎結合なコードが前提条件。人間が書いていた時代の「きれいなコード」の原則(リファクタリングの心得)が、そのままテスト自動化の土台として生き続けます
-
AI は渡されたコンテキストしか見えない — 小さいコードは AI の精度を上げる AI は巨大で散らかったコードの全体を把握できず、部分だけ見て矛盾した変更を生みます。小さく・良い名前・型付きのコードほど、AI が正しく理解できる範囲に収まる。人間向けの整理は、そのまま AI 向けの整理です
-
小さいファイル・小さい関数は、AI の読む量を減らす — 速くて安い! AI はコードを読むのにもトークン(= お金と時間)を使います。400行のファイルを全部読まないと直せないコードと、20行の関数だけ読めば直せるコード — 後者はトークン使用量が減り、応答が速くなり、料金も安くなります。人間時代の「認知の負荷を下げる」が、AI 時代には「コンテキストとコストを下げる」にそのまま置き換わったのです
だから、この章のサイズ系ルール(関数50行・ファイル400行・複雑度15…)は品質のためであると同時に、AI の精度と運用コストに直接効く投資でもあります。
ベースは「推奨セットの積み上げ」
ゼロからルールを書くのではなく、実績ある推奨セットを重ね、その上に自分のルールを足します。MulmoClaude では次の6段重ねです。
| セット | 提供元 | 何をくれるか |
|---|---|---|
eslint.configs.recommended |
ESLint 本体 | 未定義変数などの基本 |
tseslint.configs.strict + stylistic |
typescript-eslint | 型まわりの厳格ルール一式(recommended より強い strict を選ぶのがポイント) |
sonarjs.configs.recommended |
eslint-plugin-sonarjs | 認知的複雑度、重複、バグパターン検出 |
securityPlugin.configs.recommended |
eslint-plugin-security | インジェクション等の危険パターン検出 |
vuePlugin.configs["flat/recommended"] |
eslint-plugin-vue | Vue テンプレートの品質(React なら React 系に差し替え。後述) |
eslintConfigPrettier |
eslint-config-prettier | Prettier と衝突する整形系ルールを無効化(必ず最後に置く) |
サンプル設定(Vue + Express + TypeScript)
MulmoClaude の実設定から、どのプロジェクトでも使える部分を抜き出して整理したものです。コメントの説明ごとコピーして使ってください。
// eslint.config.mjs
import eslint from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import prettierPlugin from "eslint-plugin-prettier";
import sonarjs from "eslint-plugin-sonarjs";
import securityPlugin from "eslint-plugin-security";
import importPlugin from "eslint-plugin-import";
import vuePlugin from "eslint-plugin-vue";
import vueParser from "vue-eslint-parser";
export default [
{ files: ["{src,server,test}/**/*.{js,ts,vue}"] },
{ ignores: ["dist", "coverage", "node_modules"] },
// ── 土台: 推奨セットの積み上げ ─────────────────────
eslint.configs.recommended,
sonarjs.configs.recommended,
securityPlugin.configs.recommended,
...tseslint.configs.strict,
...tseslint.configs.stylistic,
...vuePlugin.configs["flat/recommended"],
// ── 本体: AI向けの上乗せルール ─────────────────────
{
languageOptions: {
globals: { ...globals.es2021, ...globals.node },
ecmaVersion: "latest",
sourceType: "module",
},
plugins: { prettier: prettierPlugin, import: importPlugin },
rules: {
// --- 量の上限: AIの「書きすぎ」を止める ---
"max-lines-per-function": ["error", { max: 50, skipBlankLines: true, skipComments: true }],
"max-lines": ["error", { max: 400, skipBlankLines: true, skipComments: true }],
complexity: ["error", { max: 15 }],
"sonarjs/cognitive-complexity": "error",
"max-depth": ["error", { max: 4 }],
"max-params": ["error", { max: 6 }],
// --- 型の穴をふさぐ ---
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/no-use-before-define": ["error", { functions: false, ignoreTypeReferences: true }],
// --- うっかりバグの検出 ---
eqeqeq: ["error", "smart"],
"array-callback-return": "error",
"consistent-return": "error",
"no-param-reassign": "error",
"no-shadow": "error",
"no-self-compare": "error",
"no-unmodified-loop-condition": "error",
"no-implicit-coercion": ["error", { boolean: true, number: true, string: true }],
"no-throw-literal": "error",
"prefer-promise-reject-errors": "error",
"default-case-last": "error",
"default-param-last": "error",
"sonarjs/no-ignored-exceptions": "error",
// --- 読みやすさの底上げ ---
"id-length": ["error", { min: 3, properties: "never", exceptions: ["_", "i", "j", "ok"] }],
"prefer-const": "error",
"prefer-template": "error",
"no-else-return": ["error", { allowElseIf: false }],
"no-lonely-if": "error",
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
"object-shorthand": "error",
"prefer-arrow-callback": "error",
"arrow-body-style": ["error", "as-needed"],
// --- 構造を守る ---
"import/no-cycle": "error",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"import/no-mutable-exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"@typescript-eslint/no-require-imports": "error",
// --- 整形は Prettier に一任し、違反は lint エラーにする ---
"prettier/prettier": "error",
indent: ["error", 2],
semi: ["error", "always"],
"linebreak-style": ["error", "unix"],
},
},
// ── テストコードだけの緩和(理由は本文参照) ─────────
{
files: ["test/**/*.{ts,js}", "e2e/**/*.{ts,js}"],
languageOptions: { globals: { ...globals.browser } },
rules: {
"max-lines-per-function": "off",
"@typescript-eslint/no-explicit-any": "warn",
"sonarjs/assertions-in-tests": "error",
},
},
// ── Vue ファイル固有(main ルールの後に置く: 後勝ち) ──
{
files: ["**/*.vue"],
languageOptions: {
parser: vueParser,
parserOptions: { parser: tseslint.parser, sourceType: "module", extraFileExtensions: [".vue"] },
globals: { ...globals.browser },
},
rules: {
"vue/no-v-html": "error",
"vue/no-useless-mustaches": "error",
"vue/no-useless-v-bind": "error",
"vue/prefer-true-attribute-shorthand": "error",
"vue/no-empty-component-block": "error",
},
},
eslintConfigPrettier, // 必ず最後
];
ルール解説 — なぜそのルールなのか
「AI がやりがちな事故」と対にして読むと、なぜこの選定なのかが分かります。
量の上限 — AIの「書きすぎ」を物理的に止める
AI の代表的な悪癖は、動くけど長くて把握できないコードを量産することです。サイズ系ルールはそれを機械的に止めます。
| ルール | 意味 | 防ぐ事故 |
|---|---|---|
max-lines-per-function: 50 |
関数は実質50行まで | 200行の「なんでも関数」。理想の目安(20行)より少し緩い天井(50行)を強制ラインにすると、ノイズを出さずに最悪ケースだけ確実に止められる |
max-lines: 400 |
1ファイル400行まで | 「なんでもファイル」。ファイルが小さいほど AI の読む量(トークン)も減る |
complexity: 15 |
分岐の数の上限 | if の海。テスト不能な関数 |
sonarjs/cognitive-complexity |
「人間が読んで疲れる度」の上限 | ネスト+分岐+再帰の合わせ技 |
max-depth: 4 |
ネストは4段まで | 矢印型(if の中の if の中の…)コード。直し方は早期 return(ガード節) |
max-params: 6 |
引数は6個まで | 引数10個の関数(オブジェクト引数への合図) |
型の穴をふさぐ — 「とりあえず動かす」抜け道の封鎖
AI は型エラーに詰まると any や as や ! で黙らせる誘惑に駆られます。全部エラーにしておくと、正面から型を直すしかなくなります。
| ルール | 防ぐ事故 |
|---|---|
no-explicit-any |
any で型チェックを丸ごと無効化。AI向け lint で一番大事な1本 |
no-non-null-assertion |
foo!.bar — 「null じゃないはず」という祈り。実行時クラッシュの温床 |
consistent-type-assertions |
乱暴な as キャストの制限。型ガード関数へ誘導 |
no-use-before-define |
定義前の変数参照(TDZ エラー)。AI がコードを継ぎ足すときに起こしがち |
うっかりバグの検出 — JavaScript の罠を全部エラーに
| ルール | 防ぐ事故 |
|---|---|
eqeqeq (smart) |
== の暗黙変換("" == 0 が true!)。x == null イディオムだけ許す設定 |
array-callback-return |
map のコールバックで return し忘れ → 全要素 undefined |
consistent-return |
「値を返す経路と返さない経路が混在」する関数 |
no-param-reassign |
引数の書き換え → 呼び出し元のオブジェクトが黙って変わる |
no-shadow |
外側と同名の変数 → 「どっちの user?」事故 |
sonarjs/no-ignored-exceptions |
catch (e) {} — エラーの握りつぶし。AI が「とりあえず動かす」ためにやりがち |
セキュリティ — 危険パターンの検出網
eslint-plugin-security と sonarjs は、eval・危険な正規表現(ReDoS)・子プロセス起動などの危険パターンに反応します。
ここで重要な考え方が誤検知(false positive)との付き合い方です。MulmoClaude では、初回監査で「527件警告が出て、本物が0件」だったルール(detect-object-injection など)は、理由をコメントに書いた上で off にしています。
// detect-object-injection: 動的キーの obj[key] に全部反応する。
// 初回監査で 329 件の警告 / 本物 0 件 → シグナルを殺すので off。
// 本物のケースは sonarjs 側のルールが拾う。
"security/detect-object-injection": "off",
逆に、本当に危ないもの(detect-unsafe-regex = ReDoS)は error に格上げし、正当な例外には1行ずつ理由つきの disable コメントを義務づけています。「厳しくする」は「全部 on」ではなく「シグナルが濃くなるように調整する」ことです。ノイズだらけの lint はAIも人間も無視するようになり、いちばん危険です。
構造を守る — アーキテクチャを lint で強制する
AI が最も壊しやすいのは、コードの「行」ではなく構造(どこから何を import してよいか)です。no-restricted-imports を使うと、アーキテクチャの境界を lint にできます。MulmoClaude の実例(プラグインは決められた API 経由でしかホストに触れない):
{
files: ["src/plugins/*/**/*.ts", "src/plugins/*/**/*.vue"],
rules: {
"no-restricted-imports": ["error", {
patterns: [
{
group: ["**/server/**"],
message: "プラグインからサーバー側モジュールを import しない。" +
"プラグインはクライアント側で動く — 通信は API 経由で。",
},
{
group: ["**/config/**"],
message: "ホストの設定を直接 import しない。../api の関数を使う。",
},
],
}],
},
},
message に「代わりにどうするか」を書くのがコツです。AI はエラーメッセージを読んで自分で直すので、メッセージがそのまま矯正装置になります。
なお、コピペ(重複コード)の検出には jscpd という専用ツールもあります。DRY 違反(リファクタリングの心得参照)を可視化したいときに lint と併用できます。
テストコードは一部だけ緩める
テストは性質が違うので、機械的に同じ厳しさを当てるとノイズになります。ただし緩めるのは狙った数本だけ。バグを拾う系(no-shadow、cognitive-complexity など)はテストでも error のままにします。
| 扱い | ルール | 理由 |
|---|---|---|
| off | max-lines-per-function |
describe() に it が10個並ぶのは正常。20行/50行の目標は本番コードの読みやすさのためで、テストには当てはまらない |
| warn に格下げ | no-explicit-any |
モック作りで DOM 型に any 的キャストが要ることがある。本番コードでは error のまま |
| error に格上げ | sonarjs/assertions-in-tests |
assert のないテスト(実行するだけで何も検証しない)は、AI がテストを「書いたことにする」典型パターン。人間の目では見逃しやすいので CI で強制 |
最後の1本は特に重要です。AI は「テストを書け」と言われると、通ることだけが目的の空っぽのテストを書くことがあります。lint がそれを検出できます。
React の場合の差分
コアの上乗せルール(量の上限・型・バグ検出・構造)はそのまま全部使えます。差し替えるのはフレームワーク層だけです。
| Vue | React での対応物 |
|---|---|
eslint-plugin-vue + vue-eslint-parser |
eslint-plugin-react + eslint-plugin-react-hooks |
| — | eslint-plugin-jsx-a11y(アクセシビリティ) |
vue/no-v-html: error |
react/no-danger: error(dangerouslySetInnerHTML 禁止 = 同じ XSS 対策) |
// eslint.config.mjs(React 版の差分イメージ)
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
export default [
// ...共通の土台と上乗せルールは Vue 版と同じ...
react.configs.flat.recommended,
jsxA11y.flatConfigs.recommended,
{
files: ["**/*.{jsx,tsx}"],
plugins: { "react-hooks": reactHooks },
rules: {
"react-hooks/rules-of-hooks": "error", // フックの呼び出し規則違反(条件分岐の中で useState 等)
"react-hooks/exhaustive-deps": "error", // useEffect の依存配列の書き漏れ — AIが最も多発させるReactバグ
"react/no-danger": "error",
"react/jsx-key": "error", // リストの key 忘れ
},
settings: { react: { version: "detect" } },
},
];
⚠️ flat config の書き方(
configs.flat.recommendedなど)はプラグインのバージョンで微妙に変わります。導入時は各プラグインの README の指示に従ってください。ルールの選定はこの表の通りで大丈夫です。
特に react-hooks/exhaustive-deps は error 推奨です。依存配列の書き漏れは「たまにしか起きない不可解なバグ」になり、AI・人間ともに原因究明に最も時間を溶かすパターンだからです。
例外(eslint-disable)のエチケット
どんなに調整しても、正当な例外は発生します。ルール化しておくべき作法:
-
行単位で、理由コメントつきでのみ許可
// ReDoS 安全性はテスト test_htmlSrcAttrs.ts で上界を検証済み // eslint-disable-next-line security/detect-unsafe-regex const SRC_ATTR = /.../; - ファイル全体・ルール全体の disable は禁止(1つの例外のために検出網ごと外さない)
- CLAUDE.md に「NEVER: disable で黙らせず根本原因を直す」と書く(第1章参照)。lint とルールの二段構えで、AI が安易な disable に流れるのを防ぐ
- 既存コードが多くて一気に直せないときは「格付けラチェット」方式: 新規コードには error、直しきれていない既存ファイルだけ一時的に warn の例外リストに載せ、返済したらリストから消す。例外リストに新規追加はしない(MulmoClaude が
max-lines-per-functionで実際にやっている運用)
導入のタイミングでも作戦が変わります。新規プロジェクトなら最初からガチガチで始めます(直すものが無いのでコストゼロ。あとから厳しくする方がずっと大変)。既存プロジェクトに後から入れるなら「最初はゆるく、少しずつ厳しく」(リファクタリングの心得の勧め)+ 上のラチェットで段階的に締めていきます。
運用に組み込む
lint は実行されなければ存在しないのと同じです。コマンドの登録は冒頭「インストールと動かし方」で済んでいるので、あとは3か所に組み込みます。
- CLAUDE.md — 「変更後は必ず
yarn format→yarn lint→yarn typecheck→yarn build」(第1章) - Claude Code のフック —
git commit前に自動実行、失敗したらコミットをブロック(第4章の実例) - CI(GitHub Actions) — push / PR のたびに全チェック。ローカルをすり抜けても最後はここで止まる(CI/CD入門)
この三段構えで、「約束(CLAUDE.md)→ 手元の強制(フック)→ 最終防衛線(CI)」が揃います。
次章は、コードを書いた AI とは別の目にチェックさせるしくみ — subagent と cross review です。