mizdra's blog

ぽよぐらみんぐ

ESLint の Suggestions から学ぶ、コードの自動修正の奥深さ

これは、はてなエンジニアアドベントカレンダー2023 4日目の記事です。

3日目は id:mechairoi さんの「SQLiteでLinderaを使った日本語全文検索」でした。

blog.chairoi.me


今日のテーマは、JavaScript 向けの Linter 「ESLint」についてです。ESLint を使うと、JavaScript で書かれたコードを静的解析して、よくある間違いを検出したり、コーディングスタイルを統一できます。

通常、ESLint のルールによって報告された問題 (error や warn) は人が手で修正します。ただし、ルールが報告する問題の中には「fixable」な性質を持ったものがあります。こうした fixable な問題は、eslint --fix で自動修正できます。例えば、object-shorthand ルールによって報告された問題は、以下のように自動修正できます。

 const foo = 'str1';
 const obj = {
-   foo: foo,
+   foo,
   bar: 'str2',
 };

エディタの ESLint の拡張機能を使えば、ファイルの保存時に自動修正を適用する、なんてこともできます。

fixable なもの、fixable ではないもの

基本的に ESLint は、安全に修正できる問題にしか自動修正を提供しません。噛み砕いて言うと、コードの実行時の挙動を変えない修正方法がある問題だけが fixable になってます。

これは sort-imports ルールを触ってみると分かりやすいと思います。sort-imports は「import 文の { ... } の中がソートされているか」「from '...' で指定されたモジュール名がアルファベット順になるよう import 文がソートされているか」を検証するルールです。このうち前者は fixable ですが、import 文の順序を変えるとモジュールの評価順が変わり、実行時の挙動が変わる恐れがあるため、後者は fixable ではありません。

/*eslint sort-imports: "error"*/
import {d, c} from 'foo.js';
//      ^^^^
// `c, d` の順で書けとエラーが報告される。これは fixable。
import {a, b} from 'bar.js';
//                  ^^^^^^
//  `bar.js` は `foo.js` よりも前で import しろとエラーが報告される。
//  これは fixable ではない。

Suggestions

とはいえコードを自動修正できれば、日々のコーディングの手間を軽減できます。ときには実行時の挙動が変わってしまうことを覚悟してでも、自動修正したいこともあるでしょう。

そうしたケースのために、ESLint は「Suggestions」という機能を提供してます。

suggestions を持つ問題は、fixable な問題と同じようにコードを自動修正できますが、eslint --fix からはできません。エディタ上から手動でのみ自動修正を適用できます。

そんな問題を報告するルールあったっけ? と思っている人も多いかもしれませんが、皆さんご存知 eslint-plugin-react-hooks plugin の exhaustive-deps ルールが、まさにそれです。useEffect の第二引数の書き換えは実行時の挙動を変える恐れがあるため、この修正が suggestions として提供されてます。

exhaustive-deps ルールによる自動修正の様子。VS Code の Code Action のメニューからボタンを押すことで修正を適用する。
exhaustive-deps ルールによる自動修正の様子。VS Code の Code Action のメニューから手動で修正を適用する。

fixable な問題と違い、CLI から一括で適用するコマンドは用意されていません *1。本質的に安全ではない書き換えなので、人の目で確かめてから適用せよ、という訳です。

自動修正の候補が複数あるものも扱える

Suggestions が扱う自動修正は、なにも「実行時の挙動を変える恐れがあるもの」だけではありません。Suggestions は「問題の自動修正の候補が複数あるもの」も扱えます。

例えば no-useless-escape ルールは "\'" というコードを "'""\\'" のどちらかに修正できると suggestions を使って報告してくれます。

エディタの Code Action のメニューに自動修正の候補が複数表示される様子。

自動修正先が複数あるというの、言われてみたらたしかにあるか、という気持ちになりますが、普通そんなものが存在すると考えることもないと思います。こうしたエッジケースも ESLint でカバーされているというのは面白いですね。

