🧊

Bun Formatterのコードを読む

久々のネタがこれかい!って感じではあるけど、ぜんぶAIが悪い。

ちなみに言うまでもなく過去の“コードを読む”シリーズと同じで、ただの個人的な観測記であり他意はないのです。

bun fmtがきた

Bun on X: “In the next version of Bun bun fmt is a 320x faster Prettier alternative Passes 99% of Prettier’s test suite & supports 20 languages https://t.co/PP0fOF3VNp” / X https://x.com/bunjavascript/status/2109861465100632567

晴天の霹靂!ってほどではないけど、まさかJS/TSX以外まで出してくるとは思ってなかった。

PRはこれ。

Add bun lint and bun format by Jarred-Sumner · Pull Request #44805 · oven-sh/bun https://github.com/oven-sh/bun/pull/44805

FormatだけでなくLintも入ってるし、2230コミットで1PRですって奥さん。

スタイルとしては、Prettier 3.9.9互換を目指してるとのこと。 冒頭のポストでも99%のテストパス!って書いてあった。

そういうわけなので、基本的にほとんど全ての機能がRustで移植されてる。

ちなみにOxfmtメンテナとして書いておくと、OxfmtはPrettierを尊重こそすれ、100%一致は目指してない。 そもそもPrettierにもバグはあるし、言語内外で一貫してない挙動もあるので、そういうのを地道に是正していってるから。

CLI

おもしろい(?)と思ったのは、.prettierrcも探すし.oxfmtrcも探すところ。.oxfmrtcを検知するとOxfmt 0.72.0互換モードになって、挙動とアウトプットを変えるようになってる。 is_oxfmt()でコード検索するといっぱい出てくるけど、そこまでやるモチベーションに感心した。

ファイルの探し方は、先にwalkしてVecにしてから、ディレクトリ階層ごと並列で読んでいく、大きいファイルから先にフォーマットって感じ。

Oxfmtはstreaming方式なので、リポジトリのサイズやファイルサイズの分布によっては差が生まれそう。

各言語

OxfmtはBiome由来のIRとプリンターがあって、それを各言語で共通基盤として使ってる。

が、Bunは言語によってコードが違う。めっちゃ違う。

  • JSON: IRをそもそも作ってない(ので速い、コメントがない場合に限り)
  • TOML: oxc-toml互換の移植
  • CSS,YAML,Markdownなど: PrettierのDocとprintDocToString()の移植
  • JS,HTMLなど: Oxfmt(Biome)のIRとプリンターの移植

って感じ。

コードの重複なんかお構いなしとも言えるけど、Prettierのプリンターはその挙動にクセがあるので、互換性を優先するとなると、こういう選択になるのかもしれない、知らんけど。

ただコードの重複を厭わないがゆえに、言語ごとに異なる戦略で処理できてて、それが性能に寄与してるところも確かにある。 css-in-jsみたく戦略が違う境界では、変換層が必要になるのはトレードオフなはず。

Prettierプラグインまわり

もちろんめぼしいところは移植してある。

importのソート

なんと5種類の実装がございます!

  • @trivago/prettier-plugin-sort-imports
  • @ianvs/prettier-plugin-sort-imports
  • prettier-plugin-organize-imports
    • これらはBabel ASTでソートする実装
    • なので、bunのHIR上でそれを模倣してる(@babel/generatorの移植っぽいのまである)
    • ファイルごと書き戻して、その上で通常のフォーマット処理へ
  • OxfmtのsortImports: perfectionist/sort-imports相当
    • こっちはフォーマット処理中にやる
  • Native
    • Oxfmtの挙動に加えて、sort-named-imports相当と、同名importの統合を足した機能

なんというか、すごいね・・・。

TailwindCSS

ユニークだなと思ったのはTailwindの呼び方で、ここはJSを呼んでた。

  • Tailwindの並び替えが有効な場合、各ファイルを最初に処理したとき、そのクラス名をメモしておく
    • a.js: flex p-4, b.tsx: m-8, etc
    • このファイルは後でまた処理するリストに回る
  • すべてのファイルからクラス名を集め切ってから、まとめて並びを確認する
    • flex p-4 m-8を一括でJS呼び出しする
    • なのでCLI実行1回につき、TailwindのJS呼び出しも1回だけ
  • 改めて、並べ替えが必要だったファイルたちをこの並び順を使ってフォーマット

というわけで、JS呼び出しは1回だが、その分classがあるファイルは2回処理する戦略になってる。

Oxfmtは各ファイルごとにJSを呼ぶ必要がある代わりに、1回で済む。

その他

OxfmtのsortPackageJsonもあるし、JSDocのフォーマット機能もあった。

他にも、設定ファイルにPrettierプラグインを検知すると、Prettierが呼び出されるようにもなってる。 JSのresolveConfig()とformat()を個別に呼んでた。

ちなみにSvelteは初回リリースには入ってなかったけど、さっき見たらもう移植済みになってた。

まとめ

まあ・・・、色々と思うところはあるよね・・・。