🧊

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は言語によってコードが違う。めっちゃ違う。

  • JS/TS/Flow: ParserしたHIRを、Oxfmt由来のIRにしてから文字列に
  • GraphQL,HTML系: ASTにして、Oxfmt由来のIRにしてから文字列に
  • YAML,Markdown: Prettier由来のDocにしてprintDocToString()相当
  • CSS系: 1行でそのまま書いてみて、だめならPrettier由来のDocにするみたいな2段構え
  • JSON: コメントがない場合、IRを作らずそのまま文字列に

って感じ。

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

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

Parserについては、Bun.YAMLみたいなAPIのために、元から実装を持ってるものはそっちを使うけど、それ以外は、Prettierが依存してる各実装を丸ごと移植してきてる。つまりpostcssとかangular-estree-parserとかあらゆるものを。

JS系についてもう少し詳しく書いておくと、

  • Typecheck/Lint/Fmtは、ほぼ同時期に実装された
  • これをやるための、bun_semaというParser実装がある
  • そしてこれが、方言としてJS/TS/JSX/Flowに対応してて、HIRを出せる
  • それを使ってTypecheck/Lint/Fmtする

bun_js_parserっていう別のParserもあるが、これはbun runとかで使うみたい。 あとJSCもあるって考えると、3系統もParserがあることになるのね。

ちなみに、Flowの対応はFmtだけ。TS用のHIRに無理やり同居させて実装されてるからとのこと。

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は初回リリースには入ってなかったけど、さっき見たらもう移植済みになってた。

まとめ

力こそパワー!

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