🤖

lint-staged を導入して Lint が通らないコードをコミットできないようにしよう!

2023/05/04に公開

はじめに

ESLint や Stylelint などの Linter はコードを静的解析することで問題のあるコードを素早く発見することができるとても便利なツールです。
しかし、時にはうっかり Linter によるチェックが通らないコードをコミットしてしまうという事態が起こりえます。

このようなミスを防ぐためにコミット前に自動に Lint を実行し Lint が通らないコミットを防止する仕組みを作ることができたら嬉しいです。
これは lint-staged と Git hooks を使うことで実現できます。

この記事では lint-staged を導入する方法について説明します。

lint-staged について

https://github.com/okonet/lint-staged

lint-staged は Git でステージング状態のファイルに対してのみ ESLint などの Linter を実行することを主な目的としたツールです。
このツールを Git hooks とともに用いることでコミット前に lint-staged を自動で実行し、Lintの通らないコードのコミットを防ぐことができます。

著名なプロジェクトとしては Next.js や Ant Design が2023年5月現在 lint-staged を導入しています。

https://github.com/vercel/next.js

https://github.com/ant-design/ant-design

導入手順

ここでは Git hooks の設定ツールとして lint-staged の README でも紹介されている Husky を用いる場合を考えます。

https://github.com/typicode/husky

Linter の設定はすでに完了しているものとします。

パッケージのインストール

lint-staged と Husky を devDependencies にインストールします。

  • npm
npm install --save-dev lint-staged husky
  • yarn
yarn add --dev lint-staged husky

今回インストールした lint-staged と Husky のバージョンはそれぞれ以下の通りです。

$ npm view lint-staged version
13.2.2
$ npm view husky version
8.0.3

lint-staged の設定

lint-staged の設定方法は複数通りあります。
詳しくは公式リポジトリの README を参照してください。

おそらく最も簡単なのは package.json に lint-staged オブジェクトを書く方法であり、多くの記事ではその手法が紹介されていると思います。
しかし、ここではより複雑な設定が可能である lint-staged.config.js による設定方法を紹介します。

設定ファイルを書く前にどのファイルに対してどのような Lint を実行するかを決めます。

ここでは、以下のようにしました。

  • もしも、ts, tsx ファイルが1つでもステージングされたならば、プロジェクト全体に対して tsc による静的型チェックを実行する
  • ステージングされた src ディレクトリ以下の js, jsx, ts, tsx ファイルに対して、 eslint を実行する
  • ステージングされた src ディレクトリ以下の css, scss ファイルに対して、 stylelint を実行する
  • ステージングされた src ディレクトリ以下のすべてのファイルに対して prettier を実行する

以下の内容を記述して lint-staged の設定をします。

// lint-staged.config.js
export default {
  '**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit',
  'src/**/*.{js,jsx,ts,tsx}': 'eslint --fix --no-ignore --max-warnings=0',
  'src/**/*.{css,scss}': 'stylelint --fix',
  'src/**/*': 'prettier --write --ignore-unknown'
}

Export するオブジェクトの key にリントの実行対象を glob pattern で指定し、 value に実行するコマンドの文字列または関数を指定します。

単に実行するコマンドの文字列を指定した場合はステージング状態で glob pattern にマッチするファイルがそれぞれ引数として渡されます。
一方で関数を指定した場合は、ステージング状態で glob pattern にマッチしたファイルの文字列の配列が関数の引数として渡されます。

(filenames: string[]) => string | string[] | Promise<string | string[]>

ここでは、tscによる型チェックをプロジェクト全体に対して行いたいので、引数を省略しています。
もしも glob pattern にマッチするものがあれば、 tsc -p tsconfig.json --noEmit が一度実行され、なければ何もしません。

ここで、注意すべきはそれぞれのコマンドはデフォルトでは並列で実行されるということです。

そして、設定した glob パターンが重なる場合は競合を引き起こす可能性があります。
今回の例では、tscとeslintとprettier、stylelintとprettierのglobパターンがそれぞれ重なっています。

このような場合は glob パターンが重ならないようにするかあるいは、コマンドを同時実行しないように --concurrent false オプションをつけて lint-staged を実行するのが良いと思います。

Husky による Git Hooks の有効化

次に Husky で lint-staged をコミット前に自動で実行するように git hook を設定します。

Hasky で git hook を有効化するために husky install を一度実行する必要があります。
そこで以下のように package.json > prepare に npm script を設定し、一度だけ実行します。

  • npm / yarn1
npm pkg set scripts.prepare="husky install"
npm run prepare

prepare という npm script は Life Cycle Scripts の一種で特別な意味を持ちます。

https://docs.npmjs.com/cli/v9/using-npm/scripts

preparenpm install を引数なしで実行したときにも実行されます。
そのため、これで実質 Husky による Git Hooks の有効化を自動で実行するように設定したことになります。

  • yarn

バージョン2以降の yarn は prepare Life Cycle をサポートしていないため、異なる方法を使う必要があります。
公式ドキュメントでは pinst を使う方法を紹介しています。

https://typicode.github.io/husky/#/?id=yarn-2

  • Install pinst
yarn add pinst --dev # ONLY if your package is not private
  • Enable Git hooks
yarn husky install
  • package.json を編集して、インストール後の Git hooks の有効化を自動化
// package.json
{
  "private": true, // ← your package is private, you only need postinstall
  "scripts": {
    "postinstall": "husky install"
  }
}

Husky による pre-commit hook の追加

以下のコマンドで Husky で pre-commit hook を設定できます。

npx husky add .husky/pre-commit "npx --no lint-staged --concurrent false"

これを実行すると、以下のように .husky ディレクトリが作成されます。

$ tree .husky/
.husky/
├── _
│   └── husky.sh
└── pre-commit

2 directories, 2 files
$ cat .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no lint-staged --concurrent false

.husky/pre-commit を git add して commit します。

git add .husky/pre-commit
git commit

実行例

lint-staged と Husky を上記の手順で設定をした場合、以下のように Lint が通らないコードのコミットを防止できます。

$ git diff
diff --git a/src/App.tsx b/src/App.tsx
index f02a0eb..2724988 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,9 @@ import './App.css'

 function App() {
   const [count, setCount] = useState(0)
+  let age = 42
+  age = '42'
+  console.log(age)

この例では tsc による型チェックが通らないため、コミットが失敗します。

終わりに

今回は lint-staged と husky を用いて、コミット前に静的チェックを自動で実行する方法について紹介しました。
静的チェックは GitHub Actions などの CI で実行する場合も多いと思いますが、コミット前に実行することでコミットログを綺麗に保ちやすくなります。

設定もかなり楽なので、今後私は積極的に導入していこうと思います。

参考文献

Discussion