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
2 changes: 1 addition & 1 deletion benches/js/lib/gate_counts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const CORPUS_FORMAT_UNKNOWN_PIN: Record<Language, number> = {
*/
export const CORPUS_FORMAT_PARTIAL_PIN: Record<Language, number> = {
svelte: 1,
typescript: 41,
typescript: 40,
css: 9
};

Expand Down
4 changes: 1 addition & 3 deletions crates/tsv_debug/src/cli/commands/gap_audit_known.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# the gate rather than being pinned.
#
# Format: KIND<TAB>SHAPE<TAB>PAYLOADS
# shapes: 527
# shapes: 525
DROPPED !)⟨⟩!. annotation,block,jsdoc_cast
DROPPED #⟨⟩IDENT annotation,block,jsdoc_cast,line,multiline
DROPPED #⟨⟩\ annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -336,7 +336,6 @@ DROPPED case⟨⟩␣ line
DROPPED class⟨⟩< annotation,block,jsdoc_cast,line,multiline
DROPPED class⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
DROPPED const⟨⟩, line
DROPPED export⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
DROPPED function⟨⟩* annotation,block,jsdoc_cast,line,multiline
DROPPED function⟨⟩*/* annotation,block,jsdoc_cast,line,multiline
DROPPED function⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -442,7 +441,6 @@ DROPPED ␣⟨⟩[/* annotation,block,jsdoc_cast,multiline
DROPPED ␣⟨⟩] annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩]; annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩as annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩class annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩{ annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩{}) annotation,block,jsdoc_cast,line,multiline
DROPPED ␣⟨⟩} annotation,block,jsdoc_cast,line,multiline
Expand Down
5 changes: 1 addition & 4 deletions crates/tsv_debug/src/cli/commands/ignore_audit_known.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# ARE pinned (this is a ratchet over a live bug family, born red).
#
# Format: KIND<TAB>POSITION
# shapes: 28
# shapes: 25
UNHONORED ArrowFunctionExpression.body
UNHONORED ArrowFunctionExpression.typeParameters
UNHONORED BinaryExpression.left
Expand All @@ -33,9 +33,6 @@ UNHONORED ClassDeclaration.superClass
UNHONORED ConditionalExpression.alternate
UNHONORED ConditionalExpression.consequent
UNHONORED ConditionalExpression.test
UNHONORED ExportDefaultDeclaration.declaration
UNHONORED ExportNamedDeclaration.declaration
UNHONORED ExpressionStatement.expression
UNHONORED LogicalExpression.left
UNHONORED LogicalExpression.right
UNHONORED MemberExpression.object
Expand Down
14 changes: 14 additions & 0 deletions crates/tsv_ts/src/ast/internal/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ pub enum ExportDefaultValue<'arena> {
TSInterfaceDeclaration(TSInterfaceDeclaration<'arena>),
}

impl ExportDefaultValue<'_> {
/// The exported value's own span — where the `export default` keyword's gap ends,
/// whichever form follows it.
pub fn span(&self) -> Span {
match self {
ExportDefaultValue::Expression(expr) => expr.span(),
ExportDefaultValue::FunctionDeclaration(func) => func.span,
ExportDefaultValue::TSDeclareFunction(func) => func.span,
ExportDefaultValue::ClassDeclaration(class) => class.span,
ExportDefaultValue::TSInterfaceDeclaration(iface) => iface.span,
}
}
}

/// Export all declaration: `export * from "y"` or `export * as ns from "y"`
/// Also handles type-only: `export type * from "y"`
#[derive(Debug, Clone)]
Expand Down
69 changes: 46 additions & 23 deletions crates/tsv_ts/src/printer/comments/declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -379,13 +379,36 @@ impl<'a> Printer<'a> {
start: u32,
continuation_start: u32,
continuation: DocId,
) -> DocId {
self.build_keyword_header_doc_with(words, start, continuation_start, |_| continuation)
}

/// [`Self::build_keyword_header_doc`] for a caller whose continuation depends on where
/// the keyword ENDS — `build_continuation` is handed the offset just past its final
/// word, the start of the keyword→continuation gap.
///
/// That gap is a **freeze head**: an own-line format-ignore directive in it claims the
/// whole continuation ([`Printer::value_head_frozen_span`]), which is a question about
/// the continuation's own doc and so has to be answered before it is built. Without
/// this the header's shape gets respelled at the call site just to hold `keyword_end`
/// — which is how `export =` drifted from its twin `export default`.
pub(crate) fn build_keyword_header_doc_with(
&self,
words: &[&'static str],
start: u32,
continuation_start: u32,
build_continuation: impl FnOnce(u32) -> DocId,
) -> DocId {
let d = self.d();
let (keyword_doc, keyword_end) =
self.build_keyword_words_doc(words, start, continuation_start);
d.concat(&[
keyword_doc,
self.build_keyword_to_name_continuation(keyword_end, continuation_start, continuation),
self.build_keyword_to_name_continuation(
keyword_end,
continuation_start,
build_continuation(keyword_end),
),
])
}

Expand Down Expand Up @@ -462,6 +485,28 @@ impl<'a> Printer<'a> {
.concat(&[self.build_comments_between(start, end, spacing), tail])
}

/// The keyword's words joined by one space, with its end *measured* rather than
/// located — the shape used where the words cannot be located: the source does not
/// hold them, or there is no window of source to hold them in.
///
/// It assumes exactly one space per interior gap, so it scans no gap and can emit
/// no interior comment. Every caller must therefore have established that there is
/// none to emit — which an empty window proves, and which the located path below
/// makes unnecessary.
fn measured_keyword_doc(&self, words: &[&'static str], start: u32) -> (DocId, u32) {
let d = self.d();
let mut parts: DocBuf = DocBuf::new();
for (i, w) in words.iter().enumerate() {
if i > 0 {
parts.push(d.text(" "));
}
parts.push(d.text(w));
}
let width: u32 = words.iter().map(|w| w.len() as u32).sum();
let measured = start + width + words.len() as u32 - 1;
(d.concat(&parts), measured)
}

/// Build a **multi-word keyword** (`export default`, `await using`, `declare
/// const`, `export as namespace`), preserving a comment authored in one of its
/// interior gaps.
Expand All @@ -487,28 +532,6 @@ impl<'a> Printer<'a> {
/// `search_end` do: every caller bounds the search at the token before the
/// continuation, and no operator can occur in that gap. A caller that widens those
/// bounds must re-check that itself.
/// The keyword's words joined by one space, with its end *measured* rather than
/// located — the shape used where the words cannot be located: the source does not
/// hold them, or there is no window of source to hold them in.
///
/// It assumes exactly one space per interior gap, so it scans no gap and can emit
/// no interior comment. Every caller must therefore have established that there is
/// none to emit — which an empty window proves, and which the located path below
/// makes unnecessary.
fn measured_keyword_doc(&self, words: &[&'static str], start: u32) -> (DocId, u32) {
let d = self.d();
let mut parts: DocBuf = DocBuf::new();
for (i, w) in words.iter().enumerate() {
if i > 0 {
parts.push(d.text(" "));
}
parts.push(d.text(w));
}
let width: u32 = words.iter().map(|w| w.len() as u32).sum();
let measured = start + width + words.len() as u32 - 1;
(d.concat(&parts), measured)
}

pub(crate) fn build_keyword_words_doc(
&self,
words: &[&'static str],
Expand Down
110 changes: 71 additions & 39 deletions crates/tsv_ts/src/printer/ignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,28 @@ impl<'a> Printer<'a> {
self.gap_frozen_span(gap_start, value)
}

/// [`Self::value_head_frozen_span`] resolved to the doc it emits, for a value head whose
/// unfrozen emission is the plain [`Self::build_expression_doc`] — the `export`→value and
/// `export default`→value gaps, the twin heads that must not disagree.
///
/// Deliberately NOT for a head with an ordinary builder of its own (an `if` condition, a
/// `for` update clause, an assignment RHS, an object property value): those resolve the
/// span themselves and pair it with the builder the position calls for, and routing them
/// through here would silently substitute the plain one. The statement analog
/// ([`Self::build_statement_head_doc`]) takes its ordinary builder lazily for exactly
/// that reason — every statement head has a different one.
#[inline]
pub(in crate::printer) fn build_value_head_doc(
&self,
gap_start: u32,
value: &internal::Expression<'_>,
) -> DocId {
match self.value_head_frozen_span(gap_start, value.span()) {
Some(frozen) => self.build_frozen_expression_doc(value, frozen),
None => self.build_expression_doc(value),
}
}

/// The statement-side analog of [`Self::value_head_frozen_span`]: a head that introduces
/// ONE statement, clause or body behind a delimiter of its own — an `if`'s `)`→consequent
/// and `else`→alternate gaps, a `label:`→body gap, the `}`→`catch` / `}`→`finally` gap,
Expand All @@ -571,7 +593,7 @@ impl<'a> Printer<'a> {
/// parent-owned: a `case` label rides inside its own frozen case, and a class name and
/// `extends` clause stay outside the frozen body. Unlike the value heads there is no
/// paren shell to re-synthesize — a statement is never parenthesized by the printer — so
/// the span emits directly through [`Self::build_frozen_span_doc`].
/// the emission is the plain [`Self::build_frozen_node_doc`].
///
/// A head whose ordinary layout would pull the gap's comment onto the head's line must
/// ask this BEFORE choosing that layout and keep the directive's own line when it fires:
Expand All @@ -586,7 +608,7 @@ impl<'a> Printer<'a> {
ordinary: impl FnOnce() -> DocId,
) -> DocId {
match self.gap_frozen_span(gap_start, body) {
Some(frozen) => self.build_frozen_span_doc(frozen),
Some(frozen) => self.build_frozen_node_doc(frozen),
None => ordinary(),
}
}
Expand Down Expand Up @@ -698,6 +720,32 @@ impl<'a> Printer<'a> {
self.with_frozen_must_break(self.raw_source_range(span.start, span.end), span)
}

/// What a freeze over a NODE's own span EMITS: the verbatim slice
/// ([`Self::build_frozen_span_doc`]) plus the one node-level fact the span alone can't
/// carry — a block comment **glued** before the node is owned by it, rides *inside* the
/// doc the slice replaces, and is skipped by every gap emitter, so the freeze must claim
/// it or nothing prints it (docs/comments.md hazard 1, the ownership-is-about-who-PRINTS
/// rule). Prettier keeps the comment right where it is, glued before the frozen slice.
///
/// The one emitter every node-shaped freeze shares, whatever rule resolved it: a list
/// item over its own span (a declarator, a namespace binding), a statement head's body
/// ([`Self::build_statement_head_doc`], and the `export` / `export default` heads that
/// resolve their own span because their gap emitter is the shared declaration-header run
/// they need either way), and the delimiter-owned value heads whose value is span-shaped
/// rather than an `Expression` (a `for` init clause, a for-in/of left, a restricted
/// production's operand). [`Self::build_frozen_expression_doc`] is this plus the one rule
/// that belongs to an `Expression` node rather than to its span.
///
/// `gaps:audit` found every miss this claim exists to prevent, by injecting a block
/// comment before a frozen node — which the print-once ledger over the fixtures as
/// authored cannot see, since no fixture glues a comment to a frozen item: a frozen
/// argument dropped it, and so did `if (a)⏎// prettier-ignore⏎/* c */ fn( b );` and
/// `export default⏎// prettier-ignore⏎/* c */ @dec class {}`.
#[inline]
pub(in crate::printer) fn build_frozen_node_doc(&self, frozen: Span) -> DocId {
self.prepend_owned_leading_comment_at(frozen.start, self.build_frozen_span_doc(frozen))
}

