mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-06 17:34:25 +00:00
docs(formatter): update AGENTS.md comment handling (#26551)
Restructure.
This commit is contained in:
1 parent
65b65e6c0f
commit
66fda4fea0
7 files changed
+135
-110
No files matched your search
+114
-105
@@ -1,11 +1,11 @@
|
||||
# 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.
|
||||
Known divergences live in DIVERGENCES.md (not yet an exhaustive audit against the conformance snapshots).
|
||||
Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the JS/TSX-specific rules and translations.
|
||||
Known divergences live in DIVERGENCES.md.
|
||||
|
||||
## Overview
|
||||
|
||||
Prettier compatible JS/TS formatter (`oxfmt`'s Tier 1 backend), ported from [Biome](https://github.com/biomejs/biome)'s `biome_js_formatter`.
|
||||
Prettier compatible JS/TSX 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
|
||||
@@ -20,7 +20,7 @@ Call-sites use text-in entry points;
|
||||
The AST-wrapping IR primitives (`AstNode`, `Format`, `Buffer`, …) are `pub(crate)` and not part of the contract.
|
||||
|
||||
- `format`: Format a whole file (text-in)
|
||||
- `format_fragment`: Format a JS/TS fragment for js-in-xxx embedding, parameterized by `FragmentContext`
|
||||
- `format_fragment`: Format a JS/TSX fragment for js-in-xxx embedding, parameterized by `FragmentContext`
|
||||
- Drives context-dependent decisions like forced parentheses / quote style
|
||||
- The formatter knows nothing about Prettier/Vue vocabulary, callers pass wrapped source
|
||||
- `format_program`: Special-purpose AST-in entry point
|
||||
@@ -32,20 +32,22 @@ The AST-wrapping IR primitives (`AstNode`, `Format`, `Buffer`, …) are `pub(cra
|
||||
`ast_nodes/generated/` and the `Format` glue are generated by `tasks/ast_tools`.
|
||||
After changing AST shapes or the generators, regenerate with `just ast`, never hand-edit files under `generated/`.
|
||||
|
||||
## JS formatter specific features
|
||||
## JS/TSX formatter specific features
|
||||
|
||||
### Sort imports (`ir_transform/sort_imports/`)
|
||||
|
||||
- Inspired by `eslint-plugin-perfectionist`, but not a 1:1 match (default group definitions differ too)
|
||||
- Implemented purely as a Rust IR transform; requires no JS callback
|
||||
- Inspired by `eslint-plugin-perfectionist`, but not fully compatible
|
||||
- Implemented purely as a Rust IR transform; requires no `SessionServices`
|
||||
|
||||
### Format JSDoc (`formatter/jsdoc/`)
|
||||
|
||||
- Derived from `prettier-plugin-jsdoc`, but not fully compatible
|
||||
- Covered by plain fixture-pair tests (`--test jsdoc`, committed input/expected pairs);
|
||||
a mismatch is a failing test, not a tracked conformance-report entry
|
||||
- See `tests/jsdoc/fixtures` for the covered behavior
|
||||
- See also `tests/jsdoc/upstream-jsdoc-bugs.md`
|
||||
- Implemented in Rust as a comment-text rewrite at print time (`formatter/trivia.rs`), not an IR transform
|
||||
- Non-JS fenced code blocks go through the session's optional `StringEmbedder`; without it (or on failure) they stay verbatim
|
||||
- Covered by plain fixture-pair tests (`--test jsdoc`, committed input/expected pairs);
|
||||
a mismatch is a failing test, not a tracked conformance-report entry
|
||||
|
||||
### Sort Tailwind CSS
|
||||
|
||||
@@ -55,140 +57,142 @@ After changing AST shapes or the generators, regenerate with `just ast`, never h
|
||||
|
||||
### Embedded language formatting
|
||||
|
||||
Two directions: xxx-in-js (css/graphql/html in template literals) and js-in-xxx (vue/svelte).
|
||||
Two directions: xxx-in-js (e.g. CSS in template literals) and js-in-xxx (e.g. Vue `script` block).
|
||||
|
||||
- xxx-in-js goes through the `FormatDispatcher` Oxfmt assembles Rust based formatter, Prettier Doc→IR fallback otherwise
|
||||
- As the JS host, this crate also owns the parent-side concerns in `print/template/embed/`:
|
||||
- template-literal escape on returned IR,
|
||||
- placeholder marker insertion / survival validation / `${expr}` substitution,
|
||||
- and `.raw` vs `.cooked` selection
|
||||
- Language formatter crates stay free of these rules
|
||||
- See `embed/mod.rs` for the shared helpers and `embed/{css,html,graphql,markdown}.rs` for each site's wiring
|
||||
- js-in-xxx works with `prettier-plugin-oxfmt` which uses `format_fragment`
|
||||
xxx-in-js goes through the `FormatDispatcher` that Oxfmt assembles: a Rust formatter serves the language when one exists, otherwise Prettier's Doc is converted to IR as the fallback.
|
||||
|
||||
## Fixing IR construction
|
||||
As the JS host, this crate also owns the parent-side concerns in `print/template/embed/`:
|
||||
|
||||
- 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
|
||||
- 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)
|
||||
- template-literal escape on returned IR
|
||||
- placeholder marker insertion / survival validation / `${expr}` substitution
|
||||
- `.raw` vs `.cooked` selection
|
||||
|
||||
### Comment placement invariants
|
||||
Language formatter crates stay free of these rules.
|
||||
See `embed/mod.rs` for the shared helpers and `embed/{css,html,graphql,markdown}.rs` for each site's wiring.
|
||||
|
||||
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.
|
||||
js-in-xxx works with `prettier-plugin-oxfmt` which uses `format_fragment`. See `apps/oxfmt` in details.
|
||||
|
||||
Four token classes decide a comment's freedom of movement.
|
||||
Replaceability does not separate them (only the separator is replaceable); the guaranteed flush point does:
|
||||
a delimiter flushes on the comment's own side, a terminator only right BEHIND itself, so a riding line comment lands just past it and nothing else:
|
||||
## Comments
|
||||
|
||||
- (1) Statement terminator: `;` after statements and class members
|
||||
(property/accessor via `FormatClassElementWithSemicolon`, bodyless methods in `MethodDefinition`).
|
||||
Formatter-owned: a same-line trailing comment moves behind it, block and line comments alike (the move-behind table below)
|
||||
- (2) In-head terminator: the for-head's `;`s, a label's or a single-block case test's `:`.
|
||||
Block and own-line comments keep their slot (the uniform rule);
|
||||
only a same-line line comment moves, just behind the token (`for (a // c\n; b;)` -> `a; // c`, `foo // c\n: b();` -> `foo: // c` + break)
|
||||
- (3) Separator: interchangeable with another token (interface / type literal members and index signatures
|
||||
(`;` ⇄ `,`), enum members' `,`) the comment stays before it, same as any list separator.
|
||||
This matches Prettier and is the principled line, not an emulated quirk
|
||||
- (4) Delimiter: bounds a region, a body's `{` `}`, a head's `(` `)`, the `}`-to-keyword gap.
|
||||
User content: comments never cross it in either direction (the head-body comment policy below)
|
||||
Comments (and parentheses) are where a local fix has side effects; keep the rules below in view.
|
||||
The invariants and the invariants-vs-compat-tables split live in FORMATTER_POLICY.md "Comment placement invariants"; this section is their JS/TSX translation.
|
||||
Prettier's attachment artifacts that relocate a comment across tokens are divergences to pin, never rules to emulate (DIVERGENCES.md#array-hole-trailing-comment, DIVERGENCES.md#type-alias-trailing-comment-move).
|
||||
|
||||
Known gap, intentionally not covered: TS-only statements
|
||||
(`import A = B;` / `export = x;` / `export as namespace X;` / `declare function f(): void;` / `declare module "m";`).
|
||||
### Vocabulary
|
||||
|
||||
They are terminators by the rule above, but their comments currently stay in place (coinciding with Prettier).
|
||||
Uniformizing them is OUR call, deferred for impact:
|
||||
each is a small mechanical `FormatContentWithSemicolon` adoption plus a fixture pin and a DIVERGENCES entry.
|
||||
- Leading / trailing: the newline is the boundary, not the node's span.
|
||||
Trailing is the same-line run after content; a comment preceded by a newline is leading for what follows (`formatter/trivia.rs` module doc)
|
||||
- Deferred: left unprinted on purpose for a later pass (the next sibling's leading pass or the container's dangling pass)
|
||||
- Claim: a pass prints a comment and advances the cursor past it
|
||||
- Hide: a pass makes comments invisible to a sub-printer (`limit_comments_up_to`) so a later pass can print them
|
||||
- Suppression: only ever `prettier-ignore` / `oxfmt-ignore`
|
||||
|
||||
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`).
|
||||
### Mechanism
|
||||
|
||||
The "which positions keep their parens" table is intentionally encoded twice:
|
||||
No attachment pass: the parser's sorted comment array is read through a cursor (`Comments`, `formatter/comments.rs`),
|
||||
each node's generated `fmt` prints its leading and trailing runs (`formatter/trivia.rs`), and a claimed comment is never considered again.
|
||||
|
||||
- `keeps_trailing_comment_inside_parens` (statement side, walks down the rightmost expression)
|
||||
- and `write_trailing_comments_inside_parens` (expression side, matches on the parent) in `print/semicolon.rs`
|
||||
- The cursor is monotonic: a printer claims every comment inside what it printed, or hides it deliberately;
|
||||
a comment left behind lands in a later pass's slot (moved) or is lost (the fixture fingerprint detects and fails)
|
||||
- Two query families:
|
||||
- the cursor-based ones (`comments_before` and friends, the unprinted view) are for PRINTING
|
||||
- the position-based ones (`all_comments_*`) are for LAYOUT DECISIONS
|
||||
- A decision evaluated more than once for a node (cached bodies, a re-entered `fmt`) must not depend on the cursor
|
||||
|
||||
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.
|
||||
### Token classes
|
||||
|
||||
One rule decides WHAT moves, at every semicolon-terminated site:
|
||||
trailing is the same-line run only. A comment preceded by a newline is never trailing, it defers (stays unprinted) to the next node's leading pass, which keeps it own-line with its blank lines.
|
||||
The leading/trailing vocabulary itself (the newline as the semantic boundary, and why deferred comments belong to the NEXT node) is defined in `formatter/trivia.rs`'s module doc.
|
||||
Three token classes decide a comment's freedom of movement:
|
||||
|
||||
The defer contract: a deferred comment crosses only already-printed statement terminators.
|
||||
The terminator set (`;`, and the `)` of dropped or re-printed parens) is encoded twice.
|
||||
`Comments::has_semicolon_or_closing_paren_in_range` decides whether the same-line run moves, `lines_after_skipping_terminators` makes the same tokens transparent to the deferred comment's break measurement.
|
||||
So change them together, and pin any extension in a fixture. (`get_lines_before`'s `;` skip is a different token: the ASI-guard `;` the next statement's own format emitted, sitting immediately before it.)
|
||||
A downstream pass must exist to print a deferred comment. (the next sibling's leading pass or the container's dangling pass.)
|
||||
- (1) Statement terminator: the `;` after statements and class members that `semi` option governs (added under ASI, dropped under `semi: false`).
|
||||
Formatter-owned, so the same-line trailing run moves behind it, block and line comments alike ("Moving behind a terminator (class 1)")
|
||||
- (2) Separator and in-head terminator: list separators (`,`, interface / type literal members' `;` ⇄ `,`), the for-head's `;`s, a label's or a single-block case test's `:`.
|
||||
The comment stays before the token; only a same-line line comment rides its `line_suffix` past it (`for (a // c\n; b;)` -> `a; // c`, `foo // c\n: b();` -> `foo: // c` + break)
|
||||
- (3) Delimiter: a body's `{` `}`, a head's `(` `)`, the `}`-to-keyword gap.
|
||||
User content: comments never cross it in either direction ("Head-body and operator gaps (class 3)")
|
||||
|
||||
WHEN the same-line run moves is the per-site compat table (extend by measuring Prettier, not by analogy):
|
||||
Redundant expression parens are formatter-owned, not delimiters (FORMATTER_POLICY):
|
||||
a comment inside a dropped pair moves with the content's trailing run; a pair that survives keeps its comments inside (the keeps table below).
|
||||
|
||||
| Site | Source `;` required? |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
| Statements, class property/accessor, bodyless methods (`FormatContentWithSemicolon`, `FormatClassElementWithSemicolon`, `MethodDefinition`) | `;` or a `)` (bare ASI: no move) |
|
||||
| return/throw (`ReturnAndThrowStatement`) | no (moves even under ASI) |
|
||||
### Moving behind a terminator (class 1)
|
||||
|
||||
A `)` in the gap counts as a terminator (dropped or re-printed parens; the rationale lives in `trailing_comments_to_move_behind_semicolon`'s rustdoc, prettier#19930 family).
|
||||
The chain-leaf walk (`assignment_chain_leaf_end`) bounds the content at every semicolon-terminated content site (expression statements, export defaults, variable declarations, class property values, return arguments);
|
||||
which arrow bodies stop it (and the flat-JSX-body known limitation) live in `arrow_body_keeps_trailing_comment_inside_parens` and DIVERGENCES.md#paren-comment-fixpoint.
|
||||
The formatter's `;` is glued to the content.
|
||||
A same-line comment between the content and the source `;` prints after the `;`, at every site, with or without a source `;` (ASI):
|
||||
`foo /* c */;` and `foo /* c */` + newline both print `foo; /* c */`.
|
||||
An own-line comment defers to the next leading pass and crosses only already-printed terminators (`;`, and the `)` of dropped or re-printed parens).
|
||||
|
||||
JS-side mechanics of the shared "never cross" invariants:
|
||||
The only comment that stays before the `;` sits inside source parens that survive in the output;
|
||||
which parens survive is the keeps table in `print/semicolon.rs` (`keeps_trailing_comment_inside_parens`, prettier#19263), a measured compat table.
|
||||
The content bound for the move is the chain-leaf walk there (`assignment_chain_leaf_end`).
|
||||
|
||||
- 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
|
||||
- One known violation, kept for Prettier compat: an own-line comment claimed mid-line inlines onto that line (`const // c` + break), see the NOTE in `FormatLeadingComments` (`formatter/trivia.rs`)
|
||||
- User content: attachment artifacts that relocate a comment across tokens are divergences to pin, never rules to emulate (e.g. DIVERGENCES.md#array-hole-trailing-comment)
|
||||
- Suppression: when hiding comments from a node (`limit_comments_up_to`), check `has_trailing_suppression_comment` first, or the node loses its suppression
|
||||
Site specifics: return/throw's same-line-prefix dangling split (`ReturnAndThrowStatement`),
|
||||
and the single-statement-body escape for deferred own-line comments in `Comments::get_trailing_comments` (`if (1) foo\n// c\n;`).
|
||||
|
||||
Head-body comment policy ("never cross user content" applied to statement/declaration heads):
|
||||
a body's `{` `}` and a head's `(` `)` are delimiters, and an empty-statement body's `;` is content, not a terminator (the verbatim empty-statement note in "Statement terminators and suppression"), so a comment between a head and its body keeps its side of each, uniformly across constructs:
|
||||
### Head-body and operator gaps (class 3)
|
||||
|
||||
A body's `{` `}` and a head's `(` `)` are delimiters, and an empty-statement body's `;` is content, not a terminator (see "Suppression"),
|
||||
so a comment between a head and its body keeps its side of each, uniformly across constructs:
|
||||
|
||||
- same-line block comment stays inline (`while (x) /* c */ {`)
|
||||
- same-line line comment keeps its line, forcing the `{` onto the next line (`while (x) // c` + break)
|
||||
- own-line comment keeps its own line
|
||||
- comments before a `}`-to-keyword gap (`else`/`catch`/`finally`/`while`) split at the keyword and keep their side
|
||||
|
||||
The `as`/`satisfies` operator gap follows the same policy (see `as_or_satisfies_expression.rs` and DIVERGENCES.md#binary-cast-own-line-comment), with two additions of its own:
|
||||
The `as`/`satisfies` operator gap follows the same policy (`as_or_satisfies_expression.rs`, DIVERGENCES.md#binary-cast-own-line-comment), with two additions:
|
||||
|
||||
- the pre-operator slot is grammar-bounded (no line terminator may precede the operator, a multiline comment's interior counts), so once the expression's source parens are dropped, only same-line single-line block comments stay on the expression side;
|
||||
- everything else normalizes to the type side so the output re-parses
|
||||
- the pre-operator slot is grammar-bounded (no line terminator may precede the operator, a multiline comment's interior counts),
|
||||
so once the expression's source parens are dropped, only same-line single-line block comments stay on the expression side;
|
||||
everything else normalizes to the type side so the output re-parses
|
||||
- paragraph-like promotion: a line-ending multiline block goes own-line above the type.
|
||||
- `=`/`:` reach the same outputs through their own machinery (`AssignmentLike`, see DIVERGENCES.md#eol-comment-after-assign-colon for their line-comment glue);
|
||||
- the head-body `write_*` helpers split on `preceded_by_newline` alone and do not have the promotion.
|
||||
`=`/`:` reach the same outputs through `AssignmentLike` (DIVERGENCES.md#eol-comment-after-assign-colon);
|
||||
the head-body `write_*` helpers split on `preceded_by_newline` alone and do not promote
|
||||
|
||||
Implemented by the `write_*` helpers in `utils/statement_body.rs` and `FormatParenHeadExpression` (`print/mod.rs`); their rustdocs cover the trailing-pass suppression mechanics.
|
||||
Implemented by the `write_*` helpers in `utils/statement_body.rs` and `FormatParenHeadExpression` (`print/mod.rs`);
|
||||
their rustdocs cover how the head's generic trailing pass is kept from claiming the gap.
|
||||
|
||||
Prettier's comment attachment is position-heuristic and sometimes asymmetric;
|
||||
that is FORMATTER_POLICY's uniform-rule ground (reason 3): one rule over the emulated asymmetry, pinned as a divergence (e.g. DIVERGENCES.md#type-alias-trailing-comment-move).
|
||||
### Suppression
|
||||
|
||||
### Statement terminators and suppression
|
||||
A suppression comment protects content; the token classes above still apply to what the node prints around it.
|
||||
|
||||
The formatter owns statement terminators (and the trivia up to them); the user owns content.
|
||||
|
||||
- print side: `FormatContentWithSemicolon` and the move-behind table above
|
||||
- return/throw: the same-line-prefix dangling split in `ReturnAndThrowStatement`
|
||||
- capture side: `Comments::get_trailing_comments` lets deferred own-line comments escape when the statement shares its distant `;` with the enclosing statement
|
||||
(a single-statement body, `if (1) foo\n// c\n;`); a block's last statement keeps them inside instead
|
||||
- suppressed side: a suppressed node keeps the token classes above: content verbatim, terminator per `semi`
|
||||
(`write_suppressed_statement`, `FormatClassElementWithSemicolon`); a node without a terminator of its own prints its whole span.
|
||||
- Target: a trailing suppression comment counts like a leading one for every node (`is_span_suppressed` in the generated `fmt`),
|
||||
and the outermost node ending there claims it; a union leaves it to its member instead (Prettier's `handleUnionTypeComments`)
|
||||
- Statements and class members: content verbatim, terminator per `semi` (`write_suppressed_statement`, `FormatClassElementWithSemicolon`);
|
||||
a node without a terminator of its own prints its whole span.
|
||||
Prettier re-adds a statement's `;` only when the source had one and prints class members whole (DIVERGENCES.md#suppressed-terminator-per-semi)
|
||||
- A trailing suppression comment counts like a leading one for every node (`is_span_suppressed` in the generated `fmt`);
|
||||
the outermost node ending there claims it, except a union, whose member does
|
||||
- A statement is decided before its own generated `fmt`, so leading and trailing comments take one path and a pre-`export` decorator is inside the ignored range (`statement_span`)
|
||||
- Any site printing a statement outside the generated `Statement` fmt (an `if` consequent before `else`) must ask `write_suppressed_statement` first,
|
||||
or the generic verbatim path prints the source `;` regardless of `semi`
|
||||
- The import sorter's partition test and the printer share `is_node_suppressed`: a suppressed import must be a boundary to both
|
||||
- Keep `suppressed_statement_content_end` in step with the reprint's `;`-printing sites (`FormatContentWithSemicolon` / `OptionalSemicolon`): same content end, same terminator
|
||||
- A `for` head declaration has no terminator of its own and stays verbatim (DIVERGENCES.md#suppressed-for-head-declaration)
|
||||
- suppressed expression side: `write_suppressed_expression` (`utils/suppressed.rs`) owns the whole sequence
|
||||
for expression-shaped nodes (the generated `fmt` and the arrow sequence-body site call it before anything of the node is printed),
|
||||
so a cast target keeps its source cast parens (excluded from its span, `utils/typecast.rs`'s `write_suppressed_cast_target`) and in-paren comments print in place via the verbatim range
|
||||
- Expressions: `write_suppressed_expression` (`utils/suppressed.rs`) owns the whole sequence for expression-shaped nodes
|
||||
(the generated `fmt` and the arrow sequence-body site call it before anything of the node is printed),
|
||||
so a cast target keeps its source cast parens (`utils/typecast.rs`'s `write_suppressed_cast_target`) and in-paren comments print in place via the verbatim range
|
||||
|
||||
Accepted edges (semantically inert, idempotent):
|
||||
Accepted edge (semantically inert, idempotent):
|
||||
the `semi: false` ASI guard is decided from the guarded statement alone, never from the previous statement's output.
|
||||
Sound because no statement leaves its own trailing `;` (a suppressed one hands it back to the formatter),
|
||||
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`.
|
||||
|
||||
- The `semi: false` ASI guard is decided from the guarded statement alone, never from the previous statement's output;
|
||||
sound because no statement leaves its own trailing `;` (a suppressed one hands it back to the formatter),
|
||||
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`
|
||||
### Couplings to keep in step
|
||||
|
||||
Each row pairs a decision with the site that acts on it, on purpose. No assert catches drift, so change both and pin the change in a fixture.
|
||||
|
||||
| Coupling | Drift symptom |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| keeps table: `keeps_trailing_comment_inside_parens` (statement side) ⇄ `write_trailing_comments_inside_parens` (expression side), plus its arrow-body slice `arrow_body_keeps_trailing_comment_inside_parens` | a comment silently lands elsewhere |
|
||||
| terminator set: `Comments::has_semicolon_or_closing_paren_in_range` (does the run move) ⇄ `lines_after_skipping_terminators` (deferred break measurement) | a deferred comment's blank lines miscounted |
|
||||
| suppressed content end: `suppressed_statement_content_end` ⇄ the reprint's `;` sites (`FormatContentWithSemicolon` / `OptionalSemicolon`); same terminator, NOT the same content end (see its rustdoc) | a suppressed statement's `;` differs from the reprint's |
|
||||
| `is_node_suppressed`: the import sorter's partition test ⇄ the printer | a suppressed import is a boundary to one and not the other |
|
||||
| `limit_comments_up_to` after `has_trailing_suppression_comment` | the node loses its suppression |
|
||||
| any site printing a statement outside the generated `Statement` fmt (an `if` consequent before `else`) asks `write_suppressed_statement` first | the generic verbatim path prints the source `;` regardless of `semi` |
|
||||
|
||||
### Open debts
|
||||
|
||||
Behavior that follows Prettier where our own rules want one answer. Not accepted, only not fixed yet; each fix is a uniform-rule DIVERGENCES entry plus a fixture pin.
|
||||
|
||||
- TS-only statements (`import A = B;` / `export = x;` / `export as namespace X;` / `declare function f(): void;` / `declare module "m";`)
|
||||
and class index signatures keep a same-line comment before their `;` (`import A = B /* c */;`), instead of the glued `;` above.
|
||||
The fix is a `FormatContentWithSemicolon` adoption each
|
||||
- An own-line comment claimed mid-line inlines onto that line (`const // c` + break), violating "own-line comments stay own-line".
|
||||
See the NOTE in `FormatLeadingComments` (`formatter/trivia.rs`)
|
||||
|
||||
The flat JSX arrow body is a limitation, not a debt: the paren decision is a group fit, unknowable at comment time (DIVERGENCES.md#paren-comment-fixpoint).
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -209,3 +213,8 @@ 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
|
||||
```
|
||||
|
||||
### Prettier conformance
|
||||
|
||||
The failing files in `tests/snapshots/conformance__prettier-{js,ts}.snap` are only partly audited:
|
||||
some are covered by an existing DIVERGENCES.md entry, the rest are unclassified, and no per-file list exists yet.
|
||||
@@ -341,7 +341,7 @@ Drop when: the union printer's claiming is bounded to its own gap; this exclusio
|
||||
- Why: invariant (prettier/prettier#19894, prettier/prettier#12880, prettier/prettier#7745, prettier/prettier#5900)
|
||||
- Pin: see the per-shape list below
|
||||
|
||||
The head-body comment policy family (see AGENTS.md "Comment placement invariants"): comments between a head and its body keep their position, where Prettier's attachment relocates them across the delimiters (a body's braces, a head's parens), user content.
|
||||
The head-body comment policy family (see AGENTS.md "Head-body and operator gaps (class 3)"): comments between a head and its body keep their position, where Prettier's attachment relocates them across the delimiters (a body's braces, a head's parens), user content.
|
||||
|
||||
```js
|
||||
// input
|
||||
|
||||
@@ -209,6 +209,9 @@ impl<'a> Comments<'a> {
|
||||
|
||||
/// Temporarily limits the unprinted comments view to only those before the given position.
|
||||
/// Returns the previous view limit to allow restoration.
|
||||
///
|
||||
/// Check [`Self::has_trailing_suppression_comment`] BEFORE hiding a node's trailing comments,
|
||||
/// or the node loses its suppression.
|
||||
pub fn limit_comments_up_to(&mut self, end_pos: u32) -> Option<usize> {
|
||||
let original_limit = self.view_limit;
|
||||
let limit_index = self.printed_count
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
//! Such comments stay unprinted (defer) and the next leading pass claims them;
|
||||
//! the already-printed terminator left in the gap is transparent to their break measurement
|
||||
//! (`lines_after_skipping_terminators`).
|
||||
//! A downstream pass must exist to print a deferred comment:
|
||||
//! the next sibling's leading pass or the container's dangling pass.
|
||||
//!
|
||||
//! The per-site rules for WHEN a trailing run moves behind its terminator live in AGENTS.md,
|
||||
//! "Comment placement invariants" (the move-behind table).
|
||||
//! The rule for a trailing run and its terminator lives in AGENTS.md, "Moving behind a terminator (class 1)".
|
||||
//!
|
||||
//! ## Comment Formatting Implementation
|
||||
//!
|
||||
|
||||
@@ -79,8 +79,11 @@ pub fn trailing_comments_to_move_behind_semicolon<'a>(
|
||||
///
|
||||
/// `gated` is `true` when `expr` itself sits in one of those positions.
|
||||
///
|
||||
/// This is the statement side of the table: it promises not to hide/move the comment,
|
||||
/// [`write_trailing_comments_inside_parens`] (the expression side) actually prints it.
|
||||
/// [`arrow_body_keeps_trailing_comment_inside_parens`] encodes the arrow-body slice of this table (plus JSX)
|
||||
/// for the chain-leaf walk; change them together.
|
||||
/// for the chain-leaf walk. Change all three together: if they drift, the comment silently lands elsewhere
|
||||
/// (no assert catches it), so pin every position change in a fixture.
|
||||
pub fn keeps_trailing_comment_inside_parens(expr: &Expression<'_>, gated: bool) -> bool {
|
||||
match expr {
|
||||
Expression::SequenceExpression(_) => gated,
|
||||
@@ -110,6 +113,8 @@ pub fn keeps_trailing_comment_inside_parens(expr: &Expression<'_>, gated: bool)
|
||||
/// assigned = a = c; /* c */
|
||||
/// ```
|
||||
///
|
||||
/// Bounds the content at every semicolon-terminated content site
|
||||
/// (expression statements, export defaults, variable declarations, class property values, return arguments).
|
||||
/// The chain also passes through arrow expression bodies (prettier#19930 family);
|
||||
/// bodies matching [`arrow_body_keeps_trailing_comment_inside_parens`] stop the walk.
|
||||
///
|
||||
@@ -257,6 +262,8 @@ where
|
||||
/// and returns whether it was: its leading comments, then the source text up to its content end,
|
||||
/// then the formatter's terminator per `semi` like every other statement (the token-class table in AGENTS.md).
|
||||
/// A statement without a terminator of its own prints its whole span.
|
||||
/// Any site printing a statement outside the generated `Statement` fmt (an `if` consequent before `else`)
|
||||
/// must call this first, or the generic verbatim path prints the source `;` regardless of `semi`.
|
||||
/// Same-line comments between the content end and a later-line source `;` stay on the content's line,
|
||||
/// own-line ones are left for the next node's leading pass;
|
||||
/// the caller (the generated `Statement` fmt) prints the trailing comments.
|
||||
@@ -295,6 +302,9 @@ const CONTINUE_KEYWORD_LEN: u32 = "continue".len() as u32;
|
||||
/// `None` for a statement without a terminator of its own (a block, a declaration with a body, ...);
|
||||
/// a statement ending in a body answers for its rightmost body.
|
||||
/// Mirrors the `;`-printing sites of the reprint (`FormatContentWithSemicolon` / `OptionalSemicolon`): keep them in step.
|
||||
/// Same terminator, NOT the same content end, and not to be unified:
|
||||
/// the reprint's is the comment-move boundary (chain leaf, cast target),
|
||||
/// this one is the token end past the source parens the reprint drops (`p = (q = 1)`).
|
||||
pub fn suppressed_statement_content_end(
|
||||
stmt: &Statement<'_>,
|
||||
f: &JsFormatter<'_, '_>,
|
||||
|
||||
@@ -39,6 +39,8 @@ pub trait SourceTextExt {
|
||||
/// Encodes JS/TS leading-trivia rules:
|
||||
/// it skips an ASI semicolon (`;(function(){});`)
|
||||
/// and discounts newlines inside non-preserved parens (`(`…`)`).
|
||||
/// That `;` is the ASI guard the next statement's own format emitted, sitting immediately before it;
|
||||
/// a different token from the already-printed terminators [`Self::lines_after_skipping_terminators`] skips.
|
||||
///
|
||||
/// `first_unprinted_comment` is the span of the first not-yet-printed comment, or `None`.
|
||||
/// When that comment ends before `span.start`, its leading trivia is included in the count.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Shared policy for every formatter crate in the oxc ecosystem:
|
||||
|
||||
- `oxc_formatter` (JS/TS)
|
||||
- `oxc_formatter` (JS/TSX)
|
||||
- `oxc_formatter_json`
|
||||
- `oxc_formatter_css`
|
||||
- `oxc_formatter_graphql`
|
||||
|
||||
Reference in new issue
Block a user