🧊

Prettier のコードを読む Part 1

とは言っても、あんな巨大コードベースを一気に読み切れるわけがないので、まずは大枠のイメージを掴むことを目標にしてみる。

バージョンは、現時点で最新のリリースである3.3.3で。v4系はなんとなくやめておいた。

https://github.com/prettier/prettier/tree/3.3.3

知りたいのはメインの整形処理だけではあるけど、いちおう大枠から見ていく。何か発見があるかもしらんし。

はじめに

  • TSじゃなくてJSで書かれてるの知らんかった
    • それ自体はそこまで否定的ではない派
  • JSDoc TSなところと、そうでないところがある
    • そして思ったよりexclude指定されてる・・・
    • 知りたい!と思ったところに型がない
    • まあ型つけるのも大変そうではあるけども

おま環かもしれないが、LSPが機能しない場面もあって、コードリーディングの難易度は高めかもと思った。 LSPに慣れきった現代人の地力を見極めようとしてる?!って。(メンテしてる人たちはどうやってんの。)

いちおうd.tsは置いてあるけど、手動管理っぽいし、コード内では参照されてなさそう?

https://github.com/prettier/prettier/blob/3.3.3/src/index.d.ts

エントリーポイント

閑話休題、まずはpackage.jsonから見えてるこのあたりから。

  • bin: ./bin/prettier.cjs
    • ./src/cli/index.jsを呼んでる
  • require: ./src/index.cjs
  • default: ./src/index.js
  • browser: ./standalone.js
    • ./src/standalone.jsに同じ

PrettierといえばCLIなイメージが強いけど、Node.jsのAPIとして使ったり、ブラウザで動かせたり、エディタから呼ばれたり、いろんな用途もあるなあ確かに。

src/cli/index.js

https://github.com/prettier/prettier/blob/3.3.3/src/cli/index.js

その名の通りのCLI。

CLI引数を精査して、./cli/format.jsで定義したこの2つを呼び分けるだけ。

  • formatStdin(context)
  • formatFiles(context)

src/cli/format.js

https://github.com/prettier/prettier/blob/3.3.3/src/cli/format.js

標準入力にしろファイルにしろ、だいたいの流れは同じで、

  • 実行条件を精査し
  • ファイル名からパーサーなどオプションを準備
  • ファイルからコードを読み出し
  • format(context, input, options)に渡して整形

ファイルに書き出す場合、その処理があったり高速化のためにキャッシュを保存したりもしてた。

format(context, input, options)は、隠しCLI引数である--debug-*シリーズに応じて挙動が変わる。 (デバッグ方法についてはまた別途で調べる)

けど実態としては、./src/index.jsformatWithCursor(input, opt)を呼ぶだけ。

src/index.js, index.cjs

https://github.com/prettier/prettier/blob/3.3.3/src/index.js https://github.com/prettier/prettier/blob/3.3.3/src/index.cjs

似てるようで、どこか違うようで・・・。 詳細は追わないけど、.cjsのほうで.jsをimportしてるので、.jsのほうが主体なのであろう。

さっきのformatWithCursor()は、

したもの。

どうやらメインの整形ロジックは、./main/core.jsあたりから追っていけそう。

withPlugin()でやってるビルトインプラグインの自動ロードが、standalone verではなくなってる。

src/standalone.js

https://github.com/prettier/prettier/blob/3.3.3/src/standalone.js

見た感じ、./src/index.jsからNode.js依存を取り除いたバージョンとみた。

コードを見る限り、デフォルトのオプションやプラグインなども一切なさそうで、使いたいものだけ自分でセットアップして使うものっぽい。

// These file paths are based on prettier repo root
import { format } from "./standalone.js";
import * as meriyahPlugin from "./src/plugins/meriyah.js";
import * as estreePlugin from "./src/plugins/estree.js";

const SOURCE = `
let a
= 'x'
`;

