🧊

Biome Formatterのコードを読む Part 1

バージョンは、現時点で最新の1.9.4を。

Release CLI v1.9.4 · biomejs/biome https://github.com/biomejs/biome/releases/tag/cli/v1.9.4`

CLIから本丸まで

この辺りはなんとなく想像つくので、ざーっと読み流してしまう。

まずはmain()から。

https://github.com/biomejs/biome/blob/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_cli/src/main.rs#L33

Biomeはオールインワン!ということで、npx @biomejs/biome formatみたくサブコマンドでFormatterが起動する。

というあたりはRustのCLI界隈ではお馴染みのbpafでやってた。

pacak/bpaf: Command line parser with applicative interface https://github.com/pacak/bpaf

設定はこのあたり。

https://github.com/biomejs/biome/blob/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_cli/src/commands/mod.rs#L270

で、そのあとは・・・、

  • biome_cli:main.rs: main()
    • run_workspace(console, command)
    • CliSession.run(command)
  • biome_cli:lib.rs: run_command(session, cli_options, command)
    • CommandRunner.run(session, cli_options)
  • biome_cli:commands/mod.rs: execute_mode(execution, session, cli_options, paths)
  • biome_cli:execute/mod.rs: traverse(execution, session, cli_options, paths)
  • biome_cli:execute/traverse.rs: traverse_inputs(fs, inputs, ctx)
    • TraversalScope.evaluate(context, path)
    • handle_path(path)
    • handle_file(path)
  • biome_cli:execute/process_file.rs: process_file(ctx, biome_path)
  • biome_cli:execute/process_file/format.rs: format(ctx, path)
    • format_with_guard(ctx, workspace_file)
  • biome_service:workspace.rs: format_file()
    • Workspace.format_file(params)
  • biome_service:workspace/server.rs: WorkspaceServer.format_file(params)
    • get_file_capabilities(biome_path): このファイルはどういう言語があり、どういうツールセットを対応させるかなど
    • get_parse(biome_path): ここで対応したparserによってCSTができる
  • biome_service:file_handlers/mod.rs: get_capabilities(biome_path, language_hint)
  • biome_service:file_handlers/javascript.rs: capabilities()
    • biome_js_parserでパースしてbiome_js_formatterでフォーマットして・・・などが定義されてる
  • biome_service:workspace/server.rs: WorkspaceServer.format_file(params)
    • format(path, document_file_source, parse, workspace)
  • biome_service:file_handlers/javascript.rs: format(biome_path, document_file_source, parse, settings)
    • format_node(options, tree)
  • biome_js_formatter/lib.rs: format_node(options, root)

とまぁ、こんな感じでメインの整形処理へ。さすがの重厚さであるな。

コード文字列からCSTへの変換、Formatter側ではなくもっと手前でやってるのね。

https://github.com/biomejs/biome/blob/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_service/src/workspace/server.rs#L262

パスごとに結果が保存されるようで、同じパース結果をLinterとか他の処理でも使うんかね?

biome_formatterbiome_js_formatterの関係

ここまで読んできたとおり、

  • biome_serviceで、各言語ごとの対応ツールが決まる
    • biome_js_parser(およびbiome_js_syntax)とbiome_js_formatterはそこで指定されてる
  • biome_js_formatterは、biome_formatter::format_node(root, language)を呼び出す

という流れになっていて、 各言語側としては、CSTのノードごとのAsFormatや、そのFormatLanguageやら各種Traitを実装しておく必要がある。

ということが、実はガイドにも書いてあった。親切。

https://github.com/biomejs/biome/blob/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_formatter/CONTRIBUTING.md

詳細はひとまず置いておくけど、この流儀に従って、Biome本体にはhtml / css / js /json / glaphql / gritのフォーマッタが実装されてるとのこと。

見た感じ、Biome内で扱ってる言語共通のインフラではあるけど、Biomeの外の世界に対してもオープンなのか?ってのはまだわかってない。

このガイドや型をみる限り、biome_rowanというcrateを使ったCSTやらのみを想定してるようにも見える。

biome_formatter

https://github.com/biomejs/biome/tree/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_formatter

フォーマッタとしてのインフラという立ち位置。

己のRust力が低すぎて、Traitやらmacroやらが入り混じったコードをうまく追えなくてつらい!

まあでも共通機能を提供する層だからこその書き方なんやろうとも思うし、あまり深追いはしないでおく。 Rustだからこその制約でこうなってるみたいな記述もあったし。

ともあれ、各言語側はノードと対応する整形処理を用意して、biome_formatter::format_node(root, language)を呼べばOKってことか。

pub fn format_node<L: FormatLanguage>(
    root: &SyntaxNode<L::SyntaxLanguage>,
    language: L,
) -> FormatResult<Formatted<L::Context>> {
  // ...

この引数の型はこう。

やっぱり、RowanもといCSTなツリーを受け取ることを想定してそうである。

https://github.com/biomejs/biome/tree/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_rowan

biome_js_formatter

https://github.com/biomejs/biome/tree/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_js_formatter

インフラを利用する各言語側。

あとは、jsjsxtsディレクトリ配下に、すべてのCSTノードに対してfmt_fields(node, f)needs_parantheses(item)を実装するコードがあるだけ。

だいたい400ノードくらいに対して、FormatNodeRuleを実装するコードがある。400て!

fmt_fields(node, f)内で見つかった子ノードは、その子ノードのformat()をその都度呼び出すことで、再起で処理されていく。

needs_parantheses()は、そもそもCSTのノードを定義する側に実装があるようだった。

https://github.com/biomejs/biome/tree/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_js_syntax/src/parentheses

コメントに関するfmt_xxx_comments()も個別に実装の必要があれば、biome_formatter側でformat_xxx_comments()みたいなのが公開されてるのでそれを使う。

https://github.com/biomejs/biome/blob/fa93a147abe64e9c85908d317a8dd1de343ad132/crates/biome_formatter/src/trivia.rs#L44

ノードごとの対応が必要ない場合は、大元でやってもらえるのに任せておけばOKという感じ。

おわりに

Prettier互換というだけあってか、やってることの流れはPrettierと一緒だなって印象だった。 他言語向けへのインフラとしての機能や、その取り回しなんかは圧倒的にクリーンになってるけど。

OXCでのbiome_formatter利用は望み薄かなと思いつつも、次回はもう少し整形処理の詳細を見ておく。