formatter-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
在实现或修改Biome的格式化工具时使用本指南。内容涵盖基于 trait 的格式化系统、IR生成、注释处理以及与Prettier对比的测试方法。

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
  1. 安装所需工具:
    just install-tools
    (包含
    wasm-bindgen-cli
    wasm-opt
  2. 必须存在对应语言的 crate:
    biome_{lang}_syntax
    biome_{lang}_formatter
  3. 若要与Prettier对比:安装
    bun
    并在仓库根目录执行
    pnpm install

Common Workflows

常见工作流

Generate Formatter Boilerplate

生成格式化工具模板

For a new language (e.g., HTML):
shell
just gen-formatter html
This generates
FormatNodeRule
implementations for all syntax nodes. Initial implementations use
format_verbatim_node
(formats code as-is).
针对新语言(如HTML):
shell
just gen-formatter html
此命令会为所有语法节点生成
FormatNodeRule
实现。初始实现使用
format_verbatim_node
(按原样格式化代码)。

Implement FormatNodeRule for a Node

为节点实现FormatNodeRule

Example: Formatting
JsIfStatement
:
rust
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(())
    }
}
示例:格式化
JsIfStatement
rust
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

使用IR原语

Common formatting building blocks:
rust
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")])
        }
    }),
])?;
常用格式化构建块:
rust
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

处理注释

rust
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.
rust
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(),
            ]
        )
    }
}
前置和后置注释会由格式化工具基础设施自动处理。

Compare Against Prettier

与Prettier对比

After implementing formatting, validate against Prettier:
shell
undefined
实现格式化后,与Prettier进行验证:
shell
undefined

Compare a code snippet

对比代码片段

bun packages/prettier-compare/bin/prettier-compare.js --rebuild 'const x={a:1,b:2}'
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'
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
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.
echo 'const x = 1' | bun packages/prettier-compare/bin/prettier-compare.js --rebuild -l js

**务必使用`--rebuild`参数**,确保WASM包与你的Rust代码修改一致。

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
:
javascript
if (condition) {
  doSomething();
}

if (condition) doSomething();

if (condition) {
  doSomething();
} else {
  doOther();
}
Run tests:
shell
cd crates/biome_js_formatter
cargo test
Review snapshots:
shell
cargo insta review
tests/specs/
目录下按功能组织测试文件:
crates/biome_js_formatter/tests/specs/js/
├── statement/
│   ├── if_statement/
│   │   ├── basic.js
│   │   ├── nested.js
│   │   └── with_comments.js
│   └── for_statement/
│       └── various.js
示例测试文件
basic.js
javascript
if (condition) {
  doSomething();
}

if (condition) doSomething();

if (condition) {
  doSomething();
} else {
  doOther();
}
运行测试:
shell
cd crates/biome_js_formatter
cargo test
查看快照:
shell
cargo insta review

Test with Custom Options

使用自定义选项测试

Create
options.json
in the test folder:
json
{
  "formatter": {
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "asNeeded"
    }
  }
}
This applies to all test files in that folder.
在测试目录下创建
options.json
json
{
  "formatter": {
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "asNeeded"
    }
  }
}
此配置会应用到该目录下的所有测试文件。

Format and Build

格式化与构建

After changes:
shell
just f              # Format Rust code
just l              # Lint
just gen-formatter  # Regenerate formatter infrastructure if needed
修改代码后执行:
shell
just f              # 格式化Rust代码
just l              # 代码检查
just gen-formatter  # 若需要,重新生成格式化工具基础设施

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
  • format_verbatim_node:初始生成的代码会使用该方法,在实现格式化时需替换为合适的IR实现
  • 空格令牌:使用
    space()
    而非
    token(" ")
    来实现语义化空格
  • 换行:使用
    soft_line_break()
    实现可选换行,
    hard_line_break()
    实现强制换行
  • 分组:将相关元素包裹在
    group()
    中,尽可能保持内容在同一行
  • 缩进:使用
    block_indent()
    实现块级缩进,
    indent()
    实现行内缩进
  • 列表:使用
    join_nodes_with_soft_line()
    join_nodes_with_hardline()
    格式化列表
  • 必填令牌:对于AST中存在的令牌,使用
    node.token().format()
    而非
    token("(")
  • 调试:使用
    dbg_write!
    宏(类似
    dbg!
    )查看IR元素:
    dbg_write!(f, [token("hello")])?;
  • 不要修复代码:格式化工具应仅格式化现有代码,不要尝试修复语法错误

IR Primitives Reference

IR原语参考

rust
// 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
rust
// 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
  • 完整指南:
    crates/biome_formatter/CONTRIBUTING.md
  • JavaScript专属指南:
    crates/biome_js_formatter/CONTRIBUTING.md
  • Prettier对比工具:
    packages/prettier-compare/
  • 示例:
    crates/biome_js_formatter/src/js/
    下的真实实现