skills/biomejs/biome/formatter-development

formatter-development

SKILL.md

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

  1. Install required tools: just install-tools (includes wasm-bindgen-cli and wasm-opt)
  2. Language-specific crates must exist: biome_{lang}_syntax, biome_{lang}_formatter
  3. 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(&format_args![
        token("("),
        soft_block_indent(&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(&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
Weekly Installs
49
Repository
biomejs/biome
GitHub Stars
24.0K
First Seen
Feb 18, 2026
Installed on
opencode49
github-copilot49
codex49
kimi-cli49
gemini-cli49
amp49