biome-developer について
BiomeコードベースでのRust開発の実践方法と注意点をまとめたリファレンスです。構文木、テキスト抽出、埋め込み言語、Cargo依存関係、コードスタイルを扱い、パーサーやアナライザー、フォーマッターの変更時に役立ちます。
主な機能
- 01ASTと構文ノード構造の確認
- 02トークンテキストと範囲の適切な抽出
- 03埋め込み言語と構文バリアントへの対応
- 04ルールに沿ったCargo依存関係の設定
- 05RustとClippyの規約への準拠
活用シーン
- 01Biomeのパーサーやアナライザーの開発
- 02HTML、Vue、Svelte構文の処理
- 03テキスト処理時の割り当て削減
- 04変更提出前のRustスタイル確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ biomejs/ biome --skill biome-developerインストール後、Claude Code で/biome-developerを入力して読み込まれたか確認します。
詳細
目的
このスキルでは、コードベースのさまざまな領域で役立つ、一般的な開発のベストプラクティス、よくある落とし穴、Biome 固有のパターンを紹介します。使い慣れていない API に遭遇したときや、よくある間違いを避けたいときに参照してください。
前提条件
- Rust の基本的な知識
- Biome のアーキテクチャ(パーサー、アナライザー、フォーマッター)の理解
- 開発環境のセットアップ(CONTRIBUTING.md を参照)
よくある落とし穴とベストプラクティス
AST と構文ノードの扱い
推奨事項:
- 実装前に、パーサークレートの
quick_testを使って AST の構造を確認します - ノード階層と親子関係を把握します
- 一般的なケースと具体的な型の両方を確認します(例:Vue には
VueDirectiveとVueV*ShorthandDirectiveの両方があります) - 最初に見つけたものだけでなく、関連するすべてのバリアント型で解決策が機能することを確認します
- 呼び出し元に早期リターンを散在させるのではなく、
Option<T>またはSyntaxResult<T>を返すヘルパー関数を切り出します。これにより、コードの可読性と合成可能性が高まります
禁止事項:
- 構文を確認するためだけに Biome バイナリ全体をビルドしないでください(コストがかかります)。代わりにパーサークレートの
quick_testを使います - 最初に AST を調べずに、構文パターンを推測しないでください
例 — AST の確認:
// In crates/biome_html_parser/tests/quick_test.rs
// Modify the quick_test function:
#[test]
pub fn quick_test() {
let code = r#"<button on:click={handleClick}>Click</button>"#;
let source_type = HtmlFileSource::svelte();
let options = HtmlParserOptions::from(&source_type);
let root = parse_html(code, options);
dbg!(&root.syntax()); // Shows full AST structure
}
実行:just qt biome_html_parser
例 — CST ナビゲーションロジックの切り出し:
// WRONG: Many early returns scattered in the caller
fn visit_attribute(&self, attr: JsxAttribute, collector: &mut Collector) {
let Ok(name_node) = attr.name() else { return };
let name_text = match name_node {
AnyJsxAttributeName::JsxName(n) => match n.value_token() {
Ok(t) => t.token_text_trimmed(),
Err(_) => return,
},
AnyJsxAttributeName::JsxNamespaceName(_) => return,
};
if name_text != "class" && name_text != "className" {
return;
}
let Some(jsx_string) = attr.initializer().and_then(|i| i.value().ok()) else {
return;
};
// ... do the real work
}
// CORRECT: Extract helper that returns Option<T>
fn visit_attribute(&self, attr: JsxAttribute, collector: &mut Collector) {
if let Some(inner) = self.extract_class_attribute_inner(&attr) {
self.collect_classes(&inner, collector);
}
}
fn extract_class_attribute_inner(&self, attr: &JsxAttribute) -> Option<TokenText> {
let name_node = attr.name().ok()?;
let name_text = match name_node {
AnyJsxAttributeName::JsxName(n) => n.value_token().ok()?.token_text_trimmed(),
AnyJsxAttributeName::JsxNamespaceName(_) => return None,
};
if name_text != "class" && name_text != "className" {
return None;
}
let jsx_string = attr.initializer().and_then(|i| i.value().ok())?;
jsx_string.inner_string_text().ok()
}
このヘルパーでは ? 演算子と Option のコンビネーターを使っているため、else { return } ブロックを散在させるよりもはるかにすっきりしています。呼び出し元は、意図を明確に表す単一の if let Some になりました。
文字列の抽出とテキストの処理
推奨事項:
- 引用符で囲まれた文字列から内容を抽出するときは、
inner_string_text()を使います。前後の引用符を取り除き、同じグリーントークンに裏付けられたTokenTextを返すため、割り当てが発生しません - 先頭と末尾の空白を除いたトークン全体のテキストが必要な場合は、
text_trimmed()を使います HtmlAttributeNameなどのノードからテキスト内容を取得するには、token_text_trimmed()を使います- 値が
HtmlString(引用符)とHtmlTextExpression(中かっこ)のどちらを使っているか確認します - トークンの部分範囲を取得するには、
TokenText::slice()またはinner_string_text()を使います。どちらも同じGreenTokenに裏付けられたTokenTextを返すため、参照カウントが増えるだけでヒープ割り当ては発生しません
禁止事項:
- 引用符で囲まれた文字列の内容を抽出する必要があるときに、
inner_string_text()の代わりにtext_trimmed()を使わないでください SyntaxTokenに.text()を呼び出さないでください。前後のトリビア(空白、改行など)を含む生のテキストが返されます。必ず.text_trimmed()を使います&s[1..s.len()-1]で引用符を手動で取り除かないでください。代わりにinner_string_text()を使います。正確で、割り当てが発生せず、意図も明確に伝わります- 文字列トークンから分割した個々の単語を保存するために、
word.to_string()やString::from(word)を使わないでください。代わりに、トークン全体のTokenTextと、トークン内を基準とするTextRangeを保存します(下記参照)
例 — 文字列の抽出:
// WRONG: text_trimmed() includes quotes
let html_string = value.as_html_string()?;
let content = html_string.value_token()?.text_trimmed(); // Returns: "\"handler\""
// CORRECT: inner_string_text() removes quotes
let html_string = value.as_html_string()?;
let inner_text = html_string.inner_string_text().ok()?;
let content = inner_text.text(); // Returns: "handler"
例 — CssClassSelector からの CSS クラス名の抽出:
// WRONG: .text() includes trivia
let name = selector.name().ok()?.value_token().ok()?.text(); // may include whitespace
// CORRECT: always use text_trimmed() on SyntaxToken
let name: &str = selector.name().ok()?.value_token().ok()?.text_trimmed();
// For owned value:
let name: TokenText = selector.name().ok()?.value_token().ok()?.token_text_trimmed();
割り当てを行わずに分割したトークンの単語を保存する
文字列トークン(例:class="foo bar baz")を個々の単語に分割して保存する必要がある場合、単語ごとに String を割り当てないでください。代わりに、トークン全体の TokenText と、ファイル内ではなくトークンテキスト内を基準とする TextRange を保存します。
// WRONG: allocates a String per word
for word in content.split_ascii_whitespace() {
collected.push(word.to_string()); // heap allocation per word
}
// CORRECT: store token + token-relative range
// Use inner_string_text() to get the quote-stripped TokenText first.
let inner: TokenText = html_string.inner_string_text()?;
let content = inner.text();
let mut offset: u32 = 0;
for word in content.split_ascii_whitespace() {
let word_offset = content[offset as usize..]
.find(word)
.map_or(offset, |pos| offset + pos as u32);
let start = TextSize::from(word_offset);
let end = start + TextSize::from(word.len() as u32);
collected.push(MyEntry {
token: inner.clone(), // refcount bump only
range: TextRange::new(start, end),
});
offset = word_offset + word.len() as u32;
}
// Later, to read the word back:
fn text(&self) -> &str {
&self.token.text()[usize::from(self.range.start())..usize::from(self.range.end())]
}
要点:
inner_string_text()は、.text()が引用符を除いた内容のバイト 0 から始まるTokenTextを返します。その中の単語のオフセットは、トークン内を基準とする範囲としてそのまま使用できますTokenText::clone()は、基底のGreenTokenの参照カウントを増やすだけで、文字列データをコピーしません- トークン内を基準とする範囲からファイル全体での診断範囲を生成するには、トークンの絶対ファイルオフセットを加算します:
u32::from(value_token.text_trimmed_range().start()) + 1(+1は開始の引用符を飛ばします)
埋め込み言語の扱い
DO:
- 複数のフレームワークを扱う場合は、値の形式(引用符で囲まれた文字列とテキスト式)ごとに変更が機能することを確認します
- コンテキスト(Vue、Svelte、Astro など)に適した
EmbeddingKindを使用します - 埋め込みコンテンツに
is_source: true(script タグ)が必要か、is_source: false(テンプレート式)が必要かを確認します - オフセットを正しく計算します。開始引用符にはトークンの開始位置 + 1 を使用するか、テキスト式には
text_range().start()を使用します
DON'T:
- すべてのフレームワークで同じ構文が使われると想定しないでください(Vue では引用符、Svelte では波括弧を使用します)
- 根拠なく「広く使われている」パターン向けの機能を実装しないでください。まずユーザーに確認してください
例 — 異なる値の形式:
// Vue directives use quoted strings: @click="handler"
let html_string = value.as_html_string()?;
let inner_text = html_string.inner_string_text().ok()?;
// Svelte directives use text expressions: on:click={handler}
let text_expression = value.as_html_attribute_single_text_expression()?;
let expression = text_expression.expression().ok()?;
借用チェッカーと一時値
DO:
- 一時値への借用が破棄されてしまうのを避けるため、中間の
let束縛を使用します - 所有値を返すメソッドの結果は、そのメソッドに対してさらにメソッドを呼び出す前に保存します
DON'T:
- 使用前に破棄されてしまう一時値への借用を作成しないでください
例 — 借用に関する問題の回避:
// WRONG: Temporary borrow gets dropped
let html_string = value.value().ok()?.as_html_string()?;
let token = html_string.value_token().ok()?; // ERROR: html_string dropped
// CORRECT: Store intermediate result
let value_node = value.value().ok()?;
let html_string = value_node.as_html_string()?;
let token = html_string.value_token().ok()?; // OK
Clippy とコードスタイル
DO:
letチェーンを使ってネストしたif let文をまとめます(より簡潔で、Rust の慣用的な書き方です)- コミット前に
just lを実行して、Clippy の警告を検出します - Clippy の提案は、従わない十分な理由がない限り修正します
- 型は必ずファイルの先頭でインポートし、型の位置にクレートパスをインラインで書かないでください
- コンパイラーが型を推論できる場合は推論に任せ、明示的な型注釈はコンパイラーが推論できない場合にのみ追加します
DON'T:
- Clippy の警告を無視しないでください。実際の問題を見つけたり、よりよいパターンを提案したりすることがよくあります
- 型の位置にクレートパスをインラインで書かないでください(例:
Vec<some_crate::module::Type>)。代わりにuseインポートを追加してください - コンパイラーがすでに推論できる型に注釈を付けないでください
例 — 折りたたみ可能な If:
// WRONG: Nested if let (clippy::collapsible_if warning)
if let Some(directive) = VueDirective::cast_ref(&element) {
if let Some(initializer) = directive.initializer() {
// ... do something
}
}
// CORRECT: Use let chains
if let Some(directive) = VueDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
// ... do something
}
例 — パスをインラインで書かず、型をインポートする:
// WRONG: Inlined crate path in type position
enum Frame {
Visit(biome_css_semantic::model::RuleId),
}
// CORRECT: Import the type at the top of the file
use biome_css_semantic::model::RuleId;
enum Frame {
Visit(RuleId),
}
例 — コンパイラーの型推論に任せる:
// WRONG: Redundant type annotation — the compiler infers this from FxHashSet::default()
let mut visited: FxHashSet<RuleId> = FxHashSet::default();
// CORRECT: No annotation needed
let mut visited = FxHashSet::default();
コードコメント
コメントと rustdoc のスタイルについては、doc-comments スキルを参照してください。このスキルでは、読者の想定、/////////! のそれぞれの役割、削除テスト、禁止されているパターンを定めています。ここで同じガイダンスを重複して記載しないでください。
Cargo の依存関係:workspace = true と path = "..."
[dev-dependencies] に記載する内部の biome_* クレートでは、workspace = true ではなく、必ず path = "../<crate_name>" を使用してください。開発用依存関係に workspace = true を使用すると、Cargo がローカルのワークスペースではなくレジストリからクレートを解決する場合があり、これは誤りです。
通常の [dependencies] では、従来どおり workspace = true を使用します。このルールは [dev-dependencies] にのみ適用されます。
DO:
- すべての
biome_*開発用依存関係にpath = "../biome_foo"を使用します - 変換時に
featuresなどの追加属性を保持します
DON'T:
[dev-dependencies]のbiome_*クレートにworkspace = trueを使用しないでください
例:
# WRONG: may resolve from registry
[dev-dependencies]
biome_js_parser = { workspace = true }
biome_formatter = { workspace = true, features = ["countme"] }
# CORRECT: always resolves locally
[dev-dependencies]
biome_js_parser = { path = "../biome_js_parser" }
biome_formatter = { path = "../biome_formatter", features = ["countme"] }
すべてのクレートは crates/ の下に兄弟ディレクトリとして配置されているため、相対パスは常に ../biome_<name> です。
レガシー構文と非推奨構文
DO:
- 非推奨/レガシー構文への対応を実装する前に、ユーザーに確認します
- レガシー機能に時間をかける前に、ユーザーからの要望を待ちます
- レガシーであることを理由に機能を意図的にサポートしない場合は、その旨を記載します
DON'T:
- まずユーザーに確認することなく、レガシー/非推奨構文を実装しないでください
- 根拠なく、パターンが「広く使われている」または「一般的」だと主張しないでください
例:
Svelte の on:click イベントハンドラー構文はレガシーです(Svelte 3/4)。モダンな Svelte 5 の runes モードでは、通常の属性を使用します。ユーザーから特に要望がない限り、レガシー構文への対応は実装しないでください。
テストと開発
テスト用コマンド、スナップショットのワークフロー、コード生成については、 testing-codegen スキルを参照してください。Biome の開発パターンに特有の重要な注意事項は次のとおりです。
- enum を扱う場合は、複数のバリエーションを使ってテストします(例:すべての
VueV*ShorthandDirective型) - 埋め込み言語(Vue/Svelte のディレクティブなど)のテストには CLI テストを使用します
- アナライザーパッケージで埋め込み言語をテストしようとしないでください(埋め込み機能がありません)
パターンマッチングのヒント
ノードバリアントの扱い
enum バリアント(AnySvelteDirective など)を扱う場合は、対応が必要な非 enum 型も存在するか確認してください。
// Check AnySvelteDirective enum (bind:, class:, style:, etc.)
if let Some(directive) = AnySvelteDirective::cast_ref(&element) {
// Handle special Svelte directives
}
// But also check regular HTML attributes with specific prefixes
if let Some(attribute) = HtmlAttribute::cast_ref(&element) {
if let Ok(name) = attribute.name() {
// Some directives might be parsed as regular attributes
}
}
複数のバリアント型の確認
複数のディレクティブ構文を持つフレームワークでは、それぞれの型に対応してください。
// Vue has multiple shorthand types
if let Some(directive) = VueVOnShorthandDirective::cast_ref(&element) {
// Handle @click
}
if let Some(directive) = VueVBindShorthandDirective::cast_ref(&element) {
// Handle :prop
}
if let Some(directive) = VueVSlotShorthandDirective::cast_ref(&element) {
// Handle #slot
}
if let Some(directive) = VueDirective::cast_ref(&element) {
// Handle v-if, v-show, etc.
}
よくある API の混同
文字列/テキストのメソッド
| メソッド | 使用する場面 | 戻り値 |
|---|---|---|
inner_string_text() | 引用符で囲まれた文字列から内容を抽出する場合 | TokenText としての引用符を除いた内容(割り当てなし) |
text_trimmed() | 空白を除いたトークンテキストを取得する場合 | &str — トークンのテキスト全体 |
token_text_trimmed() | 所有され、クローン可能なトークンテキストを取得する場合 | TokenText — green token を参照 |
text() | トリビアを含む生テキストを取得する場合 | &str — 記述されたとおりの正確なテキスト |
Text と TokenText と String
| 型 | サイズ | クローンのコスト | 使用する場面 |
|---|---|---|---|
TokenText | 16 bytes | 参照カウントの増加 | SyntaxToken があり、割り当てを伴わない所有権が必要な場合 |
Text | 16 bytes | 参照カウントの増加(token)またはヒープコピー(owned) | TokenText と所有文字列の共用型です。ソースが token でない可能性がある場合に使用します |
String | 24 bytes | ヒープコピー | 所有され、変更可能な文字列が実際に必要な場合のみ使用します(診断メッセージなど) |
Text はより豊かな型です。From<TokenText> が実装されているため、TokenText は常に低コストで Text にラップできます。構文トークンから直接抽出したデータを保存する場合は、TokenText または token+range パターンを優先してください。
値の抽出メソッド
| 型 | メソッド | フレームワーク |
|---|---|---|
HtmlString | inner_string_text() | Vue(引用符) |
HtmlAttributeSingleTextExpression | expression() | Svelte(波括弧) |
HtmlTextExpression | html_literal_token() | テンプレート式 |
参考資料
- 主なコントリビューションガイド:
../../CONTRIBUTING.md - テストのワークフロー:
../testing-codegen/SKILL.md - パーサー開発:
../parser-development/SKILL.md - Biome 内部のドキュメント:https://biomejs.dev/internals
ドキュメントと Markdown の書式設定
推奨事項:
- 表の区切り文字の前後にスペースを入れます:
| --- | --- | --- |(|---|---|---|ではなく) - すべての Markdown 表が適切なスペースを使った「コンパクト」スタイルになっていることを確認します
- コミットする前に、Markdown リンターでドキュメントの変更をテストします
非推奨事項:
- スペースのないコンパクトな表の区切り文字は使用しないでください(CI の lint チェックで失敗します)
例 — 表の書式設定:
| Method | Use When | Returns |
|--------|----------|---------|
| Method | Use When | Returns |
| --- | --- | --- |
CI では markdownlint-cli2 を使用しており、スペースを必要とする「コンパクト」スタイルを適用します。
避けるべきよくある間違い
markup!ブロックで文字列をフォーマットする際に、format!()(文字列を割り当てます)を呼び出すこと。markup!は補間をサポートしています。例:markup! { "Hello, "{name}"!" }。SyntaxTokenまたはSyntaxNodeに対して.to_string()や.to_string_trimmed()(文字列を割り当てます)を呼び出すこと。構文ノードに対してこれらのメソッドを呼び出す必要は、まずありません。構文トークンの場合は、token.text()を使えば新しい文字列を割り当てずにトークンのテキストから&strを簡単に借用できます。
このスキルを使用する場面
次の場合にこのスキルを読み込んでください:
- なじみのない Biome API を扱う場合
- 一時値に関する借用チェッカーのエラーが発生した場合
- 構文ノードから文字列やテキストを抽出する場合
- 埋め込み言語(Vue、Svelte など)のサポートを実装する場合
- AST の検査結果が予想と異なる理由を調べる場合
- レガシー構文や非推奨構文のサポートについて判断する場合
- Markdown ドキュメントを作成または更新する場合