フロントエンドのLintツールをXOに統一した話

  • URLをコピーしました!

こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。

PR TIMESではこれまでLintツールとしてESLintを使用していましたが、2023年9月からXOを使うようにし始めました。本エントリーでは、XOを導入した経緯や進め方、そして導入した結果についてご紹介します。

目次

導入をした経緯

これまでPR TIMESでは .eslintrc.jsを自分たちで一から作り、ESLintルールを運用していました。しかし、その設定は最初期の環境構築時に色々なドキュメントを見ながら、つぎはぎで作成したものになっており、なぜこのルールが入っているのかや、このルールは必要なのかなどの疑問がありました。

そのような状況であったため、各々がESLintの設定を自由に変え、本来であればエラーになって欲しいルールもoffにされていたりしました。

そこでアドバイザーとして入って頂いている 1000chさん におすすめのルールセットやツールを伺ったところ、XOが良いということだったので導入を始めました。

XOとは

Opinionated but configurable ESLint wrapper with lots of goodies included. Enforces strict and readable code. Never discuss code style on a pull request again! No decision-making. No .eslintrc to manage. It just works!

和訳(DeepL)

意見が多いが設定可能なESLintラッパー。厳密で読みやすいコードを強制します。もうプルリクエストでコードスタイルを議論する必要はありません!意思決定不要。.eslintrcを管理する必要もありません。ただ動くだけです!

https://github.com/xojs/xo

XOとは、READMEに書かれている通りESLintのラッパーであり、ルールについては作者の sindresorhus 氏の好みで決めたものとなっています。また、Prettierも内包しているため、XOのバージョンだけを気にしていれば、ESLint、ESLintの各種Plugin、Prettierのバージョン管理を自分たちでする必要がなくなります。

XO導入の進め方について

最初にXOをインストールし、1つのプロジェクトで実行してみると、fixableなerror, warningも含め7817個のエラーが出ました。

これらのエラーをすぐに修正することはできないため、まずはエラーとなっているルールを一度全てoffにして初期導入を進めました(結果として181個のルールをoffにしました)。

その後、1 Plugin/1 Pull Requestの粒度でPR TIMESのフロントエンドメンバーでルールの適用を行なっていきました。

また、ルールを適用していく中でコードをルールに合わせて修正することが難しかったものや、やむを得ずoffにしたプラグインがあるので紹介します。

ルールに合わせてコードを修正するのが難しかったESLintプラグイン

@typescript-eslint/strict-boolean-expressions

このプラグインは条件式や論理演算での型の厳密なチェックを強制します。例えば、以下のように、 int|nullstring|null の型が指定されている変数をそのまま条件式に入れることはできません。

// nullable numbers are considered unsafe by default
let num: number | undefined = 0;
if (num) {
  console.log('num is defined'); // 0 と undefined の時に実行されないため安全ではない
}

// nullable strings are considered unsafe by default
let str: string | null = null;
if (!str) {
  console.log('str is empty'); // 空文字と null の時に実行されてしまうため安全ではない
}

弊社では空文字と null、undefined を等しく扱っている時とそうではない時があったりし、アプリケーションの挙動をそのままにこのルールを適用するためにはある程度のドメイン知識が必要でした。また、エラーとなっている箇所もかなりあったため、修正作業がなかなか進まず、現状ではエラーレベルを warning にして運用しています。

@typescript-eslint/prefer-nullish-coalescing

このプラグインは || 演算子ではなく ?? 演算子を使うことを強制します。

// ❌ Incorrect
const defaultValue: string | undefined = '';
const value: string = defaultValue || 'hoge'; // value には hoge が代入されてしまう


// ✅ Correct
const defaultValue: string | undefined = '';
const value: string = defaultValue ?? 'hoge'; // value には空文字が代入される

こちらも @typescript-eslint/strict-boolean-expressions と同様の理由で修正作業があまり進まず、エラーレベルを warning で運用しています。

@typescript-eslint/no-floating-promises

このプラグインは非同期関数(Promiseを返す関数や async 関数)の戻り値を awaitしていないことを検出します。

// ❌ Incorrect
const promise = new Promise((resolve, reject) => resolve('value'));
promise;

async function returnsPromise() {
  return 'value';
}
returnsPromise().then(() => {});

Promise.reject('value').catch();

Promise.reject('value').finally();

[1, 2, 3].map(async x => x + 1);

// ✅ Correct
const promise = new Promise((resolve, reject) => resolve('value'));
await promise;

async function returnsPromise() {
  return 'value';
}
returnsPromise().then(
  () => {},
  () => {},
);

