formatter-development について
Biome のフォーマッター開発者向けスキルです。構造化フォーマット、トークンとコメントの処理、レイアウト、テスト、レビューを扱います。
主な機能
- 01フォーマッターの手順とレビュー項目
- 02構造化 IR レイアウトの指針
- 03トークンとコメントの保持規則
- 04抑制コメントの処理方法
- 05対象を絞ったテストと冪等性確認
活用シーン
- 01フォーマッタールールの実装や調整
- 02フォーマット時のコメントとトークンの保持
- 03レイアウト変更とフォーマットの安定性のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ biomejs/ biome --skill formatter-developmentインストール後、Claude Code で/formatter-developmentを入力して読み込まれたか確認します。
詳細
フォーマッターの開発
アーキテクチャの基準資料として、crates/biome_formatter/CONTRIBUTING.md と各言語のフォーマッターガイドを使用してください。IR プリミティブを選ぶ前に、近隣のノード実装を確認してください。
ワークフロー
- 対象を絞った内部フォーマッター仕様または
quick_testで動作を再現します。 - ノードのフィールド、コメント、近隣のフォーマット規則を確認します。
- フォーマッター IR を使って、最小限のレイアウト変更を実装します。
- 対象を絞ったフォーマッターテストを実行し、スナップショットを確認します。
- 互換性が重要な場合は Prettier と比較します。
- コミットする前にフォーマットとリントを実行します。
出力の規律
フォーマッターの出力は、ソースツリーを構造化して書き換えたものです。新たに整形するものではありません。処理されていないノードやトークン、確認されていない抑制、追跡されていない置換は、スタイルに関するフィードバックではなく、フォーマッターのバグとして扱ってください。
- フォーマット対象の範囲内にあるソーストークンは、すべてちょうど一度消費しなければなりません。トークンは、フォーマットするか、
format_removedで削除するか、format_replacedで置換するか、これらのいずれかを実行する言語固有のヘルパーを使った場合にのみ消費されます。 - フォーマット対象の範囲内にあるソースノードは、毎回すべて処理しなければなりません。ノード独自のルールでノードをフォーマットし、抑制を確認し、フォーマッターのインフラストラクチャを通じてコメントを処理できるよう、
node.format()またはnode.format().with_options(...)を優先してください。 - 親フォーマッターは、都合のよいレイアウトにするためだけに、子ノードのフィールドをインライン化してはなりません。レイアウトの決定は子ノードのルールに移すか、オプションを子フォーマッターに渡してください。
- アーキテクチャ固有のフォーマッターがノードのルールを迂回する場合、そのノードを黙ってスキップしてはなりません。
f.context().comments().is_suppressed(node.syntax())を確認しなければなりません。ノードが抑制されている場合は、ノード本体をフォーマットする代わりに、言語のformat_suppressed_node(...)ヘルパーを書き出さなければなりません。この確認がないと、デバッグビルドでは抑制のカバレッジ確認に失敗し、ユーザーの抑制が無視される場合があります。 - ソーストークンは、型付きアクセサーを通じて出力しなければなりません。
token("...")を使うのは、ソーストークンが存在しない箇所にフォーマッターが構文を挿入する場合に限ります。 - ソーストークンを出力から削除する場合は、
format_removed(&token)を使用しなければなりません。format_removedを通じて消費せずに、フィールドを破棄したり、_に束縛したり、write!から省いたりしてはなりません。スキップされたトリビアも引き続きそのトークンに属します。 - ソーストークンのテキストを置き換える場合は、
format_replaced(&token, &replacement)を使用しなければなりません。置換テキストを直接出力したり、token("...")を使ったりしてはなりません。元のトークンにはトリビアが残っており、消費済みとしてマークする必要があるためです。 - カスタムフォーマットは、
Format<Context>を実装する小さな構造体で担わなければなりません。フォーマッターの状態やレイアウト上の不変条件を保持するために、自由関数やクロージャ値の保存を使ってはなりません。format_withは、直ちに書き出す単発のローカルな接着処理にのみ使用してください。
ノードのルール
生成されたノードルールは FormatNodeRule を実装します。fmt_fields では、次のようにします。
- 生成された
*Fields型を明示的に分解します。 - 型付きアクセサーを通じてソーストークンをフォーマットします。
- 同じフォーマット経路の別の箇所でフィールドを消費する場合、または
format_removed/format_replacedで意図的に処理する場合に限り、..ではなく_を使います。それ以外の場合、ノードまたはトークンフィールドに_を使うのは、ツリーの要素を破棄するバグです。 - フォーマーターの契約で意図的に削除または置換する場合を除き、すべてのトークンとコメントを保持します。
- レイアウトの決定は、それを所有する型の近くに置きます。
format_verbatim_* メソッドは、ノードのソーステキストを保持します。有効な形式、誤った形式、コメントを含む形式がテストでカバーされている場合にのみ、逐語的なフォーマットを構造化フォーマットに置き換えてください。
トークンのルール
すべてのトークンをフォーマット、置換、または削除してください。フォーマッターテストでは、未処理のトークンがあるとパニックが発生し、ソースが誤って失われるのを防ぎます。
トークンを置換する場合は format_replaced を、削除する場合は format_removed を使用してください。
アドホックなフォーマット
可能な場合は、node.format() を通じてノードをフォーマットします。通常のルールでは、通常のフォーマット処理の一部としてフォーマッター抑制コメントを確認します。
ヘルパーが FormatNodeRule の外でノードまたはそのトークンをフォーマットする場合は、フォーマッターテストを実行します。抑制チェックのアサーションでノードが報告された場合は、報告されたノードに対して f.context().comments().mark_suppression_checked(node.syntax()) を呼び出します。このアサーションは、ヘルパーがノードの通常の抑制チェックを迂回していることを示しています。
IR の構成
空白を任意のテキストとして書き込むのではなく、意味を持つ IR を使用します。
- 必須のスペースには
space()を使用します。 - 任意の折り返しにはソフト改行を使用します。
- 必須の改行にはハード改行を使用します。
- フラットなレイアウトと展開されたレイアウトを選択するにはグループを使用します。
- 外側の構文に合ったインデントプリミティブを使用します。
- グループの収まり具合の判断を制御するグループに、条件付きコンテンツを結び付けます。
フォーマット上の独立した関心事には、Format を実装する名前付き型を使用します。&mut Formatter を渡すフリー関数のまとまりでは、すでに何が書き込まれているのか、どのレイアウト不変条件が適用されるのかが分かりにくくなります。
複数パターンのレイアウトは、一度だけ選択される列挙型で表現します。複数の書き込み箇所でレイアウトを再計算すると、出力の不整合や冪等性の失敗につながることがあります。
コメント
先頭コメントと末尾コメントは、通常、フォーマッターのインフラストラクチャが処理します。ノードが、子ノードのどれにも割り当てられない位置を所有する場合は、ぶら下がりコメントを明示的にフォーマットします。
変更によって影響を受ける各構造境界でコメントをテストします。対象は、最初の子ノードの前、子ノード間、最後の子ノードの後、空ノードの前後です。ソースコメントを削除したり移動したりすることは、データの損失です。
テストと冪等性
スナップショットのコマンドとレビューについては、testing-codegen を読み込みます。
内部仕様には、正準出力を確立するために必要な、対象に絞ったソース形状を含めます。有用な場合は、すでにフォーマット済みの入力と、意図的にフォーマットを崩した入力の両方を含め、どちらも同じ結果に収束することを確認します。
フォーマッターテストのインフラストラクチャは、テスト実行時にエラーのない範囲指定なしの出力を再フォーマットし、2回目の結果が異なる場合は失敗します。出力が異なる場合、IR は診断のための根拠になりますが、独立した等価性の契約ではありません。
Prettier のスナップショットが変更または削除される場合でも、動作変更には内部仕様を追加します。外部コーパスの入力ひとつと一致していても、変更されたエッジケースを網羅したことにはなりません。
Prettier との比較
互換性が要件の一部である場合は、リポジトリのツールを使用します。
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l js 'const value={a:1}'
bun packages/prettier-compare/bin/prettier-compare.js --rebuild -f path/to/file.js
--rebuild は Biome の WASM バンドルを再ビルドし、ビルド出力を書き込みます。これは実装中に適していますが、読み取り専用のレビュー中には使用しません。
差異は自動的にバグとみなすのではなく、設計の検討材料として扱います。文書化された動作やアーキテクチャ上の要件により、Biome が意図的に異なる結果を出す場合があります。
生成と検証
言語フォーマッタークレートのソースを変更した後は、次の手順を実行します。
- 最も対象を絞ったフォーマッタークレートまたは仕様テストを実行し、スナップショットを確認してから採用します。
- 互換性が重要な場合は Prettier と比較し、レビュー済みの意図的な差異は維持します。
- コミットする前に
just fとjust lを実行します。
レビューチェックリスト
- 生成されるフィールドをすべて明示的に処理している。
- すべての子ノードをそれぞれのルールを通じてフォーマットしている。あるいは、アーキテクチャ固有の迂回処理では、ノード本体を手動でフォーマットする前に
f.context().comments().is_suppressed(node.syntax())を確認している。 - すべてのソーストークンをフォーマットしているか、
format_removedで削除しているか、format_replacedで置き換えている。ソーストークンを暗黙にスキップしたり、_に束縛したり、静的テキストとして再作成したりしていない。 - コメントが意図した位置に保持されている。
- カスタムフォーマットを、フリー関数やクロージャ値ではなく、
Format<Context>を実装する構造体で表現している。 - レイアウトを一度だけ選択し、意味を持つ IR で構成している。
- エラー構文や不正な構文を、フォーマッターのパニックを起こさずに表現できる。
- 変更した動作を内部仕様で網羅している。
- 1 回のテスト実行で再フォーマットが収束する。
参考資料
- フォーマッターのガイド:
crates/biome_formatter/CONTRIBUTING.md - JavaScript フォーマッターのガイド:
crates/biome_js_formatter/CONTRIBUTING.md - フォーマッターのテストインフラストラクチャ:
crates/biome_formatter_test/src/spec.rs - Prettier との比較:
packages/prettier-compare/README.md