🧊

OXCのFormatter、`oxc_prettier`の現状をまとめる

oxc/crates/oxc_prettier at main · oxc-project/oxc https://github.com/oxc-project/oxc/tree/main/crates/oxc_prettier

実はOXCにもあるんですよFormatter、WIPやけど。

oxlintと違ってバイナリも公開されてないし。

あらすじ

最新情報がわかるIssueはこちらに。

Rework oxc_prettier · Issue #5068 · oxc-project/oxc https://github.com/oxc-project/oxc/issues/5068

ざっと書いておくと、現状のコードは、氏が例のPrettierチャレンジの時にざっと書き上げた状態のままなので、それをなんとか形にしたいね〜という話。

これまでの流れとしては、

  • まずPrettierのコードを読んでみる
    • 読んだ結果、ものすごい労力が必要だとわかった
  • もしやPrettierの100%互換を目指すつもり?
    • 互換性は高いほど良いけど、100%である必要はない
    • コメントまわりめちゃめちゃ頑張らないと無理やし・・
  • 長い目でみたとき、独自のカスタマイズができるとなお良い
  • biome_formatterのコード、使えたりしない?
    • 調べてみたけど難しそうだったわ
  • やっぱり独自でがんばるしかないな〜

って感じ。

で、Biomeの資産が使えないことがわかった今、どうリスタートする?何から着手する?ってのを、まさに今考えてるところ。

兎にも角にも情報が欲しいので、まずは現状を把握していきたいというわけ。(書いておかないと、フルタイムでやってるわけじゃないからすぐ忘れちゃう)

Prettierとの互換性

いちおう、Prettierのsnapshotと比較してるテストがある。

https://github.com/oxc-project/oxc/tree/main/tasks/prettier_conformance

最新の値だと、JS: 39%, TS: 34%という感じ。

ただBiomeのそれと比べると、計算方法もignoreしてるファイルも違うので、なるほどな〜くらいで。 (そのうち揃えたいとは思う)

コードの構成

では本題へ。

/oxc/crates/oxc_prettier/src
├── printer
│   ├── command.rs
│   └── mod.rs
├── format
│   ├── array.rs
│   ├── arrow_function.rs
│   ├── assignment.rs
│   ├── binaryish.rs
│   ├── block.rs
│   ├── call_arguments.rs
│   ├── call_expression.rs
│   ├── class.rs
│   ├── function.rs
│   ├── function_parameters.rs
│   ├── misc.rs
│   ├── mod.rs
│   ├── module.rs
│   ├── object.rs
│   ├── property.rs
│   ├── statement.rs
│   ├── string.rs
│   ├── template_literal.rs
│   └── ternary.rs
├── comments
│   ├── mod.rs
│   └── print.rs
├── lib.rs
├── options.rs
├── doc.rs
├── macros.rs
├── needs_parens.rs
├── binaryish.rs
└── utils
    ├── document.rs
    └── mod.rs

treeしてちょっと並べ替えたものがこちら。

ファイル数だけで見るとぜんぜん少なくて、これまで見てきたのに比べたらかわいいもんよ。

lib.rs

  • Prettier structがある
  • build(parsed.program)という形式で、OXCのASTを受け取って、整形後Stringを返す
  • 各ノードのformat()が呼ばれるとき、必ず渡される
  • いわばコンテキストのような状態としても使われる
    • stackというVec<AstKind<'a>>がある
    • enter_node(kind)leave_node()によって、処理の途中で親を遡れるようになってる
  • source_textを使って、has_newline()のようなユーティリティも提供する

もう少し役割ごとに正規化できそうな感じはするけど、まあ。

options.rs

Prettier同様のオプションが定義された、PrettierOptions structがある。

特記事項なし。

comments/

mod.rsには、Commentという、PrettierやBiomeでいうDecoratedComment相当がある、が、WIPである。

というか、現状はどこでも使われてない。

print.rsにはそんなコメントをIRに変換するコード片はあるけど、中身は空っぽという感じ。

なんかのタイミングで、中途半端なコードをいったん決してなかったことにする!ってなってた記憶がある。

doc.rs

  • PrettierのDoc相当がある
    • ただPrettierが定義してるすべてが揃ってるわけではない
    • align, trim, line-suffix-boundary, labelあたりがない
    • 省略してるというより、単に未着手な雰囲気
  • DocBuilder trait
    • lib.rsにあったPrettier structが実装してる
    • 本体のASTと同じく、oxc_allocatorを使ってDocを積むために必要な諸々
    • join()というDocを結合するやつだけ、少し毛色が違う
  • print_doc_to_debug()fmt::Display
    • デバッグ用に、PrettierのDocCommandのフォーマットに表示できる

