🧊

JS/TSで`import`をソートできる選択肢のまとめ

formatter: built-in sorting and aesthetically features · Issue #13610 · oxc-project/oxc https://github.com/oxc-project/oxc/issues/13610

これを検討するにあたり、まずは市場調査を・・・ということで。

どういう関心が世にあって、どういう先行実装があってどう違ってて・・・みたいなことをざっくり知りたかった。

(不備や有力情報があったらぜひ教えてください。)

最初におまけ: Stylisticなツール

最初に思い当たるのは、やはりeslint-stylisticかな?

eslint-stylistic/eslint-stylistic: Monorepo for ESLint Stylistic plugins and configs https://github.com/eslint-stylistic/eslint-stylistic

ESLintが、コアでメンテするルールからstylisticなものを外す!という方針になって注目されたやつ。

Deprecation of formatting rules - ESLint - Pluggable JavaScript Linter https://eslint.org/blog/2023/10/deprecating-formatting-rules/

ドキュメントによると、なんと95もルールがあって、あらゆるスタイルを強制できる。 実装としてはESLintのFixerを使っていて、ASTを参照しつつコードをがんばって切り貼り修正してる。仕組み上、それぞれのルールは独立して動くと思うので、パフォーマンスはあまりよくないと思われる。

Stylisticな問題・・・つまりはフォーマッターってところでは、Prettierも忘れちゃいけない。

prettier/prettier: Prettier is an opinionated code formatter. https://github.com/prettier/prettier/

しかし、eslint-stylisticも引き続き支持されてる理由としては、PrettierのprintWidthが万人受けしてないことや、挙動がopinionatedすぎるというものが多そう。

その点、どれだけ使われてるかは知らんけど、Prettierよりも細かくスタイル指定ができる点が人気とのこと。

Why? | ESLint Stylistic https://eslint.style/guide/why

当時はこの記事もよく見た記憶がある。

Why I don’t use Prettier https://antfu.me/posts/why-not-prettier

とはいえ、JS/TS以外の言語については、世間もPrettierを広く受け入れてるようで、そこは興味深いところ。

あくまでESLint上で、JS/TS以外の言語向けにPrettierを動かすESLintプラグインがあるくらいには。

antfu/eslint-plugin-format: Format various languages with formatters in ESLint https://github.com/antfu/eslint-plugin-format/

仕組みとしては、

  • eslint-parser-plainをカスタムなparserとして拡張子で指定
  • ルールの実装でそのProgramを待ち構えて、中ではprettierのAPIを呼ぶだけ
    • もちろんdprintでもいい

って感じだった。

importを並べ替えたい

さて、ここからが本題で、いわゆるsortImportsとかorganizeImportsとかそういう単語で語られるものたち。

冒頭で紹介した@stylistic/eslint-pluginには、むしろこの機能がないのがやや驚きだった。@stylistic/jsx-sort-propsだけはある。

我らがPrettierも、デフォルトではこの機能を持たない。(というか、PrettierはポリシーとしてASTノードの削除や移動をやらない)

ゆえにこの領域は野良プラグインがたくさん存在してて、npmで調べるとこち亀の某パロディが作れるくらい無限に出てきたので、いくつかだけ抜粋。

ESLintコア

実はeslint本体にも、sort-importsがあった。

まぁでもみんな本家で満足できるならプラグインは必要ない・・・というわけで。

ESLintプラグイン

改めて有名なプラグインだとこのあたり。

eslint-plugin-import

eslint-plugin-import/docs/rules/order.md at main · import-js/eslint-plugin-import https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md

  • import関連のプラグインといえば?な代表格
  • ルールは他にもたくさんあるけど、その中のimport/orderで並びの指定ができる
  • 11ものオプションがあって、require()にも対応してる
  • 並び替えはfrom "xxx"側を見て、組み込みのキーワード指定で行う
    • ["builtin", ["sibling", "parent"], "type"]みたいな
    • このキーワードを拡張(追加ではなく)するオプションもある
    • この分類の間に空行を入れるか?などもオプション
  • 副作用importが変な位置にあったら警告するオプションなんかもある
    • そこはLinterなので

後続の実装たちからたびたび引用されてるのを見るに、先駆け的な実装なのであろう。

https://github.com/un-ts/eslint-plugin-import-x というfork版もあるそう。

eslint-plugin-simple-import-sort

