mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
docs(formatter_core): tighten FORMATTER_POLICY (#28206)
This commit is contained in:
1 parent
36e5ddb0f2
commit
49fbffbfe4
2 files changed
+42
-44
No files matched your search
@@ -1,35 +1,35 @@
|
||||
# Formatter policy
|
||||
|
||||
Shared policy for every formatter crate in the oxc ecosystem:
|
||||
|
||||
- `oxc_formatter` (JS/TSX)
|
||||
- `oxc_formatter_json`
|
||||
- `oxc_formatter_css`
|
||||
- `oxc_formatter_graphql`
|
||||
- `oxc_formatter_yaml`
|
||||
- `oxc_formatter_markdown`
|
||||
|
||||
using `oxc_formatter_core`, integrated by `apps/oxfmt`.
|
||||
Shared policy for every formatter crate whose `AGENTS.md` links here, built on `oxc_formatter_core` and integrated by `apps/oxfmt`.
|
||||
|
||||
Each crate's `AGENTS.md` holds only language-specific rules and the crate-local translation of these policies.
|
||||
A conflict between the two is a bug in one of them: until it is fixed, the crate file governs that crate, and the crate file records the conflict as a known violation.
|
||||
|
||||
`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`.
|
||||
## Fixing
|
||||
|
||||
- Fix the cause, never stack patches: a special case added only to make one failing input pass (a context check, a flag, a magic number) is a patch
|
||||
- A root fix removes the patches it makes redundant
|
||||
- Look for the bug's relatives (sites with the same check, assumption, or root cause) and fix them in the same change, or report the ones out of scope
|
||||
- Fix in the layer that owns the cause (parser, core, another crate), never compensate downstream
|
||||
- The parsers are ours to keep the formatters small and fast: a parser bug is fixed in the parser, even when the fix waits for a release
|
||||
|
||||
## Prettier compatibility
|
||||
|
||||
- We promise CONSISTENT, PREDICTABLE, EXPLAINABLE behavior, not 100% parity; higher conformance is better, parity is not the contract
|
||||
- The reference is Prettier's OUTPUT at the pin: the Prettier version pinned in `apps/oxfmt/package.json`
|
||||
- The goal is CONSISTENT, PREDICTABLE, EXPLAINABLE behavior, not 100% parity with Prettier
|
||||
- Consistency and a sound rationale win over parity: the `DIVERGENCES.md` files record where they already did
|
||||
- Conformance is a measure, not a target: a deliberate divergence lowers it and is still correct
|
||||
- The single oracle is Prettier's OUTPUT at the pin, the version in `apps/oxfmt/package.json`: the bundle, the conformance suite (`oxc_formatter_tests`) and fixture verification all use it
|
||||
- Its source code is an analysis aid, never a porting target: pin behavior with fixtures, do not mirror its logic
|
||||
- A mismatch is matched unless an admission reason decides otherwise (see "Known divergences")
|
||||
- The pin is the single oracle: the bundle, the conformance suite (`oxc_formatter_tests`) and fixture verification all use that one version
|
||||
- Prettier `main` is evidence, not an oracle: it shows whether the pin's output is Prettier's intent or a bug already fixed there
|
||||
- A `main` fix is followed as a divergence from the pin: it is a (1)-(3) case, entered under that reason citing the PR
|
||||
- A mismatch is fixed to match the pin, unless an admission reason decides otherwise (see "Known divergences")
|
||||
- Prettier `main` and its issues are evidence, not an oracle: they show whether the pin's output is Prettier's intent, a known bug, or a bug already fixed there
|
||||
- Before fixing or admitting a mismatch, check them along with the `DIVERGENCES.md` entries and open oxc issues
|
||||
- Following a `main` fix is a judgment call: follow clear bug fixes, stay on the pin for unstable changes or ones the community pushes back on
|
||||
- A followed fix is a divergence from the pin, entered under (1)-(3) citing the PR
|
||||
|
||||
### Known divergences
|
||||
|
||||
Admission reasons (1)-(4) are checked in order: the first that applies is the `Why`, the other traits go in the prose.
|
||||
(5) is outside that order: a temporary hold on a pin change that no reason grounds, pending an adoption decision.
|
||||
Admission reasons (1)-(4) are checked in order: the first that applies is the `Why`, the other applicable reasons go in the prose.
|
||||
(5) is outside that order: a temporary hold, only when none of (1)-(4) applies.
|
||||
Every reason is OUR decision factor; Prettier's mechanism (comment attachment, `lineSuffix`, token lexing, ...) is explanation for the prose, never a reason.
|
||||
|
||||
- (1) `semantics`: Prettier's output would change program semantics (fails to re-parse, or re-parses to a different value/structure)
|
||||
@@ -37,16 +37,16 @@ Every reason is OUR decision factor; Prettier's mechanism (comment attachment, `
|
||||
- (2) `invariant`: Prettier's output breaks one of our formatter contract invariants; that set is closed, extending it is a policy change
|
||||
- Lossless: every comment is printed, exactly once; comments are the user's, and losing one outranks moving one
|
||||
(whatever the compiler makes of it: a comment may carry tool meaning, `/*! */`, `-disable`, etc)
|
||||
- The comment placement invariants (see "Comment placement invariants" below): the output moves content the user owns
|
||||
- Comment placement: the output moves a comment in a way "Comment placement invariants" below forbids
|
||||
- Idempotency: a second pass must reproduce the first, layout included; a formatter with no fixpoint has no defined output
|
||||
- Only the comment itself moving is (2); when the comment stays and the tokens around it change layout, that is (3) `uniform-rule (comment presence never changes layout)`
|
||||
- No reason parenthetical (an upstream issue reference is still fine): the prose names which invariant in one sentence (crosses content, changes line, loses a suppression target, is not a fixpoint)
|
||||
- The `Why` takes no parenthetical except upstream issues; the prose names the broken invariant in one sentence (crosses content, changes line, loses a suppression target, is not a fixpoint)
|
||||
- (3) `uniform-rule`: our output follows a rule we apply uniformly; Prettier's output is admissible, we chose otherwise
|
||||
- The only judgment call among (1)-(3); the `Why` names the rule
|
||||
- The usual trigger is Prettier's internal inconsistency, one construct printed differently by context:
|
||||
- `uniform-rule (same construct, same output: <sibling>)`, we print the sibling's shape everywhere
|
||||
- The sibling is a construct Prettier ITSELF prints that way (measured), never a shape of our own; an entry that cannot name it is not admitted under this rule
|
||||
- The rarer trigger is Prettier being consistent but sidestepping one of our rules (an option, the `line_suffix` width rule, ...):
|
||||
- The rarer trigger is Prettier being consistent but sidestepping one of our rules (a user option governs everywhere, the `line_suffix` width rule, ...):
|
||||
- `uniform-rule (<rule>)`, and the entry body cites where the rule already applies (a section of this document, another crate, another construct)
|
||||
- A rule with no other application is not uniform; it needs a second application first
|
||||
- When both a named rule and a sibling apply, the `Why` names the rule: it holds regardless of what Prettier prints for the sibling
|
||||
@@ -62,16 +62,15 @@ Rules:
|
||||
|
||||
- Every divergence has an entry in the owning layer's `DIVERGENCES.md` and a pin fixture
|
||||
- The owning layer decides the behavior: a language crate for single-language behavior, `apps/oxfmt` for embedding
|
||||
- A behavior core decides for every language (see "Text width") is a rule in this document, not an entry;
|
||||
each language crate where it shows pins it as an entry citing that section
|
||||
- The pin is a minimal fixture in the owner's `tests/fixtures/`, distilled from whatever exposed the divergence; a conformance file is never the pin, it stays a regression net
|
||||
- The fixture points back at its entry: it is named after the slug, or carries a one-line `DIVERGENCES.md#<slug>` marker
|
||||
(a fixture pinning several entries, or a language where a comment is content); the why and the deviating lines live in the entry
|
||||
- `DIVERGENCES.md#<slug>` is the one reference form everywhere (docs, source, fixture markers); never a quoted or bare slug, never an upstream issue alone
|
||||
- A behavior core decides for every language (see "Text width") is a rule in this document;
|
||||
each language crate where it shows has its own entry and pin fixture, citing that section
|
||||
- The pin fixture is minimal and lives in the owning layer, distilled from whatever exposed the divergence; a conformance file is never the pin fixture, it stays a regression net
|
||||
- The entry's `Pin:` names the fixture, which needs no link back; the why and the deviating lines live in the entry
|
||||
- `DIVERGENCES.md#<slug>` is the one form for referring to an entry (docs, source, fixtures); never a quoted or bare slug, never an upstream issue alone
|
||||
- Entry format, with OUR behavior as the spec
|
||||
- H2 slug: the stable anchor
|
||||
- `Why:` required; the reason keyword, its parenthetical, then upstream issues as bare references: `uniform-rule (same construct, same output: X; prettier/prettier#NNNN)`
|
||||
- `Pin:` required
|
||||
- `Pin:` required; the fixture path
|
||||
- `Conformance:` optional; the failing files of the owner's own suite
|
||||
- `Oxfmt:` optional, language crates only; the oxfmt externals failing on the entry
|
||||
- `Drop when:` optional; only for a condition more specific than the pin-bump default
|
||||
@@ -81,7 +80,7 @@ Rules:
|
||||
- A new conformance failure is acceptable only under this policy; it stays counted as a failure
|
||||
- A failing file no entry lists is unclassified; the crate's `AGENTS.md` conformance section holds only those
|
||||
- At a pin bump, re-run conformance: an entry no longer observed is deleted, the rest are re-checked against their reason
|
||||
- The rule cuts both ways: never "fix" a conformance failure by following Prettier into a documented divergence; check the entries and open oxc issues before treating a diff as a plain bug
|
||||
- A documented divergence is never "fixed" by following Prettier
|
||||
- `DIVERGENCES.md` holds entries only, opening with one back-reference to this section; `AGENTS.md` keeps policy translations and mechanism prose
|
||||
|
||||
## Comment placement invariants
|
||||
@@ -93,12 +92,12 @@ Two layers of rules; know which one you are editing:
|
||||
|
||||
The invariants (placement only; losing a comment is the lossless contract under reason (2), above all of these):
|
||||
|
||||
- A comment never crosses user content (code, other comments, other tokens): it stays on its source side of every token
|
||||
- A comment never crosses user content (tokens and other comments): it stays on its source side of every token
|
||||
- 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
|
||||
- One exception, Prettier's stance since prettier/prettier#1267:
|
||||
- A same-line comment right after an opening delimiter (`{ // c`, `( # c`, `[ // c`) is the body's first leading comment and moves to its own line;
|
||||
- `-line` directive there loses its target (`-next-line` variants do not)
|
||||
- A `-line` directive there loses its target (`-next-line` variants do not)
|
||||
- Applies to every opener uniformly; where Prettier's attachment does something else for a particular opener, that is an artifact to pin as a divergence, not a table to extend
|
||||
- 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
|
||||
@@ -109,7 +108,7 @@ The invariants (placement only; losing a comment is the lossless contract under
|
||||
- Grammar-fixed DELIMITER (braces, a head's parens, an argument list's parens) is neither: it bounds a region and stays user content, never crossed
|
||||
- (JS/TS) Redundant expression parentheses are NOT delimiters: the formatter drops them and re-derives parens by its own rules, so any paren in the output is formatter-owned
|
||||
- Trailing comment inside the dropped pair moves behind the terminator, even across a re-printed pair
|
||||
- The source pair stays user content only where a sub-printer claims it and prints the comment inside (per-language keeps tables)
|
||||
- The source pair stays user content only where a sub-printer claims it and prints the comment inside (per-language tables)
|
||||
- The line-boundary invariant still gates the move: never across a re-printed paren that ends up on its own line
|
||||
|
||||
Per-language translations (which tokens are terminators, the compat tables, cursor bounds disciplines) live in each crate's AGENTS.md.
|
||||
@@ -131,7 +130,7 @@ never a byte or UTF-16 length, even where Prettier uses a JS `.length`.
|
||||
|
||||
- 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
|
||||
- The caller (oxfmt) decides what happens next
|
||||
|
||||
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.
|
||||
@@ -161,7 +160,8 @@ Fixture tests also assert the lossless contract through `FixtureFormatter::finge
|
||||
### 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.
|
||||
`build.rs` generates a test per fixture 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>
|
||||
@@ -169,7 +169,7 @@ cargo test -p <crate>
|
||||
cargo insta test --accept -p <crate>
|
||||
```
|
||||
|
||||
Every expected output must be verified against the pin; in a fixture pinning a "Known divergence", every line except the ones its comments mark as deviating.
|
||||
Every expected output must be verified against the oracle; in a pin fixture, every line except the ones its entry shows as deviating.
|
||||
|
||||
### Prettier conformance
|
||||
|
||||
@@ -183,13 +183,13 @@ cargo test -p <crate> --test conformance
|
||||
PRETTIER_FILTER=<path> cargo test -p <crate> --test conformance -- --nocapture
|
||||
```
|
||||
|
||||
The Prettier suite lives under `crates/oxc_formatter_tests/prettier/` and is self-provisioned on the first conformance run. It is gitignored, so use `rg --no-ignore` (or `-u`) when searching it.
|
||||
|
||||
Failures must be either fixed or classified under "Known divergences".
|
||||
The Prettier suite lives under `crates/oxc_formatter_tests/prettier/` and is self-provisioned on the first conformance run.
|
||||
It is gitignored, so use `rg --no-ignore` (or `-u`) when searching it.
|
||||
|
||||
### E2E 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.
|
||||
The embedded-language features (e.g. xxx-in-js / js-in-xxx) are validated end-to-end through Oxfmt.
|
||||
Requires a dev build first.
|
||||
A language crate's fixtures cover only its own side (e.g. `format_to_ir` on a fragment): they run without a dispatcher, so an embedded body stays verbatim.
|
||||
|
||||
There are also conformance tests for each language that use real-world repositories,
|
||||
compared on Prettier's output of each file (what a Prettier user hands over), not as written; test suites stay as written.
|
||||
Some categories also run on real-world repositories; see `apps/oxfmt`'s `AGENTS.md`.
|
||||
@@ -82,8 +82,6 @@ node -e 'const p=require("./apps/oxfmt/node_modules/prettier");p.__debug.printTo
|
||||
|
||||
Every failing file is accounted for by a DIVERGENCES.md entry's `Conformance:` line; there are no unclassified failures.
|
||||
|
||||
Pin fixtures are named after their entry's slug (a comment would be an HTML block and change the document).
|
||||
|
||||
### Fixture fingerprint
|
||||
|
||||
`tests/fixtures/fingerprint.rs` serializes the AST's meaning (structure, decoded text, destinations, labels) and ignores what formatting may change
|
||||
|
||||
Reference in new issue
Block a user