🧊

Prettier のデバッグ方法について

コードリーディングをする過程で、Prettierのデバッグ方法についてもなんとなくわかってきたのでまとめておく。

Playground

https://prettier.io/playground/

左のサイドバーにDebugのセクションがあって、

  • show AST: 指定したパーサーから出力されるAST
  • show comments: コメントがASTにどう紐づけられたのかの詳細
  • show preprocessed AST: そのASTに対して、commentsを各ノードに生やしたAST
  • show doc: そのASTから生成されたDoc

というのをそれぞれ見られる。

あらゆるオプションを有効にすると、右ペインが7窓になってなかなかに壮観である。

あとは、パーサーの中からdoc-explorerというものを選ぶと、Doc自体を解析できる特別なモードになる。

const a = 42;を入力した場合、これがDocで、

[
  group(["const", " ", group([group("a"), " =", " ", "42"]), indent([]), ";"]),
  hardline,
]

こっちが解析されたDocのAST。

[
  {
    "type": "group",
    "contents": [
      "const",
      " ",
      {
        "type": "group",
        "contents": [
          {
            "type": "group",
            "contents": "a",
            "break": false
          },
          " =",
          " ",
          "42"
        ],
        "break": false
      },
      {
        "type": "indent",
        "contents": []
      },
      ";"
    ],
    "break": false
  },
  [
    {
      "type": "line",
      "hard": true
    },
    {
      "type": "break-parent"
    }
  ]
]

DocはJSONではなくただのJavaScriptなので、って感じかね。

CLI

--helpしても出てこないけど、隠しフラグがある・・・!

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/cli/cli-options.evaluate.js#L139-L160

  • --debug-benchmark: ベンチマーク実行してくれる
  • --debug-check: 2重に整形して、差分があるかチェックする
  • --debug-print-ast: 指定されたパーサーから出力されるASTを表示
  • --debug-print-comments: コメントのASTへの紐づけ結果を表示
  • --debug-print-doc: Doc(ASTじゃないほう)へ変換した結果を表示
  • --debug-repeat N: N回実行して、平均実行時間を取ってくれる

いくつかのフラグについては、CONTRIBUTING.mdにも記載がある。

https://github.com/prettier/prettier/blob/3.3.3/CONTRIBUTING.md

ちなみに、これらのフラグをさばいてるコードはこのへん。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/cli/format.js#L110

API

ってことは、生のAPIもあるんじゃろ?ってことで探すとビンゴだった。

Playgroundでも使われた。

https://github.com/prettier/prettier/blob/3.3.3/website/static/worker.js

通常版でもstandalone版でも、__debugというオブジェクトがこっそり公開されてる。

https://github.com/prettier/prettier/blob/52829385bcc4d785e58ae2602c0b098a643523c9/src/standalone.js#L39-L45

  • parse(text, options): 指定したパーサーで出力したASTと元コードを返す
  • formatAST(ast, options): ASTを渡すと整形後の文字列を返す
    • options.originalTextが生えてることが必須(なんでやねん)
  • printToDoc(text, options): ASTを渡すとDoc ASTを返す
  • formatDoc(doc, options): Doc ASTを渡すとDocを返す
    • __js_expressionという内部的なパーサーを強制的に使用するらしく、それが(なぜか)実装されているbabelのプラグインが必要
    • Playgroundのdoc-explorerのインプットと同じ形式
  • printDocToString(doc, options): Doc ASTを渡すと整形後の文字列にして返す

全部まとめるとこういうイメージ。

import { __debug } from "./standalone.js";
import * as babelPlugin from "./src/plugins/babel.js";
import * as meriyahPlugin from "./src/plugins/meriyah.js";
import * as estreePlugin from "./src/plugins/estree.js";

const plugins = [estreePlugin, babelPlugin, meriyahPlugin];
const source = `
// 1
let a
 = 42 // 2
`.trim();

for (const parser of ["meriyah"]) {
  console.log(`🦄 ${parser}`);
  const options = { parser, plugins };

  const parsed = await __debug.parse(source, options);
  console.log(JSON.stringify(parsed, null, 2));

  const f = await __debug.formatAST(parsed.ast, {
    ...options,
    originalText: parsed.text, // 🤔
  });
  console.log(f);

  const doc = await __debug.printToDoc(source, options);
  console.log(JSON.stringify(doc, null, 2));

  const ff = await __debug.formatDoc(doc, options);
  console.log(ff);

  const fff = await __debug.printDocToString(doc, options);
  console.log(fff.formatted);
}

というわけで、結局すべての道はDocに通ずるのであるなあ。

__debug.parse()の隠しオプション

秘密の第3引数が存在してた・・。

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

  • preprocessForPrint: boolean: Doc化の直前に行うコメントの紐づけまでやるようにする
  • massage: boolean: デバッグに不要な情報を削ってる?

ちなみに、同時に利用することはできない。