lydell/eslint-plugin-simple-import-sort: Easy autofixable import sorting. https://github.com/lydell/eslint-plugin-simple-import-sort

  • importの並べ替えに特化したプラグインで、名前の通りシンプル
    • simple-import-sort/imports
  • exportを並べ替えるsimple-import-sort/exportsもある
  • こちらもfrom "xxx"を基準にグルーピングして並べ替えるのが特徴
  • 副作用importでチャンク(import群)を分ける
  • オプションもシンプルに1つだけ
    • https://github.com/lydell/eslint-plugin-simple-import-sort#custom-grouping
    • どういうグループ分けにするか?を決める正規表現の配列のみ
    • 組み込みのキーワードなどもなく、空行を入れるなら配列のネストを深くするといった徹底具合
    • 副作用import"^\\u0000"で、import type"^.+\\u0000$"でキャプチャする・・・
  • 各グループ内の並びは..を見て遠いものから近いものへ、その他はIntl.Collatorのみ
  • このシンプルさに不満があるなら、eslint-plugin-importimport/orderを使えとのこと
  • とはいえ、eslint-plugin-importではサポートされてない機能やバグ対応もいろいろ入ってる

すごくシュッとしてるし、実はこれくらい割り切ってもいいのかもしれない・・・?個人的には好き。

eslint-plugin-perfectionist

