そろそろ終わらせていきたい気持ちが強くなってきたシリーズ。
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の各ノードの扱いをやる部分。
本体側との接点はこのファイルだが、実体は別のところにある。
print(astPath, options, mainPrint[, args])
ざっくりやってることは、
- 指定された
astPath
によって、そのノードをDoc化 - デコレータを各自で対処するかどうかの判定
- するならearly return
- デコレータのDoc化
()
と先頭;
の必要性の判定- 必要ならそれぞれのDocを追加して返す
という大枠を担当してる。
読み進めたいコードとしては、やはりそれぞれのノードをDoc化するprintWithoutParentheses()
というやつ。
その構文を()
で囲むべきか、不要な()
はついてないか?を判定するneedsParens(astPath, options)
も、気にはなるけどなにせこれだけで1000行以上あった。
基本的にはswitch-caseがひたすら続くけど、もちろん祖先を遡ったり、正規表現を使ったり、いろいろと泥臭い感じだった。
先頭;
をつけるべきかを判定するshouldPrintLeadingSemicolon(astPath, options)
はもう少しだけ優しかった。
printWithoutParentheses(astPath, options, mainPrint[, args])
まずは、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])
いよいよ核心へ。
処理としてはシンプルで、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()
はこんな感じだった。
- File:
print("program")
に丸投げ - Program:
printBlock()
- VariableDeclaration: いろいろやってる
- VariableDeclarator:
printVariableDeclarator()
- Identifier:
node.name
に加えて、?.
と!.
および型パラメータも - NumericLiteral:
printLiteral()
コメントは割愛しつつ、だいたい予想通りの動き。
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()
で生まれて、その下層のノードで使われる感じだった。
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でやるには限界があるよな〜と素人目に思うなどした。