いやそれはバグでは?って思うかもしれないが、実はそうとも言い切れない・・・というか、PrettierもBiomeももちろんOxfmtも、既に同じようにそう動いてるし。
つまりは、特定の条件が揃った時には、コードの一部を削除しても問題ない、ということになってるってこと。少なくとも、コードの挙動には影響を与えないなら。
あらすじ
そもそもコードフォーマッターは、コードの挙動を変えることはない、と思うのがまあ一般的な理解かと。
つまりは、改行を足したり消したり、スペースを空けたり詰めたりはしても、ASTに現れるような具体的なコード片にはノータッチなんでは?という認識だった。
つまり、
- コードをASTにする
- ここでいったんスナップショット(BF)を保存
- ASTをフォーマッターに渡してフォーマット
- そのコードを再びASTにする
- ここでまたスナップショット(AF)を保存
そして、BFとAFでスナップショットを比較すれば、フォーマッターが実装ミスやら漏れで大事なコードを消し去ってないことを保証できるのでは?と思ってた。
が、実際にこの仕組みを実装してみると、めちゃめちゃASTの差分が出るやんけ!となった。
たとえば?
いちばん簡単な例をあげるとこういうの。
// BEFORE
const a = 1;
; // <-
const b = 2;
// AFTER
const a = 1;
// <-
const b = 2;
;に対応するASTである、EmptyStatementがなくなる。(取り残されるコメントさん・・・)
挙動という意味では、フォーマット時に消しちゃっても問題ないと直感的にわかるけど、ASTの構造が変わることまで意識してなかった。
ほかにも、オブジェクトのキーをクオートで囲うかどうか?っていうオプションがあるけど、それによってもASTの構造が変わる。
// BEFORE
const obj = { "key": 1 };
// AFTER
const obj = { key: 1 };
キー"key"が、文字列Literalだったのが、フォーマットでクオートが外されると、keyという名前のIdentifierに変わっちゃう。
そしてこれは、TSの型リテラルとか、classのメソッドとかプロパティとか、いたるところで発生する。
TSの型も、冗長な記述の場合はラッパーのノードが消える。
// BEFORE
type T1 = | A;
type T2 = & A;
// AFTER
type T1 = A;
type T2 = A;
というようなのが、他にもたくさんある・・・!という話。
JSXText
JSXにおいては、空白や改行がカジュアルに消える。
// BEFORE: JSXText(\n ) + JSXExpressionContainer + JSXText(\n)
<div>
{children}
</div>
// AFTER: JSXExpressionContainer
<div>{children}</div>
// BEFORE: JSXExpressionContainer+JSXText( ) x N
<div>{" "}{' '}</div>
// AFTER: JSXText( )
<div> </div>
このへん、CSSとして微妙に挙動が変わるので個人的には納得いってない。
// BEFORE
<p>abc : def </p>
// AFTER
<p>abc : def </p>
そんなとこまで消すんかい!ってなった。
コメント
空白や改行関連でいうと、コメントの中もフォーマットされる。がんばってる・・・。
// BEFORE
/* These trailing ->
* and also ->
* <- this marker */
// AFTER
/* These trailing ->
* and also ->
* <- this marker */
なんなら、行コメントにいたってはマージされちゃうこともある。
// BEFORE
for (x
in //a
y); //b
// AFTER
for (x in y); //a //b
まあそんな場所に書くのが悪いという話かもしれんけど・・・。
括弧
みんな大好きParenthesizedExpressionももちろん関係してくる。
そもそも、フォーマッターを通すと不要な括弧はだいたい全部消されちゃう。言われてみれば、めっちゃ消えてる。
その方が読みやすかろうと思ってわざわざつけてても、容赦無く消される。フォーマッターにコード消されてる!
// BEFORE
const total = (100 * numOfApples) + (80 * numOfBananas);
// AFTER
const total = 100 * numOfApples + 80 * numOfBananas;
(個人的にはこれが一番不満かもしれない。そっとしておいて欲しい。)
というように、他にも括弧がなくなることでASTの構造が変わるケースがいろいろある。
// BEFORE: SequenceExpression: 3
(a, (b, c), (d, e))
// AFTER: SequenceExpression: 1
(a, b, c, d, e)
// BEFORE: ChainExpression: 2
(a?.b)?.().c
// AFTER: ChainExpression: 1, CallExpression(computed<a.b>) -> CallExpression(b)
a?.b?.().c
まあリアルワールドではそんな見かけることないと思うけど・・・。
という話でした。