macros.rs

  • 基本的には、Prettierでいうbuilders相当
    • IRを生み出すためのmacroが定義されてる
  • しかし実態としては、macroを使わず直接Doc::で書かれてるコードも多い
  • macroである必要性は不明だが、Biomeみたいにただの関数にしてもいいのかも
    • string!は必要・・・?
  • wrap!というmacroだけは特別で、BiomeでいうFormatNodeRulefmtのような存在
#[macro_export]
macro_rules! wrap {
    ($p:ident, $self:expr, $kind:ident, $block:block) => {{
        let kind = AstKind::$kind($p.alloc($self));
        $p.enter_node(kind);
        let leading = $p.print_leading_comments(kind.span());
        let doc = $block;
        let doc = $p.wrap_parens(doc, kind);
        let trailing = $p.print_trailing_comments(kind.span());
        let doc = $p.print_comments(leading, doc, trailing);
        $p.leave_node();
        doc
    }};
}

Doc化の要というやつか。

needs_parens.rs

  • wrap!macroでも呼ばれてるwrap_parens()の実装がある
    • Prettierに対して実装してる
  • そのAstKindごとに、()が必要かどうかを判定する長大な処理
    • Prettierもこのスタイル
    • Biomeはそれぞれのノード側で定義してたので、そういう風にもできるのであろう
      • BiomeのAST/CSTだからこそ可能なのかもしれんけど不明

binaryish.rs

BinaryOperatorLogicalOperatorをまとめてBinaryishOperatorと呼んでる。

そのまま、BinaryExpressionLogicalExpressionを処理するときに使われる便利なもの。

utils/

現状、document.rsのwill_break()という関数だけが存在する。

Prettierにも同名のユーティリティがあるけど、挙動はちょっと違う・・?

format/

整形処理の本丸であるmod.rsと、その他のノード種類ごとの処理がある。

まずmod.rsから。

  • mod.rsだけで3000Lもある
  • Format traitがあり、自身のDocを返すformat()を実装する
  • 現状でも、OXC ASTとして存在するstructやenumのほぼすべてに対してFormatが実装されてる
    • TSまわりなど、漏れてるやつもある
    • Formatは実装されてるが、その中身はとりあえずってやつも多い
  • wrap!を使わず手動でenter_node()してるものもある
  • macroも使ったり使わなかったり

あとはそれ以外のファイル。

  • ファイル構成は、Prettierのlanguage-js/print配下に似てる
    • ほとんど同じだが、リテラルまわりだけ差異
    • 足りてないものもある
  • 実装はまちまち
    • Prettierをそのままコピーしてるのもあれば
    • リファクタした風になってるものもあり
    • WIPなのか簡略化されてたり

これだけコードがあって、40%にも届かんのか〜って感じ。

むしろこうなってくると気になるのは、それぞれの完成度がどれほど?ってところかな。 1つずつ見て、Prettierのコードと見比べないと、何がDONEで何がTODOかがわからない。

printer/

  • ネストを含むDocを受け取り、Stringにする
  • Prettierはネストを再起で処理してたところ、フラットなループでやってる
    • command.rsにあるCommand structでループ
    • breakモードと、インデントの情報を状態として管理
  • Docの種類ごとに、handle_xxx()する
  • お馴染みのpropagate_breaks()fits()もある

その他のローカルな変数もPrinterが抱えてるけど、ちゃんとPrettier準拠になってるように見える。

ので、これもまた完成度を測るところからかな。

まとめ

というわけで・・・、

  • 名の通り、Prettierクローンを地で行く実装
  • しかし細かいところの完成度はまちまち
    • 具体的にどこに過不足があるかも不明

というのが現状。

この先どうするかは今から考えるけど、どこまでPrettier or Biomeに寄せるのかが悩ましい。

Biomeの設計は綺麗やけど、AST/CSTの構造がぜんぜん違うので、細かい実装では参考にできないであろう懸念があるのと、普通にその内容を理解するのに時間がかかりすぎる気がする。

Prettierは、扱うASTに差がないことと、コードは良くも悪くもJSではあるけど、デバッグが圧倒的にやりやすい。

というわけで、やっぱり基本的にはPrettierに寄せつつ、リファクタの一環としてBiomeのエッセンスを取り入れる・・・みたいなバランスがいいのかな〜。