あと一息。
Prettier のコードを読む Part 8 | Memory ice cubes https://leaysgur.github.io/posts/2024/10/04/094401/
ASTをDocにする工程の続き。
おさらい
- コード文字列をASTに変換し
- ASTを微調整しながら
- コメントを各ノードに紐付けて
- そのASTの各ノードとコメントを、Docという中間表現に
するところまで読んできたので、あとはこのDocを整形後文字列にしていくだけ!
coreFormat(text, options[, addAlignmentSize = 0])
の続き
というわけで、Doc化の直後から。
addAlignmentSize
が0以上ならその反映printDocToString()
でDocの文字列化addAlignmentSize
でインデントの調整- カーソル位置の復元
addAlignmentSize
に任意の値が入ってくるのは、formatRange()
経由で呼ばれたときだけなのでとりあえず無視。
Docを文字列にするprintDocToString(doc, options)
というズバリなやつを見ていく。
printDocToString(doc, options)
いざdocument
ディレクトリへ。
この処理の基本的な流れは、
propagateBreaks(doc)
してcmds: Command[]
に積まれたDocをひたすら文字列にoptions.printWidth
などのオプションを反映しながら
out: string[]
に処理された文字列が積まれていくので、最後にjoin("")
という感じ。
処理されるべきDocのコマンドに応じたswitch-caseがずっと続いてる。
propagateBreaks(doc)
traverseDoc(doc, onEnter, onExit[, shouldTraverseConditionalGroups = false])
というもので、Docを走査しながら動的に更新してるぽい。
break-parent
group
どうやらこの2種類に対してbreakParentGroup(groupStack)
を実行していて、条件に合致するもののdoc.break
を"propagated"
にするらしい。
ただこの"propagated"
という文字列、実際の処理ではboolean
相当としてしか見られてない模様。(デバッグ用のAPIでだけ、文字列として比較してる)
なんしかこの処理の意味としては、ここに書いてあるように、
Breaks are propagated to all parent groups, so if a deeply nested expression has a hard break, everything will break. https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/commands.md?plain=1#L27
ということなんやろう。
doc.break
を判定に使ってるコードは各所にあった。
- document/utils.js
- document/printer.js
{ ind, mode, doc } = cmds.pop()
ループの最初に展開されてる重要な変数と見られるものたち。
珍しくJSDocによってCommand
という型がついてると思ったけど、やっぱり中身はこう。
type Command = {
doc: any;
ind: any;
mode: Symbol("MODE_BREAK") | Symbol("MODE_FLAT");
};
doc
は今まで見てきたDocであり、いろんな種類があるのでany
にしちゃってるんでしょう。
Command.ind
とは
ind
はどうやらインデントに関する状態のことで、
rootIndent()
makeIndent(ind, options)
makeAlign(indent, widthOrDoc, options)
という3つの関数から生まれるようだった。
あれこれ試して導き出した結果としては、こういう型になってた。
type Ind = {
value: string;
length: number;
queue: Array<
| { type: "indent" }
| { type: "numberAlign"; n: number }
| { type: "stringAlign"; n: string }
>;
root?: Ind;
};
まぁ、詳細はさっぱりわからない。
Command.mode
とは
Symbol
で定義されてて、BREAK
モードかFLAT
モードかという状態を引き回してる。
これによって変わってくるのは、
if-break
indent-if-break
このあたりの処理。
たぶん、基本的にはDocで指示されたように配置しようとするけど、最終的にどうなるかはもちろん過程で決まるのであり、そのための変数って感じかな。
pos
とprintWidth
行あたりの最大文字数を決めるoptions.printWidth
って、どうやって計算してるんやろ?って思ったので。
ループ前にpos = 0
という変数が初期化されてて、処理ごとに足したり引いたりされてた。
そして必要なときにwidth - pos
するなどして、残りを気にしながら整形してる模様。
https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/printer.js#L385 https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/printer.js#L462
breakしたらまた0に戻る、と。
fits()
詳細は定かではないけど、
- 1行に並べたい要素があったとき
- いま残ってる余白に対して
- それらはフィットする?
っていうのを調べてる。
true
が返ってこなかったら、breakするようにして次へ行く・・・みたいな使い方。
またも最小コードで
またもおなじみのこのコード。
// 1
let a = 42; // 2
これを--no-semi --debug-print-doc
でDocコマンドとして表示した結果がこう。
[
"// 1",
hardline,
group([
"let",
" ",
group([
group("a"),
" =",
" ",
"42",
]),
indent([]),
]),
lineSuffix([" ", "// 2"]),
breakParent,
hardline,
];
VariableDeclaration
がlet
を囲むgroup()
と直下のindent()
をVariableDeclarator
がa = 42
を囲むgroup()
を- https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/language-js/print/assignment.js#L32
printAssignment()
のlayout
はnever-break-after-operator
printTrailingComment()
がlineSuffix()
とbreakParent
を- 最後の
hardline
はProgram
のprintBlock()
で
今ならなんとなくわかる・・・気がするな・・・?
まとめ
ついに整形後のコードが手に入るまでの一連の流れ読み切ることができた。
コメント紐付けのところほどではないとはいえ、やっぱり単純な関数の組み合わせというわけにはいかず、ifが連なる重厚な処理だったなあ。
正直もう終わってもいい!って感じやけど、あと少しだけ調べたいことが残ってるので、次回のPart 10でキリよく終わろうと思う。