Promise.reject('value').catch(() => {});

await Promise.reject('value').finally(() => {});

await Promise.all([1, 2, 3].map(async x => x + 1));

このルールを適用するために、初めは単に Promise を返している関数に await をつけるという修正を行いました。しかし、元々非同期で動いていた処理を、急に同期的にしたことにより、アプリケーションの挙動が変わってしまった箇所が見つかりました。そのため、慎重に修正を行う必要が出てきました。

しかし、同期的にすることでどこに影響があるのかを特定するのは難しく、エラー箇所も多かったため、明らかに同期的に扱って良い箇所は await をし、確証が持てない箇所は void 演算子を用いることでルールに適応しました。

https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-floating-promises.md#ignorevoid

offにしたESLintプラグイン

capitalized-comments

このプラグインは英語で書かれたコメントの頭文字を大文字にすることを強制します。

// ❌ Incorrect
// lowercase comment
// zod を用いてバリデーションを実装
// localStorage を用いて状態管理をしている

// ✅ Correct
// Lowercase comment
// Zod を用いてバリデーションを実装
// LocalStorage を用いて状態管理をしている

このルールを適用すると、上記の例でもあるように、 zod という単語を Zod にする必要があったり、 localStorageLocalStorage と書く必要があったりし、固有名詞の頭文字すら大文字にする必要が出てきます。

この問題は日本語でコメントを書くために発生するので、英語でコメントを書いているプロジェクトでは発生しないかもしれません。ただ、弊社では日本語でコメントを書く文化があるため、このような問題が発生します(コメントは日本語で書くと決めているわけではありません)。

そのような状況のため、そもそも英語でコメントを書いている箇所もあまりなく、このルールを適用するメリットをあまり享受することができないと判断したため、このルールは off にして運用しています。

import/extensions

このプラグインはモジュールをインポートする際にファイルの拡張子を書くか書かないかを強制します。XOでは設定が always になっており、インポートする際は必ずファイルの拡張子まで書く必要があります。

// ❌ Incorrect
import { Cat } from '../cat';
import { Dog } from './dog';
import { Card } from './card';
import { Box } from './box';


// ✅ Correct
import { Cat } from '../cat.js';
import { Dog } from './dog.ts';
import { Card } from './card.tsx';
import { Box } from './box/index.ts';

弊社のフロントエンドでは TypeScript を使用しており、元々拡張子がない状態で統一されていたため、このルールは off にしました。

n/file-extension-in-import

このプラグインは Node.js でモジュールをインポートする際にファイルの拡張子を書くか書かないかを強制します。これも import/exntensions と同様に off にしました。

導入してみた結果

XOを導入して、細かいコードの記法を統一できるようになりました。例えば、XOでは react/function-component-definition というプラグインが入っており、そのルールにより React のコンポーネントを書くときにアロー関数ではなく function で書くことが統一されたりしました。

導入した経緯でお話しした、なぜこのルールが入っているのかや、このルールは必要なのかなどの疑問についても、解消することができ、今後の運用ルールについての共通認識を作ることができました。

その他にも、ESLintおよびその周辺プラグインの依存関係を含めたバージョンアップのケアをする必要がなくなり、運用コストも軽減しました。

また、記法の統一だけではなく、不要なFragmentがあったときにエラーとなるルールや、@typescript-eslint/strict-boolean-expressions のような boolean の扱いを厳格にしてくれるルールなどのおかげで、コードの品質も向上したと思っています。

XOにはJavaScriptの記法を強制する記法も入っており、 colors[colors.length -1] のような配列の一番最後の値を取得したいコードを colors.at(-1) に置き換えられることや、 indexOfsome といった関数を includes に置き換えれることなど、純粋なJavaScriptの書き方について学びがありました。

デメリットとしては、実行が遅い点があります。約270,000行のコードに対してXOを実行すると、私の手元のMacBook Pro(チップ:M2 Pro、メモリ:32 GB)では2分ほどかかります。

そのため、CI(GitHub Actions)上で実行する際には、Gitの差分があるファイルに対してのみ XO を実行して高速化していたり、今後 XO のキャッシュを使った高速化に取り組みたいと考えています。

We are hiring!

フロントエンドはもちろん、各種ポジションで採用を行っています!

あわせて読みたい
株式会社PR TIMES
02.開発部 の求人一覧 - 株式会社PR TIMES 株式会社PR TIMESが公開している、02.開発部 の求人一覧です
  • URLをコピーしました!

この記事を書いた人

株式会社PR TIMES 開発本部 フロントエンドエンジニア

目次