あと一息。
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-parentgroup
どうやらこの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-breakindent-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でキリよく終わろうと思う。