🧊

`@babel/generator`の`experimental_preserveFormat`オプション

というものを見つけて、個人的に大変学びだったので。

名前こそ実験的って言ってるけど、追加されたのはもう2年も前だった。

Add experimental format-preserving mode to @babel/generator https://github.com/babel/babel/pull/16708

v7.26で追加されてたので、latest majorであるv8になっても、prefixが取れなかったということにはなる。

どういうオプションか

端的にいうと、

  1. まずコード文字列をASTにする
  2. そのASTをtransformしたりして
  3. ASTをまた文字列に戻す

この最後の段階で、できるだけ“元コード文字列に近い状態に復元するのをがんばる”よっていうオプション。

がんばらない場合

たとえば、

const    x = [ 1,  2 ];

というコードをASTにすると、基本的には、

  • constで宣言された変数で、名前はx
  • 中身は配列で、数字の12

という情報だけが残る。

なのでASTを文字列に戻すと、たいていこうなる。

const x = [1, 2];

スペースとか改行とか、意味のないものたちは消える。

コメントが絡むと自体はより複雑になる。

たとえば、

// c0
for /*c1*/ (;/*c2*/;) { /*c3*/ }

この手の実装では、コメントの所有をそれぞれASTのノードにattachする。

しかしこの場合は、

  • c0: ForStatementの前、leadingなコメントとわかる
  • c1: ForStatementの内側ではあるな?.initよりは前かな?
  • c2: .init.test.updateもないので、これもなし崩し的にForStatementの内側としか言えない?
  • c3: .bodyBlockStatementの内側だ

という分類になる。

で、問題は、c1もc2も、ForStatementの内側ってことはわかってるが、具体的な位置まではよくわからんってこと。

なのでこれを文字列に復元しようとしても、

// c0
for /*c1*/ /*c2*/ (;;) {/*c3*/}

みたいになりがち。 まぁ完全に元通りにはなってないけど、コメントは喪失してないのがポイント。

ASTをいじってコードに戻すユースケースでは、まあほぼ困ることはないと思う。 というか、コメント自体いらないってケースも多いと思う。

ただコメントが必要なケースもあり、regionコメントとかPUREコメントとか、JSDocとか、大事なものは残したい。ただこういうのは、だいたいわかりやすいleadingな位置にあることが多いので、そこまでがんばらなくてもなんとかなる。

ベストエフォートでがんばる

そこでexperimental_preserveFormatオプション。

ざっとAIで調べた限りやってることは、

  • ASTノードに、トークンの所有まで紐づける
  • トークンからline/colが引ける
  • その都度で現在位置と比較して、過不足を補う

ということをやってるらしい。

なのでさっきのforでも、コメントを出力するときに(;のトークン位置を見て、タイミングをはかってる。

ベストエフォートというのは、フォーマッターみたく綺麗に出力するものではなく、あくまで元ソースに近づけることを目的としてるのがポイントとのこと。

なのでたとえば、

const foo = 3;
const bar = 3;

この変数たちをリネームすると、

const x   = 3;
const longer=3;

みたいになるってこと。元のトークン位置を基準にしてるだけなので。

そういうわけで、

  • parse()するときには、tokens: trueと元コード文字列が必須
    • createParenthesizedExpressionsも推奨
  • generate()するときも、retainLines: trueが必須
    • これがexperimentalな所以とのこと

という条件が揃ってないと使えない。

すごいニッチな機能やなとは思うが、codemodみたいなシーンでは嬉しいのかも。

ちなみに

フォーマッターもがんばってる側、PrettierやOxfmtなど。

  • forを出力したあと、(までの間にコメントがないかソースを確認
  • ;を出力するたび、前後にコメントがないかソースを確認

みたいなことをやってる。 JS/TSXはあらゆる場所にコメントが書けてしまう言語なので、あらゆる場所でこれをやる必要がある。もちろんパフォーマンス影響は避けられない。

そのうえ、オプションによって;がなくなったり()が増えたり減ったり、挙げ句の果てには要素をソートしたいとか色々あってまじで(ry

だからフォーマッター実装としては、基本的にASTノードにくっついてる情報はアテにならない、というかアテにできない。 紐づくノードが存在しないコメントもあるし、どのみち元ソースを見ないと判断できないケースもままあるから。

コメントを書ける位置が構文として定まってる言語なら、ぜひattachしてほしいけど・・・。

そういうわけで、フォーマッターでもないのにcodegenでがんばってるのはすごい!と感銘を受けたのが、この記事を書いたきっかけ。