🧊

Prettier のコードを読む Part.8

そろそろ終わらせていきたい気持ちが強くなってきたシリーズ。

Prettier のコードを読む Part.7 | Memory ice cubes https://leaysgur.github.io/posts/2024/09/27/174320/

おさらい

  • 元コード文字列をASTにして
  • ASTをDoc(IR)にして
  • Doc(IR)を整形後文字列にする

この流れの2つ目、Doc化の工程を読んでたところ。

JS/TSのコードはestreeのプリンターでDoc化されるようになってて、各ASTノードと同じく、コメントもDoc化されてるというのを前回で読んだ。

今回は、本丸のASTの各ノードの扱いをやる部分。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/printer.js

本体側との接点はこのファイルだが、実体は別のところにある。

print(astPath, options, mainPrint[, args])

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/index.js#L63

ざっくりやってることは、

  • 指定されたastPathによって、そのノードをDoc化
  • デコレータを各自で対処するかどうかの判定
    • するならearly return
  • デコレータのDoc化
  • ()と先頭;の必要性の判定
    • 必要ならそれぞれのDocを追加して返す

という大枠を担当してる。

読み進めたいコードとしては、やはりそれぞれのノードをDoc化するprintWithoutParentheses()というやつ。

その構文を()で囲むべきか、不要な()はついてないか?を判定するneedsParens(astPath, options)も、気にはなるけどなにせこれだけで1000行以上あった。 基本的にはswitch-caseがひたすら続くけど、もちろん祖先を遡ったり、正規表現を使ったり、いろいろと泥臭い感じだった。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/needs-parens.js#L29

先頭;をつけるべきかを判定するshouldPrintLeadingSemicolon(astPath, options)はもう少しだけ優しかった。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/semicolon.js#L9

printWithoutParentheses(astPath, options, mainPrint[, args])

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/index.js#L21

まずは、prettier-ignoreやらでスキップしていいかの判定をしてる。

そのあと、JS/TSが書かれるであろう各言語のプリンターを順に呼び、Docが返ってきたらそれを使う。

抜粋するとこんなやつ。

for (const printer of [
  printAngular,
  printJsx,
  printFlow,
  printTypescript,
  printEstree,
]) {
  const doc = printer(path, options, print, args);
  if (doc !== undefined) {
    return doc;
  }
}

なんか効率悪そう・・って思ったけど、それぞれノード名を見てearly returnするようになってた。

printEstree(astPath, options, mainPrint[, args])

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/estree.js#L88

いよいよ核心へ。

処理としてはシンプルで、node.typeを見てひたすらprintXxx(astPath, options, mainPrint)していく。 printEstree()は最後の砦だったはずなので、どのnode.typeにも合致しないやつが現れたら、それはバグということでthrowしてる。

ただここで使われてるprintXxx()シリーズは、だいたい40ファイルくらいあるので、個別に見ていくのは諦める。

気休めにwc -l | sortした結果だけ置いておく。

  23 expression-statement.js
  30 cast-expression.js
  38 html-binding.js
  44 statement.js
  47 comment.js
  67 member.js
  68 interface.js
  92 mapped-type.js
  94 decorators.js
  94 literal.js
  97 enum.js
 103 index.js
 108 block.js
 114 hook.js
 117 angular.js
 132 call-expression.js
 135 semicolon.js
 138 misc.js
 142 component.js
 197 property.js
 199 type-parameters.js
 237 array.js
 248 object.js
 291 template-literal.js
 300 function-parameters.js
 317 flow.js
 329 arrow-function.js
 331 function.js
 352 binaryish.js
 378 ternary-old.js
 386 module.js
 391 typescript.js
 392 class.js
 396 call-arguments.js
 427 member-chain.js
 431 ternary.js
 475 assignment.js
 597 type-annotation.js
 650 estree.js
 860 jsx.js
9867 total

この行数だけだと大したことないようにも見えるけど、なにせ取り巻きのutilsが1000行越えなので・・・。

ともあれこれが再帰で処理されていって、最終的に1つのDocとなることで、この工程が完了するというわけ。

最低限のコード

このシリーズおなじみ、この最小コードを整形する部分だけ見ておく。

  // 1
let a
 = 42; // 2

node.typeをログに出してみると、こういう並びで、それぞれのprintXxx()はこんな感じだった。

コメントは割愛しつつ、だいたい予想通りの動き。

Babel ASTにも直接対応してるからか、特別対応がちょいちょいあった。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/literal.js#L13 https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/class.js#L362

こうなってくるとやはり気になるのは、AstPathと再帰処理だからこそ実現できてる部分はないか?ってところかな・・。

args引数

printXxx()mainPrint()の再帰の間で引きまわされてるやつ。

コードをgrepした感じだと、

  • assignmentLayout: string
    • "break-after-operator"
    • "never-break-after-operator"
    • "fluid"
    • "break-lhs"
    • "chain"
    • "chain-tail"
    • "chain-tail-arrow-chain"
    • "only-left"
  • expandFirstArg: boolean
  • expandLastArg: boolean

これらを含むオブジェクトになってるけど、常にいずれか1つのみが利用されるらしい。

それなりのnode.typeで観測されるけど、ただ引きまわされてる過程という場合もありそうだった。

args.assignmentLayout

assignmentLayoutは、printAssignment()で生まれて、その下層のノードで使われる感じだった。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/assignment.js#L102

assignmentつまり、代入がどのように書かれてたかを細かく分類してる。

  • printAssignment()
  • printTernaly()
  • printArrowFunction()

これらで参照される。

args.expand(First|Last)Arg

printCallArguments()で生まれる。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/call-arguments.js#L114 https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/call-arguments.js#L149

そして、

  • printFunction()
  • printArrowFunction()

この2つで参照される。

いずれのオプションも、思ってたよりスコープが狭くて安心した。

ここまでのまとめ

想像してた通り、ASTのDoc化の工程は、Prettierがopinionatedなformatterであることを体現する部分。

コードを読み解くも何も、これは歴史書なので、必要に応じて参照するしかないなと思った。

そういう意味では、ASTにコメント紐づける工程のほうが難易度は高かったと思うし、やっぱりASTでやるには限界があるよな〜と素人目に思うなどした。