PrettierESLintStylelint は強力なツールです。組み合わせてしっかり設定しておくと、自分とチーム全員のコーディング速度と効率が大きく上がり、コード全体の品質も向上、バグが減り、コードレビューも速くなるため、チームが本当に重要なことに集中できるようになります。

多くのチームは ESLint をコミット時、あるいは CI アクション内でしか走らせていません。それでも問題は拾えますが、ESLint が提供する最も役立つ自動化、つまり「保存時の修正」を取りこぼしています。

この記事の内容

  • プロジェクトで ESLint の保存時の修正を使うべき理由
  • 入れるべきルールの種類
  • プロジェクトへの追加方法
  • 推奨するルールセット一式の入手先

Prettier の哲学を受け入れる

Prettier の哲学 をまだ読んでいなければ、ぜひ一度目を通してみてください。

Prettier は保存時に修正するツールで、ファイルを保存するとフォーマットルールが自動で適用されます。

副次的な利点として、Prettier はコードのどこかに構文エラーがあるかを知る手段にもなります。保存しても期待どおりにフォーマットされない場合は、余分な文字や閉じ括弧・タグの欠落など、構文エラーが潜んでいるということ。逆に予想外のフォーマットになった場合は、閉じ括弧やタグの位置がずれている可能性があります。

ESLint の保存時の修正とは

多くの ESLint ルールは --fix flag を受け付けており、可能な範囲でコードを自動的にリファクタリングしてルールに沿わせてくれます。

Prettier と同じく、コードはリアルタイムで自動修正され、修正できないものは即座に通知されます。

JetBrains IDE (WebStorm など)

  1. 設定を開く (⌘+,)、または Windows では Ctrl + Alt + S
  2. 言語とフレームワーク > コード品質ツール > ESLint を選択
  3. 「eslint --fix を実行」の横のチェックボックスをオンにする

VS Code

VS Code で ESLint 拡張機能 を使っている場合、settings.json ファイルを開きます。

  1. +Shift+P、または Windows では Ctrl + Shift + P を押す
  2. 「ユーザー設定 (JSON) を開く」と入力
  3. 次のコードを settings.json に追加
{
  "editor.codeActionsOnSave": {
    "source.fixAll": "explicit"
  }
}

Vim

Vim で ALE Vim プラグイン を使っている場合、次のコードを .vimrc に追加します。

let g:ale_fix_on_save = 1

Stylelint とは

Stylelint は Prettier や ESLint と同じことを CSS に対してやってくれるツールです。

ESLint の保存時の修正の利点

人生の多くのことと同様、ESLint の保存時の修正の利点は説明されるより使ってみたほうが早く実感できます。とはいえ、特に大きいものをいくつか挙げておきます。

  • 問題を即座に防ぐ
  • 高品質なコードを自動的に強制する
  • コーディング中の不要な意思決定が減り、集中力が上がる
  • コードレビューでのコードスタイル議論が減るか、なくなる
  • 一貫したコードのおかげで、プロジェクトでの共同作業がやりやすくなる
  • フォーマットやルールを気にせずコードを書ける
  • ファイルの保存が、書いた内容を「整理」する強力なツールになる
  • 時間が経つにつれ、ESLint に直してもらわなくてもルールに沿った高品質なコードを誰もが自然に書くようになる
  • 経験の浅い開発者にとっては、より良いコードの書き方を学ぶための教師が組み込みで付いてくる

ルールの選択

Prettier の哲学にあるとおり、

人はコードの書き方の細部にとても感情的になる…… Prettier がすべてのコードを 100% 望みどおりにフォーマットしてくれなかったとしても、その「犠牲」を払う価値がある

Prettier のカスタマイズは意図的に絞り込まれています。一方で ESLint には大量のルールがあり、それに比例して大量の意見も存在します。

プロジェクトのニーズやチームの好みに合わせてルールをカスタマイズできる一方で、強固な土台を作る標準的なルールもいくつかあります。

何より大事なのは、ルールセットがありがちな(そしてあまりない)問題を回避してくれること、そして開発者の負担を減らすために保存時の修正ルールをできる限り多く備えていることです。

土台を整えたあとは、どちらに振っても構わないルールがいくつか出てきます。好みの問題に近いものもあり、どう決めるかは企業文化次第ですが、これらをスキップして各自の判断に委ねるのはやめてください。混乱を招くだけ。決断を下す「犠牲」を払う価値はあり、特にルールが --fix に対応している場合はなおさらです。