/// The frozen verbatim slice for a value in a paren-context position, with the clarity
/// parens that context supplies re-synthesized around it: an assignment prints as
/// `fn((a = b))` as an argument and as `if ((a = b))` as a condition, and those parens
Expand Down Expand Up @@ -740,35 +788,28 @@ impl<'a> Printer<'a> {
self.build_frozen_value_doc(arg, frozen, ParenContext::Argument)
}

/// The frozen verbatim slice for an EXPRESSION, with the glued leading comment the
/// expression OWNS claimed onto it.
/// [`Self::build_frozen_node_doc`] for an EXPRESSION — the verbatim slice and the
/// owned-comment claim, plus the one rule that is a property of the *node* rather than
/// of its span.
///
/// A block comment glued before a node is owned by it and rides *inside* that node's
/// ordinary doc, which is exactly the doc a freeze replaces — and the slice starts at
/// the node's own span, so the comment is not in it either. Nothing else prints it (the
/// gap emitters all skip owned comments), so without this claim it is DROPPED:
/// docs/comments.md hazard 1, the ownership-is-about-who-PRINTS rule. Prettier keeps
/// the comment right where it is, glued before the frozen slice.
/// A **`SequenceExpression`** gets back the grouping parens it prints for itself. Those
/// parens are required (`fn((0, 1))` passes ONE argument; `fn(0, 1)` passes two) but
/// they are printed by `build_sequence_doc`, not by the enclosing context — which is why
/// `needs_parens` deliberately answers `false` for it — and they sit OUTSIDE the node's
/// span, so a verbatim slice of the span alone drops them and changes the meaning. The
/// rule belongs here rather than at [`Self::build_frozen_value_doc`] because it is a
/// property of the node, not of the position: the cast interior needs it too.
///
/// Found by `gaps:audit` injecting a block comment before a frozen argument — the
/// print-once ledger over the fixtures as authored could not see it, since no fixture
/// glues a comment to a frozen item.
///
/// A **`SequenceExpression`** additionally gets back the grouping parens it prints for
/// itself. Those parens are required (`fn((0, 1))` passes ONE argument; `fn(0, 1)`
/// passes two) but they are printed by `build_sequence_doc`, not by the enclosing
/// context — which is why `needs_parens` deliberately answers `false` for it — and they
/// sit OUTSIDE the node's span, so a verbatim slice of the span alone drops them and
/// changes the meaning. The rule belongs here rather than at
/// [`Self::build_frozen_value_doc`] because it is a property of the node, not of the
/// position: the cast interior needs it too.
/// The value heads whose value is span-shaped rather than an `Expression` (a `for` init
/// clause, a for-in/of left, a restricted production's operand — where the layout's own
/// hanging parens ARE the grouping, so a re-synthesized pair would double it) take
/// [`Self::build_frozen_node_doc`] directly.
pub(in crate::printer) fn build_frozen_expression_doc(
&self,
expr: &internal::Expression<'_>,
frozen: Span,
) -> DocId {
let doc =
self.prepend_owned_leading_comment_at(frozen.start, self.build_frozen_span_doc(frozen));
let doc = self.build_frozen_node_doc(frozen);
if matches!(expr, internal::Expression::SequenceExpression(_)) {
self.d().parens(doc)
} else {
Expand Down Expand Up @@ -810,11 +851,12 @@ impl<'a> Printer<'a> {
/// families whose item is not an `Expression`, so no list loop spells the dispatch itself.
///
/// The slice is the item's own node span; the list's `,` is parent-owned and stays
/// outside it. A block comment **glued** before the item is owned by it and rides inside
/// the doc the slice replaces, so the freeze claims it (docs/comments.md hazard 1) — the
/// same node-level fact [`Self::build_frozen_expression_doc`] carries for arguments. The
/// outside it — so the emission is the plain [`Self::build_frozen_node_doc`], which
/// claims the glued comment the item owns (docs/comments.md hazard 1). The
/// grouping-paren fact does not apply here: none of these item kinds prints parens of its
/// own around its span.
/// own around its span. A caller whose ordinary path is the rest of a loop body rather
/// than a closure (the variable declarator list) asks [`Self::list_item_frozen`] and that
/// emitter directly, so the two dispatch shapes still share one emitter.
///
/// `build_ordinary` is lazy so the common path builds the item doc exactly once;
/// [`Self::list_item_frozen`] already opens on the document-level flag, so a
Expand All @@ -828,22 +870,12 @@ impl<'a> Printer<'a> {
build_ordinary: impl FnOnce() -> DocId,
) -> DocId {
if self.list_item_frozen(container_start, item_span, i) {
self.build_frozen_span_item_doc(item_span(i))
self.build_frozen_node_doc(item_span(i))
} else {
build_ordinary()
}
}

/// What a resolved span-shaped list-item freeze EMITS: the verbatim slice with the
/// glued leading comment the item owns claimed onto it. Split from
/// [`Self::build_span_item_doc`] for the caller whose ordinary path is the rest of a
/// loop body rather than a closure (the variable declarator list), so the two dispatch
/// shapes still share one emitter.
#[inline]
pub(in crate::printer) fn build_frozen_span_item_doc(&self, frozen: Span) -> DocId {
self.prepend_owned_leading_comment_at(frozen.start, self.build_frozen_span_doc(frozen))
}

/// Paren-transparent frozen doc for a union / intersection member — and, through
/// [`Self::build_frozen_head_doc`], for a single-child head's frozen value too (the
/// type-parameter constraint/default site passes a `member_parens` that keeps a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ impl<'a> Printer<'a> {
if let Some(init) = &stmt.init {
inner_parts.push(init_frozen.map_or_else(
|| self.build_for_init_doc(init),
|frozen| self.build_frozen_span_item_doc(frozen),
|frozen| self.build_frozen_node_doc(frozen),
));
}
// The init clause→`;` gap comments bind to the `;` like a list separator.
Expand Down Expand Up @@ -1767,7 +1767,7 @@ impl<'a> Printer<'a> {
// directive in the keyword→first-declarator gap or between two
// declarators freezes the FOLLOWING one over its own node span.
if self.list_item_frozen(keyword_end, &item_span, i) {
decl_docs.push(self.build_frozen_span_item_doc(declarator.span));
decl_docs.push(self.build_frozen_node_doc(declarator.span));
continue;
}
let mut one: DocBuf = smallvec![self.build_expression_doc(&declarator.id)];
Expand Down Expand Up @@ -1872,7 +1872,7 @@ impl<'a> Printer<'a> {
&& let Some(frozen) =
self.value_head_frozen_span(open + 1, Span::new(spans.left_start, spans.left_end))
{
let doc = self.build_frozen_span_item_doc(frozen);
let doc = self.build_frozen_node_doc(frozen);
return if wrap_async_paren { d.parens(doc) } else { doc };
}
match left {
Expand Down
Loading