# 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.

- Skill: `qdhenry/biome-developer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add qdhenry/biome-developer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qdhenry/biome-developer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: qdhenry (https://skillmd.com/u/qdhenry)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qdhenry/biome-developer

---


## 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

**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:**
```rust
// 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`

### String Extraction and Text Handling

**DO:**
- Use `inner_string_text()` when extracting content from quoted strings (removes quotes)
- 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)

**DON'T:**
- Do NOT use `text_trimmed()` when you need `inner_string_text()` for extracting quoted string contents

**Example - String Extraction:**
```rust
// 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"
```

### Working with Embedded Languages

**DO:**
- Verify changes work for different value formats (quoted strings vs text expressions) when handling multiple frameworks
- Use appropriate `EmbeddingKind` for context (Vue, Svelte, Astro, etc.)
- Check if embedded content needs `is_source: true` (script tags) vs `is_source: false` (template expressions)
- Calculate offsets correctly: token start + 1 for opening quote, or use `text_range().start()` for text expressions

**DON'T:**
- Do NOT assume all frameworks use the same syntax (Vue uses quotes, Svelte uses curly braces)
- Do NOT implement features for "widely used" patterns without evidence - ask the user first

**Example - Different Value Formats:**
```rust
// 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()?;
```

### Borrow Checker and Temporary Values

**DO:**
- Use intermediate `let` bindings to avoid temporary value borrows that get dropped
- Store method results that return owned values before calling methods on them

**DON'T:**
- Do NOT create temporary value borrows that get dropped before use

**Example - Avoiding Borrow Issues:**
```rust
// 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 and Code Style

**DO:**
- Use `let` chains to collapse nested `if let` statements (cleaner and follows Rust idioms)
- Run `just l` before committing to catch clippy warnings
- Fix clippy suggestions unless there's a good reason not to

**DON'T:**
- Do NOT ignore clippy warnings - they often catch real issues or suggest better patterns

**Example - Collapsible If:**
```rust
// 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
}
```

### Code Comments

Comments exist for the next developer who reads this code, not for the developer currently writing it.

**DO:**
- Explain code that is hard to read, or document exceptions and edge cases
- Provide context when names alone are not descriptive enough
- Describe the business logic a function implements
- Clarify contextual words like "normalize" — e.g., "normalize a file path" and "normalize a URL" mean different things; spell out what normalization means here

**DON'T:**
- Do NOT embed the context of the current work into comments. A comment like `// As per issue #1234, we skip this case` ties the code to a transient artifact. Instead, explain *why* the case is skipped in terms any future reader would understand.
- Do NOT scope comments to the specific trigger that prompted the change. For example, if a bug was reported for Astro but the fix applies broadly, do NOT write `// Fix for Astro embedding`. Write a comment that describes the general condition being handled.

**Think big picture, not current task.** Before writing a comment, ask: "If someone reads this a year from now with no knowledge of the issue or PR, does this comment give them the context they need?"

**Example:**
```rust
// WRONG: Carries issue/task context
// Fix for #5678: Astro files need special handling here
if is_embedded_script(node) {
    return normalize_offset(node);
}

// WRONG: Describes what the code does (the code already says that)
// Check if the node is an embedded script and normalize the offset
if is_embedded_script(node) {
    return normalize_offset(node);
}

// CORRECT: Explains why and clarifies "normalize"
// Embedded script blocks (e.g. <script> inside .vue/.svelte/.astro files)
// report offsets relative to the embedding document, not the script itself.
// Normalize here means: subtract the script block's start position so the
// offset is relative to the script content.
if is_embedded_script(node) {
    return normalize_offset(node);
}
```

### Cargo Dependencies: `workspace = true` vs `path = "..."`

Internal `biome_*` crates listed under `[dev-dependencies]` **MUST** use `path = "../<crate_name>"`, not `workspace = true`. Using `workspace = true` for dev-dependencies can cause Cargo to resolve the crate from the registry instead of the local workspace, which is incorrect.

