Skip to content
Development
Skill

/biome-developer

General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.

From plugin
biome
26k12 skills
Install
$ npx -y skills add biomejs/biome --skill biome-developer --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/biome-developer

Context preview

The summary Claude sees to decide when to auto-load this skill.

General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.

SKILL.md

biome-developer.SKILL.md
name: biome-developer
description: General development best practices and common gotchas when working on Biome. Use for avoiding common mistakes, understanding Biome-specific patterns (AST, syntax nodes, string extraction, embedded languages), and learning technical tips.
compatibility: Designed for coding agents working on the Biome codebase (github.com/biomejs/biome).

Purpose

This skill provides general development best practices, common gotchas, and Biome-specific patterns that apply across different areas of the codebase. Use this as a reference when you encounter unfamiliar APIs or need to avoid common mistakes.

Prerequisites

  • Basic familiarity with Rust
  • Understanding of Biome's architecture (parser, analyzer, formatter)
  • Development environment set up (see CONTRIBUTING.md)

Common Gotchas and Best Practices

Working with AST and Syntax Nodes

**DO:**

  • Use parser crate's `quick_test` to inspect AST structure before implementing
  • Understand the node hierarchy and parent-child relationships
  • Check both general cases AND specific types (e.g., Vue has both `VueDirective` and `VueV*ShorthandDirective`)
  • Verify your solution works for all relevant variant types, not just the first one you find
  • Extract helper functions that return `Option<T>` or `SyntaxResult<T>` instead of scattering early returns throughout the caller — this makes code more readable and composable

**DON'T:**

  • Do NOT build the full Biome binary just to inspect syntax (expensive) - use parser crate's `quick_test` instead
  • Do NOT assume syntax patterns without inspecting the AST first

**Example - Inspecting 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
}

Run: `just qt biome_html_parser`

**Example - Extracting CST Navigation Logic:**

// 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()
}

The helper uses `?` operator and `Option` combinators — much cleaner than scattered `else { return }` blocks. The caller now has a single `if let Some` that clearly expresses intent.

String Extraction and Text Handling

**DO:**

  • Use `inner_string_text()` when extracting content from quoted strings — it strips the surrounding quotes and returns a `TokenText` backed by the same green token (no allocation)
  • Use `text_trimmed()` when you need the full token text without leading/trailing whitespace
  • Use `token_text_trimmed()` on nodes like `HtmlAttributeName` to get the text content
  • Verify whether values use `HtmlString` (quotes) or `HtmlTextExpression` (curly braces)
  • Use `TokenText::slice()` or `inner_string_text()` to get sub-ranges of a token — both return a `TokenText` backed by the same `GreenToken` (ref-count bump only, no heap allocation)

**DON'T:**

  • Use `text_trimmed()` when you need `inner_string_text()` for extracting quoted string contents
  • Call `.text()` on a `SyntaxToken` — it returns raw text including surrounding trivia (whitespace, newlines). Always use `.text_trimmed()` instead.
  • Strip quotes manually with `&s[1..s.len()-1]` — use `inner_string_text()` instead; it is correct, allocation-free, and communicates intent
  • Use `word.to_string()` or `String::from(word)` to store individual words split out of a string token — store the `TokenText` of the whole token plus a token-relative `TextRange` instead (see below)

**Example - String Extraction:**

// 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"

**Example - CSS class name extraction from `CssClassSelector`:**

// 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();

Storing Split Token Words Without Allocation

When you need to split a string token (e.g. `class="foo bar baz"`) into i

Read more
Ships withbiome

A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.

Get the whole plugin
Stats
25,543
Stars
1,180
Forks
Active
Maintenance
Rust
Language
Apache-2.0
License
42m ago
Last commit
3y ago
Created

Repo: biomejs/biome

Other skills on biome.