推奨事項

もうプロジェクトごとにルールセットを手で組み立てることはしていません。推奨するルール一式は、バージョン管理された npm パッケージ @gaia-react/lint として公開済み。その中身と自分のプロジェクトに合わせて調整する方法は、本稿末尾の「推奨されるルール」セクションで扱います。

"warning" を使わない

ESLint のルールは "error" か "off" のどちらかであるべきです。

強制されない限り、ルールはルールではありません。断固とした決断を下してください。

コミット時に実行

保存時の修正に加えて、コミット時にも ESLint を自動で走らせるべきです。明示的に開いていないファイルにリファクタリングの影響で変更が入った場合など、こうした問題はここで拾えます。

TypeScript を使っているなら、tsc もコミット時に走らせておきましょう。ESLint では拾えない TypeScript の問題を検出できます。


ESLint や TypeScript のエラーがあるとコミットが拒否される、というのは決定的に重要です。これにより不注意なミスやバグが防がれ、無駄が減ります。誰しも人間で、一つのコーディングセッションの中ですら何かを忘れることはあるもの。コミット時に ESLint や TypeScript の違反を直すのは一瞬の作業です。

問題のあるコードがコミットされるのを止めなければ、見つけて直す負担は他のメンバーに回り、最悪の場合は見逃されて本番に流れていきます。

ESLint の --max-warnings=0 フラグがこれを自動で処理してくれます。


コミット時に走らせる最も簡単で一般的な方法は、husky 経由で lint-staged を使うことです。

lint-staged

変更のあったファイルだけに linter を走らせることで、時間を節約できます。

我々が使っている .lintstagedrc ファイルがこちら。

{
    "{src,.storybook}/**/*.{ts,tsx}": [
        "eslint --fix --max-warnings=0 --cache --cache-location ./node_modules/.cache/eslint .",
        "prettier --write"
    ],
    "src/**/*.css": [
        "stylelint --fix",
        "prettier --write"
    ],
    "src/**/*.json": [
        "eslint --cache --fix",
        "prettier --write"
    ]
}
 

React Router 7 プロジェクトでは、srcapp に置き換えてください。

husky

husky は、コードに変更があった場合だけ実行するように設定できます。

我々はテストハーネスもコミット時に走らせています。lint-staged とテストは変更されたファイルのみに絞れる一方で、tsc はプロジェクト全体に対して走るため、関連ファイルが変わっていないコミット(例えばドキュメントの変更など)では時間を節約できます。

husky の pre-commit 設定はこちら。

#!/usr/bin/env sh
 
declare HAS_SRC_CHANGED=$(git diff --cached --name-only --diff-filter=ACDM | grep 'src/')
declare HAS_TEST_CHANGED=$(git diff --cached --name-only --diff-filter=ACM | grep 'test/')
 
if [[ $HAS_SRC_CHANGED || $HAS_TEST_CHANGED ]]
then
  npm run typecheck
  npx lint-staged
  npm run test:lint-staged
else
    echo "\n✅  No changed files -- skipping lint-staged\n"
fi
 

React Router 7 プロジェクトでは、src/app/ に置き換えてください。

package.json

関連する package.json スクリプトの例がこちら。

{
  ...
  "scripts": {
    "lint": "eslint --fix --max-warnings=0 --ext js,jsx,ts,tsx src --cache --cache-location ./node_modules/.cache/eslint .",
    "lint-all": "npm run typecheck && npm run lint && npm run format && npm run stylelint",
    "format": "prettier --ignore-path .prettierignore --write \"**/*.{ts,tsx,html,md,mdx,css,json}\"",
    "stylelint": "stylelint --fix src/**/*.css",
    "typecheck": "tsc",
    "test": "vitest",
    "test:ci": "vitest --run --passWithNoTests --coverage --bail 1",
    "test:lint-staged": "vitest --run --changed --passWithNoTests --bail 1"
  },
  ...
}

React Router 7 プロジェクトでは、srcapp に置き換えてください。

必要に応じて手動で npm run できるスクリプトを含めてあります。

  • lint はすべてのファイルに対して ESLint を実行
  • lint-all はすべてのファイルに対してすべて(tsc、Prettier、ESLint、Stylelint)を実行
  • format はすべてのファイルに対して Prettier を実行(必要に応じて対象ファイル種を変更してください)
  • stylelint はすべてのファイルに対して Stylelint を実行

