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
として拡張子で指定- https://github.com/so1ve/eslint-parser-plain
- 受け取ったコード文字列を単に
{ type: "Program" }
で包んで返すだけのパーサー
- ルールの実装でその
Program
を待ち構えて、中ではprettier
のAPIを呼ぶだけ- もちろん
dprint
でもいい
- もちろん
って感じだった。
import
を並べ替えたい
さて、ここからが本題で、いわゆるsortImports
とかorganizeImports
とかそういう単語で語られるものたち。
冒頭で紹介した@stylistic/eslint-plugin
には、むしろこの機能がないのがやや驚きだった。@stylistic/jsx-sort-props
だけはある。
我らがPrettierも、デフォルトではこの機能を持たない。(というか、PrettierはポリシーとしてASTノードの削除や移動をやらない)
ゆえにこの領域は野良プラグインがたくさん存在してて、npmで調べるとこち亀の某パロディが作れるくらい無限に出てきたので、いくつかだけ抜粋。
ESLintコア
実はeslint
本体にも、sort-imports
があった。
- 2016年にリリースされてる
- 機能もコードも最小限で200行ほど、オプションも5つ
import xxx
側の名前を基準に並べ替えるタイプfrom "xxx"
側で並べ替えるのが昨今の主流らしい
- だからか、今はあまり使われてないのかもしれない
- 上述のブログにあるdeprecatedなリストには含まれてなかったけど、今はもうfrozenなルールとのこと
oxlint
でも実装されてたの知らんかった・・・!
まぁでもみんな本家で満足できるならプラグインは必要ない・・・というわけで。
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-import
のimport/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もある- Docsによると、
eslint-plugin-import
のimport/order
の拡張版とのこと - https://perfectionist.dev/rules/sort-imports#options
- Docsによると、
- チャンク(
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
をモンキーパッチするか・・・!
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つになった
- https://github.com/ianvs/prettier-plugin-sort-imports#options
- グルーピングで指定できる組み込みキーワードの種類も増えた
- グルーピングを正規表現で指定できるのに加えて、
"<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
でも使われてたやつ。
コードはここから。
挙動を読み解くに、
- 3つの動作モードがある
SortAndCombine
: 並べ替えと統合RemoveUnused
: 未使用を削除All
: どっちも
- 並べ替えルールは決められてて、オプションは7つある
さすがTypeScriptを使うだけあって、未使用import
もまとめて削除できるのが強そうではある。
ただLSの機能だからか、あまり知名度は高くない気がする。これだけやるCLIがあったら実はみんな幸せだったりするのかね・・・?
dprint
続いて、Denoで使われてるフォーマッターでもあるdprint
も見ておく。
- SWCのASTを使ってJS/TSをサポートしてる
import
の並べ替えに関するオプションもある- TSのそれと同じく、実装はとてもシンプルなもの
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という別の機能群によるサポートらしい import
とexport
の両方に対応- 当然
from "xxx"
でグルーピングしてからソートするタイプ - オプションは2つだけ
- コメントや副作用
import
の他にも、関係ないノードでもチャンクを区切る - 各グループ内の並びは設定で変更できる
- 正規表現ではなくGlobで指定、
:BLANK_LINE:
のようなキーワード指定も7つ用意されてる
- 正規表現ではなくGlobで指定、
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 type
やimport { 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