Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,13 +855,25 @@ fuzzer, and round-trip are all blind to it; only a prettier `compare` finds it
why the rule lives in one emitter per question rather than at each container.

⚠️ **A trailing GAP inside a construct is the third emitter of that same rule** —
`Printer::push_trailing_comments_in_range` (a paren shell's `)`, an indexed access's `]`).
It asks the separator question of the **source** ("did the author give this comment its own
line?"), since these comments may be legitimately glued, and the break rides **inside** the
`line_suffix` — a real break between two deferred comments splits the very construct they
are escaping. Only a **line** comment defers by construction; a block defers solely to stay
behind one, because deferring is what carries a comment out past the closer. Back-to-back
emission welds the run, and an inline block mixed into a deferred one **reorders** it.
`Printer::push_trailing_comments_in_range` (a paren shell's `)`, an indexed access's `]`, a
mapped-type member's value). It asks the separator question of the **source** ("did the
author give this comment its own line?"), since these comments may be legitimately glued,
and the break rides **inside** the `line_suffix` — a real break between two deferred
comments splits the very construct they sit in. Only a **line** comment defers by
construction; a block defers solely to stay behind one. Back-to-back emission welds the run,
and an inline block mixed into a deferred one **reorders** it — so open-coding this loop is
the recurring bug rather than a shortcut.

⚠️ **A deferred run must not leave the construct it was written in.** Deferring is *end of
line*, not *escape*: a construct that closes without breaking carries the comment past its
own closer, re-binding it and landing it on a line that may already hold a deferred comment,
where the two weld irreversibly. So a **line** comment in one of these gaps forces its
construct **open** — the closer drops to its own line and the comment flushes inside. Every
bracketed type region does this (`{}`, `<>`, tuple `[]`, function-type `()`, indexed-access
`[]`), and a paren shell is **retained** rather than stripped for the same reason. The one
sanctioned exception is a union / intersection member a `|`/`&` separator still **follows**,
whose per-member break ends the line where the shell ends
(`Printer::type_member_separator_follows`); the last member has no separator and retains.

⚠️ **A deferred run's FLUSH must end the line, so a `lineSuffixBoundary` belongs only where
nothing else does.** The renderer drains the buffer at a break-mode `line` or at a boundary,
Expand Down
16 changes: 1 addition & 15 deletions crates/tsv_debug/src/cli/commands/gap_audit_known.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# the gate rather than being pinned.
#
# Format: KIND<TAB>SHAPE<TAB>PAYLOADS
# shapes: 522
# shapes: 508
DROPPED !)⟨⟩!. annotation,block,jsdoc_cast
DROPPED &&⟨⟩␣ annotation,block,jsdoc_cast,multiline
DROPPED &⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -409,11 +409,8 @@ SWALLOW )=>⟨⟩( line
SWALLOW )=>⟨⟩(( line
SWALLOW )=>⟨⟩({ line
SWALLOW )=>⟨⟩/* line
SWALLOW )[]⟨⟩; line
SWALLOW )⟨⟩. line
SWALLOW )⟨⟩; line
SWALLOW )⟨⟩>; line
SWALLOW )⟨⟩[]; line
SWALLOW )⟨⟩␣ line
SWALLOW */(⟨⟩IDENT line
SWALLOW */*⟨⟩/* line
Expand Down Expand Up @@ -447,7 +444,6 @@ SWALLOW =>(⟨⟩( line
SWALLOW =>(⟨⟩IDENT line
SWALLOW =>(⟨⟩{ line
SWALLOW =>⟨⟩␣ line
SWALLOW =⟨⟩␣ line
SWALLOW >⟨⟩() line
SWALLOW >⟨⟩(): line
SWALLOW >⟨⟩; line
Expand All @@ -467,15 +463,9 @@ SWALLOW IDENT⟨⟩:(/ line
SWALLOW IDENT⟨⟩; line
SWALLOW IDENT⟨⟩;// line
SWALLOW IDENT⟨⟩?(/ line
SWALLOW IDENT⟨⟩] line
SWALLOW IDENT⟨⟩]; line
SWALLOW IDENT⟨⟩{ line
SWALLOW IDENT⟨⟩␣ line
SWALLOW NUM⟨⟩; line
SWALLOW [];⟨⟩␣ line
SWALLOW ];⟨⟩␣ line
SWALLOW ]⟨⟩; line
SWALLOW ]⟨⟩␣ line
SWALLOW `,⟨⟩␣ line
SWALLOW `⟨⟩); line
SWALLOW `⟨⟩, line
Expand All @@ -502,7 +492,6 @@ SWALLOW },⟨⟩␣ annotation,block,jsdoc_cast,multiline
SWALLOW }⟨⟩) annotation,block,jsdoc_cast,line,multiline
SWALLOW }⟨⟩); line
SWALLOW }⟨⟩)}; line
SWALLOW }⟨⟩]; line
SWALLOW }⟨⟩␣ line
SWALLOW ␣⟨⟩& line
SWALLOW ␣⟨⟩( line
Expand All @@ -519,7 +508,6 @@ SWALLOW ␣⟨⟩/* line
SWALLOW ␣⟨⟩// line,multiline
SWALLOW ␣⟨⟩: line
SWALLOW ␣⟨⟩; line
SWALLOW ␣⟨⟩= line
SWALLOW ␣⟨⟩? line
SWALLOW ␣⟨⟩IDENT annotation,block,jsdoc_cast,line,multiline
SWALLOW ␣⟨⟩[ line
Expand All @@ -534,6 +522,4 @@ SWALLOW ␣⟨⟩{' line
SWALLOW ␣⟨⟩{/* line
SWALLOW ␣⟨⟩{} line
SWALLOW ␣⟨⟩| line
SWALLOW ␣⟨⟩}; line
SWALLOW ␣⟨⟩}>( line
SWALLOW ␣⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
47 changes: 36 additions & 11 deletions crates/tsv_ts/src/printer/statements/type_declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ fn type_has_internal_breaking(printer: &Printer<'_>, ts_type: &TSType<'_>) -> bo
// is the disagreement `build_conditional_check_doc` names for its own union gate.
// A narrow arm, not the recursion the wider enumeration wants (`Ref<…>[]`,
// `keyof Ref<…>`, `Ref<…>['k']` are still missing) — that one needs a corpus A/B.
// The first disjunct needs no comment carve-out: a parenthesized element always
// breaks once it carries one, since a trailing line comment RETAINS the shell over
// real hardlines ([`Printer::paren_retains_for_trailing_run`]) and a leading one
// takes its own `hardline`. The flat-shell exclusion this arm used to carry existed
// only while a trailing run was deferred out past the closer, leaving the shell
// rendering flat while still claiming a break.
// The suffix's own `[]` is the same argument once it holds a comment
// (`string[⏎↹// c⏎]`): the break is inside a delimiter the array owns, so it hugs
// `=` exactly as the empty tuple type `[…]` and the empty object `{…}` already do.
Expand All @@ -68,18 +74,19 @@ fn type_has_internal_breaking(printer: &Printer<'_>, ts_type: &TSType<'_>) -> bo
// the first disjunct misses — an element the author left bare that the printer
// parenthesizes anyway (`typeof x /* c */[]`), where the AST holds no
// `Parenthesized` node to match on.
// ⚠️ The first disjunct asks whether the parens BREAK, so a shell whose whole
// comment run is deferred to end of line is excluded: its parens are stripped and
// nothing breaks inside them (`(U // c)[]` prints flat as `U[]; // c`). Claiming
// the internal break there sent it to `fluid`, whose marker broke on the shell's
// `break_parent` and split the `=` — a split the reparse can't reproduce, the
// parens being gone (F1). A LEADING comment keeps its real `hardline`, so it still
// answers `true` here.
TSType::Array(a) => {
(matches!(a.element_type, TSType::Parenthesized(_))
&& !printer.paren_defers_its_whole_run(a.element_type))
matches!(a.element_type, TSType::Parenthesized(_))
|| matches!(printer.array_suffix_layout(a), ArraySuffixLayout::Split { .. })
}
// An indexed access whose index→`]` gap holds a line comment breaks inside the
// brackets it owns, exactly as the array suffix's `[]` does above — so it hugs
// `=` like the tuple / type-literal / type-argument siblings, which all keep an
// internally-breaking value on the `=` line in both formatters. Comment-free
// input never breaks here, so this disjunct cannot move any input the old
// enumeration covered.
TSType::IndexedAccess(i) => {
printer.has_line_comments_between(i.index_type.span().end, i.span.end)
}
_ => false,
}
}
Expand Down Expand Up @@ -253,15 +260,24 @@ impl<'a> Printer<'a> {
// on `has_format_ignore`).
TSType::Array(a) => {
self.paren_interior_routed_inner(a.element_type).is_some()
|| self.paren_defers_its_whole_run(a.element_type)
|| self.paren_retains_for_trailing_run(a.element_type)
}
// A redundant paren shell whose comments are ALL in its trailing gap
// (`= (U // c)[]`, `= (A // c)`). The shell emits a `break_parent` so that a
// sibling member can't absorb the comment's line, but with nothing after the
// shell that break reaches only the `=` — where it renders a split the reparse
// cannot reproduce, the parens being gone by then. The run itself is deferred,
// so the value prints flat: the `=` must not break either.
TSType::Parenthesized(_) => self.paren_defers_its_whole_run(ty),
TSType::Parenthesized(_) => self.paren_retains_for_trailing_run(ty),
// A single-member union / intersection prints transparently as its member
// (prettier drops the node in postprocess), so the `=` asks the member —
// `= | (A // c)` collapses to the retained shell, which hugs.
TSType::Union(u) if u.types.len() == 1 => {
self.value_owns_its_comment_break(&u.types[0])
}
TSType::Intersection(x) if x.types.len() == 1 => {
self.value_owns_its_comment_break(&x.types[0])
}
TSType::Literal(internal::TSLiteralType::TemplateLiteral(t)) => {
self.template_literal_type_breaks_for_comment(t)
}
Expand Down Expand Up @@ -444,6 +460,15 @@ impl<'a> Printer<'a> {
// expansion, so keep `= {` together like other internally-breaking types
parts.push(d.text(" "));
parts.push(make_rhs(type_doc));
} else if u.types.len() == 1 && self.value_owns_its_comment_break(&u.types[0]) {
// A single-member union prints transparently as its member (prettier
// drops the node in postprocess), so a member that owns its comment
// break hugs the `=` exactly as it would bare — `= | (A // c)`
// collapses to the retained shell, whose parens own the break. The
// hang below would split the `=` for a break the reparse (seeing the
// bare shell) reproduces at the shell, not the `=` (F1).
parts.push(d.text(" "));
parts.push(make_rhs(type_doc));
} else if lead_space {
// Normal unions: break after `=` with leading `| ` and a hanging indent.
parts.push(hang_after_operator(d, make_rhs(type_doc)));
Expand Down
22 changes: 11 additions & 11 deletions crates/tsv_ts/src/printer/types/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1249,15 +1249,17 @@ impl<'a> Printer<'a> {
}
}

// Trailing comments after the value type. A block comment trails
// inline before the `;` (`V /* c */;`); a line comment goes through
// `line_suffix` (`build_trailing_comment_doc`) so it floats to
// end-of-line *after* the `;` (`V; // c`) instead of swallowing it —
// the `;` is emitted separately by the multiline/one-line branch below.
// Trailing comments after the value type, via the shared trailing-gap
// emitter: a block trails inline before the `;` (`V /* c */;`), a line
// comment rides `line_suffix` so it floats to end-of-line *after* the `;`
// (`V; // c`) instead of swallowing it — the `;` is emitted separately by
// the multiline/one-line branch below. Open-coding the loop here dropped
// the emitter's separator and ordering rules, welding a run onto one line
// (`V; // c1 // c2`) and reordering an inline block ahead of a deferred
// line comment; see [docs/comments.md](../../../../../docs/comments.md)
// §Trailing and dangling runs.
let body_end = m.span.end.saturating_sub(1); // before `}`
for comment in comments_to_emit_in_range(self.comments, type_end, body_end) {
body_parts.push(self.build_trailing_comment_doc(comment));
}
self.push_trailing_comments_in_range(&mut body_parts, type_end, body_end);
} else {
// No value type (`{ [K in T] }`): comments after the `]` (or the
// optional modifier) still trail the member the same way — dropping
Expand All @@ -1266,9 +1268,7 @@ impl<'a> Printer<'a> {
body_parts.push(d.text(marker));
}
let body_end = m.span.end.saturating_sub(1); // before `}`
for comment in comments_to_emit_in_range(self.comments, bracket_close, body_end) {
body_parts.push(self.build_trailing_comment_doc(comment));
}
self.push_trailing_comments_in_range(&mut body_parts, bracket_close, body_end);
}

self.build_mapped_type_shell(
Expand Down
Loading