🧊

Prettier のコードを読む Part 10

感動の(?)最終回となります。

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に置いてあった。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/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";

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/document/constants.js

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ともひとまずお別れ。