そうは言っても Suggestions も一括適用したい!

「安全ではないから」「人がどれを適用するか見極める必要があるから」。そういった理由で suggestions は CLI から一括で自動修正を適用する方法が適用されていない訳ですが、そうはいっても一括で適用したい時はあるでしょう。

例えば sort-imports。import 文のソートをしない気持ちはまあ分かりますが...別にソートしたって大抵は挙動は変わりません。polyfill として読み込んでいるモジュールを先頭にすることさえ気をつければ、あとは自由に並べ替えてよいケースが多いでしょう。そうしたプロジェクトでは、挙動が変わるのを覚悟の上で一括適用する手段が欲しくなります。

...というような声を受けてか、3rd part の ESLint plugin では「安全ではない自動修正」を fixable として扱うものがあります。eslint-plugin-importorder ルールとかがそうです。

また、ESLint の Node.js API から suggestions の情報を取れるので、それを使って suggestions の一括適用ツールのようなものも作れます。というか作りました。大量の ESLint の問題を効率よく修正するeslint-interactive というツールがあるのですが、これに suggestions を一括適用するモードを実装してます。一括適用モードを起動すると suggestions をエディタ *2 でスクリプトファイルが開かれます。このスクリプトファイルでどの suggestions を適用するか、捨てるかを記述します。完成したらスクリプトファイルを保存して閉じると、一括適用が走ります (git commit する時にエディタが開いて commit message を編集できるあの機能を真似て作ってます。)。 格好良いですね。

Image from Gyazo

www.mizdra.net

余談: Biome における自動修正の分類

Biome には ESLint の Suggestions のような機能はありませんが、自動修正を安全なもの (safe fixes) と安全でないもの (unsafe fixes) に分けて扱うようになってます。safe fixes だけを適用する biome check --apply とは別に、safe fixes と unsafe fixes の両方を適用する biome check --apply-unsafe が用意されています。ESLint では安全ではない自動修正を CLI から一括適用することは (3rd-party の plugin を使わない限り) できないので、ここは Biome ならではという感じがします。

biomejs.dev

余談: Suggestions の実装の経緯

exhaustive-deps ルールが実装された当初は ESLint に Suggestions はありませんでした。しかしそれでは困るということで React 開発チームから ESLint 側に要望が出され、Suggestions が実装されたという背景があります *3。Suggestions の RFC の共著者に Dan Abramov が名を連ねているのはそのためです。

Authors: Ilya Volodin (@ilyavolodin), Dan Abramov (@gaearon) https://github.com/eslint/rfcs/blob/a6497bf1053e9a7ab30803ebbf405e9a91bafa40/designs/2019-suggestions/README.md#summary

余談: Suggestions が扱えるその他の自動修正

「実行時の挙動を変える恐れがあるもの」「問題の自動修正の候補が複数あるもの」以外にも、「自動修正はできるが、適用するかは議論が分かれる (opinionated) もの」なども suggestions で扱うのが適しています。他にも色々あるかもしれません。基本的には人が目で見て適用するか確認すべきものであれば、Suggestions で扱うべきでしょう。

まとめ

ひとえに自動修正、といっても色々あることを紹介しました。皆さんも Linter を触ったり、カスタムルールを書いたりする時に、ここで紹介したことを思い出してもらえればと思います。

以上、はてなエンジニアアドベントカレンダー2023 4日目の記事でした。 次は id:maku693 さんです。

*1:Suggestions の RFC でも、そのようなコマンドを用意しないと明に述べられています。 https://github.com/eslint/rfcs/blob/a6497bf1053e9a7ab30803ebbf405e9a91bafa40/designs/2019-suggestions/README.md#summary

*2:EDITOR 環境変数で指定しているエディタが起動します。

*3:もちろん、Suggestions 相当の機能の提案自体は昔から ESLint の issue で議論されていましたが、細かい設計や機能の形は決めきれていませんでした。そこに React 開発チームによる貢献があり、RFC が作られ、実装が開始されました。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.