关于 biome-developer
为 Biome 代码库开发者提供 Rust 开发实践与常见问题参考,涉及语法树处理、文本提取、嵌入语言、Cargo 依赖和代码风格。适合在修改解析器、分析器或格式化相关代码时查阅。
主要功能
- 01检查 AST 与语法节点结构
- 02正确提取令牌文本与范围
- 03处理嵌入语言及不同语法变体
- 04按规则配置 Cargo 依赖
- 05遵循 Rust 与 Clippy 习惯
适用场景
- 01开发 Biome 解析器或分析器
- 02处理 HTML、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 和语法节点
应该:
- 实现之前,使用解析器 crate 的
quick_test检查 AST 结构 - 了解节点层级以及父子关系
- 检查一般情况和特定类型(例如,Vue 同时有
VueDirective和VueV*ShorthandDirective) - 确认解决方案适用于所有相关变体,而不只是你首先找到的那个
- 提取返回
Option<T>或SyntaxResult<T>的辅助函数,而不是在调用方中散布提前返回语句——这样代码更易读,也更易组合
不应该:
- 不要只为检查语法就构建完整的 Biome 二进制文件(开销很大)——请改用解析器 crate 的
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()——它会去除外围引号,并返回由同一个 green token 支持的TokenText(不会分配内存) - 需要不含前导或尾随空白的完整 token 文本时,使用
text_trimmed() - 对
HtmlAttributeName等节点使用token_text_trimmed()获取文本内容 - 确认值使用的是
HtmlString(引号)还是HtmlTextExpression(花括号) - 使用
TokenText::slice()或inner_string_text()获取 token 的子范围——两者都会返回由同一个GreenToken支持的TokenText(只增加引用计数,不进行堆分配)
不应该:
- 需要提取带引号的字符串内容时,不要使用
text_trimmed()代替inner_string_text() - 不要对
SyntaxToken调用.text()——它会返回包含周围 trivia(空白、换行)的原始文本。请始终使用.text_trimmed() - 不要用
&s[1..s.len()-1]手动去除引号——请改用inner_string_text();它处理正确、无需分配内存,并能清晰表达意图 - 不要使用
word.to_string()或String::from(word)存储从字符串 token 中拆分出的单个单词——应改为存储整个 token 的TokenText,以及相对于 token 的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();
无分配地存储拆分后的 Token 单词
需要将字符串 token(例如 class="foo bar baz")拆分为单个单词并存储时,不要为每个单词分配一个 String。应改为存储整个 token 的 TokenText,以及一个相对于 token 文本(而非文件)的 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()返回的TokenText中,.text()的起始位置是未加引号内容的字节 0。单词在其中的偏移可直接用作相对于 token 的范围。TokenText::clone()只会增加底层GreenToken的引用计数,不会复制字符串数据。- 要根据相对于 token 的范围生成文件级诊断范围,请加上 token 在文件中的绝对偏移:
u32::from(value_token.text_trimmed_range().start()) + 1(+1用于跳过左引号)。
处理嵌入式语言
应该:
- 处理多个框架时,验证更改对不同的值格式(带引号的字符串与文本表达式)是否有效
- 根据上下文使用合适的
EmbeddingKind(Vue、Svelte、Astro 等) - 检查嵌入内容是否需要设置
is_source: true(script 标签)或is_source: false(模板表达式) - 正确计算偏移量:起始令牌位置 + 1(开引号),或对文本表达式使用
text_range().start()
不应该:
- 不要假设所有框架都使用相同语法(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()?;
借用检查器与临时值
应该:
- 使用中间
let绑定,避免借用即将被释放的临时值 - 在对返回拥有所有权的值的方法结果调用其他方法前,先存储该结果
不应该:
- 不要创建在使用前就会被释放的临时值借用
示例——避免借用问题:
// 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 与代码风格
应该:
- 使用
let链合并嵌套的if let语句(更简洁,也符合 Rust 惯用法) - 提交前运行
just l,以发现 clippy 警告 - 除非有充分理由,否则修复 clippy 建议的问题
- 始终在文件顶部导入类型——不要在类型位置内联 crate 路径
- 编译器能够推断类型时,就让编译器推断——只有编译器无法推断时才添加显式类型标注
不应该:
- 不要忽略 clippy 警告——它们通常能发现实际问题或建议更好的模式
- 不要在类型位置内联 crate 路径(例如
Vec<some_crate::module::Type>)——应改用use导入 - 不要为编译器已经能够推断的类型添加标注
示例——合并条件语句:
// 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_* crate 必须使用 path = "../<crate_name>",而非 workspace = true。对开发依赖使用 workspace = true 可能导致 Cargo 从注册表而非本地工作区解析 crate,这是不正确的。
常规 [dependencies] 仍按惯例使用 workspace = true——此规则仅适用于 [dev-dependencies]。
应该:
- 所有
biome_*开发依赖都使用path = "../biome_foo" - 转换时保留
features等额外属性
不应该:
- 不要对
[dev-dependencies]中的biome_*crate 使用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"] }
所有 crate 都作为同级目录位于 crates/ 下,因此相对路径始终是 ../biome_<name>。
旧版与已弃用语法
应该:
- 实现已弃用或旧版语法支持前先询问用户
- 等用户提出需求后,再投入时间开发旧版功能
- 说明因属于旧版功能而有意不提供支持的功能
不应该:
- 未先与用户确认,不要实现旧版或已弃用语法
- 没有证据时,不要声称某种模式“广泛使用”或“常见”
示例:
Svelte 的 on:click 事件处理器语法属于旧版语法(Svelte 3/4)。现代 Svelte 5 的 runes 模式使用常规属性。除非用户明确提出需求,否则不要实现对旧版语法的支持。
测试与开发
有关测试命令、快照工作流和代码生成,请参阅 testing-codegen 技能。Biome 开发模式的关键提醒:
- 使用枚举时,针对多个变体进行测试(例如,所有
VueV*ShorthandDirective类型) - 测试嵌入式语言(Vue/Svelte 指令等)时使用 CLI 测试
- 不要尝试在分析器包中测试嵌入式语言(它们不具备嵌入能力)
模式匹配提示
处理节点变体
处理枚举变体(如 AnySvelteDirective)时,请检查是否还有需要处理的非枚举类型:
// 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 令牌支持 |
text() | 获取包含 trivia 的原始文本 | &str — 按原样书写的确切文本 |
Text 与 TokenText 与 String
| 类型 | 大小 | 克隆成本 | 使用场景 |
|---|---|---|---|
TokenText | 16 字节 | 增加引用计数 | 你有一个 SyntaxToken,并希望在不分配内存的情况下取得所有权 |
Text | 16 字节 | 增加引用计数(令牌)或堆复制(拥有所有权) | TokenText 与拥有所有权的字符串的联合类型 — 源可能不是令牌时使用 |
String | 24 字节 | 堆复制 | 只有在确实需要拥有所有权且可变的字符串时使用(例如,用于诊断消息) |
Text 是更丰富的类型:已实现 From<TokenText>,因此可以始终以低成本将 TokenText 包装在 Text 中。存储直接从语法令牌提取的数据时,优先使用 TokenText 或令牌+范围模式。
值提取方法
| 类型 | 方法 | 框架 |
|---|---|---|
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 检查失败)
示例 - 表格格式:
| 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 文档