azat-io/eslint-plugin-perfectionist: ☂️ ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc. https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/docs/content/rules/sort-imports.mdx

  • importに限らず、ありとあらゆるものを並べ替えたい完璧主義者用プラグインときた
  • その中に、perfectionist/sort-importsがあって、さすがオプションもいっぱいでなんと17もある
  • チャンク(import群)の処理単位を決めるのもオプション
    • コメント、空行、副作用import
  • 基本的には他同様、特別なキーワードでfrom "xxx"を分類してからソートする
  • グループ分けは、SelectorとModifier(s)という概念で細かく指定できる
    • import type { FC } from 'react'という物に対して、優先度に応じて複数の記述が可能ということ
    • named(M)-type(S)とか、named(M)-type(M)-import(S)とか、単にexternal(S)とか
  • グループ分けの定義において、キーワードと正規表現やGlobを混ぜることはしない
    • 代わりに、キーワードを自作させる方針
  • 各グループ内での並びまでカスタマイズできるのも、他にはない特徴
    • その他のperfectionist/*ルールと同じで、行の長さによってソートできたり
    • なんとカスタムしたアルファベットの並びで並べ替えできたりもする
    • パス文字列中の_/を無視するようにもできる
  • 特定のグループのみASCではなくDESCにするとか、compare結果がイコールだったときのfallbackロジックまで指定できる
  • import/orderから引き継いで、require()なんかにも対応してる

いや〜、これはすごい。今までみた実装の中でもっとも柔軟な指定ができる。 ただ、これだけカスタマイズしてる人類はこの世にどれほどいるん・・・とも思う。

Prettierプラグイン

続いて、Prettierのプラグイン。

ただそもそもPrettierのプラグイン機構は、サポート外の言語に対応するためのもので、JS/TSのように1stサポートな言語を拡張する用途ではない、という認識。 なので、JS/TSの特定のAST部分だけを拡張したい・・・ということができない。

そういうわけなので、Prettier本体がコード文字列をパースしてASTにしてIRを作ってコード文字列に戻して・・というメイン工程の前後で、オーバーヘッド覚悟で改めて同じようにASTをいじるステップを踏むしか方法がない。

もしくは、preprocessをモンキーパッチするか・・・!

https://github.com/ArnaudBarre/prettier-plugin-sort-imports/blob/c4dc65ae7b2a3b3750fefe7ea3dbd9f0edf2319e/src/index.ts#L14

Prettierプラグインのデザイン制約からか、ESLintに比べるとそんなに種類はなさそうに見える。

prettier-plugin-sort-imports

IanVS/prettier-plugin-sort-imports: An opinionated but flexible prettier plugin to sort import statements https://github.com/ianvs/prettier-plugin-sort-imports

  • @trivago/prettier-plugin-sort-importsのfork版
    • 検索すると最初に出てくるスター数も多いのはこっちのTrivago版ではあるけど
  • 基本的な挙動は引き継ぎつつ、コメント処理も頑張ってるとのこと
    • from "xxx"でグルーピングするタイプ
  • 副作用importとignoreコメントでチャンクを区切る
  • オプションも改良されてて、3つになった
  • グルーピングを正規表現で指定できるのに加えて、"<THIRD_PARTY_MODULES>"のように組み込みの指定も可能
    • "<TYPES>^(node:)"のように、これらを混ぜて表現することもできる
  • なんと、同一のソースからのimportをまとめるorganize機能もある
    • Prettierの禁忌を破ってるよと注釈されてたけど

最近リリースされた@prettier/plugin-oxcにも対応してて推せるなと思った。

prettier-plugin-organize-imports

simonhaenisch/prettier-plugin-organize-imports: Make Prettier organize your imports using the TypeScript language service API. https://github.com/simonhaenisch/prettier-plugin-organize-imports

  • 他のプラグインと決定的に違うのは、TypeScriptのLSのorganizeImportsに丸投げしているところ
    • これの詳細は後述
  • なのでオプションもない(!)

発想の勝利やこれは。

TypeScript LS: organizeImports

VSCodeユーザーでもないので、最近まで存在すら知らんかったけど、prettier-plugin-organize-importsでも使われてたやつ。

コードはここから。

https://github.com/microsoft/TypeScript/blob/e9bcbe6ef706e0b5a34678964988eb6a9cd86cc6/src/services/organizeImports.ts#L76

挙動を読み解くに、

さすがTypeScriptを使うだけあって、未使用importもまとめて削除できるのが強そうではある。

ただLSの機能だからか、あまり知名度は高くない気がする。これだけやるCLIがあったら実はみんな幸せだったりするのかね・・・?

dprint

続いて、Denoで使われてるフォーマッターでもあるdprintも見ておく。

dprintも、何気にオプションがいっぱいあるflexibleなフォーマッターだと学んだ。

Configuration - TypeScript / JavaScript - dprint - Code Formatter https://dprint.dev/plugins/typescript/config/

Biome assist: organizeImports

最後は大御所のBiomeについて。

Docsを検索すると、organizeImportsについて記載があった。

organizeImports | Biome https://biomejs.dev/assist/actions/organize-imports/

  • どうやらbiome format|lintではなく、biome checkから使えるAssistという別の機能群によるサポートらしい
  • importexportの両方に対応
  • 当然from "xxx"でグルーピングしてからソートするタイプ
  • オプションは2つだけ
  • コメントや副作用importの他にも、関係ないノードでもチャンクを区切る
  • 各グループ内の並びは設定で変更できる
    • 正規表現ではなくGlobで指定、:BLANK_LINE:のようなキーワード指定も7つ用意されてる
  • perfectionist/sort-importsみたいに、このキーワードを拡張できるようにする案も出てる
  • 単なるsortではなくorganizeImportsなので、同一ソースを統合する機能もある

コードはこのあたりから。

https://github.com/biomejs/biome/blob/3f06e19c6eb8476ad9de4e3dac00c50a2d6f0aed/crates/biome_js_analyze/src/assist/source/organize_imports.rs https://github.com/biomejs/biome/tree/3f06e19c6eb8476ad9de4e3dac00c50a2d6f0aed/crates/biome_rule_options/src/organize_imports

v2のリリースにあたり再実装されただけあって、既存のツールたちの中でもいいバランスを見出してる印象。

さすが長大なRFCを経てるだけのことはあるなあと思った。

Import sorter revamping · biomejs/biome · Discussion #3015 https://github.com/biomejs/biome/discussions/3015

perfectionist/sort-importsほどの柔軟性はないけど、それで十分という判断なのかな?このRFCでも言及されてないみたい。

しかしこれだけ備えても、まだまだIssueやDisucussionは立つらしくて震える。

https://github.com/biomejs/biome/discussions?discussions_q=is%3Aopen+organizeImports

まとめ

というわけで・・・、ことimport宣言を並べ替える機能に限っていえば、だいたいこれらの判断軸がありそうだとわかった。

  • まず、どういうチャンク(複数のimportの塊)で処理するかの判断
    • コメントや空行、副作用import、それ以外の要素があったら、別のチャンクにするとか
    • これらの指定をユーザーに委ねるのか、実装側でコントロールするのか
  • そのチャンクのそれぞれを、どういうルールで並べ替えるのか
    • from "xxx"部か、import { xxx }部か
    • 前者のほうが受け入れられてそうで、後者はESLint本家くらい
  • グループ指定をユーザーが設定できるようにするか
    • その場合は、組み込みキーワード指定だけか、任意の正規表現やGlobだけか、または混在できるか
    • それはどういう記法にするか
  • どういうロジックで並べ替えするか
    • 大文字小文字を区別するか、localeCompare()のような機能が必要か
  • import typeimport { type }もサポートするか
  • require()や、import X =もサポートするのか
  • 副作用import "xxx"をどう扱うか
  • 並べ替え時に、コメントをうまく処理できるか

ここから少し発展して、

  • 名前付きimport { specifier1, specifier2 }部も並べ替えるのか
    • asがあったらどっちを基準にするのか
  • with { type, metadata }も並べ替えるのか
  • sort以上のことをやるか
    • 未使用コードを削除するか
    • 同一sourceをマージするか
  • tsconfig.jsonなどを読んで、パスエイリアスを解決して処理するか
  • export宣言もサポートするか

というあたりかな。

さすが10年の歴史がある分野だけあって、奥が深かった〜。

調査の過程で生まれたClaude君によるまとまってるようでまとまってないレポートも置いておく。

https://gist.github.com/leaysgur/5a4b94ae62b934d91233744ccc3c4c67