mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
docs(formatter_core): clean up FORMATTER_POLICY (#26745)
This commit is contained in:
1 parent
4faab5f20e
commit
0c16983067
1 file changed
+28
-32
@@ -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
|
||||
|
||||
Reference in new issue
Block a user