🧊

コードフォーマッターはコードを削除することもある

いやそれはバグでは?って思うかもしれないが、実はそうとも言い切れない・・・というか、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

まあリアルワールドではそんな見かけることないと思うけど・・・。

という話でした。