docs(formatter_core): extract FORMATTER_POLICY (#25233)

This commit is contained in:
leaysgur committed 2026-08-03 05:56:42 +00:00
1 parent 8c2ed1a04f
commit eaa7c692d2
9 files changed
+252 -338

No files matched your search

+3
View File
@@ -276,6 +276,9 @@ These test suites are pre-cloned and ready to search:
- **TypeScript** (`tasks/coverage/typescript/`) - TypeScript syntax and semantics
- **Prettier** (`tasks/prettier_conformance/prettier/`) - Formatting expectations
NOTE: These suites are script-cloned and fully gitignored. ripgrep respects `.gitignore`, so a plain `rg` inside them silently returns nothing.
Use `rg --no-ignore` (or `-u`) when searching them.
### Snapshot Testing
- Uses `insta` crate for snapshot testing
+11 -14
View File
@@ -1,19 +1,21 @@
# Coding agent guides for `apps/oxfmt`
Oxfmt is the integration layer that dispatches between the Rust formatters built on `oxc_formatter_core` and Prettier.
## Overview
The `oxfmt` implemented under this directory serves several purposes.
- Pure Rust CLI
- Minimum feature set, CLI usage only, no LSP, no Stdin support
- Formats JS/TS, JSON, CSS, GraphQL, YAML and TOML files, no xxx-in-js support
- Entry point: `main()` in `src/main.rs`
- Build with `cargo build --no-default-features`
- JS/Rust hybrid CLI using `napi-rs`
- Full feature set like CLI, Stdin, LSP, and more
- Format many file types with embedded language formatting support like Prettier
- Format many languages with embedded language formatting support like Prettier
- Entry point: `src-js/cli.ts` which uses `run_cli()` from `src/main_napi.rs`
- Build with `pnpm build`
- Pure Rust CLI
- Limited feature set, CLI usage only, no LSP, no Stdin support
- Formats only languages which we rewrite formatter in Rust
- Entry point: `main()` in `src/main.rs`
- Build with `cargo build --no-default-features`
- Node.js API using napi-rs
- Entry point: `src-js/index.ts` which uses `format()` from `src/main_napi.rs`
- Build with `pnpm build`
@@ -38,15 +40,10 @@ Oxfmt utilizes different implementations depending on the file extension and fil
- Tier 1: Rust implementations using `oxc_formatter`, `oxc_formatter_json`, etc found in this repository
- Tier 2: Rust implementations using external libraries like `oxc_toml`
- Tier 3: Delegations to Prettier via NAPI-JS calls (e.g., for Vue or Markdown)
- Tier 4: Delegations to Prettier that require additional plugins (e.g., for Svelte)
- Tier 4: Delegations to Prettier that require additional Prettier plugins (e.g., for Svelte)
NOTE: Rust written formatters never fall back to Prettier, since they exist to reduce the dependency on Prettier.
`oxc_formatter_css` (uses `oxc-css-parser`, a `raffia` fork) and `oxc_formatter_graphql` (uses `oxc-graphql-parser`, an `apollo-parser` fork) both should cover everything the bundled Prettier can parse for their language.
However, parse errors may be reported as diagnostics, what the forks reject.
This is genuinely broken input or, for CSS, the tail of postcss's error tolerance (e.g. IE star hacks, some postcss-plugin specific syntax).
Embedded languages (e.g. css-in-js) go through `src/core/external_formatter.rs`, which assembles a `FormatDispatcher` (defined in `oxc_formatter_core`) that maps each language to a Rust formatter where implemented and to the Prettier Doc→IR path otherwise.
A separate string-out channel (the `embedded_callback` in the same file, NOT the dispatcher) carries the cases that don't fit IR integration.
@@ -109,7 +106,7 @@ pnpm build-test
# Show help
node ./dist/cli.js --help
# Stdin (`npx prettier --config=<cfg> <file>` equivalent)
# Stdin (Prettier's `--config=<cfg> <file>` equivalent)
cat <file> | node ./dist/cli.js --config=<cfg> --stdin-filepath=<file>
# With log
OXC_LOG=debug node ./dist/cli.js --threads=1 <file>
@@ -123,7 +120,7 @@ To compare formatting output with Prettier:
# Use a shared config file (e.g., fmt.json) because Oxfmt and Prettier have different default printWidth.
# Example fmt.json: { "printWidth": 80 }
cat <file> | node ./dist/cli.js --config=fmt.json --stdin-filepath=<file>
npx prettier --config=fmt.json <file>
node ./node_modules/prettier/bin/prettier.cjs --config=fmt.json <file>
```
## Test organization (`test/` directory)
+27 -56
View File
@@ -1,8 +1,10 @@
# Coding agent guides for `crates/oxc_formatter`
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the JS/TS-specific rules and translations.
## Overview
Prettier compatible JS/TS formatter (`oxfmt`'s Tier 1 backend), ported from [Biome](https://github.com/biomejs/biome).
Prettier compatible JS/TS formatter (`oxfmt`'s Tier 1 backend), ported from [Biome](https://github.com/biomejs/biome)'s `biome_js_formatter`.
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
@@ -41,10 +43,11 @@ After changing AST shapes or the generators, regenerate with `just ast`, never h
- Derived from `prettier-plugin-jsdoc`, but not fully compatible
- See `prettier_conformance/jsdoc` for the covered behavior
- See also prettier_conformance/jsdoc/upstream-jsdoc-bugs.md
### Sort Tailwind CSS
- Derived from `prettier-plugin-tailwindcss`
- Backed by `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`)
@@ -65,25 +68,17 @@ Two directions: xxx-in-js (css/graphql/html in template literals) and js-in-xxx
- 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.
- Biome works on a CST rather than an AST, so its code and strategy differ in detail from ours (and both differ from Prettier's)
### 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
The shared two-layer split (invariants vs compat tables) and the base invariants live in FORMATTER_POLICY.md "Comment placement invariants";
this section records their JS/TS translation and the measured compat tables.
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_:
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`).
@@ -97,9 +92,7 @@ The `;` rule applies to statement _terminators_ only, never to member _separator
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;
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:
@@ -108,8 +101,7 @@ The "which positions keep their parens" table is intentionally encoded twice:
- 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.
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:
@@ -121,17 +113,12 @@ When extending to a new node, pick by measuring Prettier, not by analogy:
| Class property/accessor (`FormatClassElementWithSemicolon`) | yes | cancels the move (stays own-line) |
| Bodyless methods (`MethodDefinition`) | yes | cancels the move |
Never let it cross:
JS-side mechanics of the shared "never cross" invariants:
- 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)
- Line boundary: 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
- User content: e.g. Prettier relocates a comment after a trailing array hole backward across commas to the last real element — an attachment artifact, we keep the comment in place and diverge intentionally
- Suppression: 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).
@@ -160,46 +147,30 @@ Accepted edges (byte-identical to Prettier, semantically inert, idempotent):
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`
## Known divergences
Admission reasons and rules: see FORMATTER_POLICY.md "Known divergences".
Entries documented in this file so far — both of the comment-attachment-artifact class, details in "Comment placement invariants" above; this is not yet an exhaustive audit against the conformance snapshots:
- A comment after a trailing array hole stays in place; Prettier relocates it backward across commas to the last real element
- Asymmetric attachment like `export type T = string /* c */;` (comment moved behind `;` only in the exported form): one uniform rule instead of emulating the asymmetry
## Verification
Feature configs to check:
```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.
### Fixture tests
### 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.
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
```
+15 -23
View File
@@ -2,9 +2,9 @@
## Overview
Language-agnostic formatting infrastructure, ported from [Biome](https://github.com/biomejs/biome)'s `biome_formatter` crate.
Language-agnostic formatting infrastructure.
Every language-specific formatter in the oxc ecosystem (`oxc_formatter` for JS/TS, `oxc_formatter_json`, and future CSS/GraphQL/etc.) builds on this crate.
Every language-specific formatter in the oxc ecosystem builds on this crate.
It owns the IR and the printing pipeline; it knows nothing about any concrete language (no comment placement rules, no quote rules, those live in the consumer crates).
### The IR ("Document") and pipeline
@@ -19,10 +19,7 @@ Key IR pieces are all exported from the crate root.
The semantics of each building block live in the `builders.rs` rustdocs.
e.g. the mechanisms for verbatim multi-line content (`exact_line_breaks()` for blank runs exempt from newline collapsing, `literal_line_break()`, multiline `text()`, `text(..).without_expand_parent()`, and `mark_as_root` / `dedent_to_root`), with the non-obvious behaviors pinned by printer tests verified against Prettier's `printDocToString`.
Prettier doc primitives are ported on demand; still missing:
- `hardlineWithoutBreakParent` (markdown tables)
- and the `trim` doc
Prettier doc primitives are ported on demand; still missing: `hardlineWithoutBreakParent` (for markdown tables) and the `trim`.
### The printer never trims
@@ -58,7 +55,7 @@ The core is parameterized over a consumer-supplied context so it stays language-
- `FormatContext` trait: no lifetime parameter
- (avoids `oxc_allocator`'s `'ast` propagating through struct bounds and blocking anonymous lifetimes)
- The allocator lives on `FormatState`, not the context
- `FormatOptions` trait: `indent_style()`, `indent_width()`, `line_width()`, `line_ending()`, `apply_core(CoreFormatOptions)`;
- `FormatOptions` trait:
- `as_print_options() -> PrinterOptions` is provided from the getters
- Core option types: `IndentStyle`, `IndentWidth`, `LineWidth`, `LineEnding` (exactly the `PrinterOptions` inputs; see the boundary section below)
- `CoreFormatOptions`: the four bundled, for handing them across a host boundary (config resolver → language options) in one piece;
@@ -69,23 +66,18 @@ The core is parameterized over a consumer-supplied context so it stays language-
### Embedded-language infrastructure (`embedded.rs`)
`EmbeddedContext` / `FormatDispatcher` / `DispatchResult` / `TailwindCollector` let one
formatter's IR be built inside another's document (e.g. graphql-in-js):
`EmbeddedContext` / `FormatDispatcher` / `DispatchResult` / `TailwindCollector` let one formatter's IR be built inside another's document (e.g. graphql-in-js):
- The orchestrator (oxfmt) assembles the dispatcher, mapping language names to
formatter implementations (or a Prettier fallback); formatter crates only invoke it
- The orchestrator (oxfmt) assembles the dispatcher, mapping language names to formatter implementations (or a Prettier fallback)
- Formatter crates only invoke it
- Parent and child share one arena and one `GroupId` space through `EmbeddedContext`
- A language crate's `format_to_ir` entry returns `EmbeddedIr` (IR + pre-sort
Tailwind classes) — one shape for every child language, no per-crate tuples
- Cross-language contract data is first-class on `DispatchResult` (`tailwind_classes`);
only truly language-pair specific data crosses as `dyn Any` (e.g. HTML's `has_multiple_root_elements`),
core never learns concrete languages
- Consumers access `DispatchResult.docs` directly
(single-doc takes `docs.into_iter().next()`, multi-doc walks `docs`);
call `DispatchResult::remap_tailwind_into` first when the child may carry classes,
the printer's `debug_assert` catches a forgotten merge
- A language crate's `format_to_ir` entry returns `EmbeddedIr` (IR + pre-sort Tailwind classes), one shape for every child language, no per-crate tuples
- Cross-language contract data is first-class on `DispatchResult` (`tailwind_classes`)
- Only truly language-pair specific data crosses as `dyn Any` (e.g. HTML's `has_multiple_root_elements`), core never learns concrete languages
- Consumers access `DispatchResult.docs` directly (single-doc takes `docs.into_iter().next()`, multi-doc walks `docs`)
- Call `DispatchResult::remap_tailwind_into` first when the child may carry classes, the printer's `debug_assert` catches a forgotten merge
### What belongs in core (the boundary)
## What belongs in core (the boundary)
Three layers, three admission rules. A type/fn that fits none of them belongs in a consumer crate.
@@ -142,11 +134,11 @@ Consumers still need their own `insta` dev-dep so the recorded `source:` header
## Verification
Feature configs to check:
```sh
cargo c -p oxc_formatter_core
cargo c -p oxc_formatter_core --features test_harness
```
Run `clippy` for both configurations and resolve all warnings.
This crate has basic tests only of its own, it is exercised through the conformance/snapshot tests of its consumers.
@@ -0,0 +1,146 @@
# Formatter policy
Shared policy for every formatter crate in the oxc ecosystem:
- `oxc_formatter` (JS/TS)
- `oxc_formatter_json`
- `oxc_formatter_css`
- `oxc_formatter_graphql`
- `oxc_formatter_yaml`
using `oxc_formatter_core`, integrated by `apps/oxfmt`.
Each crate's `AGENTS.md` holds only language-specific rules and the crate-local translation of these policies; when they conflict, the crate file wins for that crate (and the conflict should be fixed).
`apps/oxfmt` is bound by this policy as the integration side of its contracts (error handling, embedded conformance); everything about tier dispatch, configuration, and delegation to Prettier stays in its own `AGENTS.md`.
## Prettier compatibility
- Matching Prettier's output is the GOAL, not an absolute rule
- The higher the conformance coverage the better, but 100% parity is not what we promise
- What we provide above all is CONSISTENT, PREDICTABLE, EXPLAINABLE behavior
- When parity conflicts with that, "Known divergences" below arbitrates
- The canonical reference is Prettier's OUTPUT
- Its source code is an analysis aid, not a porting target
- Match its layout decisions, do not invent new ones
- Never mirror its internal logic 1:1; pin behavior with fixtures instead
- Comments explain BEHAVIOR and mechanism, never Prettier's code structure
- Naming an upstream function as a search key is fine (greppable in any Prettier version) — most valuable when pinning an artifact explanation or a compat-table entry
- File names / line numbers are not (not reproducible without a commit hash)
- Implementation strategies legitimately differ (e.g. Prettier pre-classifies comments per context; we decide on the spot with positional cursors)
- Compatibility is judged on bytes out, not code shape
- Before matching a mismatch, always consider whether it is a Prettier bug or artifact (see "Known divergences")
- The oracle version is the one pinned by the conformance submodule and `apps/oxfmt` bundles; fixtures are verified against it
- The LATEST Prettier is still worth consulting as a forward-looking aid: whether a bug we diverged on has been fixed upstream, or a behavior is about to change
- When the pin catches up to an upstream fix, converge and drop the divergence entry
### Known divergences
Deliberate divergences from Prettier's output. Admission reasons:
- (1) Prettier's output would change program semantics: Formatting must NEVER do that (this reason is mandatory to act on, the others are judgment calls)
- Idempotency breakage is a symptom of this class: output that fails to re-parse, or re-parses to a different value/structure
- Verify semantics claims with the reference compiler/parser (tsc, dart-sass, lessc, ...), not intuition
- (2) Prettier's behavior is a bug: acknowledged as an open issue, OR explainable as an artifact (of comment attachment, parser structure, token lexing, ...) rather than an intended layout rule
- Typical case: comment relocations that are attachment artifacts of Prettier's parser
- "Looks wrong" without an artifact explanation is NOT enough; follow Prettier until the mechanism is understood
- (3) Prettier's behavior conflicts with a uniform rule across our formatter crates
- The uniform rule wins, even where Prettier's behavior is normal and internally consistent
- e.g. re-quoting SCSS `@warn "x"` per `singleQuote` where Prettier keeps the raw string verbatim
- This also covers Prettier's internal inconsistencies (same construct, different output depending on node kind or context): one principle beats emulating the inconsistency
- (4) The impact does not justify the matching cost (layout-only, rare trigger)
Rules:
- Style debates (`status:needs discussion` issues) are still followed; do not "improve" on taste
- Applying a rule already established across our crates (reason 3) is not taste; inventing a new style neither Prettier nor our crates have is
- Every divergence is documented in the owning crate's AGENTS.md "Known divergences" section, with the reason
- Every divergence is pinned by a fixture whose comments say which lines deviate from Prettier and why
- Affected conformance fixtures stay counted as failures; a new conformance failure is acceptable only under this policy and must be documented
- The rule cuts both ways: never "fix" a conformance failure by following Prettier into a documented divergence
- Check the crate's divergence list and open oxc issues for intent before treating a diff as a plain 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
- Compat tables record measured Prettier behavior that is not derivable from principle: extend them by measuring Prettier, never by analogy, and pin every entry in a fixture
The invariants:
- A comment never crosses user content (code, other comments, other tokens): it stays on its source side of every token
- When Prettier relocates a comment across tokens, that is an attachment artifact to diverge from (see "Known divergences"), not a rule to emulate
- A comment never crosses a line boundary: line-based directives (`eslint-disable-line`, ...) must keep their meaning
- Line comments print via `line_suffix`; own-line comments stay own-line
- A suppression comment (`prettier-ignore` / `oxfmt-ignore`) never loses its target, and its original text is preserved
- Repositioning is allowed only relative to formatter-OWNED punctuation: the formatter owns terminators (e.g. a statement's `;`) and the trivia up to them; the user owns content
- Terminator vs separator: a terminator cannot be replaced by another token (`;` after a JS statement); a separator can (`,`/`;` between interface members)
- Comments may move behind a terminator (per-language compat tables decide when); they always stay before a separator
Per-language translations (which tokens are terminators, the compat tables, cursor bounds disciplines) live in each crate's AGENTS.md.
## Error semantics
`format()` / `format_to_ir()` return `Err` whenever they cannot produce output they can stand behind:
- Any parse error bails out, even from an error-tolerant parser: never format a broken AST
- Print-stage internal errors are also `Err`
- The caller (oxfmt) decides what happens next: diagnostics for standalone files, template-as-is for embedded
A parse error is a SAFE failure (the input is left as-is); silent token corruption is the UNSAFE one.
When triaging a report: don't corrupt → then accept → then pretty-print.
## Verification
Every crate:
```sh
cargo c -p <crate> # plus any crate-specific feature configs listed in its AGENTS.md
```
Run `clippy` for the same configurations and resolve all warnings.
Whenever comparing output against Prettier (fixture verification, manual checks), use the oxfmt-bundled one:
```sh
# A bare `npx prettier` is NOT installed at the repo root; the bundled one is the same version as the oracle
node apps/oxfmt/node_modules/prettier/bin/prettier.cjs --parser <parser> --print-width=100 <file>
```
NOTE: Prettier's default `printWidth` is `80`, but Oxfmt is `100`.
### Fixture tests
Snapshot tests driven by fixture files under `tests/fixtures/`; they cover what the Prettier conformance suite does not (suppression, divergence pins, embedded shapes, ...).
`build.rs` auto-discovers every fixture file and generates a test per file — add a case by dropping a file in, no registration needed. Options are resolved from the nearest `options.json` up the directory tree.
```sh
cargo test -p <crate>
# Review / accept snapshots after intentional changes
cargo insta test --accept -p <crate>
```
Every expected output must be verified against Prettier, except fixtures pinning a "Known divergence".
### 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
# Debug a specific test
cargo run -p oxc_prettier_conformance -- --filter <path>
```
Failures must be either fixed or classified under "Known divergences".
### Embedded conformance (`apps/oxfmt`)
The embedded-language features (e.g. xxx-in-js / js-in-xxx) are validated end-to-end through Oxfmt. Requires a dev build first.
```sh
pnpm --dir apps/oxfmt build-dev
pnpm --dir apps/oxfmt test
pnpm --dir apps/oxfmt conformance
```
+28 -100
View File
@@ -1,5 +1,7 @@
# Coding agent guides for `crates/oxc_formatter_css`
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the CSS/SCSS/Less-specific rules and translations.
## Overview
Prettier compatible CSS/SCSS/Less formatter (`oxfmt`'s Tier 1 backend), using the `oxc_formatter_core` APIs.
@@ -8,13 +10,7 @@ Prettier compatible CSS/SCSS/Less formatter (`oxfmt`'s Tier 1 backend), using th
- See `crates/oxc_formatter_core/AGENTS.md` for the IR/pipeline details
- Two entry points:
- `format()`: standalone files (returns a printable `Formatted`)
- `format_to_ir()`: embedded use via the dispatcher (e.g. css-in-js);
tolerates `${}` placeholders and `TopLevelDeclaration`
- The canonical reference is Prettier's OUTPUT (the conformance snapshots); its source is an analysis aid, not a porting target
- match its layout decisions, do not invent new ones
- never mirror its internal logic 1:1: pin behavior with fixtures, and keep implementation details of Prettier's code out of comments
- ONE exception: never reproduce output that changes program semantics
- See "Known divergences" section
- `format_to_ir()`: embedded use via the dispatcher (e.g. css-in-js); tolerates `${}` placeholders and `TopLevelDeclaration`
### Forked parser
@@ -47,40 +43,30 @@ Prettier operates on `postcss` + three sub-parsers (`postcss-selector-parser`, `
### Error semantics
`format()` / `format_to_ir()` return `Err` whenever they cannot produce output they can stand behind:
The shared policy applies; CSS specifics:
- `oxc-css-parser` is error-tolerant via `parser.recoverable_errors()`, but any parse error bails out;
- Never format a broken AST
- Exception: `TopLevelDeclaration`, tolerated ONLY by `format_to_ir()`
- The dominant css-in-js shape, `` css`display: flex;` ``)
- Standalone `format()` still rejects it as invalid CSS/SCSS/Less (Dart Sass rejects it too)
- print-stage internal errors are also `Err`
- The caller (oxfmt) decides what happens next
(diagnostics for standalone files, template-as-is for embedded)
- `oxc-css-parser` is error-tolerant via `parser.recoverable_errors()`, but any parse error still bails out
- Exception: `TopLevelDeclaration`, tolerated ONLY by `format_to_ir()`
- The dominant css-in-js shape, `` css`display: flex;` ``
- Standalone `format()` still rejects it as invalid CSS/SCSS/Less (Dart Sass rejects it too)
### Comments
`oxc-css-parser` does not attach comments to the AST;
they are collected via `ParserBuilder::comments()` into a positional cursor over `CssComment { span, inline }` (`inline` = `//`).
- Statement-level comments: flushed before each statement
(`flush_leading_comments`); consecutive same-line comments stay glued
(`*/ /*!`), but a comment is always followed by a line break before a node
- Value-level comments: flushed inside fill entries before the component they
precede (`flush_value_comments`); `//` comments expand the parent group and force a hardline after
- Statement-level comments: flushed before each statement (`flush_leading_comments`);
consecutive same-line comments stay glued (`*/ /*!`), but a comment is always followed by a line break before a node
- Value-level comments: flushed inside fill entries before the component they precede (`flush_value_comments`);
`//` comments expand the parent group and force a hardline after
- Trailing (`value /* c */;`): flushed by `write_declaration` with the source gap before `;` preserved
- After each statement, the sequence DISCARDS unclaimed comments inside the
statement span (cursor must never point before a printed position)
- After each statement, the sequence DISCARDS unclaimed comments inside the statement span
(cursor must never point before a printed position)
#### Placement invariants
The placement rules follow `crates/oxc_formatter/AGENTS.md` "Comment placement invariants"
(the invariant / compat-table two-layer split, and the terminator vs separator ownership rule);
The shared invariants (FORMATTER_POLICY.md "Comment placement invariants") apply; this section records their CSS translation:
this section records their CSS translation:
- A comment never crosses user content (other values, other comments):
- it stays on its source side of every value/argument
- `,` is a list SEPARATOR: a comment between an element and its comma stays BEFORE the comma
- `a /* c */, b`; comments after it lead the next element
- Declaration value lists (`write_value_groups`) and function arguments (`write_function`) route every comma through `write_group_comma` with the comma offset paired to its group
@@ -92,12 +78,11 @@ this section records their CSS translation:
- The positional cursor makes ownership a bounds discipline, not an attachment one:
- a flush's upper bound must never extend past the next piece of user content,
- and a declaration's `tail_bound` may only be consumed by the LAST comma group (`write_value_groups` clears it for every other group)
- Never let a comment cross a line boundary
- `//` comments force a hardline after; own-line comments stay own-line, and never move a suppression comment off its target
- Line-boundary rule in CSS terms: `//` comments force a hardline after; own-line comments stay own-line
### Line endings
`parse_stylesheet` normalizes `\r\n` / lone `\r` to `\n` BEFORE parsing.
`parse_stylesheet` normalizes `\r\n` and lone `\r` to `\n` BEFORE parsing.
Unlike other formatters that normalize locally where needed, CSS has too many verbatim slices to handle case by case.
And without this, raw `\r` reaching the core `text()` builder would panic.
@@ -134,7 +119,7 @@ the exact set of supported positions (incl. id / attribute-value / class selecto
### Unknown at-rule params print VERBATIM
Prettier's parser hands params to sub-parsers only for a fixed allowlist (see `parser-postcss.js`, `is_value_parsed_at_rule`);
Prettier's parser hands params to sub-parsers only for a fixed allowlist (`is_value_parsed_at_rule`);
everything else (`@apply`, `@tailwind`, `@custom-variant`, `@variant`, `@source`, ICSS `@value`, etc) stays a plain string the printer emits raw (`write_verbatim_at_rule_tail`).
Re-spacing those tokens CORRUPTS Tailwind syntax: `dark:bg-x` → `dark: bg-x`, `py-1.5` → `py-10.5`, `@custom-variant dark (&:is(...))` → `dark(&: is(...))`.
@@ -173,27 +158,20 @@ So the target behavior is finite: never destroy tokens Prettier wouldn't destroy
When a dialect report comes in, first translate it: "which GENERAL postcss behavior are we missing?" Not "how do we support plugin Xxx?".
Then absorb it at the highest possible rung of the escape-hatch hierarchy (top = cheapest, each rung covers whole classes of dialects at once):
1. Unknown at-rule prelude verbatim (`write_verbatim_at_rule_tail`)
zero-cost bucket: Tailwind, postcss-mixins, ICSS ride it for free
2. Raw fallbacks when the typed grammar rejects (raw component values, `TokenSeq`, `ImportPrelude.modifiers`)
`[attr=;]`, weird import tails
3. postcss word rules at the separator layer (`is_word_glued_number`, the `1#{$var}` glue, solidus words)
variant-agnostic, fixes xstyled + `theme()` + future unknown tokens in one place
4. `ParserOptions` flag + typed node (postcss-simple-vars)
ONLY when the formatter must make layout decisions INSIDE the construct.
1. Unknown at-rule prelude verbatim (`write_verbatim_at_rule_tail`) zero-cost bucket: Tailwind, postcss-mixins, ICSS ride it for free
2. Raw fallbacks when the typed grammar rejects (raw component values, `TokenSeq`, `ImportPrelude.modifiers`) `[attr=;]`, weird import tails
3. postcss word rules at the separator layer (`is_word_glued_number`, the `1#{$var}` glue, solidus words) variant-agnostic, fixes xstyled + `theme()` + future unknown tokens in one place
4. `ParserOptions` flag + typed node (postcss-simple-vars) ONLY when the formatter must make layout decisions INSIDE the construct.
Promotion criteria, all three:
(a) real user demand, (b) Prettier itself formats it structurally (not verbatim), (c) rungs 1-3 can't express it
5. A dedicated `CssVariant`
only for real languages with reference compilers (css/scss/less).
5. A dedicated `CssVariant` only for real languages with reference compilers (css/scss/less).
Never for a plugin.
Parser-side leniencies (in the `oxc-css-parser` fork) must be additive:
accept only input that previously errored, and never change the AST of input that already parsed
(e.g. dotted words try the typed `foo.$var` / `foo.bar(...)` parse first; only dart-sass-invalid shapes take the lenient path).
accept only input that previously errored, and never change the AST of input that already parsed (e.g. dotted words try the typed `foo.$var` / `foo.bar(...)` parse first; only dart-sass-invalid shapes take the lenient path).
Every lenient path carries a comment citing the reference-compiler vs postcss behavior, a test pinning the strict shapes, and shows up as a visible expected-error flip in the parser's conformance snapshots.
Triage order for reports (failure modes are asymmetric, a parse error is a SAFE failure, oxfmt leaves the file/template as-is; silent token corruption like `sandstone.10` → `sandstone 0.1` is the UNSAFE one):
don't corrupt (verbatim paths) → then accept (leniency) → then pretty-print (structure).
Triage order for reports follows the shared policy (don't corrupt → then accept → then pretty-print); the CSS-shaped example of the UNSAFE failure is silent token corruption like `sandstone.10` → `sandstone 0.1`.
Red flags that the approach is drifting:
specific plugin names accumulating in code, or a leniency that reinterprets previously-valid input.
@@ -209,15 +187,9 @@ Covered:
NOT covered: `$(var)` interpolation (`margin-$(dir): 10px`, `.icon.is-$(network)`), selector-position bare `$var` (`.$prefix`), comment substitutions (`<<$(var)>>`).
### Known divergences
## Known divergences
Deliberate divergences from Prettier. Three admission reasons:
1. Prettier's output would change program semantics (formatting must never do that)
2. The impact does not justify the matching cost
3. A uniform principle across our formatter crates beats emulating a Prettier inconsistency
Notable divergences are:
Admission reasons and rules: see FORMATTER_POLICY.md "Known divergences". Notable divergences are:
- An end-of-line trailing `//` comment never counts toward the print width (reason 3)
- Prettier's postcss printer does not distinguish `//` from `/* */`: it prints both inline,
@@ -335,45 +307,12 @@ Notable divergences are:
## Verification
```sh
cargo c -p oxc_formatter_css
```
Run `clippy` and resolve all warnings.
### Fixture tests
Snapshot tests driven by fixture files; covers what the Prettier conformance suite does not (placeholder at-rules, custom-property re-parsing, embedded css-in-js, etc.).
Fixtures are grouped per language (`format/{css,scss,less}/`; test modules mirror the directories), with the shared `options.json` at the `format/` / `embedded/` level (the harness walks up to the nearest one).
`embedded/scss/` is explicit about the dispatcher's variant=Scss hardcoding.
Unit tests in `tests/fixtures/mod.rs` cover parse-error `Err` semantics (`parse_error_is_err`).
Fixtures under `embedded/` route through `format_to_ir` instead of `format()`; the `embedded_debug` example formats files the same way for quick comparison.
Every expected output must be verified against Prettier (3.9.6, the current submodule).
`npx prettier@3.9.6 --parser <variant>` at both `--print-width 80` and `100` (the harness snapshots both).
Exception: a fixture may pin an entry from "Known divergences" (e.g. `map-item-parens.scss`);
its comments must say which lines deviate from Prettier and why.
```sh
cargo test -p oxc_formatter_css
# Review / accept snapshots after intentional changes
cargo insta review -p oxc_formatter_css
```
The harness snapshots both `--print-width 80` and `100`; verify fixtures at both widths.
### Prettier conformance
Compares output against Prettier's snapshots and tracks failures (not passes);
results live in `tasks/prettier_conformance/snapshots/prettier.css.snap.md` / `prettier.scss.snap.md` / `prettier.less.snap.md`.
```sh
cargo run -p oxc_prettier_conformance
# Debug a specific test
cargo run -p oxc_prettier_conformance -- --filter css/atrule
```
At the current version (v3.9.6), the divergences of seven files have been confirmed and are intentional (see "Known divergences"):
- CSS: `css/stylefmt-repo/at-media/at-media.css`, `css/stylefmt-repo/cssnext-example/cssnext-example.css`, `css/postcss-plugins/postcss-nesting.css`
@@ -394,17 +333,6 @@ Two more files fail with MIXED hunks; they can't pass as files (the intentional
- within-a-word runs (`1+1+1+1`, `calc(100%+2px)`) match
- glued number-ish runs are ONE postcss word and print raw (see `is_word_glued_number`)
### Embedded conformance (`apps/oxfmt`)
The embedded-language features (css-in-js) 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
```
### Manual checks
```sh
+11 -64
View File
@@ -1,5 +1,7 @@
# Coding agent guides for `crates/oxc_formatter_graphql`
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the GraphQL-specific rules and translations.
## Overview
Prettier compatible GraphQL formatter (`oxfmt`'s Tier 1 backend), using the `oxc_formatter_core` APIs.
@@ -8,12 +10,7 @@ Prettier compatible GraphQL formatter (`oxfmt`'s Tier 1 backend), using the `oxc
- See `crates/oxc_formatter_core/AGENTS.md` for the IR/pipeline details
- Two entry points:
- `format()`: standalone files (returns a printable `Formatted`)
- `format_to_ir()`: embedded use via the dispatcher (e.g. graphql-in-js);
allocates from the shared `EmbeddedContext` arena, emits no BOM / trailing newline,
and leaves `propagate_expand()` to the parent document
- The canonical reference is Prettier's OUTPUT (the conformance snapshots); its source is an analysis aid, not a porting target
- match its layout decisions, do not invent new ones
- never mirror its internal logic 1:1: pin behavior with fixtures, and keep implementation details of Prettier's code out of comments
- `format_to_ir()`: embedded use via the dispatcher (e.g. graphql-in-js)
### Forked parser
@@ -31,12 +28,10 @@ The fork aligns with graphql-js v17.x, which Prettier 3.9 targets:
### Error semantics
`format()` / `format_to_ir()` return `Err` whenever they cannot produce output they can stand behind:
The shared policy applies; GraphQL specifics:
- `oxc-graphql-parser` is error-tolerant (returns an AST even for invalid input), but any parse error bails out; never format a broken AST.
- `oxc-graphql-parser` is error-tolerant (returns an AST even for invalid input), but any parse error still bails out
- Several printer shortcuts (e.g. `close_delim_start`) are sound only under this guarantee
- The caller (oxfmt) decides what happens next
- Diagnostics for standalone files, template-as-is for embedded
### Comments
@@ -46,7 +41,7 @@ Prettier collects them from the token stream and attaches leading/trailing/dangl
This crate instead collects comment spans into a positional cursor, drained in source order by claim points spread through the printers
(the `flush_*` helpers in `src/comments.rs`; behavior pinned by `tests/fixtures/graphql/comments-inside-node-spans.graphql`).
Placement invariant (same as `oxc_formatter`'s): a comment stays between the source tokens it sat between, and a same-line trailing comment stays on its line.
The shared placement invariants apply: a comment stays between the source tokens it sat between, and a same-line trailing comment stays on its line.
Two bounded exceptions:
@@ -89,8 +84,8 @@ Blank-line runs inside block strings are part of the string VALUE and are emitte
## Known divergences
Deliberate divergences from Prettier, all one class: Prettier relocates a comment (an attachment artifact of `graphql-js` node boundaries, not a layout rule),
we keep it between its source tokens on its source line.
Admission reasons and rules: see FORMATTER_POLICY.md "Known divergences".
All current entries are one class: Prettier relocates a comment (an attachment artifact of `graphql-js` node boundaries, not a layout rule), we keep it between its source tokens on its source line.
- `"desc" type # c`: Prettier pulls the comment backwards across the keyword onto the description's line
- `"""d"""` + break + `# c` + break + `type A`: Prettier pushes the comment forward across the keyword (`type # c` + break + `A`)
@@ -100,65 +95,17 @@ we keep it between its source tokens on its source line.
## Verification
```sh
cargo c -p oxc_formatter_graphql
```
Run `clippy` and resolve all warnings.
### Fixtures tests
Snapshot tests driven by fixture files under `tests/fixtures/graphql/`,
covering what the Prettier conformance suite does not (suppression, string re-encoding, comment positions, extensions, ... — see the file names).
`build.rs` auto-generates a test case from every `.graphql` file using the core `test_support` harness.
Unit tests in `tests/fixtures/mod.rs` cover parse-error `Err` semantics and BOM preservation.
```sh
cargo test -p oxc_formatter_graphql
# Review / accept snapshots after intentional changes
cargo insta review -p oxc_formatter_graphql
```
Add a case by dropping a new `.graphql` file into the directory, the build script picks it up.
### Prettier conformance
Compares output against Prettier's snapshots and tracks failures (not passes);
results live in `tasks/prettier_conformance/snapshots/prettier.graphql.snap.md`.
The `graphql` language is part of the shared conformance binary.
```sh
cargo run -p oxc_prettier_conformance
# Debug a specific test
cargo run -p oxc_prettier_conformance -- --filter graphql/<dir>/<file>
```
### Embedded conformance (`apps/oxfmt`)
The embedded-language features (gql-in-js) 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
```
### Manual checks
Manual checks:
```sh
cargo run -p oxc_formatter_graphql --example graphql_formatter [filename]
# Compare with Prettier
npx prettier --parser=graphql [filename]
```
A good large real-world stress input is GitHub's public GraphQL schema (~72k lines).
It is too large and third-party to commit as a fixture,
bug-catching shapes are distilled into `tests/fixtures/graphql/implements-width.graphql`):
It is too large and third-party to commit as a fixture, bug-catching shapes are distilled into `tests/fixtures/graphql/implements-width.graphql`):
```sh
curl -sL https://docs.github.com/public/fpt/schema.docs.graphql -o /tmp/github-schema.graphql
diff <(npx prettier --parser=graphql /tmp/github-schema.graphql) \
diff <(node apps/oxfmt/node_modules/prettier/bin/prettier.cjs --parser=graphql /tmp/github-schema.graphql) \
<(cargo run -q -p oxc_formatter_graphql --example graphql_formatter /tmp/github-schema.graphql)
```
+2 -30
View File
@@ -1,5 +1,7 @@
# Coding agent guides for `crates/oxc_formatter_json`
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the JSON-specific rules and translations.
## Overview
Prettier compatible JSON/JSONC/JSON5/JSON.stringify formatter (`oxfmt`'s Tier 1 backend), using the `oxc_formatter_core` APIs.
@@ -37,33 +39,3 @@ Parsing always uses `SourceType::default()` (JS); `variant` only gates comment v
- Because the lenient JS parse accepts them (also matching Prettier, which routes every variant through its JS printer)
See the doc comments on `JsonVariant` in `src/options.rs` for the per-variant rules.
## Verification
```sh
cargo c -p oxc_formatter_json
```
Run `clippy` and resolve all warnings.
### Fixtures tests
Snapshot tests driven by fixture files under `tests/fixtures/{json,jsonc,json5,json-stringify}/` (one directory per variant).
`build.rs` auto-generates a test case from every `.{json,jsonc,json5}` file using the core `test_support` harness.
Each directory's `options.json` sets the `variant` (and any per-case options); `json-stringify` fixtures use the `.json` extension.
```sh
cargo test -p oxc_formatter_json
# Review / accept snapshots after intentional changes
cargo insta review -p oxc_formatter_json
```
Add a case by dropping a new file into the matching variant directory, the build script picks it up.
### Prettier conformance
Compares output against Prettier's snapshots and tracks failures (not passes); results live in `tasks/prettier_conformance/snapshots/`. The `json` language is part of the shared conformance binary.
```sh
cargo run -p oxc_prettier_conformance
```
+9 -51
View File
@@ -1,5 +1,7 @@
# Coding agent guides for `crates/oxc_formatter_yaml`
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the YAML-specific rules and translations.
## Overview
Prettier compatible YAML formatter (`oxfmt`'s Tier 1 backend), using the `oxc_formatter_core` APIs.
@@ -7,10 +9,7 @@ Prettier compatible YAML formatter (`oxfmt`'s Tier 1 backend), using the `oxc_fo
- 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
- Two entry points (see their docs in `src/format.rs`):
`format()` for standalone files, `format_to_ir()` for embedded use via the dispatcher (e.g. yaml-in-markdown)
- The canonical reference is Prettier 3.9's OUTPUT (the conformance snapshots); its source is an analysis aid, not a porting target
- match its layout decisions — EXCEPT where they are bugs or internally inconsistent (see below)
- never mirror its internal logic 1:1: pin behavior with fixtures, and keep implementation details of Prettier's code out of comments
- `format()` for standalone files, `format_to_ir()` for embedded use via the dispatcher (e.g. yaml-in-markdown)
### Parser
@@ -22,11 +21,11 @@ The split between the scalar's own output and the inter-item gap is printer poli
### Error semantics
`oxc-yaml-parser` is fail-fast (no partial AST), so `format()` / `format_to_ir()` return `Err` on any syntax error and never format a broken AST.
The caller (oxfmt) decides what happens next.
The shared policy applies; YAML specifics:
Under-indented multi-line flow scalars (prettier#8602) are one such error.
Prettier 3.9.5 also rejects them since its `yaml@2` upgrade, so the string corruption reported there cannot happen in either implementation.
- `oxc-yaml-parser` is fail-fast (no partial AST), so any syntax error is an `Err`
- Under-indented multi-line flow scalars (prettier#8602) are one such error
- Prettier 3.9.6 also rejects them since its `yaml@2` upgrade, so the string corruption reported there cannot happen in either implementation
### Line endings
@@ -44,16 +43,7 @@ Stream-tail end comments (`write_end_comments`) are the one document-layer excep
## Known divergences
Follow Prettier by default.
The exceptions (shared policy across all the formatter crates), are the cases where consistent output beats conformance percentage:
- Prettier bugs acknowledged as open issues
- behaviors that are internally inconsistent (same construct, different output depending on node kind or context)
Affected conformance fixtures stay counted as failures.
Style debates (`status:needs discussion` issues) are still followed, do not "improve" on taste for now.
Current divergences:
Admission reasons and rules: see FORMATTER_POLICY.md "Known divergences". Current divergences:
- anchor/tag order (prettier#19524): source order is preserved, never reordered
- `# prettier-ignore` range (prettier#13008): suppresses exactly one node, never every following node
@@ -78,42 +68,10 @@ Current divergences:
## Verification
```sh
cargo c -p oxc_formatter_yaml
```
Run `clippy` and resolve all warnings.
### Fixture tests
Snapshot tests driven by fixture files under `tests/fixtures/yaml/`, covering what the Prettier conformance suite does not (`# oxfmt-ignore`, divergence shapes, etc).
`build.rs` auto-generates a test case from every `.yaml` file using the core `test_support` harness; add a case by dropping a new file into the directory.
```sh
cargo test -p oxc_formatter_yaml
# Review / accept snapshots after intentional changes
cargo insta review -p oxc_formatter_yaml
```
### Prettier conformance
Compares output against Prettier's snapshots and tracks failures (not passes); results live in `tasks/prettier_conformance/snapshots/prettier.yaml.snap.md`.
The `yaml` language is part of the shared conformance binary.
```sh
cargo run -p oxc_prettier_conformance
# Debug a specific test
cargo run -p oxc_prettier_conformance -- --filter yaml/<dir>/<file>
```
Failures must be either fixed or classified: a new failure is acceptable only when it falls under the non-follow policy above, and it must be documented there.
### Manual checks
Manual checks:
```sh
cargo run -p oxc_formatter_yaml --example yaml_formatter [filename]
# Dump the formatter IR
DUMP_IR=1 cargo run -p oxc_formatter_yaml --example yaml_formatter [filename]
# Compare with Prettier
npx prettier --parser=yaml [filename]
```