既存のプロジェクトに導入する

ルールセットに合意し、すべて設定が済んだ段階で、多くのコード変更が必要になるはずです。

朗報は、変更の大半は自動で行われ、手動で直す必要があるのはごく一部だけということ。

悪い知らせは、こうした変更をコミットするとオープンブランチとのコンフリクトが発生し、解消が面倒になりがちなことです。

これに対処する方法は二つ。

  1. すべてのオープンブランチをマージするタイミングを決めておき、ESLint の変更を一つの PR にまとめ、できれば一日で片づけ切る。最もシニアな開発者にリードを任せ、他のメンバーは画面共有で見学するか、計画立て、ハッカソン、その他のチームビルディング演習などのコーディング以外のタスクに回ってもらう。
  2. ファイル単位で段階的に導入していく。

段階的な導入

手順は次のとおり。

  • eslint-plugin-eslint-comments がインストール済みで、次のルールが有効になっていることを確認してください: 'eslint-comments/no-unused-disable': 'error'
  • ESLint や lint-staged の保存時の修正は(まだ)有効にしない
  • Node スクリプト(下記参照)を使ってプロジェクト内のすべてのファイル先頭に /* eslint-disable */ を挿入し、--no-verify でコミット
  • npm run lint を実行して、(上記 no-unused-disable ルールにより)ルールに違反していないファイルから eslint-disable を取り除く
  • PR をメインブランチにマージ
  • 全員が main を自分のブランチに取り込む

eslint-disable を入れておけばコンフリクトはほぼ起きなくなり、以降は各開発者が好きなタイミングでその行を消し、保存時の修正を走らせ、必要に応じてリファクタリングできるようになります。

より広範なリファクタリングが必要なファイルでは、eslint-disable 行を一度外して部分的に修正し、後日リファクタリング専用ブランチで戻ってこられるよう再び eslint-disable を入れ直しておく、という運用も可能です。

時間が経つにつれ、コードベースからは eslint-disable 行がすべて消えていきます。恩恵をできるだけ早く受け取るためにも、この作業を最優先で進めることを強く勧めます。

eslint-disable を注入する node スクリプト

このファイルをプロジェクトのルートに追加してください(使い終わったら削除します)。

import fs from 'node:fs';
import path from 'node:path';
 
export const getAllFiles = (dirPath, arrayOfFiles = []) => {
  fs.readdirSync(dirPath).forEach(function (file) {
    if (fs.statSync(`${dirPath}/${file}`).isDirectory()) {
      arrayOfFiles = getAllFiles(`${dirPath}/${file}`, arrayOfFiles);
    } else {
      arrayOfFiles.push(path.join(dirPath, '/', file));
    }
  });
 
  return arrayOfFiles;
};
 
const INJECT = '/* eslint-disable */\n';
 
const injectEslintDisable = () => {
  const files = getAllFiles(path.resolve('src')).filter(
    (file) => ['.ts', '.js', '.tsx', '.jsx'].includes(path.extname(file))
  );
 
  const len = files.length;
 
  for (let i = 0; i < len; i++) {
    const readFile = fs.readFileSync(files[i]);
    if (readFile.includes(INJECT)) continue;
    const withInject = INJECT + readFile.toString();
    fs.writeFileSync(files[i], withInject);
  }
};
 
injectEslintDisable();

React Router 7 を使っている場合は、srcapp に変更してください。

package.json のスクリプトに以下を追加し、npm run inject で呼び出します。

"inject": "node inject-disable.mjs"

完了したら inject-disable.mjs ファイルは削除して構いません。

推奨されるルール

以前は推奨するルールセットを、プラグインごとに手で組み上げる設定として管理していました。今はもうやっていません。推奨するルール一式は @gaia-react/lint として公開済みのバージョン管理された npm パッケージにまとまっており、インストールしてフラット設定でプリセットを spread するだけで、プロジェクト間で何かをコピーして回ることなく一式が手に入ります。MIT ライセンスで公開しており、プリセット、カスタムルール、同意できないルールの上書き方法は、すべて GitHub の README にあります。

設定をパッケージ化した理由と中身については、プロジェクト間でコピペしなくなった ESLint 設定 をご覧ください。