formatter-development

Use this skill when implementing or modifying Biome's formatters. It covers the trait-based formatting system, IR generation, comment handling, and testing with Prettier comparison.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "formatter-development" with this command: npx skills add biomejs/biome/biomejs-biome-formatter-development

Purpose

Use this skill when implementing or modifying Biome's formatters. It covers the trait-based formatting system, IR generation, comment handling, and testing with Prettier comparison.

Prerequisites

  • Install required tools: just install-tools (includes wasm-bindgen-cli and wasm-opt )

  • Language-specific crates must exist: biome_{lang}syntax , biome{lang}_formatter

  • For Prettier comparison: Install bun and run pnpm install in repo root

Common Workflows

Generate Formatter Boilerplate

For a new language (e.g., HTML):

just gen-formatter html

This generates FormatNodeRule implementations for all syntax nodes. Initial implementations use format_verbatim_node (formats code as-is).

Implement FormatNodeRule for a Node

Example: Formatting JsIfStatement :

use crate::prelude::*; use biome_formatter::write; use biome_js_syntax::{JsIfStatement, JsIfStatementFields};

#[derive(Debug, Clone, Default)] pub(crate) struct FormatJsIfStatement;

impl FormatNodeRule<JsIfStatement> for FormatJsIfStatement { fn fmt_fields(&self, node: &JsIfStatement, f: &mut JsFormatter) -> FormatResult<()> { let JsIfStatementFields { if_token, l_paren_token, test, r_paren_token, consequent, else_clause, } = node.as_fields();

    write!(
        f,
        [
            if_token.format(),
            space(),
            l_paren_token.format(),
            test.format(),
            r_paren_token.format(),
            space(),
            consequent.format(),
        ]
    )?;

    if let Some(else_clause) = else_clause {
        write!(f, [space(), else_clause.format()])?;
    }

    Ok(())
}

}

Using IR Primitives

Common formatting building blocks:

use biome_formatter::{format_args, write};

write!(f, [ token("if"), // Static text space(), // Single space soft_line_break(), // Break if line is too long hard_line_break(), // Always break

// Grouping and indentation
group(&#x26;format_args![
    token("("),
    soft_block_indent(&#x26;format_args![
        node.test.format(),
    ]),
    token(")"),
]),

// Conditional formatting
format_with(|f| {
    if condition {
        write!(f, [token("something")])
    } else {
        write!(f, [token("other")])
    }
}),

])?;

Handle Comments

use biome_formatter::format_args; use biome_formatter::prelude::*;

impl FormatNodeRule<JsObjectExpression> for FormatJsObjectExpression { fn fmt_fields(&self, node: &JsObjectExpression, f: &mut JsFormatter) -> FormatResult<()> { let JsObjectExpressionFields { l_curly_token, members, r_curly_token, } = node.as_fields();

    write!(
        f,
        [
            l_curly_token.format(),
            block_indent(&#x26;format_args![
                members.format(),
                // Handle dangling comments (comments not attached to any node)
                format_dangling_comments(node.syntax()).with_soft_block_indent()
            ]),
            r_curly_token.format(),
        ]
    )
}

}

Leading and trailing comments are handled automatically by the formatter infrastructure.

Compare Against Prettier

After implementing formatting, validate against Prettier:

Compare a code snippet

bun packages/prettier-compare/bin/prettier-compare.js --rebuild 'const x={a:1,b:2}'

Compare with explicit language

bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l ts 'const x: number = 1'

Compare a file

bun packages/prettier-compare/bin/prettier-compare.js --rebuild -f path/to/file.tsx

From stdin (useful for editor selections)

echo 'const x = 1' | bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l js

Always use --rebuild to ensure WASM bundle matches your Rust changes.

Create Snapshot Tests

Create test files in tests/specs/ organized by feature:

crates/biome_js_formatter/tests/specs/js/ ├── statement/ │ ├── if_statement/ │ │ ├── basic.js │ │ ├── nested.js │ │ └── with_comments.js │ └── for_statement/ │ └── various.js

Example test file basic.js :

if (condition) { doSomething(); }

if (condition) doSomething();

if (condition) { doSomething(); } else { doOther(); }

Run tests:

cd crates/biome_js_formatter cargo test

Review snapshots:

cargo insta review

Test with Custom Options

Create options.json in the test folder:

{ "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 80 }, "javascript": { "formatter": { "quoteStyle": "single", "semicolons": "asNeeded" } } }

This applies to all test files in that folder.

Format and Build

After changes:

just f # Format Rust code just l # Lint just gen-formatter # Regenerate formatter infrastructure if needed

Tips

  • format_verbatim_node: Initial generated code uses this - replace it with proper IR as you implement formatting

  • Space tokens: Use space() instead of token(" ") for semantic spacing

  • Breaking: Use soft_line_break() for optional breaks, hard_line_break() for mandatory breaks

  • Grouping: Wrap related elements in group() to keep them together when possible

  • Indentation: Use block_indent() for block-level indentation, indent() for inline

  • Lists: Use join_nodes_with_soft_line() or join_nodes_with_hardline() for formatting lists

  • Mandatory tokens: Use node.token().format() for tokens that exist in AST, not token("(")

  • Debugging: Use dbg_write! macro (like dbg! ) to see IR elements: dbg_write!(f, [token("hello")])?;

  • Don't fix code: Formatter should format existing code, not attempt to fix syntax errors

IR Primitives Reference

// Whitespace space() // Single space soft_line_break() // Break if needed hard_line_break() // Always break soft_line_break_or_space() // Space or break

// Indentation indent(&content) // Indent content block_indent(&content) // Block-level indent soft_block_indent(&content) // Indent with soft breaks

// Grouping group(&content) // Keep together if possible conditional_group(&content) // Advanced grouping

// Text token("text") // Static text dynamic_token(&text, pos) // Dynamic text with position

// Utility format_with(|f| { ... }) // Custom formatting function format_args![a, b, c] // Combine multiple items if_group_breaks(&content) // Only if group breaks if_group_fits_on_line(&content) // Only if fits

References

  • Full guide: crates/biome_formatter/CONTRIBUTING.md

  • JS-specific: crates/biome_js_formatter/CONTRIBUTING.md

  • Prettier comparison tool: packages/prettier-compare/

  • Examples: crates/biome_js_formatter/src/js/ for real implementations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

biome-developer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

parser-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

testing-codegen

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

lint-rule-development

No summary provided by upstream source.

Repository SourceNeeds Review