サービスの開発途中から、厳しめのESLintのルールを導入するためにしたこと

こんにちは、株式会社カミナシのエンジニア@tomiです。

今回は、開発途中からESLintのルールを見直す際にどうやって行ったかを書こうと思います。

開発途中からESLintを見直すとなると、膨大にあるコードがあるので一気に直すとなると、恐ろしい量の修正が必要です。

カミナシで理想的なESLintのルールに修正しようと思ったら、修正箇所が4000件を超えていました。。。

f:id:kaminashi-developer:20210524091458p:plain

とても一人で直すには無理な量です。更に修正中も開発は進むわけですから、コンフリクトも頻繁に発生するでしょう。

ESLintを見直すための道のり

では、一気に直すのは無理なので、はじめに考えたのは「段階的にルールを厳しくしていく」ことでした。

何個もルールをoffにしてあるので、それを一つずつerrorにしてプルリクを作成していく

そこで考えたのは、pre-commitを使ってコミットされるファイルに対して、新ルールのESLintを実行する方法です。

他の開発を行いながら徐々にコードがキレイになっていく仕組みを作ろうと考えました。

ただし、pre-commitでESLintを実行するだけでは駄目で、以下の要件をクリアする必要がありました。

  • 新ルールはガチガチの厳しいものにする
  • 開発環境の起動時は旧ルールで動く
  • ビルド時のlintチェックは旧ルールで動く
  • 編集したファイルのみ新ルールを適用する

既存の.eslintrc.jsonを厳しいルールに変更してしまうと、開発環境が起動しなくなったり、ビルドが失敗してしまったりと問題が起こるので、コミットされる時のみ新ルールが適用されるように書く必要があります。

新ルールだけをまとめた設定ファイルを作る

ESLintには設定ファイルを上書きして読み込める機能があるので、それを利用します。

例えば、.eslintrc.jsonは以下だとして、

{
  "env": {...},
  "parser": "@typescript-eslint/parser",
  "parserOptions": {...},
  "plugins": [...],
  "rules": {
    "no-unused-vars": "off",
    "no-empty": "off"
  }
}

ESLintを実行すると、自動で読み込まれます。

$ eslint --print-config src/App.tsx
...
  "rules": {
    "no-unused-vars": [
      "off"
    ],
    "no-empty": [
      "off"
    ]
  },
  "settings": {},
  "ignorePatterns": []
}

さらに、別ファイル.eslintrc-override.jsonを作成する

{
  "rules": {
    "no-unused-vars": "error"
  }
}

こちらには、.eslintrc.jsonでオフ設定したルールに対して、エラーになるように設定した。

.eslintrc-override.json-cオプションを使って読み込むと、

$ eslint -c .eslintrc-override.json --print-config src/App.tsx
...
  "rules": {
    "no-unused-vars": [
      "error"
    ],
    "no-empty": [
      "off"
    ]
  },
  "settings": {},
  "ignorePatterns": []
}

.eslintrc.jsonの上に.eslintrc-override.jsonが上書きされていることがわかる。

このオプションを使えば、pre-commit時だけ別ルールというのは比較的に簡単に実装できそう。

予期せぬ上書きが起こる

ただし、注意して設定を行わないと思っていたルールと違う設定になってしまう場合がある。

それが、上書きする側.eslintrc-override.jsonextendsを使うときである。

.eslintrc-override.jsonを以下のようにextendsを追加してみる。

{
  "extends": ["eslint:recommended"],
  "rules": {
    "no-unused-vars": "error"
  }
}

そして先程同様にESLintを実行すると、

$ eslint -c .eslintrc-override.json --print-config src/App.tsx

f:id:kaminashi-developer:20210524091546p:plain

**.eslintrc.jsonでoffにしていたはずが、errorになっている。**

eslint:recommendedでは内部では"no-empty": ["error"]が設定されているためである。

中身を把握してextendsを設定するなら問題ないが、意図しないルール変更が行われる匂いがプンプンする。

そこで、元々あったルールで変更したくないものがあるときは、更に別ファイルに分けて、.eslintrc.json.eslintrc-override.jsonに読み込ませるのが安全だろう。

最終的な形としては、以下の3ファイルを用意する。

// .eslintrc.json
{
  "env": {...},
  "parser": "@typescript-eslint/parser",
  "parserOptions": {...},
  "plugins": [...],
  "extends": [
    "./.eslintrc-rules.json"
  ],
  "rules": {
    "no-unused-vars": "off"
  }
}
// .eslintrc-override.json
{
  "extends": [
    "eslint:recommended",
    "./.eslintrc-rules.json"
  ],
  "rules": {
    "no-unused-vars": "error"
  }
}
// .eslintrc-rules.json
{
  "rules": {
    "no-empty": "off"
  }
}

extendsで変更したくないルールをそれぞれのファイルに読み込ませることで、上書きされる危険性がなくなる。

pre-commit時のみ新ルールを適用する

これはもう説明も不要かもしれませんが、記録に残して起きます。

CIで実行されるlintチェックは、yarn lintで実行させます。

pre-commitはhuskyを使って、.eslintrc-override.jsonを適用させます。

"scripts": {
  "lint": "eslint ./src"
}
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "./src/**/*.{js,jsx,ts,tsx}": [
    "prettier --config .prettierrc --write",
    "eslint -c ./.eslintrc-override.json"
  ]
},

これでコミットするファイルだけ厳しいルールが適用され、ルールを守らないとコミットできない仕組みができました。

4000件以上の修正はしないといけないのは変わらないですが、みんなで少しずつコードの整理をしていきましょう!!

将来的に

まだ実現できてないのですがやりたいことがあります。

それは残りのエラー件数を定期的に通知する仕組みです。

今回実装したものの課題は、このままだとずっとビルド時のlintチェックは緩いままで決して健全とは言えません。なのでいつかは新ルールがすべての場所に適用されるのが理想です。

そのためには、エラーがなくなったかを知る仕組みが必要かなと思いました。

いつか、yarn lint -c ./eslintrc-override.json srcが定期実行されて、Slackに通知される仕組みも作っておきたいなぁと思っています。