# Coding agent guides for `crates/oxc_formatter`

## Overview

Prettier compatible JS/TS formatter (`oxfmt`'s Tier 1 backend), ported from [Biome](https://github.com/biomejs/biome).
It turns a parsed AST into an IR ("Document") of `FormatElement`s, then prints that IR via the shared `oxc_formatter_core` Printer.

- Built on `oxc_formatter_core` for the language-agnostic IR + Printer + builders + macros
  - See `crates/oxc_formatter_core/AGENTS.md` for the IR/pipeline details
- This crate holds only the JS/TSX-specific layer
- Parses with `oxc_parser`
  - Comments and other JS-specific concerns live here, not in core

### Public API

Call-sites use text-in entry points;
The AST-wrapping IR primitives (`AstNode`, `Format`, `Buffer`, …) are `pub(crate)` and not part of the contract.

- `format`: Format a whole file (text-in)
- `format_fragment`: Format a JS/TS fragment for js-in-xxx embedding, parameterized by `FragmentContext`
  - Drives context-dependent decisions like forced parentheses / quote style
  - The formatter knows nothing about Prettier/Vue vocabulary, callers pass wrapped source
- `format_program`: Special-purpose AST-in entry point
- `ExternalCallbacks` (in `external_formatter.rs`): The host-supplied `FormatDispatcher`
  (embedded-language formatting, see `oxc_formatter_core`'s `embedded` module)
  plus string-based / Tailwind callbacks delegated back to the host

### Generated code

`ast_nodes/generated/` and the `Format` glue are generated by `tasks/ast_tools`.
After changing AST shapes or the generators, regenerate with `just ast`, never hand-edit files under `generated/`.

## JS formatter specific features

### Sort imports (`ir_transform/sort_imports/`)

- Inspired by `eslint-plugin-perfectionist`, but not a 1:1 match (default group definitions differ too)
- Implemented purely as a Rust IR transform; requires no JS callback

### Format JSDoc (`formatter/jsdoc/`)

- Derived from `prettier-plugin-jsdoc`, but not fully compatible
- See `prettier_conformance/jsdoc` for the covered behavior

### Sort Tailwind CSS

- Derived from `prettier-plugin-tailwindcss`
- Classes are collected during IR construction and sorted in one batch when the IR is stringified
- Requires `ExternalCallbacks` (the sort itself is delegated to the host via `TailwindCallback`)

### Embedded language formatting

Two directions: xxx-in-js (css/graphql/html in template literals) and js-in-xxx (vue/svelte).

- xxx-in-js goes through the `FormatDispatcher` Oxfmt assembles Rust based formatter, Prettier Doc→IR fallback otherwise
- As the JS host, this crate also owns the parent-side concerns in `print/template/embed/`:
  - template-literal escape on returned IR,
  - placeholder marker insertion / survival count / `${expr}` substitution,
  - and `.raw` vs `.cooked` selection
  - Language formatter crates stay free of these rules
    - See `embed/mod.rs` for the shared helpers and `embed/{css,html,graphql,markdown}.rs` for each site's wiring
- js-in-xxx works with `prettier-plugin-oxfmt` which uses `format_fragment`

## Fixing IR construction

- Always keep the big picture in mind so a fix is not a one-off patch
  - Comments and parentheses are especially prone to side effects, handle them with care
- We aim for Prettier compatibility, but the implementation strategy differs:
  - Prettier pre-classifies comments per context, whereas oxc_formatter decides on the spot
  - Biome works on a CST rather than an AST, so its code and strategy differ in detail too

Above all, prioritize consistency, and always consider whether the divergence is a Prettier bug.

### Comment placement invariants

Two layers of rules, know which one you are editing:

- Invariants hold uniformly
  - violating one is a bug even where Prettier disagrees (the "Never let it cross" list below, and the content/terminator ownership split)
- Compat tables record measured Prettier behavior that is not derivable from principle (the variant tables below)
  - extend them by measuring Prettier, never by analogy, and pin every entry in a fixture

Repositioning a comment is allowed only relative to formatter-owned punctuation
(e.g. printing `;` before a same-line trailing comment, see `FormatContentWithSemicolon`).

The `;` rule applies to statement _terminators_ only, never to member _separators_:

- Terminator: the `;` cannot be replaced by `,` — statements, class members
  (property/accessor via `FormatClassElementWithSemicolon`, bodyless methods in `MethodDefinition`).
  A same-line trailing comment moves behind it
- Separator: interface / type literal members and index signatures (`;` is interchangeable with `,`),
  enum members' `,` — the comment stays before it, same as any list separator.
  This matches Prettier and is the principled line, not an emulated quirk
- Known gap, intentionally not covered: TS-only statements
  (`import A = B;` / `export = x;` / `export as namespace X;` / `declare function f(): void;` / `declare module "m";`).
  They are terminators by the rule above, but Prettier does not move their comments (yet);
  we follow Prettier for now. If Prettier extends the rule to them, follow;
  each is a small mechanical `FormatContentWithSemicolon` adoption

When the content's source parentheses survive in the output
(return/throw arguments, sequence/assignment in the prettier#19263 positions),
comments inside them belong to the content and stay there;
only comments after the closing paren may move behind the terminator (see `Comments::end_including_source_parens`).

The "which positions keep their parens" table is intentionally encoded twice:

- `keeps_trailing_comment_inside_parens` (statement side, walks down the rightmost expression)
- and `write_trailing_comments_inside_parens` (expression side, matches on the parent) in `print/semicolon.rs`

The statement side promises not to hide/move the comment; the expression side actually prints it.
Change them together: if they drift, the comment silently lands elsewhere (no assert catches it),
so pin every position change in a fixture.

The move-behind-the-terminator policy has four deliberate variants.
When extending to a new node, pick by measuring Prettier, not by analogy:

| Site                                                        | Source `;` required?      | Own-line comment before `;`                                                           |
| ----------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------- |
| Statements (`FormatContentWithSemicolon`)                   | yes (ASI: no move)        | deferred to the next node's leading pass                                              |
| return/throw (`ReturnAndThrowStatement`)                    | no (moves even under ASI) | deferred to the next node's leading pass (only the same-line prefix moves behind `;`) |
| Class property/accessor (`FormatClassElementWithSemicolon`) | yes                       | cancels the move (stays own-line)                                                     |
| Bodyless methods (`MethodDefinition`)                       | yes                       | cancels the move                                                                      |

Never let it cross:

- A line boundary: line-based directives (`eslint-disable-line`, ...) must keep their meaning
  - Line comments are printed via `line_suffix`, and own-line comments stay own-line (they become the next node's leading comments)
  - Both are structural guarantees, keep them
- User content (code, other comments):
  - e.g. Prettier relocates a comment after a trailing array hole backward across commas to the last real element, that is an attachment artifact, not a rule
  - We keep the comment in place and diverge intentionally
- A suppression comment's target: `prettier-ignore` / `oxfmt-ignore` needs its original text
  preserved
  - When hiding comments from a node (`limit_comments_up_to`), check `has_trailing_suppression_comment` first, or the node loses its suppression

Prettier's comment _attachment_ is position-heuristic and sometimes asymmetric
(e.g. it moves `export type T = string /* c */;`'s comment behind the semicolon but not the non-exported form).
When that happens, prefer one uniform rule over emulating the asymmetry, and pin the intentional divergence in a fixture with a note.

### Statement terminators and suppression

The formatter owns statement terminators (and the trivia up to them); the user owns content.
Prettier encodes "the trailing `;` is outside the statement" once, in `locEnd`;
deciding on the spot, we encode the same policy per site, so keep them in step:

- print side: `FormatContentWithSemicolon` and the move-behind table above
- return/throw: the same-line-prefix dangling split in `ReturnAndThrowStatement`
- capture side: `Comments::get_trailing_comments` lets deferred own-line comments escape when the statement shares its distant `;` with the enclosing statement
  (a single-statement body, `if (1) foo\n// c\n;`); a block's last statement keeps them inside instead
- suppressed side: `suppressed_statement_content_end` (`print/mod.rs`) ends the ignored range at the content,
  so even a `prettier-ignore`d statement gets the formatter's terminator (per `semi`) instead of its source one

Accepted edges (byte-identical to Prettier, semantically inert, idempotent):

- Whether a suppressed statement re-adds `;` is a compat table, not a principle:
  keyword statements (`debugger`/`break`/`continue`) always re-add,
  content-terminated ones only when a source `;` was stripped — the asymmetry is observable only for a suppressed keyword statement relying on ASI
- The `semi: false` ASI guard is decided from the guarded statement alone,
  never from the previous statement's output;
  sound because no statement leaves its own trailing `;`,
  except a verbatim empty-statement body (`with (1) ;`, that `;` IS the body, i.e. content), where guard plus verbatim `;` re-parse as one extra inert `EmptyStatement`

## Verification

```sh
cargo c -p oxc_formatter
cargo c -p oxc_formatter --features detect_code_removal
```

Run `clippy` for the same configurations and resolve all warnings.

### Fixtures tests

Snapshot tests driven by fixture files under `tests/fixtures/{js,ts}/`.
`build.rs` auto-discovers every `.{js,jsx,ts,tsx}` file and generates a test function per file; options are resolved from the nearest `options.json` up the directory tree. See `tests/README.md` for the full workflow.

```sh
# Run all fixtures tests
cargo test -p oxc_formatter --test mod
# Run a subset by module path (directory structure = module hierarchy)
cargo test -p oxc_formatter fixtures::js::comments
# Review / accept snapshots after intentional changes
cargo insta test --accept -p oxc_formatter --test mod
```

Add a case by dropping a new file into `tests/fixtures/`, no manual registration needed.

### Prettier conformance

Compares output against Prettier's snapshots and tracks failures (not passes); results live in `tasks/prettier_conformance/snapshots/`.

```sh
cargo run -p oxc_prettier_conformance
```

### Embedded conformance (`apps/oxfmt`)

The embedded-language features (xxx-in-js / js-in-xxx) are validated end-to-end through the Oxfmt.

Requires a dev build first.

```sh
pnpm --dir apps/oxfmt build-dev
pnpm --dir apps/oxfmt conformance
```