const formatted = await format(SOURCE, {
  parser: "meriyah",
  plugins: [meriyahPlugin, estreePlugin],
});
console.log(formatted); // let a = "x";

プラグインのI/Oみたいなのも後で調べておく。

src/main/core.js

format(text, options)というものもあるけど、これもformatWithCursor(text, options)を呼んでるだけなので、こっちがメインの処理。 カーソル位置をよしなにする必要がない用途にはfomrat(text, options)ということね。

https://github.com/prettier/prettier/blob/3.3.3/src/main/core.js#L268

やってるのは、

  • normalizeInputAndOptions(text, options)
    • BOMや改行コードやらを事前に処理してる風
    • このoptionsは、normalizeFormatOptions(options)を通したもの
      • ./src/main/normalize-format-options.js
    • どういうパーサーを使うかなどが決まる
  • coreFormat(text, options)

なので、つまりはcoreFormat(text, options)が整形処理の正体。

src/main/normalize-format-options.js

https://github.com/prettier/prettier/blob/3.3.3/src/main/normalize-format-options.js#L21

元のオプションに対して、デフォルト値を埋めていくとのこと。

inferParser(options)っていうずばりなやつもいて、.jsファイルに対して、パースするのにBabelを使う・・・みたいな設定はここで決まってた。

https://github.com/prettier/prettier/blob/3.3.3/src/utils/infer-parser.js#L61

手動でoptions.parserを指定してた場合は、それが使われる。

parserprinterの初期化なんかもやってるけど、ほとんどはoptionsオブジェクトをこねくり回すターンなので、あまり追ってない。

coreFormat(text, options)

https://github.com/prettier/prettier/blob/3.3.3/src/main/core.js#L25

いよいよ本丸に。

整形された文字列を手に入れるまでの流れは、

  • parse(text, options)してASTに変換
    • ./src/main/parse.js
    • parserの指定があったものを使う
  • printAstToDoc(ast, options)でDocと呼ばれる抽象構造(IR)に変換
    • ./src/main/ast-to-doc.js
    • コメントを事前にASTに対応させる
    • printerの指定があったものを使う
  • printDocToString(doc, options)で文字列化
    • ./src/document/printer.js

さて、言うは易し、この中身を理解したいというのが本題で・・・、次回へ続く。

プラグインの構成

最後にこれだけ。

https://prettier.io/docs/en/plugins#developing-plugins

ここに書いてあるように、

  • languages
  • parsers
  • printers
  • options
  • defaultOptions

これらを実装したセットのことをそう呼んでるらしい。 ビルトインで対応してない言語をサポートするために必要な一式という感じ。

ただこれは3rd向けのルールであり、ビルトインはその限りではなさそう。

たとえば、ESTree形式のASTに対応する用であろうこのビルトインのプラグインには、parsersがないし。

https://github.com/prettier/prettier/blob/3.3.3/src/plugins/estree.js

逆にparsersだけ、languagesだけのやつもあった。 原理的には、実行時に必要なものがトータルで揃ってれば、プラグインという単位は別になってても良いってことね。

plugins: [meriyahPlugin, estreePlugin],
// どっちでもいい
plugins: [{ ...meriyahPlugin, ...estreePlugin }],

standaloneの使い方のページで、estreeプラグインが別途で必要って書いてあったのはそういうことかね。

https://prettier.io/docs/en/browser#prettierformatcode-options

parsersを指定するとき、そのパーサーの出力であるastFormatをあわせて記載しておくと、それがprintersのキーから選ばれる。

ここまでのまとめ

  • 整形処理の実体は、coreFormat(text, options)
    • 指定されたパーサーを使って、ソースコードをASTに
    • 指定されたプリンターが、そのASTをDocに
    • 指定された設定を元に、Docが文字列に
  • optionsオブジェクトに気をつけろ
    • 場所によって中身が別物になってる

次回以降は、

  • coreFormat()の詳細
  • Docの構造

というあたりを。