ハウテレビジョンブログ

『外資就活ドットコム』『Liiga』『Mond』を開発している株式会社ハウテレビジョンのブログです。

フロントエンドのlinter/formatterをxoに統一し、ルールの議論から解放される

この記事は HowTelevision Advent Calender 2023 の2日目の記事です。1日目はプロダクト本部長の泉水さん (id:hc0001) による「技術負債解消プロジェクト・ツールやリソースの全社的な統廃合・イシューに向き合う部門再編などの話 - ハウテレビジョンブログ」でした。

qiita.com

外資就活の開発チームでソフトウェアエンジニアをしている根本です。

皆さんのチームでは、ESLintやPrettierのルールをうまく管理できていますか?

ESLintのルールには、よく使われる@typescript-eslint/recommended以外にもたくさんのルールがあり、どれを使うかという話だけでも人によって様々な意見があります。個人開発では自分の好きなものを使用すれば良いですが、チーム・企業における開発ではどこまでのルールを入れるか・どの程度厳密に守るかを決めることが一つの問題になることが多いかと思います。この記事では、外資就活の既存のコードベースにxoを導入した背景や導入してわかったことについて書きます。

課題

フロントエンドのlinter/formatterを見直すきっかけは、既存の運用の問題提起から始まりました。

1点目は、元々存在するESLintの設定をeslint-disableを使用して無視している例がいくつか存在していたことです。チーム内で決めたESLintルールに従わない例外を出してしまうと、ルールを守らなくなってしまい、ESLintルールを設定した意味がなくなってしまう可能性があります。

2点目は、ESLintの設定が各プロダクトのフロントエンドのコードベースごとに異なっていることです。誰かがチームを移動した時に、ルールが大きく違うとそこだけでキャッチアップのコストがかかってしまいます。

3点目がルールのメンテナンスコストです。ESLintの好みは人によって様々であり、議論の発生の頻度が多いと開発が進まなくなってしまいます。

以上の3つの問題を解消するために、新しく会社全体でxoを導入することにしました。

xoとは?

xoはいくつかのおすすめのESLintルールをまとめて入れることができるESLintのwrapperです。2023/12/1現在のスター数は、7400です。

デフォルト設定のdependeiciesを見ると、以下のパッケージが含まれていました。

dependencies:
    "@eslint/eslintrc" "^2.1.0"
    "@typescript-eslint/eslint-plugin" "^6.0.0"
    "@typescript-eslint/parser" "^6.0.0"
    arrify "^3.0.0"
    cosmiconfig "^8.2.0"
    define-lazy-prop "^3.0.0"
    eslint "^8.45.0"
    eslint-config-prettier "^8.8.0"
    eslint-config-xo "^0.43.1"
    eslint-config-xo-typescript "^1.0.0"
    eslint-formatter-pretty "^5.0.0"
    eslint-import-resolver-webpack "^0.13.2"
    eslint-plugin-ava "^14.0.0"
    eslint-plugin-eslint-comments "^3.2.0"
    eslint-plugin-import "~2.27.5"
    eslint-plugin-n "^16.0.1"
    eslint-plugin-no-use-extend-native "^0.5.0"
    eslint-plugin-prettier "^5.0.0"
    eslint-plugin-unicorn "^48.0.0"
    esm-utils "^4.1.2"
    find-cache-dir "^4.0.0"
    find-up "^6.3.0"
    get-stdin "^9.0.0"
    get-tsconfig "^4.6.2"
    globby "^13.2.2"
    imurmurhash "^0.1.4"
    json-stable-stringify-without-jsonify "^1.0.1"
    lodash-es "^4.17.21"
    meow "^12.0.1"
    micromatch "^4.0.5"
    open-editor "^4.0.0"
    prettier "^3.0.0"
    semver "^7.5.4"
    slash "^5.1.0"
    to-absolute-glob "^3.0.0"
    typescript "^5.1.6"

導入の際には、以下の極めてシンプルな設定をするだけで上記のおすすめルールを入れることができます。

module.exports = {
    prettier: true,
    space: true
};

導入

戦略

既存のコードベースに対して、開発と並行して導入するため、段階的な導入の戦略を取ることで、スムーズに移行できるように心がけました。これまでは別のESLintルールでxoでlintをかけると、たくさんlintエラーが出てしまいます。これを一度全て修正してからPRをマージしようとすると、修正までに多くの時間がかかってしまい、差分も大きくなってしまいます。

そこで、まずはたくさんのエラーが出てしまうルールを一旦無視することで、エラーの数を修正しやすい数まで減らします。そして、エラーがない状態でmainブランチにマージし、その後段階的に無視したESLintルールを減らしていくことにしました。これにより、lint実行のCIが通る状態で段階的な移行を進めることができます。

手順

  1. xoをインストールします
yarn install xo --save-dev
  1. xo.config.jsを作成します
module.exports = {
  space: 2,
  prettier: true,
  rules: {
    // ここにignoreルールを記述する
  }
}
  1. package.jsonのlintコマンドを修正します
- "lint": "eslint",
+ "lint": "xo",
  1. これでlintを実行できます
yarn lint

また、vscodeの拡張機能と自動フォーマットの設定は以下になります。

