diff --git a/benches/js/lib/gate_counts.ts b/benches/js/lib/gate_counts.ts index 0ada3a60..7009968c 100644 --- a/benches/js/lib/gate_counts.ts +++ b/benches/js/lib/gate_counts.ts @@ -187,7 +187,7 @@ export const CORPUS_FORMAT_UNKNOWN_PIN: Record = { */ export const CORPUS_FORMAT_PARTIAL_PIN: Record = { svelte: 1, - typescript: 41, + typescript: 40, css: 9 }; diff --git a/crates/tsv_debug/src/cli/commands/gap_audit_known.txt b/crates/tsv_debug/src/cli/commands/gap_audit_known.txt index b52d9cf5..52b4c260 100644 --- a/crates/tsv_debug/src/cli/commands/gap_audit_known.txt +++ b/crates/tsv_debug/src/cli/commands/gap_audit_known.txt @@ -12,7 +12,7 @@ # the gate rather than being pinned. # # Format: KINDSHAPEPAYLOADS -# shapes: 527 +# shapes: 525 DROPPED !)⟨⟩!. annotation,block,jsdoc_cast DROPPED #⟨⟩IDENT annotation,block,jsdoc_cast,line,multiline DROPPED #⟨⟩\ annotation,block,jsdoc_cast,line,multiline @@ -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 @@ -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 diff --git a/crates/tsv_debug/src/cli/commands/ignore_audit_known.txt b/crates/tsv_debug/src/cli/commands/ignore_audit_known.txt index 8a6f7a68..a7a2cc97 100644 --- a/crates/tsv_debug/src/cli/commands/ignore_audit_known.txt +++ b/crates/tsv_debug/src/cli/commands/ignore_audit_known.txt @@ -23,7 +23,7 @@ # ARE pinned (this is a ratchet over a live bug family, born red). # # Format: KINDPOSITION -# shapes: 28 +# shapes: 25 UNHONORED ArrowFunctionExpression.body UNHONORED ArrowFunctionExpression.typeParameters UNHONORED BinaryExpression.left @@ -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 diff --git a/crates/tsv_ts/src/ast/internal/modules.rs b/crates/tsv_ts/src/ast/internal/modules.rs index b0d0cdb4..e33fa99c 100644 --- a/crates/tsv_ts/src/ast/internal/modules.rs +++ b/crates/tsv_ts/src/ast/internal/modules.rs @@ -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)] diff --git a/crates/tsv_ts/src/printer/comments/declarations.rs b/crates/tsv_ts/src/printer/comments/declarations.rs index 33f2dd0e..afd766ab 100644 --- a/crates/tsv_ts/src/printer/comments/declarations.rs +++ b/crates/tsv_ts/src/printer/comments/declarations.rs @@ -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), + ), ]) } @@ -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. @@ -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], diff --git a/crates/tsv_ts/src/printer/ignore.rs b/crates/tsv_ts/src/printer/ignore.rs index c19f3990..851eeabb 100644 --- a/crates/tsv_ts/src/printer/ignore.rs +++ b/crates/tsv_ts/src/printer/ignore.rs @@ -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, @@ -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: @@ -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(), } } @@ -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 @@ -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 { @@ -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 @@ -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 diff --git a/crates/tsv_ts/src/printer/statements/control_flow/loops/for_loop.rs b/crates/tsv_ts/src/printer/statements/control_flow/loops/for_loop.rs index 023e9f62..6054c534 100644 --- a/crates/tsv_ts/src/printer/statements/control_flow/loops/for_loop.rs +++ b/crates/tsv_ts/src/printer/statements/control_flow/loops/for_loop.rs @@ -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. @@ -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)]; @@ -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 { diff --git a/crates/tsv_ts/src/printer/statements/mod.rs b/crates/tsv_ts/src/printer/statements/mod.rs index 885f6ada..5e552233 100644 --- a/crates/tsv_ts/src/printer/statements/mod.rs +++ b/crates/tsv_ts/src/printer/statements/mod.rs @@ -177,12 +177,9 @@ impl<'a> Printer<'a> { span.start < expr_start && self.source.as_bytes().get(span.start as usize) == Some(&b'(') } - /// Build a Doc for an expression statement - /// - /// Handles parentheses for object patterns and the "avoid becoming a - /// directive" rule for bare string literals (see - /// [`Printer::needs_avoid_directive_parens`]); `in_program_or_block` is - /// threaded from [`Printer::build_statement_doc`]. + /// Build a Doc for an expression statement: the value + /// ([`Self::build_expression_statement_value_doc`], or the verbatim text of a + /// directive-prologue entry) plus the `;` and the comments bound to it. fn build_expression_statement_doc( &self, stmt: &internal::ExpressionStatement<'_>, @@ -190,148 +187,199 @@ impl<'a> Printer<'a> { ) -> DocId { let d = self.d(); - let mut parts = DocBuf::new(); - - if stmt.is_directive { + let mut parts: DocBuf = if stmt.is_directive { // Directives are exact code-unit sequences; `format_directive` mirrors // Prettier's `printDirective` (swap the outer quote to single only when // the content has no quote, else verbatim). Never parenthesized. let raw = stmt.expression.span().extract(self.source); - parts.push(d.text_pooled(&format_directive(raw))); + smallvec![d.text_pooled(&format_directive(raw))] } else { - // Parens required for correctness (object expressions, object pattern - // assignments) OR to avoid a bare string statement being read as a - // directive (recomputed fresh, not preserved from source). - let needs_parens = self - .needs_parens(&stmt.expression, ParenContext::ExpressionStatement) - || self.needs_avoid_directive_parens(stmt, in_program_or_block); - - // A comment between a source `(` and the expression (`(// c⏎ expr)` / - // `(/* c */⏎ expr)` — e.g. a bare parenthesized decorated class - // expression) is preserved inside the parens, breaking them open; the flat - // `(`/`)` wrap below would drop it, since there is nowhere on one line for - // a comment the expression doesn't own to go. prettier hoists the comment - // before `(` — a divergence (`expression_statement_paren_kept_comment`, - // `decorated_expr_open_paren_comment`). - let expr_start = stmt.expression.span().start; - let source_paren = self.statement_opens_with_paren(stmt.span, expr_start); - // The `(`→expression gap, resolved in one place so the gate below and the two - // emitters that can claim it cannot read different ranges. - let paren_gap = - || comments_to_emit_in_range(self.comments, stmt.span.start + 1, expr_start); - // Deliberately **to emit**, not on-page: this branch also *prints* the comments it - // finds. A block glued to the *expression* is owned by it, rides inside its doc and - // is skipped here — which is what keeps `(/* c */ expr)` flat. - let paren_open_comments = needs_parens - && source_paren - && self.has_comments_to_emit_between(stmt.span.start + 1, expr_start); - - // When the whole expression isn't wrapped, a nested leftmost - // object/function/class still needs parens around itself - // (`(class {}).foo`, `({}).foo`, `(class {}) + 1`). The matching node's - // doc builder consumes this span-matched target and wraps itself. - if !needs_parens { - let leftmost = leftmost_no_lookahead(&stmt.expression); - if matches!( - leftmost, - Expression::ObjectExpression(_) - | Expression::FunctionExpression(_) - | Expression::ClassExpression(_) - ) { - self.expr_stmt_paren_target.set(Some(leftmost.span())); - } else if let Some(Expression::Identifier(id)) = - strip_statement_casts(&stmt.expression) - && self.with_ident_name(id, is_statement_ambiguous_keyword) - { - // `(type) as T;` / `(module) satisfies U;` — a contextual keyword - // heading an `as`/`satisfies` cast at statement level reparses as a - // `type`/`module`/… declaration without the parens. The identifier's - // doc builder consumes this span-matched target and wraps itself. - self.expr_stmt_paren_target.set(Some(id.span)); - } - } + smallvec![self.build_expression_statement_value_doc(stmt, in_program_or_block)] + }; - // Build the expression once. Context flags for chain handling: - // is_expression_statement allows short identifier names to merge with the - // first call; in_top_level_assignment selects the regular assignment - // layout (not chain formatting). Clear the (non-consuming, span-matched) - // paren target afterward so it can't leak into a sibling statement. - self.is_expression_statement.set(true); - self.in_top_level_assignment.set(true); - let expr_doc = self.build_expression_doc(&stmt.expression); - self.in_top_level_assignment.set(false); - self.is_expression_statement.set(false); - self.expr_stmt_paren_target.set(None); - - // A parenthesized *decorated* class expression breaks its parens open and - // indents the content (prettier): `(⏎\t@dec⏎\tclass {}⏎)`. The decorators - // force the break; an undecorated `(class {})` / `(function () {})` stays - // inline (flat `else` below). - let decorated_class_expr = needs_parens - && matches!( - &stmt.expression, - Expression::ClassExpression(c) if class_expr_has_decorators(c) - ); + // Comments between the expression and the `;`, with the `;` bound to the + // statement: a same-line block trails *after* it (`fn() /* c */;` → `fn(); /* c */`, + // prettier 3.9), a same-line line trails after it via `line_suffix` + // (`fn() // c` → `fn(); // c`), an own-line comment drops to its own line after it + // (emitting a line comment before the `;` would swallow it). See + // `split_separator_gap_comments`. + let expr_end = stmt.expression.span().end; + let semicolon_pos = stmt.span.end.saturating_sub(1); + self.push_semicolon_with_gap_comments(&mut parts, expr_end, semicolon_pos, true); + d.concat(&parts) + } - if paren_open_comments { - // The parens break open around the run. Only the first `hardline` is - // the site's own — the run's internal separators come from the shared - // leading-comment emitter, so a run the author glued onto one line - // stays glued and a blank line between two comments survives. - let mut inner: DocBuf = smallvec![d.hardline()]; + /// The non-directive expression statement's VALUE — the expression plus whatever parens + /// this print gives it (required, directive-avoiding, or a source pair it keeps) and the + /// comments only those parens can emit. The statement's `;` and the comments bound to it + /// stay with the caller. + /// + /// `in_program_or_block` is threaded from [`Printer::build_statement_doc`] for the + /// "avoid becoming a directive" rule (see [`Printer::needs_avoid_directive_parens`]). + fn build_expression_statement_value_doc( + &self, + stmt: &internal::ExpressionStatement<'_>, + in_program_or_block: bool, + ) -> DocId { + let d = self.d(); + let mut parts = DocBuf::new(); + // A comment between a source `(` and the expression (`(// c⏎ expr)` / + // `(/* c */⏎ expr)` — e.g. a bare parenthesized decorated class + // expression) is preserved inside the parens, breaking them open; the flat + // `(`/`)` wrap below would drop it, since there is nowhere on one line for + // a comment the expression doesn't own to go. prettier hoists the comment + // before `(` — a divergence (`expression_statement_paren_kept_comment`, + // `decorated_expr_open_paren_comment`). + let expr_start = stmt.expression.span().start; + let source_paren = self.statement_opens_with_paren(stmt.span, expr_start); + // An own-line directive in that `(`→expression gap freezes the expression + // (Rule A). Only a SOURCE paren opens the gap: without one the directive + // leads the statement, where the statement list's own rule already claims it. + let frozen = source_paren + .then(|| self.value_head_frozen_span(stmt.span.start + 1, stmt.expression.span())) + .flatten(); + + // Parens required for correctness (object expressions, object pattern + // assignments) OR to avoid a bare string statement being read as a + // directive (recomputed fresh, not preserved from source). + let mut needs_parens = self + .needs_parens(&stmt.expression, ParenContext::ExpressionStatement) + || self.needs_avoid_directive_parens(stmt, in_program_or_block); + + // When the whole expression isn't wrapped, a nested leftmost + // object/function/class still needs parens around itself + // (`(class {}).foo`, `({}).foo`, `(class {}) + 1`). + let nested_paren = if needs_parens { + None + } else { + self.expr_stmt_nested_paren_target(&stmt.expression) + }; + // A frozen slice is verbatim, so the printer has no interior left to wrap: + // the nested target's parens go around the WHOLE slice instead. Without the + // widening `({ a: 1 }.b)` would print as `{ a: 1 }.b;`, which reparses as a + // block — prettier's own output there does exactly that. + needs_parens |= frozen.is_some() && nested_paren.is_some(); + + // The `(`→expression gap, resolved in one place so the gate below and the two + // emitters that can claim it cannot read different ranges. + let paren_gap = + || comments_to_emit_in_range(self.comments, stmt.span.start + 1, expr_start); + // Deliberately **to emit**, not on-page: this branch also *prints* the comments it + // finds. A block glued to the *expression* is owned by it, rides inside its doc and + // is skipped here — which is what keeps `(/* c */ expr)` flat. + let paren_open_comments = needs_parens + && source_paren + && self.has_comments_to_emit_between(stmt.span.start + 1, expr_start); + + // Build the expression once — or not at all, when the freeze replaces its doc + // with the verbatim slice. Context flags for chain handling: + // is_expression_statement allows short identifier names to merge with the + // first call; in_top_level_assignment selects the regular assignment + // layout (not chain formatting). The matching node's doc builder consumes the + // (non-consuming, span-matched) paren target and wraps itself; clear it + // afterward so it can't leak into a sibling statement. + let expr_doc = match frozen { + Some(span) => self.build_frozen_expression_doc(&stmt.expression, span), + None => { + self.expr_stmt_paren_target.set(nested_paren); + self.is_expression_statement.set(true); + self.in_top_level_assignment.set(true); + let doc = self.build_expression_doc(&stmt.expression); + self.in_top_level_assignment.set(false); + self.is_expression_statement.set(false); + self.expr_stmt_paren_target.set(None); + doc + } + }; + + // A parenthesized *decorated* class expression breaks its parens open and + // indents the content (prettier): `(⏎\t@dec⏎\tclass {}⏎)`. The decorators + // force the break; an undecorated `(class {})` / `(function () {})` stays + // inline (flat `else` below). + let decorated_class_expr = needs_parens + && matches!( + &stmt.expression, + Expression::ClassExpression(c) if class_expr_has_decorators(c) + ); + + if paren_open_comments { + // The parens break open around the run. Only the first `hardline` is + // the site's own — the run's internal separators come from the shared + // leading-comment emitter, so a run the author glued onto one line + // stays glued and a blank line between two comments survives. + let mut inner: DocBuf = smallvec![d.hardline()]; + self.push_leading_comment_run( + &mut inner, + paren_gap(), + expr_start, + LeadingGlue::Adjacent, + d.empty(), + ); + inner.push(expr_doc); + parts.push(d.text("(")); + parts.push(d.indent(d.concat(&inner))); + parts.push(d.hardline()); + parts.push(d.text(")")); + } else if decorated_class_expr { + parts.push(self.build_break_open_parens(expr_doc)); + } else { + if needs_parens { + parts.push(d.text("(")); + } else if source_paren { + // The statement had a source `(` that this print DROPS as + // redundant. A comment inside it then has no emitter left: the + // statement-list gap ends at the `(`, and the expression's own doc + // starts past the comment — so it must be emitted here or it is + // lost. It leads the statement, which is where prettier hoists it + // too. (An owned/glued comment rides inside `expr_doc` and is + // skipped by the emit iterator; the paren-KEPT cases are the two + // branches above.) self.push_leading_comment_run( - &mut inner, + &mut parts, paren_gap(), expr_start, LeadingGlue::Adjacent, d.empty(), ); - inner.push(expr_doc); - parts.push(d.text("(")); - parts.push(d.indent(d.concat(&inner))); - parts.push(d.hardline()); + } + parts.push(expr_doc); + if needs_parens { parts.push(d.text(")")); - } else if decorated_class_expr { - parts.push(self.build_break_open_parens(expr_doc)); - } else { - if needs_parens { - parts.push(d.text("(")); - } else if source_paren { - // The statement had a source `(` that this print DROPS as - // redundant. A comment inside it then has no emitter left: the - // statement-list gap ends at the `(`, and the expression's own doc - // starts past the comment — so it must be emitted here or it is - // lost. It leads the statement, which is where prettier hoists it - // too. (An owned/glued comment rides inside `expr_doc` and is - // skipped by the emit iterator; the paren-KEPT cases are the two - // branches above.) - self.push_leading_comment_run( - &mut parts, - paren_gap(), - expr_start, - LeadingGlue::Adjacent, - d.empty(), - ); - } - parts.push(expr_doc); - if needs_parens { - parts.push(d.text(")")); - } } } - - // Comments between the expression and the `;`, with the `;` bound to the - // statement: a same-line block trails *after* it (`fn() /* c */;` → `fn(); /* c */`, - // prettier 3.9), a same-line line trails after it via `line_suffix` - // (`fn() // c` → `fn(); // c`), an own-line comment drops to its own line after it - // (emitting a line comment before the `;` would swallow it). See - // `split_separator_gap_comments`. - let expr_end = stmt.expression.span().end; - let semicolon_pos = stmt.span.end.saturating_sub(1); - self.push_semicolon_with_gap_comments(&mut parts, expr_end, semicolon_pos, true); d.concat(&parts) } + /// The nested span an *unwrapped* expression statement must parenthesize around + /// ITSELF, or `None` when nothing does — the leftmost object / function / class + /// (`(class {}).foo`, `({}).foo`, `(class {}) + 1`), or a contextual keyword heading + /// an `as` / `satisfies` cast (`(type) as T;` / `(module) satisfies U;`, which + /// reparse as a `type` / `module` declaration without the parens). + /// + /// Asked only where the whole expression isn't already wrapped. Two callers, one + /// question: the ordinary path hands the span to `expr_stmt_paren_target` for the + /// matching node's doc builder to consume, and the format-ignore path — which has no + /// interior to hand it to — reads it as "the frozen slice needs a shell". + fn expr_stmt_nested_paren_target(&self, expression: &Expression<'_>) -> Option { + let leftmost = leftmost_no_lookahead(expression); + if matches!( + leftmost, + Expression::ObjectExpression(_) + | Expression::FunctionExpression(_) + | Expression::ClassExpression(_) + ) { + return Some(leftmost.span()); + } + match strip_statement_casts(expression) { + Some(Expression::Identifier(id)) + if self.with_ident_name(id, is_statement_ambiguous_keyword) => + { + Some(id.span) + } + _ => None, + } + } + /// Whether a bare string-literal expression statement needs synthetic parens /// to avoid being read as a directive-prologue entry. /// @@ -680,7 +728,7 @@ impl<'a> Printer<'a> { // so re-synthesizing the sequence's own pair would double it. let paren_gap = open_paren.map_or(keyword_end, |p| p + 1); let expr_doc = match self.value_head_frozen_span(paren_gap, arg.span()) { - Some(frozen) => self.build_frozen_span_item_doc(frozen), + Some(frozen) => self.build_frozen_node_doc(frozen), None => match arg { Expression::SequenceExpression(seq) => self.build_sequence_doc_bare(seq), _ => self.build_expression_doc(arg), diff --git a/crates/tsv_ts/src/printer/statements/modules/mod.rs b/crates/tsv_ts/src/printer/statements/modules/mod.rs index c1c70d97..4c7345b1 100644 --- a/crates/tsv_ts/src/printer/statements/modules/mod.rs +++ b/crates/tsv_ts/src/printer/statements/modules/mod.rs @@ -22,6 +22,7 @@ use crate::printer::calls::PartitionedComments; use crate::printer::needs_parens::export_default_needs_parens; use smallvec::SmallVec; use smallvec::smallvec; +use tsv_lang::Span; use tsv_lang::comments_to_emit_in_range; use tsv_lang::doc::DocBuf; use tsv_lang::doc::arena::DocId; @@ -40,17 +41,20 @@ impl<'a> Printer<'a> { decl: &internal::TSExportAssignment<'_>, ) -> DocId { let d = self.d(); - let expr_doc = self.build_expression_doc(&decl.expression); - let argument_end = decl.expression.span().end; + let value_span = decl.expression.span(); // `export =` word by word: the `export`→`=` gap is a position an author can // comment in. Emitting the two as one text never scans it — the comment would - // be dropped. - let head = self.build_keyword_header_doc( + // be dropped. The `=`→value gap is a freeze head, `export default`'s twin, so the + // value is built from the keyword's end and an own-line directive there claims it + // whole. (The `export`→`=` gap one delimiter earlier is not a head — a `=` begins + // no node — and cannot reach this window either.) + let head = self.build_keyword_header_doc_with( &["export", "="], decl.span.start, - decl.expression.span().start, - expr_doc, + value_span.start, + |keyword_end| self.build_value_head_doc(keyword_end, &decl.expression), ); + let argument_end = value_span.end; let has_trailing_comments = self.has_comments_to_emit_between(argument_end, decl.span.end); if has_trailing_comments { // `export =` keeps a same-line trailing block comment *before* the `;` @@ -79,80 +83,37 @@ impl<'a> Printer<'a> { // the type/interface/declare keyword is part of the declaration itself let export_keyword = "export"; let export_keyword_end = decl.span.start + export_keyword.len() as u32; - let decl_start = declaration.span().start; + let decl_span = declaration.span(); + let decl_start = decl_span.start; // A comment between `export` and the declaration is preserved in // place; a line comment indents the declaration one level (uniform // header rule). The keyword→declaration gap routes through the shared // continuation helper, so block/no-comment cases stay inline. // + // An own-line directive in that gap freezes the declaration whole (Rule A), + // and the shared helper keeps its own line for free (`keyword_gap_breaks`). + // Resolved BEFORE the decorator fork below: an export-first decorated class's + // slice already carries its own decorators, so a frozen one wants the plain + // continuation rather than the decorator layout. + let frozen = self.value_head_frozen_span(export_keyword_end, decl_span); // Decorated classes are the exception: the decorators and `export` can // appear in either order (`@dec export class` / `export @dec class`), so - // the block below emits them in the source order rather than through the - // plain keyword→declaration continuation. - if let internal::Statement::ClassDeclaration(class) = declaration - && let Some(decorators) = class.decorators.filter(|d| !d.is_empty()) + // they print in the source order rather than through the plain + // keyword→declaration continuation. + if frozen.is_none() + && let internal::Statement::ClassDeclaration(class) = declaration + && let Some(doc) = + self.build_export_named_decorated_class_doc(decl, class, export_keyword_end) { - let continuation = self.build_class_declaration_without_decorators_doc(class); - // Decorators can sit before OR after `export` — prettier preserves the - // author's choice as two distinct stable forms (`@dec export class` vs - // `export @dec class`). The parser records which by the export decl's - // start: the first-decorator start for the decorator-first form, the - // `export` keyword for the export-first form. So a first decorator - // positioned *after* the export start is the export-first form. - let export_first = decorators[0].span.start > decl.span.start; - - // The token right after the decorators is the trailing-comment boundary - // for `build_decorators_doc`: the class keyword (`abstract`/`class`) for - // the export-first form, `export` for the decorator-first form. - let next_after_decorators = if export_first { - let class_kw = if class.r#abstract { - "abstract" - } else { - "class" - }; - self.find_keyword_after_decorators(class.decorators, class_kw, class.span.start) - } else { - self.find_keyword_after_decorators(class.decorators, "export", decl.span.start) - }; - - // `decorators` is non-empty, so this is always `Some`; the `if let` - // keeps the code off `expect()` (clippy::expect_used) and falls through - // to the plain declaration path in the (unreachable) `None` case. - if let Some(dec_doc) = - self.build_decorators_doc(class.decorators, next_after_decorators) - { - if export_first { - // `export` on its own line, then the (always own-line) decorators, - // then the class. - let mut parts: DocBuf = smallvec![d.text(export_keyword)]; - // A comment between `export` and the first decorator is rare but - // must be preserved (never dropped). - if let Some(c) = self.build_inline_comments_between_doc_opt( - export_keyword_end, - decorators[0].span.start, - ) { - parts.push(c); - } - parts.push(d.hardline()); - parts.push(dec_doc); - parts.push(continuation); - return d.concat(&parts); - } - - // Decorator-first (`@dec export class`): decorators, then `export`, - // then the class. - let tail = self.build_keyword_to_name_continuation( - export_keyword_end, - decl_start, - continuation, - ); - return d.concat(&[dec_doc, d.text(export_keyword), tail]); - } + return doc; } // `declaration` is always a declaration form (never an // ExpressionStatement), so `in_program_or_block` is never consulted here. - let continuation = self.build_statement_doc(declaration, true); + let continuation = match frozen { + Some(span) => self.build_frozen_node_doc(span), + None => self.build_statement_doc(declaration, true), + }; let tail = self.build_keyword_to_name_continuation( export_keyword_end, decl_start, @@ -289,134 +250,102 @@ impl<'a> Printer<'a> { } } - /// Build a Doc for an export default declaration - pub(super) fn build_export_default_declaration_doc( + /// The DECORATED `export ` forms, which emit the decorators and `export` in the + /// author's own source order rather than through the plain keyword→declaration + /// continuation — `None` when the class carries no decorators and the ordinary path + /// applies. + /// + /// Decorators can sit before OR after `export` (`@dec export class` / `export @dec + /// class`), and prettier preserves the author's choice as two distinct stable forms. The + /// parser records which by the export declaration's start: the first-decorator start for + /// the decorator-first form, the `export` keyword for the export-first form — so a first + /// decorator positioned *after* the export start is the export-first form. + /// + /// Asked only where the keyword→declaration gap did NOT freeze: an export-first + /// decorated class's frozen slice already carries its own decorators, so a frozen one + /// wants the plain continuation instead. + fn build_export_named_decorated_class_doc( &self, - decl: &internal::ExportDefaultDeclaration<'_>, - ) -> DocId { + decl: &internal::ExportNamedDeclaration<'_>, + class: &internal::ClassDeclaration<'_>, + export_keyword_end: u32, + ) -> Option { let d = self.d(); - // Decorator-*first* `@dec export default class` — the decorators precede - // `export`, and the whole thing parses as a ClassDeclaration. Emit the - // decorators, then `export default`, then the class without them. - if let internal::ExportDefaultValue::ClassDeclaration(class) = &decl.declaration { - let export_start = - self.find_keyword_after_decorators(class.decorators, "export", decl.span.start); - if let Some(dec_doc) = self.build_decorators_doc(class.decorators, export_start) { - // `export default` word by word, bounded by the class's own first - // keyword: both the gap *inside* the keyword and the one after it are - // positions an author can comment in. Emitting the two words as one - // fixed text scans neither, so both comments are dropped. - return d.concat(&[ - dec_doc, - self.build_keyword_header_doc( - &["export", "default"], - export_start, - self.class_declaration_keyword_start(class), - self.build_class_declaration_without_decorators_doc(class), - ), - ]); - } - } + let decorators = class.decorators.filter(|dec| !dec.is_empty())?; + let continuation = self.build_class_declaration_without_decorators_doc(class); + let export_first = decorators[0].span.start > decl.span.start; - // Decorator-*after*-`default` `export default @dec class {}` — the decorator - // makes it a class *expression* (acorn), so it lands in the generic Expression - // arm below. But prettier formats it declaration-style: `export default` on its - // own line, the (always own-line) decorators, and NO trailing `;`. The class- - // expression doc already renders the decorators own-line, so emit `export - // default`, a hardline, then that doc. - if let internal::ExportDefaultValue::Expression(internal::Expression::ClassExpression( - class_expr, - )) = &decl.declaration - && let Some(decorators) = class_expr.decorators.filter(|dec| !dec.is_empty()) - { - // `export default` word by word: the gap *inside* the keyword is a - // position an author can comment in, so the words are located rather than - // measured (measuring never scans that gap — the comment would be dropped). - let (keyword_doc, keyword_end) = self.build_keyword_words_doc( - &["export", "default"], - decl.span.start, - decorators[0].span.start, - ); - let mut parts: DocBuf = smallvec![keyword_doc]; - // A comment between `export default` and the first decorator is rare but - // must be preserved (never dropped). Two authorings, two owners: a comment - // *glued* to `@dec` is owned by the class expression (every glued block - // comment is), so it is skipped here by design and claimed below; one the - // author left on its own line is unowned and belongs to this gap. - if let Some(c) = - self.build_inline_comments_between_doc_opt(keyword_end, decorators[0].span.start) + // The token right after the decorators is the trailing-comment boundary for + // `build_decorators_doc`: the class keyword (`abstract`/`class`) for the + // export-first form, `export` for the decorator-first form. + let next_after_decorators = if export_first { + let class_kw = if class.r#abstract { + "abstract" + } else { + "class" + }; + self.find_keyword_after_decorators(class.decorators, class_kw, class.span.start) + } else { + self.find_keyword_after_decorators(class.decorators, "export", decl.span.start) + }; + + // `decorators` is non-empty, so this is always `Some`; the `?` keeps the code off + // `expect()` (clippy::expect_used) and falls back to the plain declaration path in + // the (unreachable) `None` case. + let dec_doc = self.build_decorators_doc(class.decorators, next_after_decorators)?; + if export_first { + // `export` on its own line, then the (always own-line) decorators, then the + // class. A comment between `export` and the first decorator is rare but must be + // preserved (never dropped). + let mut parts: DocBuf = smallvec![d.text("export")]; + if let Some(c) = self + .build_inline_comments_between_doc_opt(export_keyword_end, decorators[0].span.start) { parts.push(c); } parts.push(d.hardline()); - // This path **reassembles** the class expression rather than routing it - // through `build_expression_doc`, so the owned-comment seam there never - // runs for it — the comment must be claimed here or nothing prints it. - parts.push(self.prepend_owned_leading_comment_at( - class_expr.span.start, - self.build_class_expression_doc(class_expr), - )); - return d.concat(&parts); + parts.push(dec_doc); + parts.push(continuation); + return Some(d.concat(&parts)); } - let value_doc = match &decl.declaration { - internal::ExportDefaultValue::Expression(expr) => { - let mut expr_doc = self.build_expression_doc(expr); - // Prettier wraps the exported expression when its leftmost - // (first-printed) token is a function/class keyword — else - // `export default function () {}.m()` reparses the function as a - // *declaration* and the trailing `.m()` / `= …` / `as T` dangles. - // Mirrors prettier's `startsWithNoLookaheadToken(expr, isFunctionOrClass)` - // (parentheses/needs-parentheses.js). Decorated class expressions are - // handled above; the FunctionDeclaration/ClassDeclaration arms cover - // bare `export default function/class …`. - if export_default_needs_parens(expr) { - expr_doc = d.concat(&[d.text("("), expr_doc, d.text(")")]); - } - let argument_end = expr.span().end; - let has_trailing_comments = - self.has_comments_to_emit_between(argument_end, decl.span.end); - if has_trailing_comments { - let mut parts = smallvec![expr_doc]; - let after = self.split_terminator_gap_comments( - &mut parts, - argument_end, - decl.span.end, - false, - ); - parts.push(d.text(";")); - parts.extend(after); - d.concat(&parts) - } else { - d.concat(&[expr_doc, d.text(";")]) - } - } - internal::ExportDefaultValue::FunctionDeclaration(func) => { - self.build_function_declaration_doc(func) - } - internal::ExportDefaultValue::TSDeclareFunction(func) => { - self.build_declare_function_doc(func) - } - internal::ExportDefaultValue::ClassDeclaration(class) => { - self.build_class_declaration_doc(class) - } - internal::ExportDefaultValue::TSInterfaceDeclaration(iface) => { - self.build_interface_declaration_doc(iface) - } - }; + // Decorator-first (`@dec export class`): decorators, then `export`, then the class. + // The gap runs from the REAL `export` keyword — `next_after_decorators` here — to + // the class's own first keyword. `decl.span.start` is the DECORATOR in this form, so + // anchoring the scan on it inverted the window and dropped every comment in the gap. + // The gap sits inside the declaration's span rather than before it, so nothing + // freezes here: there is no following node for Rule A to bind to (the + // decorator→declaration gap's mirror image). + let tail = self.build_keyword_to_name_continuation( + next_after_decorators + MODULE_KW_LEN, + self.class_declaration_keyword_start(class), + continuation, + ); + Some(d.concat(&[dec_doc, d.text("export"), tail])) + } - let decl_start = match &decl.declaration { - internal::ExportDefaultValue::Expression(expr) => expr.span().start, - internal::ExportDefaultValue::FunctionDeclaration(func) => func.span.start, - internal::ExportDefaultValue::TSDeclareFunction(func) => func.span.start, - internal::ExportDefaultValue::ClassDeclaration(class) => class.span.start, - internal::ExportDefaultValue::TSInterfaceDeclaration(iface) => iface.span.start, - }; + /// Build a Doc for an export default declaration + pub(super) fn build_export_default_declaration_doc( + &self, + decl: &internal::ExportDefaultDeclaration<'_>, + ) -> DocId { + let d = self.d(); + // The two decorated forms print declaration-style layouts of their own, neither of + // which is the keyword→value shape below. + if let Some(doc) = self.build_export_default_decorated_doc(decl) { + return doc; + } + + let value_span = decl.declaration.span(); + let decl_start = value_span.start; // The `export default`→value gap (a line comment indents the value). The // keyword's own words are located, not measured — the gap *between* them is a - // position an author can comment in, and measuring never scans it. + // position an author can comment in, and measuring never scans it. Located + // before the value is built, so the value can ask whether this gap freezes it. let (keyword_doc, keyword_end) = self.build_keyword_words_doc(&["export", "default"], decl.span.start, decl_start); + let value_doc = self.build_export_default_value_doc(decl, keyword_end, value_span); + // A comment that can't stay inline forces the value onto its own indented // line, keeping the comment where the author wrote it. This gap uses the // SHARED keyword→value gate — the same one `as`/`satisfies`, `keyof`/`typeof`, @@ -441,6 +370,163 @@ impl<'a> Printer<'a> { ]) } + /// The `export default` value's own doc, built from `keyword_end` — the start of the + /// keyword→value gap, which the caller emits around this. + /// + /// An own-line directive in that gap freezes the value whole (Rule A). A DECLARATION + /// value's whole doc is the slice, so each form asks the shared statement head + /// ([`Printer::build_statement_head_doc`]); an EXPRESSION value keeps its `;`, its + /// trailing comments and the `export default (…)` clarity parens outside the freeze, so + /// only the expression's own doc is swapped. + fn build_export_default_value_doc( + &self, + decl: &internal::ExportDefaultDeclaration<'_>, + keyword_end: u32, + value_span: Span, + ) -> DocId { + let d = self.d(); + match &decl.declaration { + internal::ExportDefaultValue::Expression(expr) => { + let mut expr_doc = self.build_value_head_doc(keyword_end, expr); + // Prettier wraps the exported expression when its leftmost + // (first-printed) token is a function/class keyword — else + // `export default function () {}.m()` reparses the function as a + // *declaration* and the trailing `.m()` / `= …` / `as T` dangles. + // Mirrors prettier's `startsWithNoLookaheadToken(expr, isFunctionOrClass)` + // (parentheses/needs-parentheses.js). Decorated class expressions are + // handled by the decorated forms; the FunctionDeclaration/ClassDeclaration + // arms cover bare `export default function/class …`. + if export_default_needs_parens(expr) { + expr_doc = d.concat(&[d.text("("), expr_doc, d.text(")")]); + } + let argument_end = value_span.end; + if !self.has_comments_to_emit_between(argument_end, decl.span.end) { + return d.concat(&[expr_doc, d.text(";")]); + } + let mut parts = smallvec![expr_doc]; + let after = self.split_terminator_gap_comments( + &mut parts, + argument_end, + decl.span.end, + false, + ); + parts.push(d.text(";")); + parts.extend(after); + d.concat(&parts) + } + internal::ExportDefaultValue::FunctionDeclaration(func) => self + .build_statement_head_doc(keyword_end, value_span, || { + self.build_function_declaration_doc(func) + }), + internal::ExportDefaultValue::TSDeclareFunction(func) => { + self.build_statement_head_doc(keyword_end, value_span, || { + self.build_declare_function_doc(func) + }) + } + internal::ExportDefaultValue::ClassDeclaration(class) => { + self.build_statement_head_doc(keyword_end, value_span, || { + self.build_class_declaration_doc(class) + }) + } + internal::ExportDefaultValue::TSInterfaceDeclaration(iface) => self + .build_statement_head_doc(keyword_end, value_span, || { + self.build_interface_declaration_doc(iface) + }), + } + } + + /// The DECORATED `export default` forms, which print declaration-style layouts of their + /// own rather than the plain keyword→value shape — `None` when the value carries no + /// decorators and the ordinary path applies. + /// + /// Two source orders, two AST shapes. Decorator-*first* `@dec export default class` + /// parses as a ClassDeclaration whose span opens at the decorator, so the decorators are + /// emitted, then `export default`, then the class without them. + /// Decorator-*after*-`default` `export default @dec class {}` is a class *expression* + /// (acorn), which prettier still formats declaration-style: `export default` on its own + /// line, the (always own-line) decorators, and NO trailing `;`. + fn build_export_default_decorated_doc( + &self, + decl: &internal::ExportDefaultDeclaration<'_>, + ) -> Option { + let d = self.d(); + if let internal::ExportDefaultValue::ClassDeclaration(class) = &decl.declaration { + let export_start = + self.find_keyword_after_decorators(class.decorators, "export", decl.span.start); + let dec_doc = self.build_decorators_doc(class.decorators, export_start)?; + // `export default` word by word, bounded by the class's own first keyword: + // both the gap *inside* the keyword and the one after it are positions an + // author can comment in. Emitting the two words as one fixed text scans + // neither, so both comments are dropped. + // + // Nothing freezes in that gap: the declaration begins at its first decorator, + // so the gap sits INSIDE the value rather than before it and there is no + // following node for Rule A to bind to (the decorator→declaration gap's + // mirror image, and the same reason the decorator-first named export doesn't + // freeze). + return Some(d.concat(&[ + dec_doc, + self.build_keyword_header_doc( + &["export", "default"], + export_start, + self.class_declaration_keyword_start(class), + self.build_class_declaration_without_decorators_doc(class), + ), + ])); + } + + let internal::ExportDefaultValue::Expression(internal::Expression::ClassExpression( + class_expr, + )) = &decl.declaration + else { + return None; + }; + let decorators = class_expr.decorators.filter(|dec| !dec.is_empty())?; + // `export default` word by word: the gap *inside* the keyword is a position an + // author can comment in, so the words are located rather than measured (measuring + // never scans that gap — the comment would be dropped). + let (keyword_doc, keyword_end) = self.build_keyword_words_doc( + &["export", "default"], + decl.span.start, + decorators[0].span.start, + ); + let mut parts: DocBuf = smallvec![keyword_doc]; + // An own-line directive in the gap freezes the class expression whole — its + // slice carries the decorators, so this path's hand-rolled decorator layout + // yields to the verbatim form. The shared own-line-preserving run keeps the + // directive's line, which the inline emitter below would take away: a + // keyword-trailing placement is inert under the floor, so the relocated form + // would lose the freeze on the second pass. + if let Some(frozen) = self.value_head_frozen_span(keyword_end, class_expr.span) { + self.append_keyword_value_line_comments( + &mut parts, + keyword_end, + frozen.start, + self.build_frozen_node_doc(frozen), + ); + return Some(d.concat(&parts)); + } + // A comment between `export default` and the first decorator is rare but + // must be preserved (never dropped). Two authorings, two owners: a comment + // *glued* to `@dec` is owned by the class expression (every glued block + // comment is), so it is skipped here by design and claimed below; one the + // author left on its own line is unowned and belongs to this gap. + if let Some(c) = + self.build_inline_comments_between_doc_opt(keyword_end, decorators[0].span.start) + { + parts.push(c); + } + parts.push(d.hardline()); + // This path **reassembles** the class expression rather than routing it + // through `build_expression_doc`, so the owned-comment seam there never + // runs for it — the comment must be claimed here or nothing prints it. + parts.push(self.prepend_owned_leading_comment_at( + class_expr.span.start, + self.build_class_expression_doc(class_expr), + )); + Some(d.concat(&parts)) + } + /// Build a Doc for an export all declaration pub(super) fn build_export_all_declaration_doc( &self, @@ -626,7 +712,7 @@ impl<'a> Printer<'a> { parts.push(self.gap_comment_continuation_tail( header_end, ns_spec.span.start, - self.build_frozen_span_item_doc(frozen), + self.build_frozen_node_doc(frozen), )); } else { // keyword(s)→namespace-`*` gap, preserved in place. A line comment diff --git a/crates/tsv_ts/src/printer/statements/variable.rs b/crates/tsv_ts/src/printer/statements/variable.rs index 6d465591..ddaa8b71 100644 --- a/crates/tsv_ts/src/printer/statements/variable.rs +++ b/crates/tsv_ts/src/printer/statements/variable.rs @@ -326,7 +326,7 @@ impl<'a> Printer<'a> { // declarator over its own node span — the annotation, `=`, and initializer all // ride inside it; the separating `,` is parent-owned and was emitted above. if self.list_item_frozen(keyword_end, &item_span, i) { - let frozen_doc = self.build_frozen_span_item_doc(declarator.span); + let frozen_doc = self.build_frozen_node_doc(declarator.span); if goes_in_parts { parts.push(frozen_doc); } else { diff --git a/docs/conformance_prettier.md b/docs/conformance_prettier.md index be904107..dd268eac 100644 --- a/docs/conformance_prettier.md +++ b/docs/conformance_prettier.md @@ -1866,6 +1866,48 @@ declaration (`@dec⏎// prettier-ignore⏎export class D {}`) freezes nothing, i tsv. The decorator belongs to the declaration it decorates, so the gap is inside the statement rather than before it, and there is no following member for the rule to bind to. +**On declaration heads and parenthesized statements.** The last two statement-level heads are +the ones where prettier **relocates the directive out of the gap** and freezes anyway — the +`export`→declaration, `export default`→value and `export =`→value gaps, and the `(`→expression +gap of a statement whose parens the printer keeps. tsv freezes the same node at all four and +keeps the directive +where the author wrote it, which at the two `export` heads is the uniform declaration-header +layout an ordinary comment already takes there (the keyword alone on its line, the continuation +indented) and at the paren head is inside the broken parens. Both spellings behave alike; +placement keys the freeze. + +The `export` keyword is parent-owned and stays outside the slice, while decorators written +*after* it belong to the declaration and ride inside it. The gap one delimiter *earlier* — the +`export`→`=` interior of `export =` — is not a head at all: a `=` begins no node, so Rule A has +nothing to bind to and a directive there freezes nothing (prettier reaches past the `=` and +freezes the value). At the paren head the parens are the +printer's, so they too stay outside — and when a frozen slice's own leftmost token needs them +(`{ bbb: 2 }.ccc`, which would otherwise reparse as a block) the shell goes around the **whole** +slice, since a verbatim slice has no interior for the printer to wrap. Where the parens are +merely redundant tsv drops them and the directive leads the statement, matching prettier +(`statements/expression_statement_paren_dropped_prettier_ignore_head`). + +- **`export`→declaration head** — ◆comment_preservation — prettier pulls the directive flush + onto the `export` line (`export // prettier-ignore`) and freezes anyway; tsv keeps the + author's line, since the flush placement is inert under the floor and would lose the freeze + on the second pass. The plain-comment form of the same relocation is already sanctioned at + [export_declaration_line_comment](../tests/fixtures/typescript/syntax/comments/export_declaration_line_comment_prettier_divergence/) — + [named head](../tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/), + [default head](../tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/), + [`export =` head](../tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/) +- **Paren-kept expression statement** — ◆comment_preservation ◆prettier_bug — prettier hoists + the directive out before the `(` and glues the frozen slice back inside parens on one line. + On the leftmost-token case it also drops the shell, emitting `{ bbb: 2 }.ccc;`, which does + not reparse — + [paren head](../tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/) +- **`export`→class gap of a decorator-FIRST class** — ◆comment_preservation + ◆content_preservation — `@dec⏎export⏎// c⏎class C {}`. The declaration's span opens at the + decorator, so this gap is *inside* it, and a directive there freezes nothing in either tool — + the mirror image of the decorator→declaration gap above. Prettier hoists a line comment above + `export` and trails a block comment on the decorator; tsv keeps both in place. tsv previously + **dropped** every comment in this gap, scanning it over an inverted range — + [before export](../tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/) + See [directives.md](./directives.md) for the user-facing reference. --- diff --git a/docs/directives.md b/docs/directives.md index a73554b7..d1a05866 100644 --- a/docs/directives.md +++ b/docs/directives.md @@ -368,6 +368,38 @@ One position is inert: a directive between a **decorator** and its declaration freezes nothing, because the decorator belongs to the declaration and the gap is inside the statement rather than before it. +### On declaration heads + +The `export` and `export default` keywords introduce a declaration the same way, +so a directive in the gap after either freezes it. The keyword stays outside the +frozen slice; decorators written *after* it belong to the declaration and ride +inside: + +```ts +export + // format-ignore + const aaa = 1; + +export default + // format-ignore + @dec + class Ddd {} +``` + +A decorator written *before* `export` is the inert case again: the declaration +begins at the decorator, so the `export`→class gap is inside it and a directive +there freezes nothing. + +An expression statement the printer wraps in parens freezes the same way, with +the parens left outside the slice: + +```ts +( + // format-ignore + { aaa: 1 } +); +``` + ## `format-ignore-start` / `format-ignore-end` In Svelte templates, a pair of range markers preserves every node between them: diff --git a/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/README.md new file mode 100644 index 00000000..7971d0c4 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/README.md @@ -0,0 +1,22 @@ +# default_declaration_prettier_ignore_head_prettier_divergence + +The `export default`→value gap is the same head as the named +[`export`→declaration](../named_declaration_prettier_ignore_head_prettier_divergence/) one: an +own-line directive there freezes the value it precedes, whether that value is an expression, a +declaration, or a decorated class: + +```ts +export default + // prettier-ignore + fn ( aaa ); +``` + +Prettier **relocates** the directive flush onto the `export default` line and freezes anyway; +tsv keeps the author's line, since a head-trailing directive is inert under the placement floor +and the relocated form would lose the freeze on the second pass. **Both spellings** behave +alike — placement keys the freeze, not the comment's spelling. + +## Reason + +Rule A binds a directive to the node that follows it; ◆comment_preservation. See +[conformance_prettier.md §Format-ignore directive](../../../../../../docs/conformance_prettier.md#format-ignore-directive). diff --git a/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/expected.json new file mode 100644 index 00000000..5917d01f --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/expected.json @@ -0,0 +1,741 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 613, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " an own-line directive in the `export default`→value gap freezes the value", + "start": 20, + "end": 96, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 77 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 115, + "end": 133, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " a declaration value is the same head", + "start": 153, + "end": 192, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 40 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 211, + "end": 229, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 272, + "end": 290, + "loc": { + "start": { + "line": 13, + "column": 2 + }, + "end": { + "line": 13, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " decorators written after `default` belong to the class and ride inside its slice", + "start": 324, + "end": 407, + "loc": { + "start": { + "line": 18, + "column": 1 + }, + "end": { + "line": 18, + "column": 84 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 426, + "end": 444, + "loc": { + "start": { + "line": 20, + "column": 2 + }, + "end": { + "line": 20, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 469, + "end": 545, + "loc": { + "start": { + "line": 24, + "column": 1 + }, + "end": { + "line": 24, + "column": 77 + } + } + }, + { + "type": "Block", + "value": " prettier-ignore ", + "start": 564, + "end": 585, + "loc": { + "start": { + "line": 26, + "column": 2 + }, + "end": { + "line": 26, + "column": 23 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 612, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 603, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 28, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 98, + "end": 150, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "exportKind": "value", + "declaration": { + "type": "CallExpression", + "start": 136, + "end": 149, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 15 + } + }, + "callee": { + "type": "Identifier", + "start": 136, + "end": 138, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 4 + } + }, + "name": "fn" + }, + "arguments": [ + { + "type": "Identifier", + "start": 143, + "end": 146, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "name": "aaa" + } + ], + "optional": false, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 115, + "end": 133 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line directive in the `export default`→value gap freezes the value", + "start": 20, + "end": 96 + } + ] + }, + { + "type": "ExportDefaultDeclaration", + "start": 194, + "end": 252, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "exportKind": "value", + "declaration": { + "type": "FunctionDeclaration", + "start": 232, + "end": 252, + "loc": { + "start": { + "line": 10, + "column": 2 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "id": { + "type": "Identifier", + "start": 242, + "end": 244, + "loc": { + "start": { + "line": 10, + "column": 12 + }, + "end": { + "line": 10, + "column": 14 + } + }, + "name": "fn" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 250, + "end": 252, + "loc": { + "start": { + "line": 10, + "column": 20 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 211, + "end": 229 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " a declaration value is the same head", + "start": 153, + "end": 192 + } + ] + }, + { + "type": "ExportDefaultDeclaration", + "start": 255, + "end": 321, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 16, + "column": 3 + } + }, + "exportKind": "value", + "declaration": { + "type": "TSInterfaceDeclaration", + "start": 293, + "end": 321, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 16, + "column": 3 + } + }, + "id": { + "type": "Identifier", + "start": 304, + "end": 305, + "loc": { + "start": { + "line": 14, + "column": 13 + }, + "end": { + "line": 14, + "column": 14 + } + }, + "name": "B" + }, + "body": { + "type": "TSInterfaceBody", + "start": 307, + "end": 321, + "loc": { + "start": { + "line": 14, + "column": 16 + }, + "end": { + "line": 16, + "column": 3 + } + }, + "body": [ + { + "type": "TSPropertySignature", + "start": 312, + "end": 317, + "loc": { + "start": { + "line": 15, + "column": 3 + }, + "end": { + "line": 15, + "column": 8 + } + }, + "computed": false, + "key": { + "type": "Identifier", + "start": 312, + "end": 313, + "loc": { + "start": { + "line": 15, + "column": 3 + }, + "end": { + "line": 15, + "column": 4 + } + }, + "name": "b" + }, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 313, + "end": 317, + "loc": { + "start": { + "line": 15, + "column": 4 + }, + "end": { + "line": 15, + "column": 8 + } + }, + "typeAnnotation": { + "type": "TSLiteralType", + "start": 316, + "end": 317, + "loc": { + "start": { + "line": 15, + "column": 7 + }, + "end": { + "line": 15, + "column": 8 + } + }, + "literal": { + "type": "Literal", + "start": 316, + "end": 317, + "loc": { + "start": { + "line": 15, + "column": 7 + }, + "end": { + "line": 15, + "column": 8 + } + }, + "value": 2, + "raw": "2" + } + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 272, + "end": 290 + } + ] + } + }, + { + "type": "ExportDefaultDeclaration", + "start": 409, + "end": 466, + "loc": { + "start": { + "line": 19, + "column": 1 + }, + "end": { + "line": 22, + "column": 14 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassExpression", + "start": 447, + "end": 466, + "loc": { + "start": { + "line": 21, + "column": 2 + }, + "end": { + "line": 22, + "column": 14 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 447, + "end": 451, + "loc": { + "start": { + "line": 21, + "column": 2 + }, + "end": { + "line": 21, + "column": 6 + } + }, + "expression": { + "type": "Identifier", + "start": 448, + "end": 451, + "loc": { + "start": { + "line": 21, + "column": 3 + }, + "end": { + "line": 21, + "column": 6 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 461, + "end": 462, + "loc": { + "start": { + "line": 22, + "column": 9 + }, + "end": { + "line": 22, + "column": 10 + } + }, + "name": "D" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 464, + "end": 466, + "loc": { + "start": { + "line": 22, + "column": 12 + }, + "end": { + "line": 22, + "column": 14 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 426, + "end": 444 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " decorators written after `default` belong to the class and ride inside its slice", + "start": 324, + "end": 407 + } + ] + }, + { + "type": "ExportDefaultDeclaration", + "start": 547, + "end": 602, + "loc": { + "start": { + "line": 25, + "column": 1 + }, + "end": { + "line": 27, + "column": 16 + } + }, + "exportKind": "value", + "declaration": { + "type": "ObjectExpression", + "start": 588, + "end": 601, + "loc": { + "start": { + "line": 27, + "column": 2 + }, + "end": { + "line": 27, + "column": 15 + } + }, + "properties": [ + { + "type": "Property", + "start": 591, + "end": 598, + "loc": { + "start": { + "line": 27, + "column": 5 + }, + "end": { + "line": 27, + "column": 12 + } + }, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 591, + "end": 594, + "loc": { + "start": { + "line": 27, + "column": 5 + }, + "end": { + "line": 27, + "column": 8 + } + }, + "name": "aaa" + }, + "value": { + "type": "Literal", + "start": 597, + "end": 598, + "loc": { + "start": { + "line": 27, + "column": 11 + }, + "end": { + "line": 27, + "column": 12 + } + }, + "value": 1, + "raw": "1" + }, + "kind": "init" + } + ], + "leadingComments": [ + { + "type": "Block", + "value": " prettier-ignore ", + "start": 564, + "end": 585 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 469, + "end": 545 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/input.svelte new file mode 100644 index 00000000..6505e81b --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/input.svelte @@ -0,0 +1,28 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte new file mode 100644 index 00000000..e3812f12 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte @@ -0,0 +1,23 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/README.md new file mode 100644 index 00000000..30a2a240 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/README.md @@ -0,0 +1,26 @@ +# export_equals_prettier_ignore_head_prettier_divergence + +`export =` carries the same value head as +[`export default`](../default_declaration_prettier_ignore_head_prettier_divergence/): an own-line +directive in the `=`→value gap freezes the value, and the keyword stays outside the slice. + +```ts +export = + // prettier-ignore + aaa + bbb; +``` + +Prettier **relocates** the directive flush onto the `export =` line and freezes anyway; tsv keeps +the author's line, since a keyword-trailing directive is inert under the placement floor and the +relocated form would lose the freeze on the second pass. `unformatted_ours_spaces.svelte` pins +the freeze's scope: a perturbed `export =` sits outside the slice and normalizes. + +The gap one delimiter earlier — between `export` and `=` — is a different position: a `=` begins +no node, so Rule A has nothing to bind to there and a directive freezes nothing. It cannot reach +this seam either, since the freeze window opens only past the `=`. + +## Reason + +Rule A binds a directive to the node that follows it, and the placement floor makes a +keyword-trailing directive inert; ◆comment_preservation. See +[conformance_prettier.md §Format-ignore directive](../../../../../../docs/conformance_prettier.md#format-ignore-directive). diff --git a/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/expected.json new file mode 100644 index 00000000..fa4ff079 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/expected.json @@ -0,0 +1,200 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 179, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " an own-line directive in the `=`→value gap freezes the value, the same head as", + "start": 20, + "end": 101, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 82 + } + } + }, + { + "type": "Line", + "value": " `export default`", + "start": 103, + "end": 122, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 135, + "end": 153, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 20 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 178, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 169, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 7, + "column": 9 + } + }, + "body": [ + { + "type": "TSExportAssignment", + "start": 124, + "end": 168, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 6, + "column": 14 + } + }, + "expression": { + "type": "BinaryExpression", + "start": 156, + "end": 167, + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 13 + } + }, + "left": { + "type": "Identifier", + "start": 156, + "end": 159, + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 5 + } + }, + "name": "aaa" + }, + "operator": "+", + "right": { + "type": "Identifier", + "start": 164, + "end": 167, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 13 + } + }, + "name": "bbb" + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 135, + "end": 153 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line directive in the `=`→value gap freezes the value, the same head as", + "start": 20, + "end": 101 + }, + { + "type": "Line", + "value": " `export default`", + "start": 103, + "end": 122 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/input.svelte new file mode 100644 index 00000000..00930fd6 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/output_prettier.svelte new file mode 100644 index 00000000..d632ca2b --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/output_prettier.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte new file mode 100644 index 00000000..e1247041 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/README.md new file mode 100644 index 00000000..0f2764fa --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/README.md @@ -0,0 +1,26 @@ +# named_declaration_prettier_ignore_head_prettier_divergence + +An own-line directive in an `export`→declaration gap freezes the declaration — the node that +follows the directive, over its own span. The `export` keyword is parent-owned and stays +outside the slice; decorators written *after* `export` belong to the declaration, so they +ride inside it: + +```ts +export + // prettier-ignore + const aaa = 1; +``` + +Prettier **relocates** the directive flush onto the `export` line (`export // prettier-ignore`) +and freezes anyway. tsv keeps the line the author gave it and indents the continuation, the +same uniform header rule an ordinary comment takes at this gap +([export_declaration_line_comment](../../../syntax/comments/export_declaration_line_comment_prettier_divergence/)) — +and here it is load-bearing rather than cosmetic: a head-trailing directive is inert under the +placement floor, so following the relocation would lose the freeze on tsv's own second pass. + +**Both spellings** behave alike — placement keys the freeze, not the comment's spelling. + +## Reason + +Rule A binds a directive to the node that follows it; ◆comment_preservation. See +[conformance_prettier.md §Format-ignore directive](../../../../../../docs/conformance_prettier.md#format-ignore-directive). diff --git a/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/expected.json new file mode 100644 index 00000000..de92c732 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/expected.json @@ -0,0 +1,1039 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 697, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " an own-line directive in the `export`→declaration gap freezes the declaration", + "start": 20, + "end": 100, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 111, + "end": 129, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " every declaration form is the same head", + "start": 152, + "end": 194, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 43 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 205, + "end": 223, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 258, + "end": 276, + "loc": { + "start": { + "line": 13, + "column": 2 + }, + "end": { + "line": 13, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 315, + "end": 333, + "loc": { + "start": { + "line": 17, + "column": 2 + }, + "end": { + "line": 17, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 376, + "end": 394, + "loc": { + "start": { + "line": 23, + "column": 2 + }, + "end": { + "line": 23, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " decorators written after `export` belong to the declaration and ride inside its slice", + "start": 416, + "end": 504, + "loc": { + "start": { + "line": 26, + "column": 1 + }, + "end": { + "line": 26, + "column": 89 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 515, + "end": 533, + "loc": { + "start": { + "line": 28, + "column": 2 + }, + "end": { + "line": 28, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 558, + "end": 634, + "loc": { + "start": { + "line": 32, + "column": 1 + }, + "end": { + "line": 32, + "column": 77 + } + } + }, + { + "type": "Block", + "value": " prettier-ignore ", + "start": 645, + "end": 666, + "loc": { + "start": { + "line": 34, + "column": 2 + }, + "end": { + "line": 34, + "column": 23 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 696, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 687, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 36, + "column": 9 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 102, + "end": 149, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 19 + } + }, + "exportKind": "value", + "declaration": { + "type": "VariableDeclaration", + "start": 132, + "end": 149, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 19 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 139, + "end": 148, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 18 + } + }, + "id": { + "type": "Identifier", + "start": 139, + "end": 142, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "name": "aaa" + }, + "init": { + "type": "Literal", + "start": 147, + "end": 148, + "loc": { + "start": { + "line": 5, + "column": 17 + }, + "end": { + "line": 5, + "column": 18 + } + }, + "value": 1, + "raw": "1" + } + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 111, + "end": 129 + } + ] + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line directive in the `export`→declaration gap freezes the declaration", + "start": 20, + "end": 100 + } + ] + }, + { + "type": "ExportNamedDeclaration", + "start": 196, + "end": 246, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "exportKind": "value", + "declaration": { + "type": "FunctionDeclaration", + "start": 226, + "end": 246, + "loc": { + "start": { + "line": 10, + "column": 2 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "id": { + "type": "Identifier", + "start": 236, + "end": 238, + "loc": { + "start": { + "line": 10, + "column": 12 + }, + "end": { + "line": 10, + "column": 14 + } + }, + "name": "fn" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 244, + "end": 246, + "loc": { + "start": { + "line": 10, + "column": 20 + }, + "end": { + "line": 10, + "column": 22 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 205, + "end": 223 + } + ] + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " every declaration form is the same head", + "start": 152, + "end": 194 + } + ] + }, + { + "type": "ExportNamedDeclaration", + "start": 249, + "end": 303, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 14, + "column": 26 + } + }, + "exportKind": "type", + "declaration": { + "type": "TSTypeAliasDeclaration", + "start": 279, + "end": 303, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 26 + } + }, + "id": { + "type": "Identifier", + "start": 285, + "end": 286, + "loc": { + "start": { + "line": 14, + "column": 8 + }, + "end": { + "line": 14, + "column": 9 + } + }, + "name": "A" + }, + "typeAnnotation": { + "type": "TSTypeLiteral", + "start": 291, + "end": 302, + "loc": { + "start": { + "line": 14, + "column": 14 + }, + "end": { + "line": 14, + "column": 25 + } + }, + "members": [ + { + "type": "TSPropertySignature", + "start": 294, + "end": 299, + "loc": { + "start": { + "line": 14, + "column": 17 + }, + "end": { + "line": 14, + "column": 22 + } + }, + "computed": false, + "key": { + "type": "Identifier", + "start": 294, + "end": 295, + "loc": { + "start": { + "line": 14, + "column": 17 + }, + "end": { + "line": 14, + "column": 18 + } + }, + "name": "a" + }, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 295, + "end": 299, + "loc": { + "start": { + "line": 14, + "column": 18 + }, + "end": { + "line": 14, + "column": 22 + } + }, + "typeAnnotation": { + "type": "TSLiteralType", + "start": 298, + "end": 299, + "loc": { + "start": { + "line": 14, + "column": 21 + }, + "end": { + "line": 14, + "column": 22 + } + }, + "literal": { + "type": "Literal", + "start": 298, + "end": 299, + "loc": { + "start": { + "line": 14, + "column": 21 + }, + "end": { + "line": 14, + "column": 22 + } + }, + "value": 1, + "raw": "1" + } + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 258, + "end": 276 + } + ] + }, + "specifiers": [], + "source": null + }, + { + "type": "ExportNamedDeclaration", + "start": 306, + "end": 364, + "loc": { + "start": { + "line": 16, + "column": 1 + }, + "end": { + "line": 20, + "column": 3 + } + }, + "exportKind": "type", + "declaration": { + "type": "TSInterfaceDeclaration", + "start": 336, + "end": 364, + "loc": { + "start": { + "line": 18, + "column": 2 + }, + "end": { + "line": 20, + "column": 3 + } + }, + "id": { + "type": "Identifier", + "start": 347, + "end": 348, + "loc": { + "start": { + "line": 18, + "column": 13 + }, + "end": { + "line": 18, + "column": 14 + } + }, + "name": "B" + }, + "body": { + "type": "TSInterfaceBody", + "start": 350, + "end": 364, + "loc": { + "start": { + "line": 18, + "column": 16 + }, + "end": { + "line": 20, + "column": 3 + } + }, + "body": [ + { + "type": "TSPropertySignature", + "start": 355, + "end": 360, + "loc": { + "start": { + "line": 19, + "column": 3 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "computed": false, + "key": { + "type": "Identifier", + "start": 355, + "end": 356, + "loc": { + "start": { + "line": 19, + "column": 3 + }, + "end": { + "line": 19, + "column": 4 + } + }, + "name": "b" + }, + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 356, + "end": 360, + "loc": { + "start": { + "line": 19, + "column": 4 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "typeAnnotation": { + "type": "TSLiteralType", + "start": 359, + "end": 360, + "loc": { + "start": { + "line": 19, + "column": 7 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "literal": { + "type": "Literal", + "start": 359, + "end": 360, + "loc": { + "start": { + "line": 19, + "column": 7 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "value": 2, + "raw": "2" + } + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 315, + "end": 333 + } + ] + }, + "specifiers": [], + "source": null + }, + { + "type": "ExportNamedDeclaration", + "start": 367, + "end": 413, + "loc": { + "start": { + "line": 22, + "column": 1 + }, + "end": { + "line": 24, + "column": 18 + } + }, + "exportKind": "value", + "declaration": { + "type": "TSEnumDeclaration", + "start": 397, + "end": 413, + "loc": { + "start": { + "line": 24, + "column": 2 + }, + "end": { + "line": 24, + "column": 18 + } + }, + "id": { + "type": "Identifier", + "start": 403, + "end": 404, + "loc": { + "start": { + "line": 24, + "column": 8 + }, + "end": { + "line": 24, + "column": 9 + } + }, + "name": "C" + }, + "members": [ + { + "type": "TSEnumMember", + "start": 409, + "end": 410, + "loc": { + "start": { + "line": 24, + "column": 14 + }, + "end": { + "line": 24, + "column": 15 + } + }, + "id": { + "type": "Identifier", + "start": 409, + "end": 410, + "loc": { + "start": { + "line": 24, + "column": 14 + }, + "end": { + "line": 24, + "column": 15 + } + }, + "name": "c" + } + } + ], + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 376, + "end": 394 + } + ] + }, + "specifiers": [], + "source": null + }, + { + "type": "ExportNamedDeclaration", + "start": 506, + "end": 555, + "loc": { + "start": { + "line": 27, + "column": 1 + }, + "end": { + "line": 30, + "column": 14 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassDeclaration", + "start": 536, + "end": 555, + "loc": { + "start": { + "line": 29, + "column": 2 + }, + "end": { + "line": 30, + "column": 14 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 536, + "end": 540, + "loc": { + "start": { + "line": 29, + "column": 2 + }, + "end": { + "line": 29, + "column": 6 + } + }, + "expression": { + "type": "Identifier", + "start": 537, + "end": 540, + "loc": { + "start": { + "line": 29, + "column": 3 + }, + "end": { + "line": 29, + "column": 6 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 550, + "end": 551, + "loc": { + "start": { + "line": 30, + "column": 9 + }, + "end": { + "line": 30, + "column": 10 + } + }, + "name": "D" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 553, + "end": 555, + "loc": { + "start": { + "line": 30, + "column": 12 + }, + "end": { + "line": 30, + "column": 14 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 515, + "end": 533 + } + ] + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " decorators written after `export` belong to the declaration and ride inside its slice", + "start": 416, + "end": 504 + } + ] + }, + { + "type": "ExportNamedDeclaration", + "start": 636, + "end": 686, + "loc": { + "start": { + "line": 33, + "column": 1 + }, + "end": { + "line": 35, + "column": 19 + } + }, + "exportKind": "value", + "declaration": { + "type": "VariableDeclaration", + "start": 669, + "end": 686, + "loc": { + "start": { + "line": 35, + "column": 2 + }, + "end": { + "line": 35, + "column": 19 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 676, + "end": 685, + "loc": { + "start": { + "line": 35, + "column": 9 + }, + "end": { + "line": 35, + "column": 18 + } + }, + "id": { + "type": "Identifier", + "start": 676, + "end": 679, + "loc": { + "start": { + "line": 35, + "column": 9 + }, + "end": { + "line": 35, + "column": 12 + } + }, + "name": "eee" + }, + "init": { + "type": "Literal", + "start": 684, + "end": 685, + "loc": { + "start": { + "line": 35, + "column": 17 + }, + "end": { + "line": 35, + "column": 18 + } + }, + "value": 2, + "raw": "2" + } + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Block", + "value": " prettier-ignore ", + "start": 645, + "end": 666 + } + ] + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 558, + "end": 634 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/input.svelte new file mode 100644 index 00000000..445fb777 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/input.svelte @@ -0,0 +1,36 @@ + diff --git a/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte new file mode 100644 index 00000000..695c5689 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/named_declaration_prettier_ignore_head_prettier_divergence/output_prettier.svelte @@ -0,0 +1,29 @@ + diff --git a/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/expected.json b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/expected.json new file mode 100644 index 00000000..39be9012 --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/expected.json @@ -0,0 +1,369 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 372, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " a redundant paren pair around the expression is dropped, so the directive it holds", + "start": 10, + "end": 95, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 86 + } + } + }, + { + "type": "Line", + "value": " leads the statement and freezes it (the variant carries the authored form)", + "start": 97, + "end": 174, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 78 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 176, + "end": 194, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 19 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 213, + "end": 231, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 19 + } + } + }, + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 248, + "end": 324, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 77 + } + } + }, + { + "type": "Block", + "value": " prettier-ignore ", + "start": 326, + "end": 347, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 22 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 371, + "context": "default", + "content": { + "type": "Program", + "start": 8, + "end": 362, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "body": [ + { + "type": "ExpressionStatement", + "start": 196, + "end": 210, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 15 + } + }, + "expression": { + "type": "CallExpression", + "start": 196, + "end": 209, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 14 + } + }, + "callee": { + "type": "Identifier", + "start": 196, + "end": 198, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 3 + } + }, + "name": "fn" + }, + "arguments": [ + { + "type": "Identifier", + "start": 203, + "end": 206, + "loc": { + "start": { + "line": 5, + "column": 8 + }, + "end": { + "line": 5, + "column": 11 + } + }, + "name": "aaa" + } + ], + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " a redundant paren pair around the expression is dropped, so the directive it holds", + "start": 10, + "end": 95 + }, + { + "type": "Line", + "value": " leads the statement and freezes it (the variant carries the authored form)", + "start": 97, + "end": 174 + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 176, + "end": 194 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 233, + "end": 245, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 13 + } + }, + "expression": { + "type": "BinaryExpression", + "start": 233, + "end": 244, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 12 + } + }, + "left": { + "type": "Identifier", + "start": 233, + "end": 236, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 4 + } + }, + "name": "bbb" + }, + "operator": "+", + "right": { + "type": "Identifier", + "start": 241, + "end": 244, + "loc": { + "start": { + "line": 8, + "column": 9 + }, + "end": { + "line": 8, + "column": 12 + } + }, + "name": "ccc" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 213, + "end": 231 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 349, + "end": 361, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 13 + } + }, + "expression": { + "type": "MemberExpression", + "start": 349, + "end": 360, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 12 + } + }, + "object": { + "type": "Identifier", + "start": 349, + "end": 352, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 4 + } + }, + "name": "ddd" + }, + "property": { + "type": "Identifier", + "start": 357, + "end": 360, + "loc": { + "start": { + "line": 12, + "column": 9 + }, + "end": { + "line": 12, + "column": 12 + } + }, + "name": "eee" + }, + "computed": false, + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 248, + "end": 324 + }, + { + "type": "Block", + "value": " prettier-ignore ", + "start": 326, + "end": 347 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [] + } +} diff --git a/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/input.svelte b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/input.svelte new file mode 100644 index 00000000..79493c3f --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/input.svelte @@ -0,0 +1,13 @@ + diff --git a/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/unformatted_paren_head.svelte b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/unformatted_paren_head.svelte new file mode 100644 index 00000000..9c79a2f0 --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_paren_dropped_prettier_ignore_head/unformatted_paren_head.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/README.md b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/README.md new file mode 100644 index 00000000..040d434e --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/README.md @@ -0,0 +1,31 @@ +# expression_statement_prettier_ignore_head_prettier_divergence + +An own-line directive after the `(` of an expression statement whose parens are **required** — +an object / class / function expression at statement start — freezes the expression. The parens +are the printer's, so they stay outside the frozen slice and the directive keeps its place +inside them: + +```js +( + // prettier-ignore + { aaa: 1 } +); +``` + +Prettier hoists the directive out before the `(` and glues the frozen slice back inside parens +on one line (`// prettier-ignore⏎({ aaa: 1 });`), the same relocation it applies to an ordinary +comment here +([expression_statement_paren_kept_comment](../expression_statement_paren_kept_comment_prettier_divergence/)). +When the parens are **redundant** tsv drops them and the directive leads the statement, matching +prettier ([paren_dropped head](../expression_statement_paren_dropped_prettier_ignore_head/)). + +The last case is the one where those two regimes meet: the parens are redundant around the +*printed* expression but the frozen slice's leftmost token (`{`) would reparse as a block, and a +verbatim slice has no interior for the printer to wrap. tsv therefore keeps the author's parens +around the whole slice. Prettier drops them and emits `{ bbb: 2 }.ccc;`, which does not reparse. + +## Reason + +Comment position is authorship signal, and a shell a verbatim slice needs must go around the +whole slice; ◆comment_preservation, ◆prettier_bug. See +[conformance_prettier.md §Format-ignore directive](../../../../../docs/conformance_prettier.md#format-ignore-directive). diff --git a/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/expected.json b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/expected.json new file mode 100644 index 00000000..d5b744dd --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/expected.json @@ -0,0 +1,551 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 606, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " an own-line directive in the `(`→expression gap of a statement whose parens are", + "start": 10, + "end": 92, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 83 + } + } + }, + { + "type": "Line", + "value": " required freezes the expression; the parens stay outside the frozen slice", + "start": 94, + "end": 170, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 77 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 176, + "end": 194, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 219, + "end": 237, + "loc": { + "start": { + "line": 10, + "column": 2 + }, + "end": { + "line": 10, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 256, + "end": 332, + "loc": { + "start": { + "line": 14, + "column": 1 + }, + "end": { + "line": 14, + "column": 77 + } + } + }, + { + "type": "Block", + "value": " prettier-ignore ", + "start": 338, + "end": 359, + "loc": { + "start": { + "line": 16, + "column": 2 + }, + "end": { + "line": 16, + "column": 23 + } + } + }, + { + "type": "Line", + "value": " a slice whose leftmost token needs parens keeps them too — a verbatim slice has no", + "start": 385, + "end": 470, + "loc": { + "start": { + "line": 20, + "column": 1 + }, + "end": { + "line": 20, + "column": 86 + } + } + }, + { + "type": "Line", + "value": " interior for the printer to wrap, so the shell goes around the whole slice", + "start": 472, + "end": 549, + "loc": { + "start": { + "line": 21, + "column": 1 + }, + "end": { + "line": 21, + "column": 78 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 555, + "end": 573, + "loc": { + "start": { + "line": 23, + "column": 2 + }, + "end": { + "line": 23, + "column": 20 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 605, + "context": "default", + "content": { + "type": "Program", + "start": 8, + "end": 596, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 26, + "column": 9 + } + }, + "body": [ + { + "type": "ExpressionStatement", + "start": 172, + "end": 212, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 7, + "column": 3 + } + }, + "expression": { + "type": "ObjectExpression", + "start": 197, + "end": 208, + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 13 + } + }, + "properties": [ + { + "type": "Property", + "start": 199, + "end": 206, + "loc": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 11 + } + }, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 199, + "end": 202, + "loc": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 7 + } + }, + "name": "aaa" + }, + "value": { + "type": "Literal", + "start": 205, + "end": 206, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 11 + } + }, + "value": 1, + "raw": "1" + }, + "kind": "init" + } + ], + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 176, + "end": 194 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line directive in the `(`→expression gap of a statement whose parens are", + "start": 10, + "end": 92 + }, + { + "type": "Line", + "value": " required freezes the expression; the parens stay outside the frozen slice", + "start": 94, + "end": 170 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 215, + "end": 253, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 12, + "column": 3 + } + }, + "expression": { + "type": "ClassExpression", + "start": 240, + "end": 249, + "loc": { + "start": { + "line": 11, + "column": 2 + }, + "end": { + "line": 11, + "column": 11 + } + }, + "id": null, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 247, + "end": 249, + "loc": { + "start": { + "line": 11, + "column": 9 + }, + "end": { + "line": 11, + "column": 11 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 219, + "end": 237 + } + ] + } + }, + { + "type": "ExpressionStatement", + "start": 334, + "end": 382, + "loc": { + "start": { + "line": 15, + "column": 1 + }, + "end": { + "line": 18, + "column": 3 + } + }, + "expression": { + "type": "FunctionExpression", + "start": 362, + "end": 378, + "loc": { + "start": { + "line": 17, + "column": 2 + }, + "end": { + "line": 17, + "column": 18 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 376, + "end": 378, + "loc": { + "start": { + "line": 17, + "column": 16 + }, + "end": { + "line": 17, + "column": 18 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Block", + "value": " prettier-ignore ", + "start": 338, + "end": 359 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " an own-line block comment behaves identically — placement keys the freeze", + "start": 256, + "end": 332 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 551, + "end": 595, + "loc": { + "start": { + "line": 22, + "column": 1 + }, + "end": { + "line": 25, + "column": 3 + } + }, + "expression": { + "type": "MemberExpression", + "start": 576, + "end": 591, + "loc": { + "start": { + "line": 24, + "column": 2 + }, + "end": { + "line": 24, + "column": 17 + } + }, + "object": { + "type": "ObjectExpression", + "start": 576, + "end": 587, + "loc": { + "start": { + "line": 24, + "column": 2 + }, + "end": { + "line": 24, + "column": 13 + } + }, + "properties": [ + { + "type": "Property", + "start": 578, + "end": 585, + "loc": { + "start": { + "line": 24, + "column": 4 + }, + "end": { + "line": 24, + "column": 11 + } + }, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 578, + "end": 581, + "loc": { + "start": { + "line": 24, + "column": 4 + }, + "end": { + "line": 24, + "column": 7 + } + }, + "name": "bbb" + }, + "value": { + "type": "Literal", + "start": 584, + "end": 585, + "loc": { + "start": { + "line": 24, + "column": 10 + }, + "end": { + "line": 24, + "column": 11 + } + }, + "value": 2, + "raw": "2" + }, + "kind": "init" + } + ] + }, + "property": { + "type": "Identifier", + "start": 588, + "end": 591, + "loc": { + "start": { + "line": 24, + "column": 14 + }, + "end": { + "line": 24, + "column": 17 + } + }, + "name": "ccc" + }, + "computed": false, + "optional": false, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 555, + "end": 573 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " a slice whose leftmost token needs parens keeps them too — a verbatim slice has no", + "start": 385, + "end": 470 + }, + { + "type": "Line", + "value": " interior for the printer to wrap, so the shell goes around the whole slice", + "start": 472, + "end": 549 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [] + } +} diff --git a/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/input.svelte b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/input.svelte new file mode 100644 index 00000000..894881ff --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/input.svelte @@ -0,0 +1,26 @@ + diff --git a/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/output_prettier.svelte new file mode 100644 index 00000000..95dc5b9a --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/output_prettier.svelte @@ -0,0 +1,18 @@ + diff --git a/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte new file mode 100644 index 00000000..a9b6e3b3 --- /dev/null +++ b/tests/fixtures/typescript/statements/expression_statement_prettier_ignore_head_prettier_divergence/unformatted_ours_spaces.svelte @@ -0,0 +1,26 @@ + diff --git a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/expected.json b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/expected.json index 77df88a1..a6121e6c 100644 --- a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/expected.json +++ b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/expected.json @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 564, + "end": 747, "type": "Root", "fragment": { "type": "Fragment", @@ -137,24 +137,88 @@ "column": 23 } } + }, + { + "type": "Line", + "value": " a block comment glued before the frozen statement is OWNED by it, so the freeze", + "start": 556, + "end": 638, + "loc": { + "start": { + "line": 28, + "column": 1 + }, + "end": { + "line": 28, + "column": 83 + } + } + }, + { + "type": "Line", + "value": " claims it — nothing else would print it", + "start": 640, + "end": 682, + "loc": { + "start": { + "line": 29, + "column": 1 + }, + "end": { + "line": 29, + "column": 43 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 695, + "end": 713, + "loc": { + "start": { + "line": 31, + "column": 2 + }, + "end": { + "line": 31, + "column": 20 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 716, + "end": 723, + "loc": { + "start": { + "line": 32, + "column": 2 + }, + "end": { + "line": 32, + "column": 9 + } + } } ], "instance": { "type": "Script", "start": 0, - "end": 563, + "end": 746, "context": "default", "content": { "type": "Program", "start": 18, - "end": 554, + "end": 737, "loc": { "start": { "line": 1, "column": 0 }, "end": { - "line": 27, + "line": 33, "column": 9 } }, @@ -821,6 +885,131 @@ "end": 504 } ] + }, + { + "type": "IfStatement", + "start": 684, + "end": 736, + "loc": { + "start": { + "line": 30, + "column": 1 + }, + "end": { + "line": 32, + "column": 22 + } + }, + "test": { + "type": "Identifier", + "start": 688, + "end": 691, + "loc": { + "start": { + "line": 30, + "column": 5 + }, + "end": { + "line": 30, + "column": 8 + } + }, + "name": "aaa" + }, + "consequent": { + "type": "ExpressionStatement", + "start": 724, + "end": 736, + "loc": { + "start": { + "line": 32, + "column": 10 + }, + "end": { + "line": 32, + "column": 22 + } + }, + "expression": { + "type": "CallExpression", + "start": 724, + "end": 735, + "loc": { + "start": { + "line": 32, + "column": 10 + }, + "end": { + "line": 32, + "column": 21 + } + }, + "callee": { + "type": "Identifier", + "start": 724, + "end": 726, + "loc": { + "start": { + "line": 32, + "column": 10 + }, + "end": { + "line": 32, + "column": 12 + } + }, + "name": "fn" + }, + "arguments": [ + { + "type": "Identifier", + "start": 729, + "end": 732, + "loc": { + "start": { + "line": 32, + "column": 15 + }, + "end": { + "line": 32, + "column": 18 + } + }, + "name": "bbb" + } + ], + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 695, + "end": 713 + }, + { + "type": "Block", + "value": " c ", + "start": 716, + "end": 723 + } + ] + }, + "alternate": null, + "leadingComments": [ + { + "type": "Line", + "value": " a block comment glued before the frozen statement is OWNED by it, so the freeze", + "start": 556, + "end": 638 + }, + { + "type": "Line", + "value": " claims it — nothing else would print it", + "start": 640, + "end": 682 + } + ] } ], "sourceType": "module" diff --git a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/input.svelte b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/input.svelte index d175becd..f2c757e8 100644 --- a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/input.svelte +++ b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/input.svelte @@ -24,4 +24,10 @@ if (aaa) /* prettier-ignore */ fn( bbb ); + + // a block comment glued before the frozen statement is OWNED by it, so the freeze + // claims it — nothing else would print it + if (aaa) + // prettier-ignore + /* c */ fn( bbb ); diff --git a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/unformatted_spaces.svelte b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/unformatted_spaces.svelte index d510a2a5..bc063491 100644 --- a/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/unformatted_spaces.svelte +++ b/tests/fixtures/typescript/statements/if/branch_prettier_ignore_head/unformatted_spaces.svelte @@ -24,4 +24,10 @@ if ( aaa ) /* prettier-ignore */ fn( bbb ); + + // a block comment glued before the frozen statement is OWNED by it, so the freeze + // claims it — nothing else would print it + if ( aaa ) + // prettier-ignore + /* c */ fn( bbb ); diff --git a/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/README.md b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/README.md new file mode 100644 index 00000000..544dc8ea --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/README.md @@ -0,0 +1,29 @@ +# before_export_comment_prettier_divergence + +A decorator written *before* `export` makes the whole thing one declaration whose span opens at +the decorator, so the `export`→`class` gap sits **inside** the declaration. A comment there is +kept where the author wrote it, trailing `export` with the continuation indented — the uniform +declaration-header rule: + +```ts +@dec +export // c1 + class C {} +``` + +Prettier hoists a line comment above `export` (past the decorator) and trails a block comment on +the decorator instead. + +Because the gap is inside the declaration rather than before it, there is no following node for +Rule A to bind to, so an own-line directive there **freezes nothing** — in prettier or in tsv; +it only keeps its own line. `unformatted_ours_spaces.svelte` pins that: the spaced class heads +normalize under tsv despite the directive. This is the mirror image of the sanctioned +decorator→declaration gap in a program body. + +## Reason + +Comment position is authorship signal; ◆comment_preservation. tsv previously **dropped** all +three comments here — the gap was scanned over an inverted range (the declaration's span starts +at the decorator, *before* the `export` keyword the scan began at), so nothing found them; +◆content_preservation. See +[conformance_prettier.md §Format-ignore directive](../../../../../../docs/conformance_prettier.md#format-ignore-directive). diff --git a/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/expected.json new file mode 100644 index 00000000..3bb0ec57 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/expected.json @@ -0,0 +1,506 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 352, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " a comment in the `export`→class gap of a decorator-first class stays where it is", + "start": 20, + "end": 103, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 84 + } + } + }, + { + "type": "Line", + "value": " c1", + "start": 118, + "end": 123, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 13 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 152, + "end": 160, + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 8, + "column": 16 + } + } + }, + { + "type": "Line", + "value": " the gap sits inside the declaration, so an own-line directive there freezes nothing —", + "start": 174, + "end": 262, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 89 + } + } + }, + { + "type": "Line", + "value": " it only keeps its own line", + "start": 264, + "end": 293, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 30 + } + } + }, + { + "type": "Line", + "value": " prettier-ignore", + "start": 310, + "end": 328, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 20 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 351, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 342, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 16, + "column": 9 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 111, + "end": 136, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassDeclaration", + "start": 105, + "end": 136, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 105, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 5 + } + }, + "expression": { + "type": "Identifier", + "start": 106, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 2 + }, + "end": { + "line": 3, + "column": 5 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 132, + "end": 133, + "loc": { + "start": { + "line": 5, + "column": 8 + }, + "end": { + "line": 5, + "column": 9 + } + }, + "name": "C", + "leadingComments": [ + { + "type": "Line", + "value": " c1", + "start": 118, + "end": 123 + } + ] + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 134, + "end": 136, + "loc": { + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "body": [] + } + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " a comment in the `export`→class gap of a decorator-first class stays where it is", + "start": 20, + "end": 103 + } + ] + }, + { + "type": "ExportNamedDeclaration", + "start": 145, + "end": 171, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 27 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassDeclaration", + "start": 139, + "end": 171, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 8, + "column": 27 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 139, + "end": 143, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 5 + } + }, + "expression": { + "type": "Identifier", + "start": 140, + "end": 143, + "loc": { + "start": { + "line": 7, + "column": 2 + }, + "end": { + "line": 7, + "column": 5 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 167, + "end": 168, + "loc": { + "start": { + "line": 8, + "column": 23 + }, + "end": { + "line": 8, + "column": 24 + } + }, + "name": "D", + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 152, + "end": 160 + } + ] + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 169, + "end": 171, + "loc": { + "start": { + "line": 8, + "column": 25 + }, + "end": { + "line": 8, + "column": 27 + } + }, + "body": [] + } + }, + "specifiers": [], + "source": null + }, + { + "type": "ExportNamedDeclaration", + "start": 301, + "end": 341, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 15, + "column": 12 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassDeclaration", + "start": 295, + "end": 341, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 15, + "column": 12 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 295, + "end": 299, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 5 + } + }, + "expression": { + "type": "Identifier", + "start": 296, + "end": 299, + "loc": { + "start": { + "line": 12, + "column": 2 + }, + "end": { + "line": 12, + "column": 5 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 337, + "end": 338, + "loc": { + "start": { + "line": 15, + "column": 8 + }, + "end": { + "line": 15, + "column": 9 + } + }, + "name": "E", + "leadingComments": [ + { + "type": "Line", + "value": " prettier-ignore", + "start": 310, + "end": 328 + } + ] + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 339, + "end": 341, + "loc": { + "start": { + "line": 15, + "column": 10 + }, + "end": { + "line": 15, + "column": 12 + } + }, + "body": [] + } + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " the gap sits inside the declaration, so an own-line directive there freezes nothing —", + "start": 174, + "end": 262 + }, + { + "type": "Line", + "value": " it only keeps its own line", + "start": 264, + "end": 293 + } + ] + } + ], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "name_loc": { + "start": { + "line": 1, + "column": 8, + "character": 8 + }, + "end": { + "line": 1, + "column": 12, + "character": 12 + } + }, + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/input.svelte new file mode 100644 index 00000000..a8d181c5 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/input.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 00000000..ccade867 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,14 @@ + diff --git a/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/unformatted_ours_spaces.svelte b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/unformatted_ours_spaces.svelte new file mode 100644 index 00000000..8072508a --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/decorators/before_export_comment_prettier_divergence/unformatted_ours_spaces.svelte @@ -0,0 +1,16 @@ +