--- id: biomejs/biome/biome-developer version: "14c440f8" license: Apache-2.0 install: manual updated: 2026-07-27 --- # biome-developer — This skill guides developers working on Biome's codebase through essential patterns and pitfalls. It covers AST and syntax node navigation, efficient string extraction without allocation, and handling embedded languages across frameworks like Vue and Svelte. Use it to write cleaner, more maintainable code and avoid costly mistakes when extending Biome's parser, analyzer, and formatter. Publisher: biomejs · Stars: 25418 · Updated: 2026-07-27 Install (manual): `git clone https://github.com/biomejs/biome` ## SKILL.md ## 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` or `SyntaxResult` 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:** ```rust // In crates/biome_html_parser/tests/quick_test.rs // Modify the quick_test function: #[test] pub fn quick_test() { let code = r#""#; 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:** ```rust // 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 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 { 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:** ```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" ``` **Example - CSS class name extraction from `CssClassSelector`:** ```rust // 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 individual words and store each word, do **not** allocate a `String` per word. Instead, store the `TokenText` of the whole token and a `TextRange` that is **relative to the token text** (not the file). ```rust // 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())] } ``` Key points: - `inner_string_text()` returns a `TokenText` whose `.text()` starts at byte 0 of the unquoted content. Word offsets within that are directly usable as token-relative ranges. - `TokenText::clone()` is a refcount bump on the underlying `GreenToken` — it does not copy string data. - To produce file-level diagnostic ranges from token-relative ranges, add the token's absolute file offset: `u32::from(value_token.text_trimmed_range().start()) + 1` (the `+1` skips the opening quote). ### 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 - Always import types at the top of the file — never inline crate paths in type positions - Let the compiler infer types when it can — only add explicit type annotations when the compiler cannot infer them **DON'T:** - Do NOT ignore clippy warnings - they often catch real issues or suggest better patterns - Do NOT inline crate paths in type positions (e.g. `Vec`) — add a `use` import instead - Do NOT annotate types that the compiler can already infer **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 } ``` **Example - Import types, don't inline paths:** ```rust // 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), } ``` **Example - Let the compiler infer types:** ```rust // WRONG: Redundant type annotation — the compiler infers this from FxHashSet::default() let mut visited: FxHashSet = FxHashSet::default(); // CORRECT: No annotation needed let mut visited = FxHashSet::default(); ``` ### Code Comments For comment and rustdoc style, see the [doc-comments](../doc-comments/SKILL.md) skill. It defines who the reader is, the separate jobs of `//` / `///` / `//!`, the deletion test, and the banned patterns. Do not duplicate that guidance here. ### Cargo Dependencies: `workspace = true` vs `path = "..."` Internal `biome_*` crates listed under `[dev-dependencies]` **MUST** use `path = "../"`, 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_`. ### 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, as `TokenText` (no alloc) | | `text_trimmed()` | Getting token text without whitespace | `&str` — full token text | | `token_text_trimmed()` | Getting an owned, cloneable token text | `TokenText` — backed by green token | | `text()` | Getting raw text including trivia | `&str` — exact text as written | ### `Text` vs `TokenText` vs `String` | Type | Size | Clone cost | Use when | | --- | --- | --- | --- | | `TokenText` | 16 bytes | Refcount bump | You have a `SyntaxToken` and want allocation-free ownership | | `Text` | 16 bytes | Refcount bump (token) or heap copy (owned) | Union of `TokenText` and an owned string — use when the source may not be a token | | `String` | 24 bytes | Heap copy | Only when you actually need an owned, mutable string (e.g. for a diagnostic message) | `Text` is the richer type: `From` is implemented, so a `TokenText` can always be cheaply wrapped in `Text`. When storing data extracted directly from a syntax token, prefer `TokenText` or the token+range pattern. ### 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 | Method | Use When | Returns | |--------|----------|---------| | Method | Use When | Returns | | --- | --- | --- | ``` The CI uses `markdownlint-cli2` which enforces the "compact" style requiring spaces. ## Common Mistakes to Avoid - Calling `format!()` (allocates a string) when formatting strings in a `markup!` block. `markup!` supports interpolation, E.g. `markup! { "Hello, "{name}"!" }`. - Calling `.to_string()` or `.to_string_trimmed()` (allocates a string) on a `SyntaxToken` or `SyntaxNode`. It's highly unlikely that you actually need to call these methods on a syntax node. As for syntax tokens, you can easily borrow a `&str` from the token's text without allocating a new string, using `token.text()`. ## 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 [View on SkillFed](https://skillfed.io/biomejs/biome/biome-developer) · [View on GitHub](https://github.com/biomejs/biome)