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.
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.
DO:
quick_test to inspect AST structure before implementingVueDirective and VueV*ShorthandDirective)DON'T:
quick_test insteadExample - 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
DO:
inner_string_text() when extracting content from quoted strings (removes quotes)text_trimmed() when you need the full token text without leading/trailing whitespacetoken_text_trimmed() on nodes like HtmlAttributeName to get the text contentHtmlString (quotes) or HtmlTextExpression (curly braces)DON'T:
text_trimmed() when you need inner_string_text() for extracting quoted string contentsExample - 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"
DO:
EmbeddingKind for context (Vue, Svelte, Astro, etc.)is_source: true (script tags) vs is_source: false (template expressions)text_range().start() for text expressionsDON'T:
Example - Different Value Formats:
// 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()?;
DO:
let bindings to avoid temporary value borrows that get droppedDON'T:
Example - Avoiding Borrow Issues:
// 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
DO:
let chains to collapse nested if let statements (cleaner and follows Rust idioms)just l before committing to catch clippy warningsDON'T:
Example - Collapsible If:
// 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
}
Comments exist for the next developer who reads this code, not for the developer currently writing it. Write them like you are explaining the code to a colleague who walked into the room ten minutes ago — not to a reviewer on this specific PR.
DO:
<style> block") over abstract ones ("the host CST", "the delegated pipeline") when both convey the same ideaDON'T:
// 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.// Fix for Astro embedding. Write a comment that describes the general condition being handled.<style>" — describe the contract the code enforces for any embed, and use a concrete example only as illustration.// Invariant:, // Precondition:, // Lemma: unless the surrounding code genuinely uses those terms. For most Biome code, plain prose ("When X happens, Y must hold, otherwise …") reads better and is just as precise.Think big picture, not current task. Before writing a comment, ask three things:
Example 1 — issue/task context and over-specificity:
// 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);
}
Example 2 — jargon, narrow scope, and abstraction mismatch. This is a real example from a generic helper that replaces an embedded snippet inside any host document (HTML, Vue, Svelte, Astro, …):
// WRONG: starts with formal-methods jargon, names a specific case
// (`<style>`) even though the function handles any embed, and stacks
// technical terms ("host CST token text", "delegated pipeline") that a
// new reader has to decode before they can understand the point.
// Invariant: for a file that required no fix actions, `fix_file` and
// `format_file` must produce byte-identical output. For `<style>`
// blocks, `fix_all`'s final format pass prints embedded content
// verbatim from the host CST token text, while `format_file` routes
// through the delegated `format_embedded` pipeline and re-wraps the
// result with the host's indent. …
// CORRECT: plain language, stays at the generic level of the function,
// uses `<style>` only as a parenthetical example, and is understandable
// without prior context.
// The embedded formatter (e.g. the CSS formatter for a <style> block)
// doesn't know how deeply its code is nested inside the HTML file, so
// it always returns the code indented from column zero. If we pasted
// that code back as-is, only the first line would get the HTML
// indentation (from the leading whitespace we already captured); every
// other line would end up too far to the left. Add the same indentation
// to every line so the embed lines up with its surroundings.
The corrected comment names one concrete example (<style> / CSS) to make the reader's mental picture vivid, but the rest of the sentence is generic enough to cover any host/embed pair. That is the balance to aim for.
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:
path = "../biome_foo" for all biome_* dev-dependenciesfeatures when convertingDON'T:
workspace = true for biome_* crates in [dev-dependencies]Example:
# 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>.
DO:
DON'T:
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.
For testing commands, snapshot workflows, and code generation, see the testing-codegen skill. Key reminders specific to Biome development patterns:
VueV*ShorthandDirective types)When working with enum variants (like AnySvelteDirective), check if there are also non-enum types that need handling:
// 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
}
}
For frameworks with multiple directive syntaxes, handle each type:
// 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.
}
| 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 |
| Type | Method | Framework |
|---|---|---|
HtmlString | inner_string_text() | Vue (quotes) |
HtmlAttributeSingleTextExpression | expression() | Svelte (curly braces) |
HtmlTextExpression | html_literal_token() | Template expressions |
../../CONTRIBUTING.md../testing-codegen/SKILL.md../parser-development/SKILL.mdDO:
| --- | --- | --- | (not |---|---|---|)DON'T:
Example - Table Formatting:
<!-- 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.
format!() (allocates a string) when formatting strings in a markup! block. markup! supports interpolation, E.g. markup! { "Hello, "{name}"!" }..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().Load this skill when: