定期報告です。(義務はないけどなんとなく)
前回までのあらすじ
OXCのFormatter、
oxc_prettierの現状をまとめる | Memory ice cubes https://leaysgur.github.io/posts/2024/11/21/144412/
ざっとまとめると、
- 可能ならBiomeのコードを再利用できないか調べてた
- しかしどうやら難しそうで諦めた
oxc_prettierのソースコードの現況について把握した
というところまで。
Biomeに別れを告げて、独自路線でPrettierをクローンしていくために、さて何から取り掛かるべきか・・・?と。
今やってること
兎にも角にもゴールを見据えないと路頭に迷うので、
- まずはPrettier側の構成ファイルとその関数を洗い出し
oxc_prettier側の構成ファイルとその関数を洗い出し- ファイルと関数が存在するか、またその進捗はどれくらいか読み合わせる
ということをやっていて、その進捗がこのIssueコメントにある長大な表。
https://github.com/oxc-project/oxc/issues/5068#issuecomment-2507272735
Prettierでいうlanguage-js/print/estree.jsの、switch文でASTノードのtypeに応じて処理を分岐する部分は、OXCでは各ASTノードのstructやenumに対して、Format traitを実装する形になってる。
ここは基本的にprint_xxx()という関数に切り出されてるものを呼ぶだけになってるので、そこまで難しくはなく、こっちはだいたい完了してる。
いくつかのノードはもう少し調整の余地があるのと、ParenthesizedExpressionだけは根っこからの改修が必要。
ParenthesizedExpression
これは(と)に囲われた部分に相当するノードで、なんとESTreeには存在してないやつ。
それだと困るのでは?と思った人は正解で、JSDoc TSの@typeキャストなんかでは必須の存在だが、純ESTreeのASTでは存在しないことになる(消される)ので、整形しただけのはずなコードが普通に壊れる(というか型エラーが出る)。
Prettierも例外ではなくて、たとえばBabelをパーサーに指定してないと困ることになる。
incorrect formatting of JSDoc type assertion by JS parsers other than
babel-*· Issue #8171 · prettier/prettier https://github.com/prettier/prettier/issues/8171
oxc_prettierでも、現状はoxc_parserを介してAST化するときにpreserve_parens: falseを指定し、()を消し去る前提のコードになっちゃってるのが現状。
なのでこれを、
- まず
preserve_parens: trueでAST化するようにして - その上で不要な
()だけを消し去る下処理をする
ようにしないといけなくて、まぁまぁ大変そう。
続・今やってること
ASTノードにFormatを実装するほかには、そこから呼ばれるprint_xxx()も必要。
現状のコードは、PrettierではprintXxx()に切り出されてるものが、
- 同じように
print_xxx()に切り出されてることもあれば Formatにインラインで書かれてたり- そのどちらでもある(一部はインラインで一部は切り出し)
みたいにバラバラになってる。
なのでこれを、
- 基本的にはPrettierに倣って切り出す
- けど、OXCのASTノードの構造的に、それが手間だったり不要だったりするものもあるので吟味
- カバレッジが落ちないようリファクタ
ということをやってて、これもなかなかに大変なのよ〜。
PrettierのコードはESTreeだけでなくBabelやらFlowやらいろんなものを混ぜて扱ってるけど、JSであるがゆえに、カジュアルに.xxxがあれば処理するみたいな書き方が可能になってて、これをOXCの詳細なASTとガチガチRustにするのがとにかくしんどい。
Prettierのコードも、まず一見しただけではコード中に登場するノード名がどのパーサー由来のものかわからんし、そのプロパティ名も出自不明。
コードコメントにWhyが丁寧に書かれてるわけでもないので、OXCのASTでいうと、これはどういう記述にすれば・・・?ってなる。
また、いわゆる関数呼び出しであるCallExpressionと、動的importのImportExpressionみたく、一見はよく似てるノードも、OXC ASTでは厳密に分けられてて、構成要素が別の型だったりするものもある。
Prettierでは、printCallExpression()でまとめて扱われてるけど、OXCでそれをやろうとするとenumにまとめてほぼすべてのプロパティに対するアクセサを定義して・・・とかになる。
そういうわけで大変なこの作業を、一通りすべてのファイルで少しずつ改善してくということをやってる。 どのファイルもデカくて複雑なので、これは相当な時間を要する予感。
Docを比較したい
Prettierの実装知識ではあるけど、整形前のコードはASTに変換されたあと、Docという中間構造に変換されて、それが整形後のコードになる。
つまり、Docになった時点で、どのように整形されるかはもう決まってる。(もちろんprintWidthみたいなオプションには影響されるけど)
なのでASTノードを辿ってDocを作っていく過程で、Prettierとの齟齬があれば、それは直接的にカバレッジに響いてくる。
カバレッジを取るスクリプトでも、整形後コードに対するdiffは見れるようになってるけど、もう少しデバッグしやすくしたい・・・。
というわけで、両者のDocを比較できるツールを作った。
- ローカルで開発中のOXCのリポジトリを前提に
oxc_prettierをWASMにビルドして- Prettierのstandalone実装と一緒にdiff
ということをしていて、
- DocAST:
{ type: "group", contents: ["foo"], { break: false } }みたいなJSON - Doc:
group(["foo"])の状態で、Prettier.__debug.formatDoc(docAst)したもの - Formatted: 整形後コード
という3つを比較できるようにした。
まあまあ便利ではあるけど、たぶん慣れてくると不要になりそう。だいたいあそこでしょ?みたいな勘が働くと思うので・・・。
ただ、これでどこに実装差分があるかはわかるようになったけど、なんでその差分が発生してるのか、コードのどこが間違ってるのか足りてないのかは、今までどおり地道にデバッグするしかない。
また、このDocというやつは、必ずしも一致してる必要はないらしく、特定の条件下であれば、異なるDoc構造でも同じ整形後コードになるみたいなシーンが色々あって、とても塩梅が悩ましい。そのくせ1つ違うだけで大きく結果が変わったりもする。
そのあと
print_xxx()まわりを精査しても、まだまだ全体で見ると道半ば。
このあとは、
- ParenthesizedExpressionの件
- ASIのセミコロンまわり
- needsParens
- コメント(これが一番でかい)
- 散らばってるutils系の整理と
- 責務過多になってるメインの
pインスタンスの整理 - Docの文字列化部分の検証
- etc…
JSXもTypeScriptも丸ごと残ってるし、たぶん他にもまだ把握できていないパーツはありそう。
まとめ
できれば他の人でもできるようにタスクを分けたいけど、今時点ではいろんなものが直列に影響しあうようになってるせいで、うまくタスクを切り出せる状況ではない。
また、タスクにできたとしても、
- OXC ASTの構成を把握して、ESTree/Babelのノードに翻訳しながら
- Prettierのコードをリファクタし、不要なコードを消して、自分用にコメント書いておき
- その中から、関係のあった部分だけを丁寧に移植してくる
この作業の難易度が高いので、あまりやりたい人はおらんやろうな〜という一抹の不安もある。
JSで書かれてるせいでコードが読み解けなくて困ってるけど、DevToolsでブレークポイントが貼れるおかげで、なんとかぎりぎり生きながらえてる感じ。
そんな片手間コントリビューターの横では、本家VoidZeroチームがOXC ASTのESTree化を圧倒的スピードで終わらせてて、お口ポカーンってなってるのが最近のハイライト。