Regular `[dependencies]` still use `workspace = true` as normal — this rule only applies to `[dev-dependencies]`.

**DO:**
- Use `path = "../biome_foo"` for all `biome_*` dev-dependencies
- Preserve any extra attributes like `features` when converting

**DON'T:**
- Do NOT use `workspace = true` for `biome_*` crates in `[dev-dependencies]`

**Example:**
```toml
# 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"] }
```

All crates live as siblings under `crates/`, so the relative path is always `../biome_<name>`.

### Legacy and Deprecated Syntax

**DO:**
- Ask users before implementing deprecated/legacy syntax support
- Wait for user demand before spending time on legacy features
- Document when features are intentionally not supported due to being legacy

**DON'T:**
- Do NOT implement legacy/deprecated syntax without checking with the user first
- Do NOT claim patterns are "widely used" or "common" without evidence

**Example:**
Svelte's `on:click` event handler syntax is legacy (Svelte 3/4). Modern Svelte 5 runes mode uses regular attributes. Unless users specifically request it, don't implement legacy syntax support.

### Testing and Development

For testing commands, snapshot workflows, and code generation, see the
[testing-codegen](../testing-codegen/SKILL.md) skill. Key reminders specific to
Biome development patterns:

- Test with multiple variants when working with enums (e.g., all `VueV*ShorthandDirective` types)
- Use CLI tests for testing embedded languages (Vue/Svelte directives, etc.)
- Do NOT try to test embedded languages in analyzer packages (they don't have embedding capabilities)

## Pattern Matching Tips

### Working with Node Variants

When working with enum variants (like `AnySvelteDirective`), check if there are also non-enum types that need handling:

```rust
// 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
    }
}
```

### Checking Multiple Variant Types

For frameworks with multiple directive syntaxes, handle each type:

```rust
// 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.
}
```

## Common API Confusion

### String/Text Methods

| Method | Use When | Returns |
| --- | --- | --- |
| `inner_string_text()` | Extracting content from quoted strings | Content without quotes |
| `text_trimmed()` | Getting token text without whitespace | Full token text |
| `token_text_trimmed()` | Getting text from nodes like `HtmlAttributeName` | Node text content |
| `text()` | Getting raw text | Exact text as written |

### Value Extraction Methods

| Type | Method | Framework |
| --- | --- | --- |
| `HtmlString` | `inner_string_text()` | Vue (quotes) |
| `HtmlAttributeSingleTextExpression` | `expression()` | Svelte (curly braces) |
| `HtmlTextExpression` | `html_literal_token()` | Template expressions |

## References

- Main contributing guide: `../../CONTRIBUTING.md`
- Testing workflows: `../testing-codegen/SKILL.md`
- Parser development: `../parser-development/SKILL.md`
- Biome internals docs: https://biomejs.dev/internals

## Documentation and Markdown Formatting

**DO:**
- Use spaces around table separators: `| --- | --- | --- |` (not `|---|---|---|`)
- Ensure all Markdown tables follow "compact" style with proper spacing
- Test documentation changes with markdown linters before committing

**DON'T:**
- Do NOT use compact table separators without spaces (causes CI linting failures)

**Example - Table Formatting:**
```markdown
<!-- WRONG: No spaces around separators -->
| Method | Use When | Returns |
|--------|----------|---------|

<!-- CORRECT: Spaces around separators -->
| Method | Use When | Returns |
| --- | --- | --- |
```

The CI uses `markdownlint-cli2` which enforces the "compact" style requiring spaces.

## When to Use This Skill

Load this skill when:
- Working with unfamiliar Biome APIs
- Getting borrow checker errors with temporary values
- Extracting strings or text from syntax nodes
- Implementing support for embedded languages (Vue, Svelte, etc.)
- Wondering why your AST inspection doesn't match expectations
- Making decisions about legacy/deprecated syntax support
- Writing or updating markdown documentation