.vscode/extensions.json

{
  "recommendations": [
    "samverschueren.linter-xo"
  ]
}

.vscode/settings.json

{
    "xo.format.enable": true,
    "[javascript]": {
        "editor.defaultFormatter": "samverschueren.linter-xo"
    },
    "[javascriptreact]": {
        "editor.defaultFormatter": "samverschueren.linter-xo"
    },
    "[typescript]": {
        "editor.defaultFormatter": "samverschueren.linter-xo"
    },
    "[typescriptreact]": {
        "editor.defaultFormatter": "samverschueren.linter-xo"
    }
}

使ってみた感想

やはり、導入が簡単なことです。これまではどのESLintルールを入れるべきか調べることが多かったのですが、その時間が短縮されたことは大きいです。特に、新規のプロジェクトの場合には移行コストも発生しないので、今後はコードをすぐに書き始められそうです!

注意点

実行時間に時間がかかるため、キャッシュの利用や差分実行を行いたい

ESLintルールがあることのデメリットの一つは、一回のlintに対して時間がかかってしまうことです。外資就活のコードベースでは、PHPからNext.jsへのリプレイスの途中であるにも関わらず、1分もの時間がかかりました。

TIMING=1 yarn lintでeslintの実行時間を分析することができます。以下の結果より、Prettierの実行に約半分もの時間がかかっていることがわかりました。

Rule                                            | Time (ms) | Relative
:-----------------------------------------------|----------:|--------:
prettier/prettier                               |   744.647 |    37.6%
@typescript-eslint/no-confusing-void-expression |   206.174 |    10.4%
import/no-cycle                                 |   174.208 |     8.8%
import/no-self-import                           |   135.656 |     6.9%
@typescript-eslint/no-floating-promises         |   105.125 |     5.3%
@typescript-eslint/await-thenable               |    60.807 |     3.1%
import/no-named-as-default-member               |    46.178 |     2.3%
@typescript-eslint/promise-function-async       |    46.048 |     2.3%
@typescript-eslint/no-misused-promises          |    17.848 |     0.9%
@typescript-eslint/no-unsafe-return             |    16.013 |     0.8%

xoのREADME.mdには記載がありませんが、ESLintやPrettierのキャッシュ設定を利用することができました。xoの内部的にオプションをeslintに流しているものだと思われます。

yarn lint --cache

ESLintのcache-strategyオプションには、メタデータを使う方式とファイル全体を使う二つがあります。実行結果は以下です。

% yarn lint --cache --cache-strategy=metadata
Done in 10.21s.
% yarn lint --cache --cache-strategy=content
Done in 28.68s.

prettierのキャッシュオプションを実装した方の記事によると、このキャッシュ戦略のオプションはローカル実行かCI実行かで適切な使い方が変わります。

この2つの --cache-strategy にはトレードオフがあります。

まず metadata については、タイムスタンプは Git に乗らないので CI 上では上手く機能しません。しかし content に比べてキャッシュキーが更新されたかどうかを判定する際のパフォーマンスが良いです。

また、lint-stagedによる差分実行をすることも検討できます。こちらは、キャッシュよりも確実にgitコミット単位の差分のみを対象として実行できるので、huskyと組み合わせてコミット前の実行を行う方が管理がしやすいです。

ファイル名の強制

unicorn/filename-case によりkebabCaseのファイル名がデフォルトルールとなっています。これのためにファイル名を全て修正するか、プロジェクトに合わせてルールをカスタマイズするかどうかは議論の余地があります。 特に、Next.jsはファイル名でルーティングが行われますが、Googleによるとケバブケースが推奨されているので、可能であればデフォルトのルールに合わせたいですが、何年も続いているプロダクトのパスを変えることはSEOに影響してしまいます。

undefinedへの強制

@typescript-eslint/ban-typesにより、型にnullを使わず、undefinedだけに統一する必要があります。修正時に、変数とnullとの厳密等価比較を行なっている箇所がある場合は、同時に修正が必要ですが、以下のようなundefindとnullを厳密等価比較するコードがlintエラーにならないので注意が必要です。

type User = {
  id: number
  name: string
}
const user: User | undefined = undefined;
if (user === null) { // no error
  console.log('test');
}

これをlintで検知するためには、"unicorn/no-null": ["error", {"checkStrictEquality": true}] の設定を追加する必要がありました。ただ、xoのルールのカスタマイズしていくには抵抗があるため、xoに対して提案のPRを作成するつもりです。

自動フォーマットでimport文が冗長になる

同じファイルから複数の型のimportをしたときに、冗長な表記になってしまいます。

import {type A, type B, C, type D} from 'SomeDir/SomeFile'

一つのファイルから多数の型のimportが存在する場合には、手動でこのように修正した方が見やすいです。

import type {A, B, D} from 'SomeDir/SomeFile'
import {C} from 'SomeDir/SomeFile'

まとめ

  • xoの導入により、ESLintのメンテナンスコストを減らすことができます。
  • 段階的な導入により、既存のコードベースにもスムーズに導入することができます。
  • 一方で、実行時間などいくつか注意点が存在しました。

ハウテレビジョンでは、組織拡大のためにソフトウェアエンジニアを募集しています!