感動の(?)最終回となります。
Prettier のコードを読む Part 9 | Memory ice cubes https://leaysgur.github.io/posts/2024/10/08/095258/
最後は、Prettierの中間表現であるDocについてのおさらい。
この”Doc”という単語、今までいろんなところで見てきたけど、具体的に何?どういうものがある?というあたりを最後に整理しておきたい。
中間表現としてのDoc
ということで、どの言語のどのパーサーを使ったASTでも、最終的にはこのDocで表現されるということ。
このブログではJS/TSしか見てこなかったけど、たとえばCSSでもHTMLでもなんでもDocになる。
p {
color: red;
width: 500px;
border: 1px solid black;
}
これが、こうなる。
[
group(group(indent("p"))),
" ",
"{",
indent([
hardline,
"color",
":",
" ",
"red",
";",
hardline,
"width",
":",
" ",
"500",
"px",
";",
hardline,
"border",
":",
" ",
group(indent(fill([["1", "px"], line, "solid", line, "black"]))),
";",
]),
hardline,
"}",
hardline,
];
なるほどなあ。
DocとDocCommand
let a = 42;
というコードがあったとして、これをPlaygroundに入力し、“show doc”すると、こう表示される。
[
group(["let", " ", group([group("a"), " =", " ", "42"]), indent([]), ";"]),
hardline,
];
で、これもまあDocと表現されてるけど、同時にJavaScriptのコードでもあって、実行された後はこうなる。
[
[
[
{
type: "group",
id: undefined,
contents: [
"",
"let",
[
" ",
{
type: "group",
id: undefined,
contents: [
{
type: "group",
id: undefined,
contents: ["a", "", "", ""],
break: false,
expandedStates: undefined,
},
" =",
" ",
"42",
],
break: false,
expandedStates: undefined,
},
],
{ type: "indent", contents: [] },
";",
],
break: false,
expandedStates: undefined,
},
],
],
[{ type: "line", hard: true }, { type: "break-parent" }],
];
ゆえにフォーマットとしては、JSONとしても扱えるこの形式こそが、真にDocと呼ぶにふさわしい気がする。
展開される前のは、Docを生成するためのBuilderコマンドの集まりという感じで、明示的に区別されてるかはわからんけど、DocCommandって呼ばれてるところもあったので、そう呼ぶことにする。
ちなみに、この真のDocをPlaygroundで見るためには、パーサーのプルダウンをdoc-explorer
にして、展開前のDocをソースとして入力すればいい。
ここに載せた例は、実際のコード中でdumpした値なので、Playgroundにおいては値がundefined
なキーは削られてることに注意。
DocCommand
ASTはともかく、Docさえなんとか用意できれば、それをPrettierに文字列化させることができる!というわけで、そのDocを作るためのコマンド集も公開されてる。
Node.js限定な感じはするけど、./src/document/public.js
に置いてあった。
- join
- line
- softline
- hardline
- literalline
- JS/TSでは使われない
- group
- conditionalGroup
- fill
- lineSuffix
- JS/TSでは使われない
- lineSuffixBoundary
- cursor
- breakParent
- ifBreak
- trim
- JS/TSでは使われない
- indent
- indentIfBreak
- align
- addAlignmentToDoc
- markAsRoot
- JS/TSでは使われない
- dedentToRoot
- JS/TSでは使われない
- dedent
- hardlineWithoutBreakParent
- JS/TSでは使われない
- literallineWithoutBreakParent
- JS/TSでは使われない
- label
という24種類がexportされてて、もちろん実際のlanguage-js
配下のコードでも使われてるもの。
これらのDocCommandの詳細については、ドキュメントも用意されてる。
なぜかaddAlignmentToDoc
はドキュメントになかった。
https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/commands.md
整形後コードで複雑な、あるいは細かい調整をしようとすると、このDocCommandおよび最終的なDocの種類が増えていくのであろうな。
これらのDocCommandは、最終的に以下の15種類のDoc(とそのオプションの組み合わせ)を生み出す。
export const DOC_TYPE_STRING = "string";
export const DOC_TYPE_ARRAY = "array";
export const DOC_TYPE_CURSOR = "cursor";
export const DOC_TYPE_INDENT = "indent";
export const DOC_TYPE_ALIGN = "align";
export const DOC_TYPE_TRIM = "trim";
export const DOC_TYPE_GROUP = "group";
export const DOC_TYPE_FILL = "fill";
export const DOC_TYPE_IF_BREAK = "if-break";
export const DOC_TYPE_INDENT_IF_BREAK = "indent-if-break";
export const DOC_TYPE_LINE_SUFFIX = "line-suffix";
export const DOC_TYPE_LINE_SUFFIX_BOUNDARY = "line-suffix-boundary";
export const DOC_TYPE_LINE = "line";
export const DOC_TYPE_LABEL = "label";
export const DOC_TYPE_BREAK_PARENT = "break-parent";
DocCommandを使いこなすのには、また別の意味での熟練が必要そう。
総まとめ
というわけで、JS/TSなコード文字列が整形後のコードになるまでの一連の流れを読み切った。
けど、Prettierのコードベースとしてはまだほんの一部ということで、メンテしてる人たちは本当にすごいのである。
他の言語の実装はおろか、RangeとかCursorとか、Embedとかも読めてない上に、ユーティリティもまだまだてんこ盛りやし。
コードの読み心地という意味では、やはりダイナミックなThe JSって感じのコードなのと、とにかく再帰が多くてどうしても追いにくい。 けど、実際の運用とかメンテに関しては、ほとんどDocのI/Oくらいにしかフォーカスしないであろうし、根本のASTにコメントをくっつけるところとかは大変にしても、それ以外はほとんど変更されてないという感じと予想。
JSなおかげで、debugger
でもシュッとデバッグできるので、そういうところはよかった。
これがRustだったら読むの諦めてる・・・と一瞬思ったけど、Rustならそもそもこういうコードにもならないのかもしれない。ワーストケースでどっちがマシかを判断できる審美眼はまだない。
フォーマッターの基本的な概念というか、そういう意味での初出は、この論文に書いてあるらしい。
prettier.pdf https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
というわけで、1ヶ月ず〜っと読んでたPrettierともひとまずお別れ。