docs(formatter_core): clean up FORMATTER_POLICY (#26745)

This commit is contained in:
leaysgur committed 2026-09-17 01:13:18 +00:00
1 parent 4faab5f20e
commit 0c16983067
1 file changed
+28 -32
+28 -32
View File
@@ -13,26 +13,17 @@ 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.
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.
Vocabulary used below:
- "taste": a layout no admission reason grounds (see "Known divergences"). Never admitted
- "the pin": the Prettier version pinned in `apps/oxfmt/package.json`, the single oracle for every crate
`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
- What we provide above all is CONSISTENT, PREDICTABLE, EXPLAINABLE behavior; the higher the conformance coverage the better, but 100% parity is not what we promise
- The canonical reference is Prettier's OUTPUT
- Its source code is an analysis aid, not a porting target
- Match its layout decisions; the only way to differ is an admitted divergence (see "Known divergences"), never taste
- Never mirror its internal logic 1:1; pin behavior with fixtures instead
- Before matching a mismatch, always check whether an admission reason already decides it (see "Known divergences")
- The pin is the oracle: the bundle, the conformance suite (via `oxc_formatter_tests`), and fixture verification all derive from that one version
- Prettier `main` is consulted for every mismatch, as evidence, not as an oracle: it shows whether the pin's output is Prettier's current intent or something upstream already considers a bug, but `main` is unreleased and may still change
- An upstream fix changes nothing in admission: our output follows from the reasons, whether or not `main` agrees
- A mismatch some reason grounds is entered like any other, citing the upstream PR as a reference; our shape may differ from `main`'s
- A mismatch no reason grounds is a STYLE change on `main`: never adopted ahead of the pin, we print the pin's layout until the pin moves
- 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`
- 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
### Known divergences
@@ -41,14 +32,12 @@ Admission reasons (1)-(4) are checked in order: the first that applies is the `W
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)
- Mandatory: Formatting must NEVER do that
- Verify semantics claims with the reference compiler/parser (tsc, dart-sass, lessc, ...), not intuition
- (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
- Idempotency: a second pass must reproduce the first, layout included; a formatter with no fixpoint has no defined output
- Mandatory: the output is inadmissible whatever its layout merits
- 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)
- (3) `uniform-rule`: our output follows a rule we apply uniformly; Prettier's output is admissible, we chose otherwise
@@ -58,29 +47,38 @@ Every reason is OUR decision factor; Prettier's mechanism (comment attachment, `
- 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, ...):
- `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 is taste until a second application exists
- 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
- (4) `cost`: The impact does not justify the matching cost
- Admissible only when the difference is layout-only and rarely triggered; the entry states what matching would require
- The usual cost is structural (our IR, printer or AST differs from Prettier's): the entry names the difference, which is its natural `Drop when`
- (5) `style-hold`: Prettier changed an intended style and we deliberately stay on the PREVIOUS Prettier version's output, pending an adoption decision
- The `Why` cites the tracking issue that records the decision: `style-hold (oxc#NNNN)`; never a novel style of our own
- Resolve by following the new style or re-classifying, then drop or rewrite the entry
- Resolve by following the new style or re-classifying
- An open upstream style debate (`status:needs discussion`) is not a hold: the pin's output is still matched
Rules:
- Every divergence is documented in the owning layer's `DIVERGENCES.md` and pinned by a fixture whose comments say which lines deviate from Prettier and why
- Entry format: an H2 slug (the stable anchor external pointers use), required `Why:` (reason keyword; its parenthetical holds what the reason defines, then any upstream issue as a bare reference: `uniform-rule (same construct, same output: X; prettier/prettier#NNNN)`) and `Pin:` lines; an input/ours/prettier example is the body, written with OUR behavior as the spec
- At a pin bump, re-run conformance: an entry no longer observed is deleted, the rest are re-checked against their reason; `Drop when:` is written only for a condition more specific than that default
- 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
- 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 carries a one-line marker: which lines deviate + `see DIVERGENCES.md#<slug>`; the why lives in the entry
- `DIVERGENCES.md#<slug>` is the one reference form everywhere (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
- `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
- Body: an input/ours/prettier code example
- No status, dates, or authors: listed = accepted, condition met = delete (git holds history)
- The owning layer is where the behavior is decided: a language crate for single-language behavior, `apps/oxfmt` for embedding E2E behavior
- A divergence discovered through a real-world conformance case (oxfmt externals, Prettier suite) is pinned by DISTILLING a minimal fixture into the owning layer's `tests/fixtures/`; the big source file stays in conformance as a regression net, never as the pin
- `DIVERGENCES.md` holds entries only, opening with one back-reference line to this section; `AGENTS.md` keeps policy translations and mechanism prose;
- oxfmt conformance notes never explain, one identifying line + `See <path>/DIVERGENCES.md#<slug>`
- Affected conformance fixtures stay counted as failures; a new conformance failure is acceptable only under this policy, and the crate's `AGENTS.md` conformance section lists the file with the entry slug it belongs to
- 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
- Lifecycle
- 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
- `DIVERGENCES.md` holds entries only, opening with one back-reference to this section; `AGENTS.md` keeps policy translations and mechanism prose
## Comment placement invariants
@@ -92,7 +90,6 @@ 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
- When Prettier relocates a comment across tokens, that is a divergence under reason (2) `invariant` (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
- One exception, Prettier's stance since prettier/prettier#1267:
@@ -102,7 +99,6 @@ The invariants (placement only; losing a comment is the lossless contract under
- 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 TS interface members)
- The replaceability test only separates these two
- Comments may move behind a terminator (per-language compat tables decide when); they always stay before a separator
- Except a same-line line comment: it rides a `line_suffix` and lands just past the separator (`a // c\n, b` -> `a, // c`), the separator cannot follow it on the line;
- an own-line comment leads what follows the separator instead