🧊

Prettier のコードを読む Part.9

あと一息。

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])の続き

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/main/core.js#L36

というわけで、Doc化の直後から。

  • addAlignmentSizeが0以上ならその反映
  • printDocToString()でDocの文字列化
  • addAlignmentSizeでインデントの調整
  • カーソル位置の復元

addAlignmentSizeに任意の値が入ってくるのは、formatRange()経由で呼ばれたときだけなのでとりあえず無視。

Docを文字列にするprintDocToString(doc, options)というズバリなやつを見ていく。

printDocToString(doc, options)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/printer.js#L302

いざdocumentディレクトリへ。

この処理の基本的な流れは、

  • propagateBreaks(doc)して
  • cmds: Command[]に積まれたDocをひたすら文字列に
    • options.printWidthなどのオプションを反映しながら
  • out: string[]に処理された文字列が積まれていくので、最後にjoin("")

という感じ。

処理されるべきDocのコマンドに応じたswitch-caseがずっと続いてる。

propagateBreaks(doc)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/utils.js#L143

traverseDoc(doc, onEnter, onExit[, shouldTraverseConditionalGroups = false])というもので、Docを走査しながら動的に更新してるぽい。

  • break-parent
  • group

どうやらこの2種類に対してbreakParentGroup(groupStack)を実行していて、条件に合致するもののdoc.break"propagated"にするらしい。

ただこの"propagated"という文字列、実際の処理ではboolean相当としてしか見られてない模様。(デバッグ用のAPIでだけ、文字列として比較してる)

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/debug.js#L160

なんしかこの処理の意味としては、ここに書いてあるように、

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はどうやらインデントに関する状態のことで、

という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

このあたりの処理。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/printer.js#L541-L542

たぶん、基本的にはDocで指示されたように配置しようとするけど、最終的にどうなるかはもちろん過程で決まるのであり、そのための変数って感じかな。

posprintWidth

行あたりの最大文字数を決める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()

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/printer.js#L196

詳細は定かではないけど、

  • 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,
];

今ならなんとなくわかる・・・気がするな・・・?

まとめ

ついに整形後のコードが手に入るまでの一連の流れ読み切ることができた。

コメント紐付けのところほどではないとはいえ、やっぱり単純な関数の組み合わせというわけにはいかず、ifが連なる重厚な処理だったなあ。

正直もう終わってもいい!って感じやけど、あと少しだけ調べたいことが残ってるので、次回のPart.10でキリよく終わろうと思う。