configuration-patterns
リポジトリ: biomejs/biome 分析日: 2026-03-09
概要
Biome の設定システム(biome.json)は、カスタムデシリアライザー・derive マクロ・Merge トレイト・条件付き JSON Schema 生成を組み合わせた多層アーキテクチャで構築されている。serde のfail-fast 戦略を意図的に避け、フォールトトレラントなデシリアライズと豊富な診断メッセージを両立させている点が注目に値する。設定の継承(extends)とオーバーライド(overrides)を型安全なマージ戦略で実現しており、汎用的な設定システム設計のプラクティスが多数含まれる。
背景にある原則
- フォールトトレラント優先: serde の fail-fast ではなく「可能な限りデシリアライズし、複数の診断を報告する」戦略を採る。設定ファイルのエラーが1箇所あっても、他の部分は正常にパースされるべき、という UX 原則に基づく(
crates/biome_deserialize/src/lib.rs:7-9) - derive マクロによる宣言的定義: 設定構造体は
#[derive(Deserializable, Merge)]で宣言的に定義し、デシリアライズとマージの実装を自動生成する。手書きのボイラープレートを排除しつつ、カスタマイズポイント(with_validator,deprecated,bail_on_error等)を属性で提供する - Option 型による未設定の明示的表現: 全設定フィールドを
Option<T>で包み、「未設定」と「デフォルト値」を明確に区別する。これにより extends/overrides のマージ時に「設定されていないフィールドは親の値を引き継ぐ」セマンティクスが自然に実現される - 単一ソースからの多面的生成: 同一の Rust 構造体定義から JSON Schema(
schemars)、CLI 引数パーサ(bpaf)、シリアライズ/デシリアライズ(serde+ 独自)を生成する。設定の仕様が一箇所に集約されることで不整合を防ぐ
実例と分析
カスタムデシリアライザーフレームワーク
biome_deserialize クレートは serde にインスパイアされつつ、フォールトトレラントな独自フレームワークを構築している。コアは Deserializable トレイト(データ構造側)と DeserializableValue トレイト(データフォーマット側)の分離にある。
// crates/biome_deserialize/src/lib.rs:80-88
pub trait Deserializable: Sized {
fn deserialize(
ctx: &mut impl DeserializationContext,
value: &impl DeserializableValue,
name: &str,
) -> Option<Self>;
}Option<Self> を返すことで部分的な失敗を許容し、DeserializationContext::report() で診断を蓄積する。serde の Result<T, E> ベースの即時失敗とは対照的なアプローチ。
Merge トレイトによる設定マージ戦略
Merge トレイトは型ごとにマージのセマンティクスを定義する。プリミティブ型は上書き、Option<T> は再帰マージ、コレクション型は結合という階層的な戦略をとる。
// crates/biome_deserialize/src/merge.rs:19-28
impl<T: Merge> Merge for Option<T> {
fn merge_with(&mut self, other: Self) {
if let Some(other) = other {
match self.as_mut() {
Some(this) => this.merge_with(other),
None => *self = Some(other),
}
}
}
}Option<T> の Merge 実装がこのシステムの核心で、「self が None なら other で埋め、両方 Some なら再帰マージ」というセマンティクスにより、extends チェーンでの設定の継承が実現される。
構造体の Merge derive は全フィールドに対して再帰的に merge_with を呼ぶコードを生成する。
// crates/biome_deserialize_macros/src/merge_derive.rs:66-75
fn generate_merge_struct(ident: Ident, fields: Fields) -> TokenStream {
let field_idents = fields.into_iter().filter_map(|field| field.ident);
quote! {
impl biome_deserialize::Merge for #ident {
fn merge_with(&mut self, other: Self) {
#( biome_deserialize::Merge::merge_with(&mut self.#field_idents, other.#field_idents); )*
}
}
}
}extends チェーンのマージ順序
設定の継承では、extends リスト内の設定を左から右に reduce し、最後にカレント設定をマージする。swap テクニックでクローンを回避している点も注目。
// crates/biome_service/src/configuration.rs:593-606
let extended_configuration = configurations.into_iter().flatten().reduce(
|mut previous_configuration, current_configuration| {
previous_configuration.merge_with(current_configuration);
previous_configuration
},
);
if let Some(mut extended_configuration) = extended_configuration {
self.root = Some(self.is_root().into());
// swap で clone を避ける
std::mem::swap(self, &mut extended_configuration);
self.merge_with(extended_configuration)
}const ジェネリクスによるデフォルト値の型エンコード
Bool<const D: bool> 型は、ブール値のデフォルトを型パラメータとして表現する。これにより FormatterEnabled = Bool<true>(デフォルト有効)と UseEditorconfigEnabled = Bool<false>(デフォルト無効)が型レベルで区別される。
// crates/biome_configuration/src/bool.rs:12
pub struct Bool<const D: bool>(pub bool);
// crates/biome_configuration/src/formatter.rs:10-11
pub type FormatterEnabled = Bool<true>;
pub type UseEditorconfigEnabled = Bool<false>;条件付き JSON Schema 生成
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] で JSON Schema の生成を feature flag で制御し、通常のビルドではスキーマ生成のオーバーヘッドを避けつつ、xtask で必要時にのみスキーマを生成する。
// crates/biome_configuration/src/lib.rs:99-103
#[derive(
Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Bpaf, Deserializable, Merge,
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields, default, rename_all = "camelCase")]
pub struct Configuration { /* ... */ }カスタムの型(Extends, OverrideGlobs 等)では schemars::JsonSchema を手動実装して、内部表現と JSON Schema 表現を分離している。
// crates/biome_configuration/src/extends.rs:64-78
#[cfg(feature = "schema")]
impl schemars::JsonSchema for Extends {
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
#[derive(serde::Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
enum ExtendsSchema {
List(Vec<String>),
String(String),
}
ExtendsSchema::json_schema(generator)
}
}ルールグループのコード生成マクロ
biome_configuration_macros は lint ルールレジストリから設定構造体を自動生成する proc-macro を提供する。ルールのメタデータ(名前、推奨フラグ、fix 種別)から、対応するフィールド・フィルタ定数・有効/無効チェックメソッドを一括生成する(crates/biome_configuration_macros/src/group_struct.rs:11-344)。
DeserializableValidator によるバリデーション
with_validator 属性で、デシリアライズ後のバリデーションロジックを注入する。Configuration 構造体では root と extends の矛盾をチェックしている。
// crates/biome_configuration/src/lib.rs:195-219
impl DeserializableValidator for Configuration {
fn validate(
&mut self,
ctx: &mut impl DeserializationContext,
_name: &str,
range: TextRange,
) -> bool {
if self.root.is_some_and(|root| root.value())
&& self.extends.as_ref().is_some_and(|extends| extends.extends_root())
{
ctx.report(/* ... */);
}
true
}
}EditorConfig からの設定変換
.editorconfig ファイルを Biome の設定モデルに変換する際、global セクション (*) は formatter に、個別セクションは overrides にマッピングする(crates/biome_configuration/src/editorconfig.rs:318-348)。既存の設定モデルへの変換をアダプターパターンで実現している。
パターンカタログ
Visitor パターン (分類: 振る舞い)
- 解決する問題: データフォーマット(JSON)とデータ構造(設定型)の独立性確保
- 適用条件: 複数のデータフォーマットを同じデータ構造にデシリアライズしたい場合
- コード例:
crates/biome_deserialize/src/lib.rs:291-443のDeserializationVisitor - 注意点: serde の Visitor と同構造だが、
Option返却で部分失敗を許容する点が異なる
Adapter パターン (分類: 構造)
- 解決する問題: 外部設定フォーマット(.editorconfig)を内部設定モデルへ変換
- 適用条件: 複数の設定ソース(biome.json, .editorconfig, CLI 引数)を統一モデルに集約する場合
- コード例:
crates/biome_configuration/src/editorconfig.rs:318-348
Template Method パターン (分類: 振る舞い)
- 解決する問題: デシリアライズの基本フローを固定し、型ごとの振る舞いだけをカスタマイズ
- 適用条件: derive マクロで基本実装を生成し、
with_validatorでフックポイントを提供 - コード例:
crates/biome_deserialize_macros/src/lib.rs:80-113
Good Patterns
- Option 型 + Merge による設定継承: 全フィールドを
Option<T>にし、Mergeトレイトで再帰マージすることで、extends チェーンの設定継承を型安全に実現。マージのセマンティクスが型ごとに明確に定義されるため、予測不能な挙動が生まれにくい。
// crates/biome_configuration/src/formatter.rs:20-21
pub struct FormatterConfiguration {
pub enabled: Option<FormatterEnabled>, // None = 未設定(親を継承)
pub indent_style: Option<IndentStyle>, // Some = 明示的設定
// ...
}Feature flag による条件付き derive: スキーマ生成を
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]で制御し、通常ビルドのコンパイル時間・バイナリサイズに影響を与えない。Deprecated フィールドの宣言的管理:
#[deserializable(deprecated(use_instead = "formatter.indentWidth"))]属性でフィールドの廃止を宣言的に表現し、自動で警告診断を生成する。マイグレーションの段階的移行を設定レベルで支援する。
// crates/biome_configuration/src/overrides.rs:134-136
#[deserializable(deprecated(use_instead = "formatter.indentWidth"))]
#[bpaf(long("indent-size"), argument("NUMBER"))]
pub indent_size: Option<IndentWidth>,Anti-Patterns / 注意点
- 設定フィールドの直接アクセスとデフォルト解決の分散:
unwrap_or_default()による解決が各アクセサメソッドに分散しており、デフォルト値の定義箇所が型のDefault実装に暗黙的に依存する。
// Bad: デフォルト解決がアクセサに分散
pub fn indent_style_resolved(&self) -> IndentStyle {
self.indent_style.unwrap_or_default()
}
pub fn indent_width_resolved(&self) -> IndentWidth {
self.indent_width.unwrap_or_default()
}// Better: デフォルト値の一元管理
struct FormatterDefaults {
indent_style: IndentStyle,
indent_width: IndentWidth,
}
impl FormatterConfiguration {
pub fn resolve(self, defaults: &FormatterDefaults) -> ResolvedFormatterConfiguration { /* ... */ }
}- 手動デシリアライズと derive の混在:
FilesConfigurationはDeserializableを手動実装しているが、同じモジュール内の他の構造体は derive を使っている。手動実装が必要なケース(deprecated フィールドのカスタム診断)とそうでないケースの判断基準が暗黙的。deprecated 属性の機能拡張で derive に統一できるか検討すべき。
導出ルール
[MUST]設定フィールドはOption<T>で「未設定」を明示的に表現し、「未設定」と「デフォルト値」を区別する- 根拠: Biome は全フィールドを
Optionにすることで extends チェーンのマージを型安全に実現している(crates/biome_configuration/src/lib.rs:105-193)
- 根拠: Biome は全フィールドを
[MUST]設定の継承(extends)を実装する場合、マージのセマンティクスを型ごとに明示的に定義する(プリミティブは上書き、Optionは再帰マージ、コレクションは結合/上書き)- 根拠:
MergeトレイトのOption<T>実装が「None なら other を採用、Some 同士なら再帰マージ」を型レベルで保証している(crates/biome_deserialize/src/merge.rs:19-28)
- 根拠:
[SHOULD]設定のデシリアライズではfail-fast ではなくフォールトトレラント戦略を採り、可能な限り多くの診断を一度に報告する- 根拠:
biome_deserializeは serde の fail-fast を意図的に避け、複数エラーの同時報告を実現している(crates/biome_deserialize/src/lib.rs:7-9)
- 根拠:
[SHOULD]設定モデルの定義から JSON Schema・CLI 引数パーサ・シリアライズ/デシリアライズを単一ソースで生成し、仕様の不整合を防ぐ- 根拠:
Configuration構造体はDeserializable, Merge, schemars::JsonSchema, Bpaf, Serialize, Deserializeを一つの定義から derive している(crates/biome_configuration/src/lib.rs:99-103)
- 根拠:
[SHOULD]deprecated フィールドのマイグレーションは属性マクロで宣言的に管理し、廃止時の警告・代替候補を自動生成する- 根拠:
#[deserializable(deprecated(use_instead = "..."))]が自動でマイグレーション診断を生成する(crates/biome_deserialize_macros/src/lib.rs:210-227)
- 根拠:
[AVOID]設定継承の実装で、マージ順序に依存する暗黙的な優先順位を作らない。左から右、子が親を上書きといった規則を文書化しコードで強制する- 根拠: Biome は extends リストを左から右に reduce し、最後にカレント設定で上書きする明確な順序を
apply_extendsで実装している(crates/biome_service/src/configuration.rs:593-606)
- 根拠: Biome は extends リストを左から右に reduce し、最後にカレント設定で上書きする明確な順序を
適用チェックリスト
- [ ] 設定モデルの全フィールドが
Option<T>で定義されており、「未設定」と「デフォルト値」が区別されているか - [ ] 設定の継承・マージのセマンティクスが型ごとに定義されているか(プリミティブ/Option/コレクション)
- [ ] デシリアライズエラーが1箇所で止まらず、可能な限り多くのエラーを一度に報告できるか
- [ ] JSON Schema が設定モデルの定義から自動生成されているか(手書きスキーマとの乖離リスク)
- [ ] deprecated フィールドに対して、廃止警告と代替手段の提示が自動化されているか
- [ ] 複数の設定ソース(ファイル、CLI、外部フォーマット)が統一モデルにマージされる際の優先順位が明確か
- [ ] 設定のバリデーション(相互排他チェック等)がデシリアライズ後に実行される仕組みがあるか