mcpField
B
biome-developer

biome-developer

github.com/biomejs开发26.0k308 次安装今天更新
代码仓库

关于 biome-developer

为 Biome 代码库开发者提供 Rust 开发实践与常见问题参考,涉及语法树处理、文本提取、嵌入语言、Cargo 依赖和代码风格。适合在修改解析器、分析器或格式化相关代码时查阅。

主要功能

  1. 01检查 AST 与语法节点结构
  2. 02正确提取令牌文本与范围
  3. 03处理嵌入语言及不同语法变体
  4. 04按规则配置 Cargo 依赖
  5. 05遵循 Rust 与 Clippy 习惯

适用场景

  1. 01开发 Biome 解析器或分析器
  2. 02处理 HTML、Vue 或 Svelte 语法
  3. 03减少文本处理中的分配
  4. 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
类型大小克隆成本使用场景
TokenText16 字节增加引用计数你有一个 SyntaxToken,并希望在不分配内存的情况下取得所有权
Text16 字节增加引用计数(令牌)或堆复制(拥有所有权)TokenText 与拥有所有权的字符串的联合类型 — 源可能不是令牌时使用
String24 字节堆复制只有在确实需要拥有所有权且可变的字符串时使用(例如,用于诊断消息)

Text 是更丰富的类型:已实现 From<TokenText>,因此可以始终以低成本将 TokenText 包装在 Text 中。存储直接从语法令牌提取的数据时,优先使用 TokenText 或令牌+范围模式。

值提取方法
类型方法框架
HtmlStringinner_string_text()Vue(引号)
HtmlAttributeSingleTextExpressionexpression()Svelte(花括号)
HtmlTextExpressionhtml_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 文档

评论

支持 Markdown