From f2a3a1ef6696c93600d85a66e148e548a8e7fa41 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Wed, 15 Jul 2026 08:48:13 -0400 Subject: [PATCH 1/4] fix: dropped comments between keywords --- crates/tsv_ts/src/ast/internal/statements.rs | 17 + .../tsv_ts/src/printer/calls/import_expr.rs | 67 +- .../src/printer/comments/declarations.rs | 183 ++++- crates/tsv_ts/src/printer/statements/class.rs | 35 +- .../statements/control_flow/loops/for_loop.rs | 15 +- .../src/printer/statements/modules/mod.rs | 156 +++- .../tsv_ts/src/printer/statements/variable.rs | 26 +- docs/conformance_prettier.md | 90 ++ docs/conformance_svelte.md | 2 +- .../README.md | 24 + .../expected.json | 766 ++++++++++++++++++ .../input.svelte | 13 + .../output_prettier.svelte | 13 + .../dot_gap_comments/expected.json | 721 +++++++++++++++++ .../dot_gap_comments/input.svelte | 15 + .../README.md | 27 + .../expected.json | 568 +++++++++++++ .../input.svelte | 17 + .../output_prettier.svelte | 16 + .../README.md | 30 + .../expected.json | 256 ++++++ .../input.svelte | 6 + .../output_prettier.svelte | 6 + .../README.md | 36 + .../audit_signature.txt | 28 + .../expected.json | 219 +++++ .../input.svelte | 7 + .../output_prettier.svelte | 7 + .../README.md | 25 + .../expected.json | 148 ++++ .../input.svelte | 4 + .../output_prettier.svelte | 4 + .../README.md | 20 + .../audit_signature.txt | 28 + .../expected.json | 219 +++++ .../input.svelte | 7 + .../output_prettier.svelte | 7 + .../README.md | 19 + .../expected.json | 170 ++++ .../input.svelte | 6 + .../output_prettier.svelte | 6 + .../README.md | 26 + .../expected.json | 190 +++++ .../input.svelte | 5 + .../output_prettier.svelte | 5 + .../README.md | 30 + .../expected.json | 212 +++++ .../input.svelte | 8 + .../output_prettier.svelte | 8 + .../expected.json | 146 ++++ .../input.svelte | 4 + .../README.md | 22 + .../expected.json | 264 ++++++ .../input.svelte | 5 + .../output_prettier.svelte | 5 + .../equals_header_comment/expected.json | 632 +++++++++++++++ .../equals_header_comment/input.svelte | 16 + .../README.md | 23 + .../expected.json | 202 +++++ .../input.svelte | 5 + .../output_prettier.svelte | 5 + .../README.md | 30 + .../expected_ours.json | 762 +++++++++++++++++ .../expected_svelte.json | 1 + .../input.svelte | 17 + .../output_prettier.svelte | 17 + 66 files changed, 6576 insertions(+), 93 deletions(-) create mode 100644 tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/expected.json create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/input.svelte create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/audit_signature.txt create mode 100644 tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/audit_signature.txt create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_comment/expected.json create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_comment/input.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_ours.json create mode 100644 tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_svelte.json create mode 100644 tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/output_prettier.svelte diff --git a/crates/tsv_ts/src/ast/internal/statements.rs b/crates/tsv_ts/src/ast/internal/statements.rs index 0f2d9a9e9..b9e36a6b9 100644 --- a/crates/tsv_ts/src/ast/internal/statements.rs +++ b/crates/tsv_ts/src/ast/internal/statements.rs @@ -335,6 +335,23 @@ impl VariableDeclarationKind { Self::AwaitUsing => "await using", } } + + /// The kind's source tokens, in order — `await using` is **two**. + /// + /// A printer must locate these rather than measure [`as_str`](Self::as_str): the + /// gap *between* two words is a source position an author can write a comment in + /// (`await /* c */ using`), and measuring the joined text never scans it, so the + /// comment is dropped. + #[inline] + pub const fn words(self) -> &'static [&'static str] { + match self { + Self::Const => &["const"], + Self::Let => &["let"], + Self::Var => &["var"], + Self::Using => &["using"], + Self::AwaitUsing => &["await", "using"], + } + } } #[derive(Debug, Clone)] diff --git a/crates/tsv_ts/src/printer/calls/import_expr.rs b/crates/tsv_ts/src/printer/calls/import_expr.rs index 8e35d8ff3..20425a788 100644 --- a/crates/tsv_ts/src/printer/calls/import_expr.rs +++ b/crates/tsv_ts/src/printer/calls/import_expr.rs @@ -4,7 +4,7 @@ // - Dynamic import: `import('module')`, `import('module', options)` // - Meta properties: `import.meta`, `new.target` -use super::super::Printer; +use super::super::{CommentSpacing, Printer}; use super::arg_comments::{ PartitionedComments, has_blank_line_between_args, should_force_expansion_for_comments, }; @@ -253,14 +253,69 @@ pub(super) fn build_import_expression_doc( } /// Build a Doc for a meta property: `import.meta`, `new.target` +/// +/// Both gaps around the `.` are real source positions an author can comment in +/// (`new /* c */.target`, `new./* c */ target`), so each is located and emitted. +/// Concatenating the three pieces scans neither gap and drops whatever is in it — the +/// same class as a comment inside a multi-word keyword (see `build_keyword_words_doc` +/// in `printer/comments/declarations.rs`), and the reason that class's usual detector +/// (a `d.text` literal with an *interior* space) is only a proxy: here the pieces are +/// joined by `"."`, which has no space to find. +/// +/// Each side stays where it was authored, which is also what prettier prints: the +/// comment hugs the `.` and keeps its space on the identifier's side. A *line* comment +/// before the `.` ends its line, so `.property` continues one level down +/// (`new // c⏎\t.target`) — the shape a member access already takes. pub(super) fn build_meta_property_doc( printer: &Printer<'_>, meta: &internal::MetaProperty<'_>, ) -> DocId { let d = printer.d(); - d.concat(&[ - printer.identifier_name_doc(&meta.meta), - d.text("."), - printer.identifier_name_doc(&meta.property), - ]) + let meta_doc = printer.identifier_name_doc(&meta.meta); + let prop_doc = printer.identifier_name_doc(&meta.property); + let gap_start = meta.meta.span.end; + let gap_end = meta.property.span.start; + + // `new.target` / `import.meta` with both gaps empty — every ordinary occurrence. + if !printer.has_comments_to_emit_between(gap_start, gap_end) { + return d.concat(&[meta_doc, d.text("."), prop_doc]); + } + + let Some(dot) = printer.find_char_outside_comments(gap_start, gap_end, b'.') else { + debug_assert!( + false, + "a meta property always spells a `.` between its two names" + ); + return d.concat(&[meta_doc, d.text("."), prop_doc]); + }; + + // `.`→property: a block comment keeps its space on the property's side + // (`./* c */ target`); a line comment ends the line, so the property drops a level. + let after_dot = if !printer.has_comments_to_emit_between(dot + 1, gap_end) { + prop_doc + } else if printer.has_line_comments_between(dot + 1, gap_end) { + printer.build_continuation_indent(dot + 1, gap_end, prop_doc) + } else { + d.concat(&[ + printer.build_comments_between(dot + 1, gap_end, CommentSpacing::Trailing), + prop_doc, + ]) + }; + let tail = d.concat(&[d.text("."), after_dot]); + + // meta→`.`: a line comment takes the whole `.property` onto the next line with it. + if printer.has_line_comments_between(gap_start, dot) { + return d.concat(&[ + meta_doc, + printer.build_continuation_indent(gap_start, dot, tail), + ]); + } + if printer.has_comments_to_emit_between(gap_start, dot) { + return d.concat(&[ + meta_doc, + printer.build_comments_between(gap_start, dot, CommentSpacing::Leading), + tail, + ]); + } + d.concat(&[meta_doc, tail]) } diff --git a/crates/tsv_ts/src/printer/comments/declarations.rs b/crates/tsv_ts/src/printer/comments/declarations.rs index b99219745..f65e6b2b5 100644 --- a/crates/tsv_ts/src/printer/comments/declarations.rs +++ b/crates/tsv_ts/src/printer/comments/declarations.rs @@ -333,6 +333,147 @@ impl<'a> Printer<'a> { ]) } + /// A whole multi-word header: the keyword (see + /// [`build_keyword_words_doc`](Self::build_keyword_words_doc)) plus the + /// keyword→`continuation` gap. The shape every caller wants that has no other use + /// for the keyword's end — bounding the word search by `continuation_start` also + /// keeps a word from ever matching inside the continuation. + pub(crate) fn build_keyword_header_doc( + &self, + words: &[&'static str], + start: u32, + continuation_start: u32, + continuation: 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), + ]) + } + + /// Build a **multi-word keyword** (`export default`, `await using`, `declare + /// const`, `export as namespace`), preserving a comment authored in one of its + /// interior gaps. + /// + /// Returns the keyword's doc (no trailing space) and the source offset just past + /// its final word — the caller's own keyword→value gap starts there. + /// + /// A keyword spanning two or more words has a gap *between* them that is a real + /// source position an author can write a comment in. Deriving the keyword's extent + /// by measuring its text (`span.start + "export default".len()`) never locates that + /// gap, so nothing scans it and the comment is silently dropped. Locating each word + /// instead makes every interior gap emittable, through the same emitter the + /// keyword→name gap uses: a block comment stays inline, a line comment indents the + /// continuation. + /// + /// `words` must occur in order within `source[start..search_end]`; each is matched + /// whole-word and comment-aware, so a word appearing inside an interior comment + /// (`export /* default */ default 1`) never matches. + /// + /// A word may be a punctuator (`=`). Note what makes that safe: the whole-word test + /// rejects a match flanked by *identifier* bytes, and a punctuator has none — so it + /// does **not** rule out matching the `=` inside `=>` or `==`. Only `start` and + /// `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. + pub(crate) fn build_keyword_words_doc( + &self, + words: &[&'static str], + start: u32, + search_end: u32, + ) -> (DocId, u32) { + let d = self.d(); + debug_assert!(!words.is_empty(), "a keyword has at least one word"); + + // A one-word keyword has no interior gap, so there is nothing to locate: it + // begins at `start` and its end is arithmetic. This is the hot path — the + // single-word kinds (`const`/`let`/`var`/`using`) run through here for **every** + // declaration in every file, and the gap printer they feed is already the + // hottest of them (see the internal perf notes on `build_keyword_to_name_continuation`). + // Only a genuinely multi-word keyword pays for a scan. + if let [word] = words { + // That shortcut rests on an invariant nothing else enforces: a single-word + // caller's `start` IS the keyword. Locating it would cost the hot path a + // scan to prove what every caller already knows, so assert it in debug + // instead — a caller passing a wider span (one that leads with `export `, + // say) would otherwise silently mis-place `keyword_end` and drop the gap's + // comment, which is the very bug this function exists to prevent. + debug_assert!( + self.source + .as_bytes() + .get(start as usize..) + .is_some_and(|rest| rest.starts_with(word.as_bytes())), + "single-word keyword `{word}` must begin at `start` ({start})" + ); + return (d.text(word), start + word.len() as u32); + } + + let mut starts: SmallVec<[u32; 4]> = SmallVec::new(); + let mut cursor = start; + for word in words { + let Some(pos) = self.find_keyword_in_range(cursor, search_end, word) else { + // The source doesn't hold the shape the caller named — only reachable + // through a synthetic span or a `search_end` that precedes the words. + // Assert it in debug: this arm's measured end is the very arithmetic + // this function exists to replace (it assumes one space per gap), so a + // caller that lands here silently drops the comment it came for. Prod + // still degrades gracefully rather than panicking — a formatter must + // not crash on input it parsed. + debug_assert!( + false, + "keyword word `{word}` not found in source[{cursor}..{search_end}] \ + — caller passed a synthetic span or a bad search_end" + ); + 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; + return (d.concat(&parts), measured); + }; + starts.push(pos); + cursor = pos + word.len() as u32; + } + let keyword_end = cursor; + + // Left-to-right and FLAT: every gap emits its comments where the author wrote + // them, but none of them indents on its own — the whole tail is wrapped once, + // below. Indenting per gap would compound, and the staircase it builds is not + // just deep, it is wrong: the caller emits the keyword→value gap at the header's + // own level, so a two-broken-gap keyword would leave its last word sitting a + // level *below* the value that follows it. + let mut tail: DocBuf = DocBuf::new(); + let mut any_line = false; + for i in 0..words.len() - 1 { + let gap_start = starts[i] + words[i].len() as u32; + let (gap_doc, has_line) = self.build_keyword_gap_doc(gap_start, starts[i + 1]); + any_line |= has_line; + tail.push(gap_doc); + tail.push(d.text(words[i + 1])); + } + let tail_doc = d.concat(&tail); + // One level for the whole header — the same thing the single-gap rule says: a + // broken gap reads as one statement continuation, never as N nested ones. With + // no line comment there is no break to indent, so the wrapper is skipped and a + // comment-free keyword stays byte-identical to `words.join(" ")`. + let doc = d.concat(&[ + d.text(words[0]), + if any_line { + d.indent(tail_doc) + } else { + tail_doc + }, + ]); + (doc, keyword_end) + } + /// Build a declaration header's keyword→name gap comment followed by the rest /// of the declaration (`continuation`), indenting that continuation one level /// when a *line* comment forces the break. @@ -371,30 +512,40 @@ impl<'a> Printer<'a> { continuation: DocId, ) -> DocId { let d = self.d(); - // One search settles the gap. With no comment there is nothing to emit and - // nothing to indent, so the header is just `" " + continuation` — no empty - // child, and neither of the per-shape searches below runs. Every declaration in - // every file passes through here, so this is the hottest of the gap printers. - if !self.has_comments_to_emit_between(keyword_end, name_start) { - return d.concat(&[d.text(" "), continuation]); + let (gap_doc, has_line) = self.build_keyword_gap_doc(keyword_end, name_start); + let body = d.concat(&[gap_doc, continuation]); + if has_line { d.indent(body) } else { body } + } + + /// One header gap — the comments authored in it plus the separator that follows — + /// with **no** `indent` applied. Also reports whether a *line* comment ended the + /// line, which is the caller's cue that a break happened. + /// + /// Split out from [`build_keyword_to_name_continuation`](Self::build_keyword_to_name_continuation) + /// so a caller with *several* gaps can emit each one and then decide **once** what + /// to indent. Indenting per gap compounds: two broken gaps would put the keyword's + /// last word two levels deep, below the value that follows it at one. + #[inline] + fn build_keyword_gap_doc(&self, start: u32, end: u32) -> (DocId, bool) { + let d = self.d(); + // One search settles the gap. With no comment there is nothing to emit but the + // separator — no empty child, and neither of the per-shape searches below runs. + // Every declaration in every file passes through here, so this is the hottest of + // the gap printers. + if !self.has_comments_to_emit_between(start, end) { + return (d.text(" "), false); } - let has_line = self.has_line_comments_between(keyword_end, name_start); + let has_line = self.has_line_comments_between(start, end); let comment_doc = if has_line { - self.build_name_to_type_params_comments( - keyword_end, - name_start, - CommentSpacing::Leading, - ) - } else if let Some(c) = self.build_inline_comments_between_doc_opt(keyword_end, name_start) - { + self.build_name_to_type_params_comments(start, end, CommentSpacing::Leading) + } else if let Some(c) = self.build_inline_comments_between_doc_opt(start, end) { c } else { d.empty() }; // After a line comment the hardline provides separation; otherwise a space. let space_after = if has_line { d.empty() } else { d.text(" ") }; - let body = d.concat(&[comment_doc, space_after, continuation]); - if has_line { d.indent(body) } else { body } + (d.concat(&[comment_doc, space_after]), has_line) } /// Build a Doc for comments between a keyword and the following name/token. diff --git a/crates/tsv_ts/src/printer/statements/class.rs b/crates/tsv_ts/src/printer/statements/class.rs index 63f309f96..5f3c313c2 100644 --- a/crates/tsv_ts/src/printer/statements/class.rs +++ b/crates/tsv_ts/src/printer/statements/class.rs @@ -31,6 +31,27 @@ impl<'a> Printer<'a> { self.build_class_declaration_doc_inner(decl, false) } + /// The source position where a class declaration's own doc begins: its first + /// keyword (`declare` / `abstract` / `class`), located past any decorators. + /// + /// A caller that prints the decorators itself and then the *undecorated* class + /// (the decorators-first `export default` path) needs this to bound its own + /// keyword→value gap. Without it that gap has no end, so nothing scans it and a + /// comment authored there is dropped. + pub(in crate::printer) fn class_declaration_keyword_start( + &self, + decl: &internal::ClassDeclaration<'_>, + ) -> u32 { + let first_keyword = if decl.declare { + "declare" + } else if decl.r#abstract { + "abstract" + } else { + "class" + }; + self.find_keyword_after_decorators(decl.decorators, first_keyword, decl.span.start) + } + /// Core implementation for class declaration doc building /// /// # Arguments @@ -86,17 +107,9 @@ impl<'a> Printer<'a> { let mut parts = smallvec![]; - // Decorators, each on its own line - // Find the first keyword after decorators (declare/abstract/class) - let first_keyword = if decl.declare { - "declare" - } else if decl.r#abstract { - "abstract" - } else { - "class" - }; - let keyword_start = - self.find_keyword_after_decorators(decl.decorators, first_keyword, decl.span.start); + // Decorators, each on its own line; the first keyword after them + // (declare/abstract/class) is where this class's own text starts. + let keyword_start = self.class_declaration_keyword_start(decl); if include_decorators && let Some(dec_doc) = self.build_decorators_doc(decl.decorators, keyword_start) 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 1df25096e..2983c3314 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 @@ -1373,19 +1373,20 @@ impl<'a> Printer<'a> { /// standalone declaration uses — so the gap comment isn't dropped; byte-identical /// to `kind + " " + continuation` when the gap is comment-free, so a caller's /// enclosing `group`/`indent` is preserved. A for-header declaration is never - /// `declare`, so `keyword_end` is just past the `kind` keyword. + /// `declare`, but its kind may still be two words (`await using`), whose own + /// interior gap is emitted by `build_keyword_words_doc`. fn build_for_decl_keyword_gap( &self, decl: &internal::VariableDeclaration<'_>, binding_start: u32, continuation: DocId, ) -> DocId { - let d = self.d(); - let keyword_end = decl.span.start + decl.kind.as_str().len() as u32; - d.concat(&[ - d.text(decl.kind.as_str()), - self.build_keyword_to_name_continuation(keyword_end, binding_start, continuation), - ]) + self.build_keyword_header_doc( + decl.kind.words(), + decl.span.start, + binding_start, + continuation, + ) } fn build_for_init_doc(&self, init: &internal::ForInit<'_>) -> DocId { diff --git a/crates/tsv_ts/src/printer/statements/modules/mod.rs b/crates/tsv_ts/src/printer/statements/modules/mod.rs index d488b873c..2806042e4 100644 --- a/crates/tsv_ts/src/printer/statements/modules/mod.rs +++ b/crates/tsv_ts/src/printer/statements/modules/mod.rs @@ -40,17 +40,26 @@ impl<'a> Printer<'a> { let d = self.d(); let expr_doc = self.build_expression_doc(&decl.expression); let argument_end = decl.expression.span().end; + // `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( + &["export", "="], + decl.span.start, + decl.expression.span().start, + expr_doc, + ); 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 `;` // (operand-attached — prettier 3.9 does not move it, unlike `export default` // / named exports). A line comment still floats after the `;` via `line_suffix`. - let mut parts = smallvec![d.text("export = "), expr_doc]; + let mut parts = smallvec![head]; self.append_trailing_paren_comments(&mut parts, argument_end, decl.span.end); parts.push(d.text(";")); d.concat(&parts) } else { - d.concat(&[d.text("export = "), expr_doc, d.text(";")]) + d.concat(&[head, d.text(";")]) } } @@ -287,18 +296,25 @@ impl<'a> Printer<'a> { 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 Some(dec_doc) = self.build_decorators_doc( - class.decorators, - self.find_keyword_after_decorators(class.decorators, "export", decl.span.start), - ) - { - return d.concat(&[ - dec_doc, - d.text("export default "), - self.build_class_declaration_without_decorators_doc(class), - ]); + // 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), + ), + ]); + } } // Decorator-*after*-`default` `export default @dec class {}` — the decorator @@ -312,17 +328,33 @@ impl<'a> Printer<'a> { )) = &decl.declaration && let Some(decorators) = class_expr.decorators.filter(|dec| !dec.is_empty()) { - let keyword_end = decl.span.start + "export default".len() as u32; - let mut parts: DocBuf = smallvec![d.text("export default")]; + // `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). + // 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()); - parts.push(self.build_class_expression_doc(class_expr)); + // 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); } @@ -372,9 +404,6 @@ impl<'a> Printer<'a> { } }; - // The `export default`→value gap (a line comment indents the value). - let default_keyword = "export default"; - let keyword_end = decl.span.start + default_keyword.len() as u32; let decl_start = match &decl.declaration { internal::ExportDefaultValue::Expression(expr) => expr.span().start, internal::ExportDefaultValue::FunctionDeclaration(func) => func.span.start, @@ -382,6 +411,11 @@ impl<'a> Printer<'a> { internal::ExportDefaultValue::ClassDeclaration(class) => class.span.start, internal::ExportDefaultValue::TSInterfaceDeclaration(iface) => iface.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. + let (keyword_doc, keyword_end) = + self.build_keyword_words_doc(&["export", "default"], decl.span.start, decl_start); // A comment that can't stay inline (a line comment, or a block comment with // the value authored on a later line) forces the value onto its own indented // line, keeping the comment where the author wrote it: own-line stays on its @@ -390,14 +424,14 @@ impl<'a> Printer<'a> { // `as`/`satisfies` cast gap — prettier relocates the comment instead. A // same-line block glued to the value (`export default /* c */ x`) stays inline. if self.comment_forces_following_own_line(keyword_end, decl_start) { - let mut parts: DocBuf = smallvec![d.text("export default")]; + let mut parts: DocBuf = smallvec![keyword_doc]; self.append_keyword_value_line_comments(&mut parts, keyword_end, decl_start, value_doc); return d.concat(&parts); } // No forcing comment (inline block / none): the value stays on the keyword // line via the shared continuation helper. d.concat(&[ - d.text("export default"), + keyword_doc, self.build_keyword_to_name_continuation(keyword_end, decl_start, value_doc), ]) } @@ -712,24 +746,61 @@ impl<'a> Printer<'a> { let d = self.d(); let mut parts = DocBuf::new(); - // Export prefix if present + // The header keyword, word by word — an optional `export` prefix, `import`, and + // an optional `type` modifier. Every gap between them is a position an author + // can comment in; emitting the words as separate fixed texts scans none of + // them, so a comment in any gap is dropped. Prettier preserves all of these + // except `import`→`type`, which it moves to the binding side. + let mut words: SmallVec<[&'static str; 3]> = SmallVec::new(); if decl.is_export { - parts.push(d.text("export ")); + words.push("export"); } - - // import keyword - parts.push(d.text("import ")); - - // type modifier if present + words.push("import"); if matches!(decl.import_kind, internal::ImportKind::Type) { - parts.push(d.text("type ")); + words.push("type"); + } + parts.push(self.build_keyword_header_doc( + &words, + decl.span.start, + decl.id.span.start, + self.identifier_name_doc(&decl.id), + )); + + // identifier→`=` and `=`→module-reference gaps. Both are preserved in place + // (prettier keeps them there too). Nothing else scans them — a module + // reference is not an expression, so a comment glued to `require` is not + // owned by any node and would be dropped if this gap didn't emit it. + let module_ref_start = match &decl.module_reference { + internal::TSModuleReference::ExternalModuleReference(ext) => ext.span.start, + internal::TSModuleReference::EntityName(entity) => entity.span().start, + }; + match self.find_keyword_in_range(decl.id.span.end, module_ref_start, "=") { + Some(eq_start) => { + parts.push(self.build_keyword_to_name_continuation( + decl.id.span.end, + eq_start, + d.text("="), + )); + parts.push(self.build_keyword_to_name_continuation( + eq_start + 1, + module_ref_start, + d.empty(), + )); + } + None => { + // Same landmine as `build_keyword_words_doc`'s fallback: this arm scans + // no gap, so a comment either side of the `=` is dropped. An + // import-equals always spells its `=` between the name and the module + // reference, so this is unreachable — assert it in debug rather than + // let a future caller degrade silently. + debug_assert!( + false, + "import-equals has no `=` in source[{}..{module_ref_start}]", + decl.id.span.end + ); + parts.push(d.text(" = ")); + } } - - // identifier - parts.push(self.identifier_name_doc(&decl.id)); - - // = sign - parts.push(d.text(" = ")); // module reference match &decl.module_reference { @@ -807,8 +878,15 @@ impl<'a> Printer<'a> { ) -> DocId { let d = self.d(); let mut parts = DocBuf::new(); - parts.push(d.text("export as namespace ")); - parts.push(self.identifier_name_doc(&decl.id)); + // `export as namespace` word by word: each gap between the three is a position + // an author can comment in, and the `namespace`→name gap is one prettier keeps + // too. Emitting the keyword as one text never scans any of them. + parts.push(self.build_keyword_header_doc( + &["export", "as", "namespace"], + decl.span.start, + decl.id.span.start, + self.identifier_name_doc(&decl.id), + )); // Trailing comment between the name and `;` (mirrors `export =` / import-equals): // a same-line block comment stays before `;`, a line comment floats after it. let semicolon_pos = decl.span.end.saturating_sub(1); diff --git a/crates/tsv_ts/src/printer/statements/variable.rs b/crates/tsv_ts/src/printer/statements/variable.rs index bbbbc7b0b..a1226c37d 100644 --- a/crates/tsv_ts/src/printer/statements/variable.rs +++ b/crates/tsv_ts/src/printer/statements/variable.rs @@ -12,7 +12,7 @@ use crate::printer::{ is_single_call_on_member_chain, is_string_literal, is_type_assertion_call, needs_parens, should_inline_logical_expression, }; -use smallvec::smallvec; +use smallvec::{SmallVec, smallvec}; use tsv_lang::comments_to_emit_in_range; use tsv_lang::doc::arena::{DocArena, DocId}; use tsv_lang::doc::{DocBuf, GroupId}; @@ -143,23 +143,23 @@ impl<'a> Printer<'a> { let d = self.d(); let mut prefix: DocBuf = DocBuf::new(); - // Declare modifier + let first_decl_start = decl.declarations[0].span.start; + + // The header keyword, word by word: an optional `declare` modifier plus the + // kind (`await using` is two words). Every gap *between* those words is a + // position an author can comment in, so the words are located rather than + // measured — measuring skips the interior gaps and drops what's in them. + let mut words: SmallVec<[&'static str; 3]> = SmallVec::new(); if decl.declare { - prefix.push(d.text("declare ")); + words.push("declare"); } - - // Keyword (const, let, var) - prefix.push(d.text(decl.kind.as_str())); - + words.extend_from_slice(decl.kind.words()); // The keyword→first-declarator gap. A *line* comment here indents the whole // continuation one level (uniform declaration-header rule); block/no-comment // cases stay inline. The leading space is supplied by the gap helper below. - let keyword_end = if decl.declare { - decl.span.start + "declare ".len() as u32 + decl.kind.as_str().len() as u32 - } else { - decl.span.start + decl.kind.as_str().len() as u32 - }; - let first_decl_start = decl.declarations[0].span.start; + let (keyword_doc, keyword_end) = + self.build_keyword_words_doc(&words, decl.span.start, first_decl_start); + prefix.push(keyword_doc); // Everything after the gap is collected into `parts` (the continuation). let mut parts = DocBuf::new(); diff --git a/docs/conformance_prettier.md b/docs/conformance_prettier.md index 11aa5aa81..945e0c696 100644 --- a/docs/conformance_prettier.md +++ b/docs/conformance_prettier.md @@ -757,6 +757,18 @@ Prettier moves comments between syntactic boundaries into adjacent blocks, paren - Namespace import header comments → Binding side of `type` — [namespace_keyword_comment](../tests/fixtures/typescript/modules/imports/namespace_keyword_comment_prettier_divergence/) - Export-all header comments → After `from` — [all_keyword_comment](../tests/fixtures/typescript/modules/exports/all_keyword_comment_prettier_divergence/) - Export-all namespace `*` to `as` → After `as` (before binding) — [all_namespace_keyword_comment](../tests/fixtures/typescript/modules/exports/all_namespace_keyword_comment_prettier_divergence/) +- `export`→`default` (block) → After `default`, leading the value (`export default /* c */ 1`); tsv keeps it after `export`. The interior gap of a multi-word keyword — see [Comments inside a multi-word keyword](#comments-inside-a-multi-word-keyword) — [default_keyword_comment](../tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/) +- `export`→`default` (line) → After `default`, with the value pulled back flush (`export default // c⏎1;`); tsv keeps it after `export` and indents the `default 1;` continuation one level (the uniform forced-continuation indent) — [default_keyword_line_comment](../tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/) +- `export`→`default`, decorated class expression → After `default` (`export default /* c */⏎@dec⏎class {}`); tsv keeps it after `export`. The decorated-class path of the same gap — [default_keyword_decorator_comment](../tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/) +- `export default` keyword gaps, decorators-*first* class → Both the `export`→`default` and `default`→`class` gaps relocated onto the decorator line and stacked (`@dec /* c1 */ /* c2 */⏎export default class A {}`), collapsing two authored positions into one; tsv keeps each where it was written. Decorators before `export` keep the class a *declaration*, so it is a third path of the same keyword — [decorator_first_default_keyword_comment](../tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/) +- `declare`→kind keyword (`const`/`let`/`var`) → After the kind keyword, leading the binding (`declare const /* c */ a`); tsv keeps it after `declare` — [declare_keyword_comment](../tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/) +- `await`→`using` → After `using`, leading the binding (`await using /* c */ a`); tsv keeps it after `await`. Comments on *both* sides of `using` collapse to one position under Prettier (`await using /* c2 */ /* c3 */ b`), losing the before/after distinction; tsv keeps each on its authored side — [await_keyword_comment](../tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/) +- `export`→`=` (export assignment) → After the `=`, leading the value (`export = /* c */ value`); tsv keeps it after `export`, matching the `export`→`const`/`function` gap both formatters preserve. The operand-side gap agrees in both — [export_equals_keyword_comment](../tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/) +- `export as namespace` keyword-interior gaps (`export`→`as`, `as`→`namespace`) → Both relocated past the whole keyword and stacked before the name (`export as namespace /* c1 */ /* c2 */ Foo`), collapsing two distinct positions into one; tsv keeps each on its authored side. The `namespace`→name gap agrees in both (regular [export_as_namespace_name_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/)) — [export_as_namespace_keyword_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/) +- Meta-property dot gaps (line) → Out of the meta property entirely: past the `;` for `new.target` (`return new.target; // c`), or after `.meta` for `import.meta`; tsv keeps it in the gap and continues `.property` one level down. Block comments in the same gaps agree in both (regular [dot_gap_comments](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/)) — [dot_gap_line_comment](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/) +- `export as namespace` keyword gaps (line) → All relocated past the whole keyword and stacked flush before the name (`export as namespace // c1⏎// c2⏎// c3⏎Foo`); tsv keeps each on its authored side, continuing the header at **one** indent level rather than a staircase — [export_as_namespace_line_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/) +- Import-equals `import`→`type` → Binding side of `type` (`import type /* c */ C = require('./c')`, the same rule as a default import); tsv preserves. The **only** diverging gap of that header — the other four agree in both and are pinned by the regular [equals_header_comment](../tests/fixtures/typescript/modules/imports/equals_header_comment/) — [equals_type_keyword_comment](../tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/) +- `export default`→first decorator, comment **glued** to `@dec` → Relocated back past `default` (`export default /* c */`), then one blank line added per pass, saturating at two (Prettier non-idempotent; `output_prettier.*` is its first pass, `audit_signature.txt` pins the chain); tsv keeps it glued to `@dec`, where the parser bound it. The complementary own-line authoring is *not* glued, so it is emitted by the keyword→decorator gap instead — both authorings preserved — [default_decorator_lead_comment](../tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/) - Import binding to `from` (line) → After `;` — [from_comment](../tests/fixtures/typescript/modules/imports/from_comment_prettier_divergence/) - Import specifiers to `from` → Into the specifier braces — [from_comment](../tests/fixtures/typescript/modules/imports/from_comment_prettier_divergence/) - Export specifiers to `from` → Into the specifier braces — [from_comment](../tests/fixtures/typescript/modules/exports/from_comment_prettier_divergence/) @@ -958,6 +970,84 @@ tsv's rule is therefore about the **comment**, not the node: a comment glued to **Ownership makes a comment invisible to emit-decisions, NOT to layout.** This is the rule for anyone extending the mechanism. An owned comment is skipped by the *to-emit* lookups (`comments_to_emit_in_range`) — the owning node prints it — so any gate deciding *who emits* must use them (`has_comments_to_emit_in_range`). But the comment is still on the page, still occupies width, and still means to prettier's rules exactly what an ordinary comment means. So any gate deciding *layout* must use the *on-page* lookups (`has_comments_on_page_in_range`), which count it. Get this backwards and the comment silently disappears from a layout decision: a unary operand loses the parens that keep the comment bound to the operand rather than the operator (`!(/* @__PURE__ */ f())` → `!/* @__PURE__ */ f()`), and a last argument loses the leading comment that refuses the expand-last hug. Both were real bugs. The unary wrap is decided **positionally** — the comment sits in the operator→operand gap, so `build_unary_doc` sees it there and adds the parens itself; `needs_parens` deliberately does *not* (it would double-wrap), and needs no left-spine walk, which is why an instantiation-expression operand (`!(/** @type {A} */ (x))`) gets the wrap for free. +#### Comments inside a multi-word keyword + +A comment can sit **inside** a keyword that spans two or more words — `await /* c */ using`, +`export /* c */ default`, `declare /* c */ const`. tsv preserves it there; Prettier relocates it +past the keyword's last word, onto the value (`await using /* c */ y`). + +Preserving is the [Comment Position Philosophy](#comment-position-philosophy) default, and three +things confirm it rather than merely permit it: + +1. **The carve-out doesn't reach.** tsv trails a comment past a *pure separator* — a list + element's comma, which is structure. A keyword's words are not separators; `using`, + `default`, and the kind keyword each carry meaning, so the gap before them is a position an + author can mean. +2. **Prettier's own sibling gap preserves.** `export /* c */ const x = 1` and + `export /* c */ function fn() {}` keep the comment after `export` in *both* formatters — only + `export /* c */ default` relocates. Preserving makes the whole `export`→X family read one way; + matching Prettier would split it on nothing but the follower. +3. **Relocation collapses a distinction.** With comments on both sides of the second word, + Prettier lands them in one place (`await /* c2 */ using /* c3 */ b` → + `await using /* c2 */ /* c3 */ b`), so before-`using` and after-`using` become + indistinguishable. Text survives; the association does not. + +`for await /* c */ (…)` — the gap *after* the same class of keyword — already preserves for the +same reason ([for_await_keyword_comment](../tests/fixtures/typescript/statements/for/for_await_keyword_comment_prettier_divergence/)), +so preserving the interior gap makes the treatment uniform. + +A *line* comment in such a gap takes the [uniform forced-continuation +indent](#uniform-forced-continuation-indent) (`export // c⏎\tdefault 1;`); Prettier pulls the +value back flush. Both the authored and the relocated positions are stable under tsv. + +"Uniform" is meant literally, and a multi-gap keyword is where it has to be said: a header whose +gaps break **more than once** continues at **one** level, not one per gap +([export_as_namespace_line_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/)). +Each gap is emitted without its own indent and the whole tail is wrapped once. Indenting per gap +compounds, and the staircase is not merely deep — the keyword→value gap is emitted at the header's +own level, so the keyword's last word would sit *below* the value following it. Only a three-word +keyword (`export as namespace`, the import-equals header) can show the difference. + +Not every multi-word keyword admits an interior comment: `declare // c⏎const x` ASI-splits into +two statements, and `await /* c⏎ */ using` demotes (the comment *is* a `LineTerminator`, and +`await [no LT] using` requires none) — see the exclusions under [Declaration- and module-header +line-comment continuation indent](#comment-relocation). + +The same rule covers a keyword whose second word is a punctuator (`export /* c */ = value`) and +multi-gap headers (`export /* c1 */ as /* c2 */ namespace Foo`, the import-equals header). + +It reaches past keywords, too: a **meta property** is the same shape — `new` `.` `target` — and both +gaps around its dot are positions an author can mean (`new /* c */.target`, `new./* c */ target`). +tsv keeps each on its authored side, which is what prettier does for a block comment; a *line* +comment there is a divergence +([dot_gap_line_comment](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/)). +That case is worth knowing for a different reason: the detector for this class — a `d.text` literal +with an *interior* space — **cannot see it**, because the joining literal is `"."`. The class is +"a header that concatenates fixed pieces without scanning the gaps between them"; the interior-space +grep is a proxy for it, not a definition of it, so a punctuator-joined header needs finding by hand. + +**Preserving is not the same as diverging, and most of these gaps are neither.** Prettier's +treatment is not uniform: it relocates the `export as namespace` keyword-interior gaps, but +**preserves** the `namespace`→name gap and four of the five import-equals gaps. Where Prettier +preserves, tsv's old behavior was not a divergence at all — it **dropped** the comment, which is +content loss and strictly worse. Those gaps are pinned by *regular* fixtures +([export_as_namespace_name_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/), +[equals_header_comment](../tests/fixtures/typescript/modules/imports/equals_header_comment/)), not +divergence ones; a divergence fixture states an opinion, and there is no opinion to state where the +two formatters already agree. + +Catalogued in [§Comment relocation](#comment-relocation): +[default_keyword_comment](../tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/), +[default_keyword_line_comment](../tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/), +[default_keyword_decorator_comment](../tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/), +[decorator_first_default_keyword_comment](../tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/), +[declare_keyword_comment](../tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/), +[await_keyword_comment](../tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/), +[export_equals_keyword_comment](../tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/), +[export_as_namespace_keyword_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/), +[export_as_namespace_line_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/), +[equals_type_keyword_comment](../tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/). + #### JSDoc / paren semantics - JSDoc type cast parens (standalone TS) — [jsdoc_type_cast_ts](../tests/fixtures/typescript/syntax/comments/jsdoc_type_cast_ts_prettier_divergence/) diff --git a/docs/conformance_svelte.md b/docs/conformance_svelte.md index 84875729a..8af84c859 100644 --- a/docs/conformance_svelte.md +++ b/docs/conformance_svelte.md @@ -196,7 +196,7 @@ format everything well-formed. So a "correction" below is tsv matching **tsc/spe Svelte ❌ / Prettier ✅ / tsv ✅ in every case below: - `using` declarations (Explicit Resource Management — a finished/Stage 4 proposal, not ES2024; see [checklist_typescript.md](./checklist_typescript.md#explicit-resource-management)) — [basic](../tests/fixtures/typescript/typescript_specific/using/basic_svelte_divergence/) -- `await using` declarations — [await](../tests/fixtures/typescript/typescript_specific/using/await_svelte_divergence/) +- `await using` declarations — [await](../tests/fixtures/typescript/typescript_specific/using/await_svelte_divergence/); with a comment inside the keyword (`await /* c */ using`), which tsv preserves where prettier relocates it past `using` — [await_keyword_comment](../tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/) - `const` type params in classes — [const_type_param_class](../tests/fixtures/typescript/typescript_specific/generics/const_type_param_class_svelte_divergence/) - `const` type params in interfaces, incl. `const` before variance (``) — [const_type_param_interface](../tests/fixtures/typescript/typescript_specific/generics/const_type_param_interface_svelte_divergence/). acorn rejects the `const` token (tsc defers it to the TS1277 checker error); the mis-ordered `` is instead a *grammar* error tsv rejects like acorn, pinned by the regular fixture [type_param_modifier_order](../tests/fixtures/typescript/typescript_specific/generics/type_param_modifier_order/) - Import type options — [dynamic_attributes](../tests/fixtures/typescript/modules/imports/dynamic_attributes_svelte_divergence/) diff --git a/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..06a76046a --- /dev/null +++ b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,24 @@ +# Divergence: `declare`→kind keyword-interior comment (preserve) + +A block comment between `declare` and the declaration's kind keyword +(`declare /* c */ const a: number;`), for every kind. tsv keeps it after `declare`; prettier +**relocates** it past the kind keyword onto the binding. + +```ts +// tsv (preserve) // prettier (relocate past the keyword) +declare /* c */ const a: number; declare const /* c */ a: number; +``` + +**Why tsv preserves:** `declare` is an ambient modifier and the kind keyword is the declaration — +two separable things, so the gap between them is a position an author can mean (a comment there +plausibly annotates the ambient-ness). A keyword is not a *pure separator*, the one sanctioned +reason to trail. + +Only the **block** form exists: `declare // c⏎const x` ASI-splits into two statements (`declare;` +then `const x: number;`) in both formatters, so there is no line-comment gap to preserve — see +[contextual_keywords/declaration_keyword_own_line](../../../syntax/contextual_keywords/declaration_keyword_own_line/). +The `declare`→name gap of `function`/`class`/`enum` already preserves in both formatters +([declarations/function/declare_keyword_comment](../../function/declare_keyword_comment/)). + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..5190f2b3c --- /dev/null +++ b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,766 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 429, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `declare` and the kind keyword — stays where the author", + "start": 20, + "end": 100, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " wrote it, for every declaration kind", + "start": 102, + "end": 141, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 40 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 151, + "end": 159, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 4, + "column": 17 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 186, + "end": 194, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 17 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 219, + "end": 227, + "loc": { + "start": { + "line": 6, + "column": 9 + }, + "end": { + "line": 6, + "column": 17 + } + } + }, + { + "type": "Line", + "value": " The same gap under an `export` prefix", + "start": 246, + "end": 286, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 41 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 303, + "end": 311, + "loc": { + "start": { + "line": 9, + "column": 16 + }, + "end": { + "line": 9, + "column": 24 + } + } + }, + { + "type": "Line", + "value": " The same gap with multiple declarators", + "start": 331, + "end": 372, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 42 + } + } + }, + { + "type": "Block", + "value": " c5 ", + "start": 382, + "end": 390, + "loc": { + "start": { + "line": 12, + "column": 9 + }, + "end": { + "line": 12, + "column": 17 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 428, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 419, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 143, + "end": 176, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 34 + } + }, + "declare": true, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 166, + "end": 175, + "loc": { + "start": { + "line": 4, + "column": 24 + }, + "end": { + "line": 4, + "column": 33 + } + }, + "id": { + "type": "Identifier", + "start": 166, + "end": 175, + "loc": { + "start": { + "line": 4, + "column": 24 + }, + "end": { + "line": 4, + "column": 33 + } + }, + "name": "a", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 167, + "end": 175, + "loc": { + "start": { + "line": 4, + "column": 25 + }, + "end": { + "line": 4, + "column": 33 + } + }, + "typeAnnotation": { + "type": "TSNumberKeyword", + "start": 169, + "end": 175, + "loc": { + "start": { + "line": 4, + "column": 27 + }, + "end": { + "line": 4, + "column": 33 + } + } + } + } + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 151, + "end": 159 + } + ] + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `declare` and the kind keyword — stays where the author", + "start": 20, + "end": 100 + }, + { + "type": "Line", + "value": " wrote it, for every declaration kind", + "start": 102, + "end": 141 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 178, + "end": 209, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 32 + } + }, + "declare": true, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 199, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 22 + }, + "end": { + "line": 5, + "column": 31 + } + }, + "id": { + "type": "Identifier", + "start": 199, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 22 + }, + "end": { + "line": 5, + "column": 31 + } + }, + "name": "b", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 200, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 23 + }, + "end": { + "line": 5, + "column": 31 + } + }, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 202, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 25 + }, + "end": { + "line": 5, + "column": 31 + } + } + } + } + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 186, + "end": 194 + } + ] + } + ], + "kind": "let" + }, + { + "type": "VariableDeclaration", + "start": 211, + "end": 243, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 33 + } + }, + "declare": true, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 232, + "end": 242, + "loc": { + "start": { + "line": 6, + "column": 22 + }, + "end": { + "line": 6, + "column": 32 + } + }, + "id": { + "type": "Identifier", + "start": 232, + "end": 242, + "loc": { + "start": { + "line": 6, + "column": 22 + }, + "end": { + "line": 6, + "column": 32 + } + }, + "name": "c", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 233, + "end": 242, + "loc": { + "start": { + "line": 6, + "column": 23 + }, + "end": { + "line": 6, + "column": 32 + } + }, + "typeAnnotation": { + "type": "TSBooleanKeyword", + "start": 235, + "end": 242, + "loc": { + "start": { + "line": 6, + "column": 25 + }, + "end": { + "line": 6, + "column": 32 + } + } + } + } + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 219, + "end": 227 + } + ] + } + ], + "kind": "var" + }, + { + "type": "ExportNamedDeclaration", + "start": 288, + "end": 328, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 41 + } + }, + "exportKind": "type", + "declaration": { + "type": "VariableDeclaration", + "start": 295, + "end": 328, + "loc": { + "start": { + "line": 9, + "column": 8 + }, + "end": { + "line": 9, + "column": 41 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 318, + "end": 327, + "loc": { + "start": { + "line": 9, + "column": 31 + }, + "end": { + "line": 9, + "column": 40 + } + }, + "id": { + "type": "Identifier", + "start": 318, + "end": 327, + "loc": { + "start": { + "line": 9, + "column": 31 + }, + "end": { + "line": 9, + "column": 40 + } + }, + "name": "d", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 319, + "end": 327, + "loc": { + "start": { + "line": 9, + "column": 32 + }, + "end": { + "line": 9, + "column": 40 + } + }, + "typeAnnotation": { + "type": "TSNumberKeyword", + "start": 321, + "end": 327, + "loc": { + "start": { + "line": 9, + "column": 34 + }, + "end": { + "line": 9, + "column": 40 + } + } + } + } + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 303, + "end": 311 + } + ] + } + ], + "kind": "const", + "declare": true + }, + "specifiers": [], + "source": null, + "leadingComments": [ + { + "type": "Line", + "value": " The same gap under an `export` prefix", + "start": 246, + "end": 286 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 374, + "end": 418, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 45 + } + }, + "declare": true, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 397, + "end": 406, + "loc": { + "start": { + "line": 12, + "column": 24 + }, + "end": { + "line": 12, + "column": 33 + } + }, + "id": { + "type": "Identifier", + "start": 397, + "end": 406, + "loc": { + "start": { + "line": 12, + "column": 24 + }, + "end": { + "line": 12, + "column": 33 + } + }, + "name": "e", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 398, + "end": 406, + "loc": { + "start": { + "line": 12, + "column": 25 + }, + "end": { + "line": 12, + "column": 33 + } + }, + "typeAnnotation": { + "type": "TSNumberKeyword", + "start": 400, + "end": 406, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 12, + "column": 33 + } + } + } + } + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c5 ", + "start": 382, + "end": 390 + } + ] + }, + { + "type": "VariableDeclarator", + "start": 408, + "end": 417, + "loc": { + "start": { + "line": 12, + "column": 35 + }, + "end": { + "line": 12, + "column": 44 + } + }, + "id": { + "type": "Identifier", + "start": 408, + "end": 417, + "loc": { + "start": { + "line": 12, + "column": 35 + }, + "end": { + "line": 12, + "column": 44 + } + }, + "name": "f", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 409, + "end": 417, + "loc": { + "start": { + "line": 12, + "column": 36 + }, + "end": { + "line": 12, + "column": 44 + } + }, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 411, + "end": 417, + "loc": { + "start": { + "line": 12, + "column": 38 + }, + "end": { + "line": 12, + "column": 44 + } + } + } + } + }, + "init": null + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " The same gap with multiple declarators", + "start": 331, + "end": 372 + } + ] + } + ], + "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/declarations/variable/declare_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..8ad7bf4fb --- /dev/null +++ b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,13 @@ + diff --git a/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..fb30bd0bf --- /dev/null +++ b/tests/fixtures/typescript/declarations/variable/declare_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,13 @@ + diff --git a/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/expected.json b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/expected.json new file mode 100644 index 000000000..7b9b1ffd3 --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/expected.json @@ -0,0 +1,721 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 359, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment before the `.` — stays there, hugging the dot", + "start": 20, + "end": 82, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 63 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 112, + "end": 120, + "loc": { + "start": { + "line": 4, + "column": 13 + }, + "end": { + "line": 4, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 134, + "end": 185, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 52 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 215, + "end": 223, + "loc": { + "start": { + "line": 9, + "column": 13 + }, + "end": { + "line": 9, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " The same two gaps on `import.meta`", + "start": 237, + "end": 274, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 38 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 293, + "end": 301, + "loc": { + "start": { + "line": 13, + "column": 18 + }, + "end": { + "line": 13, + "column": 26 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 330, + "end": 338, + "loc": { + "start": { + "line": 14, + "column": 18 + }, + "end": { + "line": 14, + "column": 26 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 358, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 349, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 15, + "column": 9 + } + }, + "body": [ + { + "type": "FunctionDeclaration", + "start": 84, + "end": 131, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 93, + "end": 94, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 11 + } + }, + "name": "f" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 97, + "end": 131, + "loc": { + "start": { + "line": 3, + "column": 14 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 101, + "end": 128, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 29 + } + }, + "argument": { + "type": "MetaProperty", + "start": 108, + "end": 127, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 4, + "column": 28 + } + }, + "meta": { + "type": "Identifier", + "start": 108, + "end": 111, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 4, + "column": 12 + } + }, + "name": "new", + "trailingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 112, + "end": 120 + } + ] + }, + "property": { + "type": "Identifier", + "start": 121, + "end": 127, + "loc": { + "start": { + "line": 4, + "column": 22 + }, + "end": { + "line": 4, + "column": 28 + } + }, + "name": "target" + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment before the `.` — stays there, hugging the dot", + "start": 20, + "end": 82 + } + ] + }, + { + "type": "FunctionDeclaration", + "start": 187, + "end": 234, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 196, + "end": 197, + "loc": { + "start": { + "line": 8, + "column": 10 + }, + "end": { + "line": 8, + "column": 11 + } + }, + "name": "g" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 200, + "end": 234, + "loc": { + "start": { + "line": 8, + "column": 14 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 204, + "end": 231, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 29 + } + }, + "argument": { + "type": "MetaProperty", + "start": 211, + "end": 230, + "loc": { + "start": { + "line": 9, + "column": 9 + }, + "end": { + "line": 9, + "column": 28 + } + }, + "meta": { + "type": "Identifier", + "start": 211, + "end": 214, + "loc": { + "start": { + "line": 9, + "column": 9 + }, + "end": { + "line": 9, + "column": 12 + } + }, + "name": "new" + }, + "property": { + "type": "Identifier", + "start": 224, + "end": 230, + "loc": { + "start": { + "line": 9, + "column": 22 + }, + "end": { + "line": 9, + "column": 28 + } + }, + "name": "target", + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 215, + "end": 223 + } + ] + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 134, + "end": 185 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 276, + "end": 311, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 13, + "column": 36 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 282, + "end": 310, + "loc": { + "start": { + "line": 13, + "column": 7 + }, + "end": { + "line": 13, + "column": 35 + } + }, + "id": { + "type": "Identifier", + "start": 282, + "end": 283, + "loc": { + "start": { + "line": 13, + "column": 7 + }, + "end": { + "line": 13, + "column": 8 + } + }, + "name": "a" + }, + "init": { + "type": "MemberExpression", + "start": 286, + "end": 310, + "loc": { + "start": { + "line": 13, + "column": 11 + }, + "end": { + "line": 13, + "column": 35 + } + }, + "object": { + "type": "MetaProperty", + "start": 286, + "end": 306, + "loc": { + "start": { + "line": 13, + "column": 11 + }, + "end": { + "line": 13, + "column": 31 + } + }, + "meta": { + "type": "Identifier", + "start": 286, + "end": 292, + "loc": { + "start": { + "line": 13, + "column": 11 + }, + "end": { + "line": 13, + "column": 17 + } + }, + "name": "import", + "trailingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 293, + "end": 301 + } + ] + }, + "property": { + "type": "Identifier", + "start": 302, + "end": 306, + "loc": { + "start": { + "line": 13, + "column": 27 + }, + "end": { + "line": 13, + "column": 31 + } + }, + "name": "meta" + } + }, + "property": { + "type": "Identifier", + "start": 307, + "end": 310, + "loc": { + "start": { + "line": 13, + "column": 32 + }, + "end": { + "line": 13, + "column": 35 + } + }, + "name": "url" + }, + "computed": false, + "optional": false + } + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " The same two gaps on `import.meta`", + "start": 237, + "end": 274 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 313, + "end": 348, + "loc": { + "start": { + "line": 14, + "column": 1 + }, + "end": { + "line": 14, + "column": 36 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 319, + "end": 347, + "loc": { + "start": { + "line": 14, + "column": 7 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "id": { + "type": "Identifier", + "start": 319, + "end": 320, + "loc": { + "start": { + "line": 14, + "column": 7 + }, + "end": { + "line": 14, + "column": 8 + } + }, + "name": "b" + }, + "init": { + "type": "MemberExpression", + "start": 323, + "end": 347, + "loc": { + "start": { + "line": 14, + "column": 11 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "object": { + "type": "MetaProperty", + "start": 323, + "end": 343, + "loc": { + "start": { + "line": 14, + "column": 11 + }, + "end": { + "line": 14, + "column": 31 + } + }, + "meta": { + "type": "Identifier", + "start": 323, + "end": 329, + "loc": { + "start": { + "line": 14, + "column": 11 + }, + "end": { + "line": 14, + "column": 17 + } + }, + "name": "import" + }, + "property": { + "type": "Identifier", + "start": 339, + "end": 343, + "loc": { + "start": { + "line": 14, + "column": 27 + }, + "end": { + "line": 14, + "column": 31 + } + }, + "name": "meta", + "leadingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 330, + "end": 338 + } + ] + } + }, + "property": { + "type": "Identifier", + "start": 344, + "end": 347, + "loc": { + "start": { + "line": 14, + "column": 32 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "name": "url" + }, + "computed": false, + "optional": false + } + } + ], + "kind": "const" + } + ], + "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/expressions/misc/meta_property/dot_gap_comments/input.svelte b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/input.svelte new file mode 100644 index 000000000..a0a8509ce --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/input.svelte @@ -0,0 +1,15 @@ + diff --git a/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/README.md b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/README.md new file mode 100644 index 000000000..b257e02ca --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/README.md @@ -0,0 +1,27 @@ +# Divergence: meta-property dot-gap line comments (preserve) + +A *line* comment in either gap around a meta property's `.` (`new // c⏎.target`, +`import // c⏎.meta`). tsv keeps it where the author wrote it and continues `.property` one level +down; prettier **relocates** it — for `new.target`, all the way past the `;`. + +```ts +// tsv (preserve) // prettier (relocate) +return new // c1 return new.target; // c1 + .target; +``` + +The comment moves from *inside* the meta property to *after the whole statement*, so it no longer +reads as being about the `.target` at all. On `import.meta` it lands differently again +(`import.meta // c2⏎.url;`) — the same authored position, two different destinations, decided by +what follows. + +Block comments in these gaps are **not** a divergence: prettier keeps each on its authored side of +the dot, and so does tsv — pinned by the regular sibling [dot_gap_comments](../dot_gap_comments/). + +**Why this fixture exists:** tsv **dropped** every comment in both gaps. `build_meta_property_doc` +concatenated `meta` + `"."` + `property` and scanned neither gap — the same class as a comment +inside a multi-word keyword ([§Comments inside a multi-word keyword](../../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword)), +and the case that shows why that class's detector (a `d.text` literal with an *interior* space) is +only a proxy: a header joined by a punctuator has no space to find. + +See [conformance_prettier.md §Comment relocation](../../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..c6345c608 --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/expected.json @@ -0,0 +1,568 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 319, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Line comment before the `.` — stays there, and `.target` continues one level down", + "start": 20, + "end": 104, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 85 + } + } + }, + { + "type": "Line", + "value": " c1", + "start": 134, + "end": 139, + "loc": { + "start": { + "line": 4, + "column": 13 + }, + "end": { + "line": 4, + "column": 18 + } + } + }, + { + "type": "Line", + "value": " The same gap on `import.meta`", + "start": 157, + "end": 189, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 33 + } + } + }, + { + "type": "Line", + "value": " c2", + "start": 208, + "end": 213, + "loc": { + "start": { + "line": 9, + "column": 18 + }, + "end": { + "line": 9, + "column": 23 + } + } + }, + { + "type": "Line", + "value": " Line comment after the `.`", + "start": 229, + "end": 258, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 30 + } + } + }, + { + "type": "Line", + "value": " c3", + "start": 289, + "end": 294, + "loc": { + "start": { + "line": 14, + "column": 14 + }, + "end": { + "line": 14, + "column": 19 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 318, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 309, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 17, + "column": 9 + } + }, + "body": [ + { + "type": "FunctionDeclaration", + "start": 106, + "end": 154, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 6, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 115, + "end": 116, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 11 + } + }, + "name": "f" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 119, + "end": 154, + "loc": { + "start": { + "line": 3, + "column": 14 + }, + "end": { + "line": 6, + "column": 2 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 123, + "end": 151, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 5, + "column": 11 + } + }, + "argument": { + "type": "MetaProperty", + "start": 130, + "end": 150, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 5, + "column": 10 + } + }, + "meta": { + "type": "Identifier", + "start": 130, + "end": 133, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 4, + "column": 12 + } + }, + "name": "new", + "trailingComments": [ + { + "type": "Line", + "value": " c1", + "start": 134, + "end": 139 + } + ] + }, + "property": { + "type": "Identifier", + "start": 144, + "end": 150, + "loc": { + "start": { + "line": 5, + "column": 4 + }, + "end": { + "line": 5, + "column": 10 + } + }, + "name": "target" + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Line comment before the `.` — stays there, and `.target` continues one level down", + "start": 20, + "end": 104 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 191, + "end": 226, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 10, + "column": 12 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 197, + "end": 225, + "loc": { + "start": { + "line": 9, + "column": 7 + }, + "end": { + "line": 10, + "column": 11 + } + }, + "id": { + "type": "Identifier", + "start": 197, + "end": 198, + "loc": { + "start": { + "line": 9, + "column": 7 + }, + "end": { + "line": 9, + "column": 8 + } + }, + "name": "a" + }, + "init": { + "type": "MemberExpression", + "start": 201, + "end": 225, + "loc": { + "start": { + "line": 9, + "column": 11 + }, + "end": { + "line": 10, + "column": 11 + } + }, + "object": { + "type": "MetaProperty", + "start": 201, + "end": 221, + "loc": { + "start": { + "line": 9, + "column": 11 + }, + "end": { + "line": 10, + "column": 7 + } + }, + "meta": { + "type": "Identifier", + "start": 201, + "end": 207, + "loc": { + "start": { + "line": 9, + "column": 11 + }, + "end": { + "line": 9, + "column": 17 + } + }, + "name": "import", + "trailingComments": [ + { + "type": "Line", + "value": " c2", + "start": 208, + "end": 213 + } + ] + }, + "property": { + "type": "Identifier", + "start": 217, + "end": 221, + "loc": { + "start": { + "line": 10, + "column": 3 + }, + "end": { + "line": 10, + "column": 7 + } + }, + "name": "meta" + } + }, + "property": { + "type": "Identifier", + "start": 222, + "end": 225, + "loc": { + "start": { + "line": 10, + "column": 8 + }, + "end": { + "line": 10, + "column": 11 + } + }, + "name": "url" + }, + "computed": false, + "optional": false + } + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " The same gap on `import.meta`", + "start": 157, + "end": 189 + } + ] + }, + { + "type": "FunctionDeclaration", + "start": 260, + "end": 308, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 16, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 269, + "end": 270, + "loc": { + "start": { + "line": 13, + "column": 10 + }, + "end": { + "line": 13, + "column": 11 + } + }, + "name": "g" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 273, + "end": 308, + "loc": { + "start": { + "line": 13, + "column": 14 + }, + "end": { + "line": 16, + "column": 2 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 277, + "end": 305, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 15, + "column": 10 + } + }, + "argument": { + "type": "MetaProperty", + "start": 284, + "end": 304, + "loc": { + "start": { + "line": 14, + "column": 9 + }, + "end": { + "line": 15, + "column": 9 + } + }, + "meta": { + "type": "Identifier", + "start": 284, + "end": 287, + "loc": { + "start": { + "line": 14, + "column": 9 + }, + "end": { + "line": 14, + "column": 12 + } + }, + "name": "new" + }, + "property": { + "type": "Identifier", + "start": 298, + "end": 304, + "loc": { + "start": { + "line": 15, + "column": 3 + }, + "end": { + "line": 15, + "column": 9 + } + }, + "name": "target", + "leadingComments": [ + { + "type": "Line", + "value": " c3", + "start": 289, + "end": 294 + } + ] + } + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Line comment after the `.`", + "start": 229, + "end": 258 + } + ] + } + ], + "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/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..8e123fddc --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/input.svelte @@ -0,0 +1,17 @@ + diff --git a/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..ec4e27336 --- /dev/null +++ b/tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..e497aee21 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,30 @@ +# Divergence: `export default` keyword comments, decorators-first class (preserve) + +Block comments inside the `export default` keyword when the **decorators precede `export`** +(`@dec⏎export /* c1 */ default /* c2 */ class A {}`). tsv keeps each where the author wrote it; +prettier **relocates both** up onto the decorator line. + +```ts +// tsv (preserve) // prettier (relocate onto the decorator) +@dec @dec /* c1 */ /* c2 */ +export /* c1 */ default /* c2 */ class A {} export default class A {} +``` + +Decorators before `export` keep the class a *declaration*, so it takes a third printer path — +distinct from [default_keyword_comment](../default_keyword_comment_prettier_divergence/) (the +general path) and from +[default_keyword_decorator_comment](../default_keyword_decorator_comment_prettier_divergence/), +where a decorator *after* `default` makes the class an *expression*. All three share the gap and +the reason to preserve; this fixture pins the declaration path. + +Two gaps, both pinned: `export`→`default` (the keyword interior) and `default`→`class` (the +keyword→value gap). The path emitted the keyword as one fixed text, so it scanned neither and +dropped both. + +Relocation here also **collapses a distinction**: `c1` and `c2` are authored on opposite sides of +`default` and land side by side on the decorator line, so which one led `default` is no longer +recoverable. That is the same association loss catalogued for `await`/`using` and +`export as namespace`. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..21f8b9c7b --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,256 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 243, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comments inside the `export default` keyword when the decorators precede", + "start": 20, + "end": 101, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 82 + } + } + }, + { + "type": "Line", + "value": " `export` — the class stays a *declaration*, so this is a third printer path", + "start": 103, + "end": 181, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 79 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 196, + "end": 204, + "loc": { + "start": { + "line": 5, + "column": 8 + }, + "end": { + "line": 5, + "column": 16 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 213, + "end": 221, + "loc": { + "start": { + "line": 5, + "column": 25 + }, + "end": { + "line": 5, + "column": 33 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 242, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 233, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 189, + "end": 232, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 44 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassDeclaration", + "start": 183, + "end": 232, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 5, + "column": 44 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 183, + "end": 187, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 5 + } + }, + "expression": { + "type": "Identifier", + "start": 184, + "end": 187, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 5 + } + }, + "name": "dec" + } + } + ], + "id": { + "type": "Identifier", + "start": 228, + "end": 229, + "loc": { + "start": { + "line": 5, + "column": 40 + }, + "end": { + "line": 5, + "column": 41 + } + }, + "name": "A", + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 196, + "end": 204 + }, + { + "type": "Block", + "value": " c2 ", + "start": 213, + "end": 221 + } + ] + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 230, + "end": 232, + "loc": { + "start": { + "line": 5, + "column": 42 + }, + "end": { + "line": 5, + "column": 44 + } + }, + "body": [] + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comments inside the `export default` keyword when the decorators precede", + "start": 20, + "end": 101 + }, + { + "type": "Line", + "value": " `export` — the class stays a *declaration*, so this is a third printer path", + "start": 103, + "end": 181 + } + ] + } + ], + "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/decorator_first_default_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..64f8cbf60 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..fa15e59df --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/decorator_first_default_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/README.md new file mode 100644 index 000000000..0bb8b6349 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/README.md @@ -0,0 +1,36 @@ +# Divergence: `export default`→first-decorator glued comment (preserve the binding) + +A block comment **glued to the first decorator** of a decorated default-exported class +(`export default⏎/* c */ @dec⏎class {}`). tsv keeps it glued to `@dec`; prettier **relocates** it back +past `default` and then inserts blank lines below it. + +```ts +// tsv (preserve the binding) // prettier (relocate + blanks, 3 passes) +export default export default /* c */ +/* c */ @dec + (blank) +class {} (blank) + @dec + class {} +``` + +**Why tsv preserves:** the comment is glued to `@dec`, so the parser binds it there +(`Comment::owned_by_node` — every glued block comment is owned). Gluing it back to `@dec` on output is +what keeps that binding visible. The complementary authoring is preserved just as faithfully: a comment +the author left on its own line after the keyword (`export default /* c */⏎@dec`) is *not* glued, so it +is not owned, and the keyword→decorator gap emits it there — see the sibling +[default_keyword_decorator_comment](../default_keyword_decorator_comment_prettier_divergence/), which is +the gap on the *other* side of `default`. Two authorings, two positions, each preserved. + +Prettier is **non-idempotent** here: `output_prettier.svelte` is its first-pass output, and it then adds +one blank line per pass, saturating at two (its max-consecutive-blank-lines rule). `audit_signature.txt` +pins that chain. + +**Why this fixture exists:** tsv **dropped** this comment outright. Ownership takes a comment out of the +positional model — every gap emitter correctly skips it, because the owning node is supposed to print +it — but the decorated-`export default` path builds the class expression directly rather than through +`build_expression_doc`, so nothing claimed it. It is the owned-comment hazard in tsv's own words: *an +owned comment nothing prints is a dropped comment*. The fix claims it on this seam +(`prepend_owned_leading_comment_at`), exactly as the arrow-reassembly paths do. + +See [conformance_prettier.md §Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/audit_signature.txt b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/audit_signature.txt new file mode 100644 index 000000000..cfa716006 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/audit_signature.txt @@ -0,0 +1,28 @@ +# Auto-generated prettier chain signature. Do not edit manually. +# Regenerate: deno task fixtures:update:formatted +# +# Each %%PASS=N%% section is exactly prettier^N(output_prettier.). +# This file exists when prettier is non-idempotent on output_prettier.* — it pins +# the full chain so the audit recognizes the case and the validator catches drift. +# See docs/fixture_overview.md (rule F4) and crates/tsv_debug/src/fixtures/audit_signature.rs. + +%%PASS=2%% + + +%%PASS=3 (fixed point)%% + diff --git a/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..0d3dc6f7b --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/expected.json @@ -0,0 +1,219 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 188, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment glued to the first decorator — stays glued to it, since that is", + "start": 20, + "end": 100, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " the token the author bound it to", + "start": 102, + "end": 137, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 36 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 155, + "end": 162, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 8 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 187, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 178, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 7, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 139, + "end": 177, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassExpression", + "start": 163, + "end": 177, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 163, + "end": 167, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 13 + } + }, + "expression": { + "type": "Identifier", + "start": 164, + "end": 167, + "loc": { + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 13 + } + }, + "name": "dec" + } + } + ], + "id": null, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 175, + "end": 177, + "loc": { + "start": { + "line": 6, + "column": 7 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 155, + "end": 162 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment glued to the first decorator — stays glued to it, since that is", + "start": 20, + "end": 100 + }, + { + "type": "Line", + "value": " the token the author bound it to", + "start": 102, + "end": 137 + } + ] + } + ], + "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_decorator_lead_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..22af2aaeb --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..a8c69d78a --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..7823e9dba --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,25 @@ +# Divergence: `export`→`default` keyword-interior comment (preserve) + +A block comment inside the `export default` keyword (`export /* c */ default 1;`). tsv keeps it +after `export`; prettier **relocates** it past `default` onto the value. + +```ts +// tsv (preserve) // prettier (relocate past the keyword) +export /* c */ default 1; export default /* c */ 1; +``` + +**Why tsv preserves:** the sibling gap decides it. `export /* c */ const x = 1` and +`export /* c */ function fn() {}` keep the comment after `export` in **both** formatters — only +`export /* c */ default` relocates. Preserving makes the whole `export`→X family read one way +instead of splitting it on the follower. A keyword's words are not a *pure separator* (the one +sanctioned reason to trail), so the position is one an author can mean. + +The value-side gaps are separate fixtures: +[default_value_same_line_comment](../default_value_same_line_comment_prettier_divergence/) and +[default_value_own_line_comment](../default_value_own_line_comment_prettier_divergence/). The line +form is [default_keyword_line_comment](../default_keyword_line_comment_prettier_divergence/); the +decorated-class path is +[default_keyword_decorator_comment](../default_keyword_decorator_comment_prettier_divergence/). + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..7dffbfe79 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,148 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 139, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `export` and `default` — stays where the author wrote it", + "start": 20, + "end": 101, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 82 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 110, + "end": 117, + "loc": { + "start": { + "line": 3, + "column": 8 + }, + "end": { + "line": 3, + "column": 15 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 138, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 129, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 103, + "end": 128, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 26 + } + }, + "exportKind": "value", + "declaration": { + "type": "Literal", + "start": 126, + "end": 127, + "loc": { + "start": { + "line": 3, + "column": 24 + }, + "end": { + "line": 3, + "column": 25 + } + }, + "value": 1, + "raw": "1", + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 110, + "end": 117 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `export` and `default` — stays where the author wrote it", + "start": 20, + "end": 101 + } + ] + } + ], + "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_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..3f4ddc4e0 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,4 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..2eafb83e2 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,4 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/README.md new file mode 100644 index 000000000..529eb063f --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/README.md @@ -0,0 +1,20 @@ +# Divergence: `export`→`default` keyword-interior comment, decorated class (preserve) + +A block comment inside the `export default` keyword when the value is a **decorated class +expression** (`export /* c */ default @dec class {}`). tsv keeps it after `export`; prettier +**relocates** it past `default`. + +```ts +// tsv (preserve) // prettier (relocate past the keyword) +export /* c */ default export default /* c */ +@dec @dec +class {} class {} +``` + +The decorated-class path of [default_keyword_comment](../default_keyword_comment_prettier_divergence/) — +a decorator after `default` makes the class an *expression*, so it takes a separate printer path +(the decorators always print on their own line). Same gap, same reason to preserve; this fixture +pins that the path claims the gap too. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/audit_signature.txt b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/audit_signature.txt new file mode 100644 index 000000000..f98ebe988 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/audit_signature.txt @@ -0,0 +1,28 @@ +# Auto-generated prettier chain signature. Do not edit manually. +# Regenerate: deno task fixtures:update:formatted +# +# Each %%PASS=N%% section is exactly prettier^N(output_prettier.). +# This file exists when prettier is non-idempotent on output_prettier.* — it pins +# the full chain so the audit recognizes the case and the validator catches drift. +# See docs/fixture_overview.md (rule F4) and crates/tsv_debug/src/fixtures/audit_signature.rs. + +%%PASS=2%% + + +%%PASS=3 (fixed point)%% + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..e8ff64fb7 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/expected.json @@ -0,0 +1,219 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 229, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `export` and `default` on a decorated class expression —", + "start": 20, + "end": 101, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 82 + } + } + }, + { + "type": "Line", + "value": " the decorators print on their own line, the comment stays after `export`", + "start": 103, + "end": 178, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 76 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 187, + "end": 194, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 15 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 228, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 219, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 7, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 180, + "end": 218, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "exportKind": "value", + "declaration": { + "type": "ClassExpression", + "start": 204, + "end": 218, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "decorators": [ + { + "type": "Decorator", + "start": 204, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "expression": { + "type": "Identifier", + "start": 205, + "end": 208, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "dec" + } + } + ], + "id": null, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 216, + "end": 218, + "loc": { + "start": { + "line": 6, + "column": 7 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 187, + "end": 194 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `export` and `default` on a decorated class expression —", + "start": 20, + "end": 101 + }, + { + "type": "Line", + "value": " the decorators print on their own line, the comment stays after `export`", + "start": 103, + "end": 178 + } + ] + } + ], + "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_keyword_decorator_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..51c11d91a --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..8d17b702d --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_decorator_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/README.md new file mode 100644 index 000000000..5d26ae48a --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/README.md @@ -0,0 +1,19 @@ +# Divergence: `export`→`default` keyword-interior line comment (preserve + continuation indent) + +A line comment inside the `export default` keyword (`export // c⏎default 1;`). tsv keeps it after +`export` and drops `default 1;` to a continuation line **indented one level** (the uniform +forced-continuation indent). Prettier **relocates** the comment past `default` and pulls the value +back flush. + +```ts +// tsv (preserve + continuation indent) // prettier (relocate, value flush) +export // c export default // c + default 1; 1; +``` + +The line form of [default_keyword_comment](../default_keyword_comment_prettier_divergence/) — same +gap, same reason to preserve; only the forced break differs. Consistent with every other +module-header line comment, which tsv indents uniformly. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Uniform Forced-Continuation Indent](../../../../../../docs/conformance_prettier.md#uniform-forced-continuation-indent). diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..9fd663a37 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/expected.json @@ -0,0 +1,170 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 207, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Line comment between `export` and `default` — the comment stays after", + "start": 20, + "end": 92, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 73 + } + } + }, + { + "type": "Line", + "value": " `export` and `default 1;` drops to a continuation line indented one level", + "start": 94, + "end": 170, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 77 + } + } + }, + { + "type": "Line", + "value": " c", + "start": 179, + "end": 183, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 12 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 206, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 197, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "body": [ + { + "type": "ExportDefaultDeclaration", + "start": 172, + "end": 196, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 5, + "column": 12 + } + }, + "exportKind": "value", + "declaration": { + "type": "Literal", + "start": 194, + "end": 195, + "loc": { + "start": { + "line": 5, + "column": 10 + }, + "end": { + "line": 5, + "column": 11 + } + }, + "value": 1, + "raw": "1", + "leadingComments": [ + { + "type": "Line", + "value": " c", + "start": 179, + "end": 183 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Line comment between `export` and `default` — the comment stays after", + "start": 20, + "end": 92 + }, + { + "type": "Line", + "value": " `export` and `default 1;` drops to a continuation line indented one level", + "start": 94, + "end": 170 + } + ] + } + ], + "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_keyword_line_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..63e81372c --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/input.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..3b42f6b3a --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/default_keyword_line_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,6 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..23cd06292 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,26 @@ +# Divergence: `export as namespace` keyword-interior comments (preserve) + +Block comments in the two keyword-interior gaps of a UMD global export header +(`export /* c1 */ as /* c2 */ namespace Foo;`). tsv keeps each where the author wrote it; prettier +**relocates both** past the whole keyword, stacking them before the name. + +```ts +// tsv (preserve each) // prettier (both collapse onto the name) +export /* c1 */ as /* c2 */ namespace Foo; export as namespace /* c1 */ /* c2 */ Foo; +``` + +**Why tsv preserves:** prettier's relocation **collapses two distinct positions into one**. After +it, "before `as`" and "before `namespace`" are indistinguishable — the text survives, the +association does not. A keyword's words are not a *pure separator* (the one sanctioned reason to +trail), so each gap is a position an author can mean. + +The third gap — `export as namespace /* c */ Foo` — is **not** a divergence: the comment is already +in prettier's final position and both formatters keep it there. It lives in the regular sibling +[export_as_namespace_name_comment](../export_as_namespace_name_comment/). (tsv dropped it too, but +that was plain content loss, not a difference of opinion.) + +The same shape as the export-all header ([all_keyword_comment](../all_keyword_comment_prettier_divergence/)), +where prettier likewise relocates every header comment to one position and tsv preserves each. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..e465444a4 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,190 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 212, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comments before `as` and before `namespace` — each stays where the author", + "start": 20, + "end": 102, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 83 + } + } + }, + { + "type": "Line", + "value": " wrote it; prettier relocates both past the keyword", + "start": 104, + "end": 157, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 54 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 166, + "end": 174, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 16 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 178, + "end": 186, + "loc": { + "start": { + "line": 4, + "column": 20 + }, + "end": { + "line": 4, + "column": 28 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 211, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 202, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 9 + } + }, + "body": [ + { + "type": "TSNamespaceExportDeclaration", + "start": 159, + "end": 201, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 43 + } + }, + "id": { + "type": "Identifier", + "start": 197, + "end": 200, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "Foo", + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 166, + "end": 174 + }, + { + "type": "Block", + "value": " c2 ", + "start": 178, + "end": 186 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comments before `as` and before `namespace` — each stays where the author", + "start": 20, + "end": 102 + }, + { + "type": "Line", + "value": " wrote it; prettier relocates both past the keyword", + "start": 104, + "end": 157 + } + ] + } + ], + "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_as_namespace_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..843dfa9ad --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..5f8b63bc2 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/README.md new file mode 100644 index 000000000..6eb022213 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/README.md @@ -0,0 +1,30 @@ +# Divergence: `export as namespace` line comments (preserve, one indent level) + +A *line* comment in every gap of the three-word `export as namespace` keyword. tsv keeps each where +the author wrote it; prettier **relocates** all of them past the whole keyword and stacks them flush +before the name. + +```ts +// tsv (preserve) // prettier (relocate past the keyword, stack flush) +export // c1 export as namespace // c1 + as // c2 // c2 + namespace // c3 // c3 + Foo; Foo; +``` + +The line-comment counterpart of +[export_as_namespace_keyword_comment](../export_as_namespace_keyword_comment_prettier_divergence/) +(the block-comment form), and it pins the **rendering**, which is the reason it exists separately: +a header with several broken gaps continues at **one** indent level, not a staircase. + +Each gap is emitted without its own `indent`, and the whole tail is wrapped once — so N line +comments indent the rest of the header once rather than N times. Nesting per gap would read as +`namespace` deeper than the `Foo` that follows it. One level is also what the +[uniform forced-continuation indent](../../../../../../docs/conformance_prettier.md#uniform-forced-continuation-indent) +already specifies for the single-gap case; this keeps a multi-gap header saying the same thing. + +Only a three-word keyword can show the difference — `export as namespace` and the import-equals +header are the only ones — so a one- or two-gap header renders identically either way. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..b422e551d --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/expected.json @@ -0,0 +1,212 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 250, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " A line comment in every gap of a three-word keyword — each stays where the author", + "start": 20, + "end": 104, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 85 + } + } + }, + { + "type": "Line", + "value": " wrote it, and the whole header continues at ONE indent level (never a staircase)", + "start": 106, + "end": 189, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 84 + } + } + }, + { + "type": "Line", + "value": " c1", + "start": 198, + "end": 203, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 13 + } + } + }, + { + "type": "Line", + "value": " c2", + "start": 209, + "end": 214, + "loc": { + "start": { + "line": 5, + "column": 5 + }, + "end": { + "line": 5, + "column": 10 + } + } + }, + { + "type": "Line", + "value": " c3", + "start": 227, + "end": 232, + "loc": { + "start": { + "line": 6, + "column": 12 + }, + "end": { + "line": 6, + "column": 17 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 249, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 240, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 8, + "column": 9 + } + }, + "body": [ + { + "type": "TSNamespaceExportDeclaration", + "start": 191, + "end": 239, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 7, + "column": 6 + } + }, + "id": { + "type": "Identifier", + "start": 235, + "end": 238, + "loc": { + "start": { + "line": 7, + "column": 2 + }, + "end": { + "line": 7, + "column": 5 + } + }, + "name": "Foo", + "leadingComments": [ + { + "type": "Line", + "value": " c1", + "start": 198, + "end": 203 + }, + { + "type": "Line", + "value": " c2", + "start": 209, + "end": 214 + }, + { + "type": "Line", + "value": " c3", + "start": 227, + "end": 232 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " A line comment in every gap of a three-word keyword — each stays where the author", + "start": 20, + "end": 104 + }, + { + "type": "Line", + "value": " wrote it, and the whole header continues at ONE indent level (never a staircase)", + "start": 106, + "end": 189 + } + ] + } + ], + "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_as_namespace_line_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..9e7b03faa --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/input.svelte @@ -0,0 +1,8 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..163ccfd76 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,8 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/expected.json b/tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/expected.json new file mode 100644 index 000000000..87cce4bf4 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/expected.json @@ -0,0 +1,146 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 145, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `namespace` and the name — both formatters keep it here", + "start": 20, + "end": 100, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 81 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 122, + "end": 129, + "loc": { + "start": { + "line": 3, + "column": 21 + }, + "end": { + "line": 3, + "column": 28 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 144, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 135, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 4, + "column": 9 + } + }, + "body": [ + { + "type": "TSNamespaceExportDeclaration", + "start": 102, + "end": 134, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 33 + } + }, + "id": { + "type": "Identifier", + "start": 130, + "end": 133, + "loc": { + "start": { + "line": 3, + "column": 29 + }, + "end": { + "line": 3, + "column": 32 + } + }, + "name": "Foo", + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 122, + "end": 129 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `namespace` and the name — both formatters keep it here", + "start": 20, + "end": 100 + } + ] + } + ], + "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_as_namespace_name_comment/input.svelte b/tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/input.svelte new file mode 100644 index 000000000..d81d06b00 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/input.svelte @@ -0,0 +1,4 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..81ae4f138 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,22 @@ +# Divergence: `export`→`=` gap comment (preserve) + +A block comment between `export` and the `=` of an export-assignment (`export /* c */ = value;`). +tsv keeps it after `export`; prettier **relocates** it past the `=` onto the value. + +```ts +// tsv (preserve) // prettier (relocate past the `=`) +export /* c */ = value; export = /* c */ value; +``` + +**Why tsv preserves:** the sibling gap decides it. `export /* c */ const x = 1` keeps the comment +after `export` in **both** formatters; `export /* c */ =` is the same gap after the same keyword, +so preserving keeps the `export`→X family reading one way. The `=` here is part of the `export =` +construct, not a list separator — the *pure separator* carve-out (which is what lets tsv trail past +a comma) does not reach it. + +The operand-side gap already matches prettier (`export = /* c */ value` is stable in both) — see +[export_equals_operand_paren_comment](../export_equals_operand_paren_comment/) and the plain +[export_equals](../export_equals/). + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..d78f75454 --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,264 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 156, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `export` and `=` — stays where the author wrote it", + "start": 20, + "end": 95, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 76 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 129, + "end": 136, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 15 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 155, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 146, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 9 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 97, + "end": 120, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 24 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 103, + "end": 119, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 23 + } + }, + "id": { + "type": "Identifier", + "start": 103, + "end": 108, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 12 + } + }, + "name": "value" + }, + "init": { + "type": "ObjectExpression", + "start": 111, + "end": 119, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 23 + } + }, + "properties": [ + { + "type": "Property", + "start": 113, + "end": 117, + "loc": { + "start": { + "line": 3, + "column": 17 + }, + "end": { + "line": 3, + "column": 21 + } + }, + "method": false, + "shorthand": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 113, + "end": 114, + "loc": { + "start": { + "line": 3, + "column": 17 + }, + "end": { + "line": 3, + "column": 18 + } + }, + "name": "a" + }, + "value": { + "type": "Literal", + "start": 116, + "end": 117, + "loc": { + "start": { + "line": 3, + "column": 20 + }, + "end": { + "line": 3, + "column": 21 + } + }, + "value": 1, + "raw": "1" + }, + "kind": "init" + } + ] + } + } + ], + "kind": "const", + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `export` and `=` — stays where the author wrote it", + "start": 20, + "end": 95 + } + ] + }, + { + "type": "TSExportAssignment", + "start": 122, + "end": 145, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 24 + } + }, + "expression": { + "type": "Identifier", + "start": 139, + "end": 144, + "loc": { + "start": { + "line": 4, + "column": 18 + }, + "end": { + "line": 4, + "column": 23 + } + }, + "name": "value", + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 129, + "end": 136 + } + ] + } + } + ], + "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_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..54abbd58b --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..a394d0d7c --- /dev/null +++ b/tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_header_comment/expected.json b/tests/fixtures/typescript/modules/imports/equals_header_comment/expected.json new file mode 100644 index 000000000..6bcad125e --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_comment/expected.json @@ -0,0 +1,632 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 482, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `import` and the name", + "start": 20, + "end": 66, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 47 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 75, + "end": 83, + "loc": { + "start": { + "line": 3, + "column": 8 + }, + "end": { + "line": 3, + "column": 16 + } + } + }, + { + "type": "Line", + "value": " Block comment between the name and `=`", + "start": 106, + "end": 147, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 42 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 158, + "end": 166, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 18 + } + } + }, + { + "type": "Line", + "value": " Block comment between `=` and the module reference", + "start": 187, + "end": 240, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 54 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 253, + "end": 261, + "loc": { + "start": { + "line": 9, + "column": 12 + }, + "end": { + "line": 9, + "column": 20 + } + } + }, + { + "type": "Line", + "value": " Block comment between the `type` keyword and the name", + "start": 280, + "end": 336, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 57 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 350, + "end": 358, + "loc": { + "start": { + "line": 12, + "column": 13 + }, + "end": { + "line": 12, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Block comment between `export` and `import`", + "start": 381, + "end": 427, + "loc": { + "start": { + "line": 14, + "column": 1 + }, + "end": { + "line": 14, + "column": 47 + } + } + }, + { + "type": "Block", + "value": " c5 ", + "start": 436, + "end": 444, + "loc": { + "start": { + "line": 15, + "column": 8 + }, + "end": { + "line": 15, + "column": 16 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 481, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 472, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 16, + "column": 9 + } + }, + "body": [ + { + "type": "TSImportEqualsDeclaration", + "start": 68, + "end": 103, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 36 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 84, + "end": 85, + "loc": { + "start": { + "line": 3, + "column": 17 + }, + "end": { + "line": 3, + "column": 18 + } + }, + "name": "A", + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 75, + "end": 83 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 88, + "end": 102, + "loc": { + "start": { + "line": 3, + "column": 21 + }, + "end": { + "line": 3, + "column": 35 + } + }, + "expression": { + "type": "Literal", + "start": 96, + "end": 101, + "loc": { + "start": { + "line": 3, + "column": 29 + }, + "end": { + "line": 3, + "column": 34 + } + }, + "value": "./a", + "raw": "'./a'" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `import` and the name", + "start": 20, + "end": 66 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 149, + "end": 184, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 36 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 156, + "end": 157, + "loc": { + "start": { + "line": 6, + "column": 8 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "name": "B", + "trailingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 158, + "end": 166 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 169, + "end": 183, + "loc": { + "start": { + "line": 6, + "column": 21 + }, + "end": { + "line": 6, + "column": 35 + } + }, + "expression": { + "type": "Literal", + "start": 177, + "end": 182, + "loc": { + "start": { + "line": 6, + "column": 29 + }, + "end": { + "line": 6, + "column": 34 + } + }, + "value": "./b", + "raw": "'./b'" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between the name and `=`", + "start": 106, + "end": 147 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 242, + "end": 277, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 36 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 249, + "end": 250, + "loc": { + "start": { + "line": 9, + "column": 8 + }, + "end": { + "line": 9, + "column": 9 + } + }, + "name": "C" + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 262, + "end": 276, + "loc": { + "start": { + "line": 9, + "column": 21 + }, + "end": { + "line": 9, + "column": 35 + } + }, + "expression": { + "type": "Literal", + "start": 270, + "end": 275, + "loc": { + "start": { + "line": 9, + "column": 29 + }, + "end": { + "line": 9, + "column": 34 + } + }, + "value": "./c", + "raw": "'./c'" + }, + "leadingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 253, + "end": 261 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `=` and the module reference", + "start": 187, + "end": 240 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 338, + "end": 378, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 41 + } + }, + "importKind": "type", + "isExport": false, + "id": { + "type": "Identifier", + "start": 359, + "end": 360, + "loc": { + "start": { + "line": 12, + "column": 22 + }, + "end": { + "line": 12, + "column": 23 + } + }, + "name": "D", + "leadingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 350, + "end": 358 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 363, + "end": 377, + "loc": { + "start": { + "line": 12, + "column": 26 + }, + "end": { + "line": 12, + "column": 40 + } + }, + "expression": { + "type": "Literal", + "start": 371, + "end": 376, + "loc": { + "start": { + "line": 12, + "column": 34 + }, + "end": { + "line": 12, + "column": 39 + } + }, + "value": "./d", + "raw": "'./d'" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between the `type` keyword and the name", + "start": 280, + "end": 336 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 429, + "end": 471, + "loc": { + "start": { + "line": 15, + "column": 1 + }, + "end": { + "line": 15, + "column": 43 + } + }, + "importKind": "value", + "isExport": true, + "id": { + "type": "Identifier", + "start": 452, + "end": 453, + "loc": { + "start": { + "line": 15, + "column": 24 + }, + "end": { + "line": 15, + "column": 25 + } + }, + "name": "E", + "leadingComments": [ + { + "type": "Block", + "value": " c5 ", + "start": 436, + "end": 444 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 456, + "end": 470, + "loc": { + "start": { + "line": 15, + "column": 28 + }, + "end": { + "line": 15, + "column": 42 + } + }, + "expression": { + "type": "Literal", + "start": 464, + "end": 469, + "loc": { + "start": { + "line": 15, + "column": 36 + }, + "end": { + "line": 15, + "column": 41 + } + }, + "value": "./e", + "raw": "'./e'" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `export` and `import`", + "start": 381, + "end": 427 + } + ] + } + ], + "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/imports/equals_header_comment/input.svelte b/tests/fixtures/typescript/modules/imports/equals_header_comment/input.svelte new file mode 100644 index 000000000..cca4cc1c7 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_comment/input.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/README.md new file mode 100644 index 000000000..569d2d43d --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/README.md @@ -0,0 +1,23 @@ +# Divergence: import-equals `import`→`type` comment (preserve) + +A block comment between `import` and the `type` keyword of an import-equals declaration +(`import /* c */ type C = require('./c');`). tsv keeps it after `import`; prettier **relocates** it +to the binding side of `type`. + +```ts +// tsv (preserve) // prettier (relocate past `type`) +import /* c */ type C = require('./c'); import type /* c */ C = require('./c'); +``` + +**Why tsv preserves:** the comment plausibly annotates the `import` itself; moving it past `type` +re-attaches it to the binding. This is the same rule prettier applies to a default import, where tsv +likewise preserves — [default_keyword_comment](../default_keyword_comment_prettier_divergence/). + +This is the **only** import-equals header gap that diverges. The other four +(`import`→name, name→`=`, `=`→module-reference, `type`→name, and `export`→`import`) are preserved by +**both** formatters and live in the regular sibling +[equals_header_comment](../equals_header_comment/). tsv dropped all of them — plain content loss, +not a difference of opinion. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..865730f21 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/expected.json @@ -0,0 +1,202 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 219, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `import` and the `type` keyword — stays where the author", + "start": 20, + "end": 101, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 82 + } + } + }, + { + "type": "Line", + "value": " wrote it; prettier relocates it to the binding side of `type`", + "start": 103, + "end": 167, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 65 + } + } + }, + { + "type": "Block", + "value": " c ", + "start": 176, + "end": 183, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 15 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 218, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 209, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 5, + "column": 9 + } + }, + "body": [ + { + "type": "TSImportEqualsDeclaration", + "start": 169, + "end": 208, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 4, + "column": 40 + } + }, + "importKind": "type", + "isExport": false, + "id": { + "type": "Identifier", + "start": 189, + "end": 190, + "loc": { + "start": { + "line": 4, + "column": 21 + }, + "end": { + "line": 4, + "column": 22 + } + }, + "name": "C", + "leadingComments": [ + { + "type": "Block", + "value": " c ", + "start": 176, + "end": 183 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 193, + "end": 207, + "loc": { + "start": { + "line": 4, + "column": 25 + }, + "end": { + "line": 4, + "column": 39 + } + }, + "expression": { + "type": "Literal", + "start": 201, + "end": 206, + "loc": { + "start": { + "line": 4, + "column": 33 + }, + "end": { + "line": 4, + "column": 38 + } + }, + "value": "./c", + "raw": "'./c'" + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `import` and the `type` keyword — stays where the author", + "start": 20, + "end": 101 + }, + { + "type": "Line", + "value": " wrote it; prettier relocates it to the binding side of `type`", + "start": 103, + "end": 167 + } + ] + } + ], + "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/imports/equals_type_keyword_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..036959662 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/input.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..e9c5fe9f3 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,5 @@ + diff --git a/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/README.md b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/README.md new file mode 100644 index 000000000..1efe18d53 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/README.md @@ -0,0 +1,30 @@ +# Divergence: `await`→`using` keyword-interior comment (preserve) + +A block comment inside the `await using` keyword (`await /* c */ using a = f();`), including the +for-await-of head. tsv keeps it after `await`; prettier **relocates** it past `using` onto the +binding. + +```ts +// tsv (preserve) // prettier (relocate past the keyword) +await /* c */ using a = f(); await using /* c */ a = f(); +``` + +**Why tsv preserves:** relocating **collapses a distinction**. With comments on both sides of +`using`, prettier lands both in one place — `await /* c2 */ using /* c3 */ b` becomes +`await using /* c2 */ /* c3 */ b` — so "before `using`" and "after `using`" become +indistinguishable. The text survives; the association does not. tsv keeps each on its authored +side. A keyword is not a *pure separator*, the one sanctioned reason to trail. + +Only the **block** form exists: a comment containing a newline *is* a `LineTerminator` (ecma262 +§sec-comments) and `await [no LT] using` demotes, so `await /* c⏎ */ using y = f()` is correctly +not a declaration in either formatter. + +## Parser divergence + +Svelte's parser (acorn-typescript) rejects `using` / `await using` outright — the pre-existing +divergence pinned by [await_svelte_divergence](../await_svelte_divergence/), not anything about the +comment. Hence `expected_ours.json` + `expected_svelte.json` and the `_svelte_prettier_divergence` +suffix. See [conformance_svelte.md §TypeScript Corrections](../../../../../../docs/conformance_svelte.md#typescript-corrections). + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_ours.json b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_ours.json new file mode 100644 index 000000000..c0519580e --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_ours.json @@ -0,0 +1,762 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 461, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment between `await` and `using` — stays where the author wrote it", + "start": 20, + "end": 98, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 79 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 134, + "end": 142, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 16 + } + } + }, + { + "type": "Line", + "value": " Comments on both sides of `using` stay distinct and in order", + "start": 163, + "end": 226, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 64 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 260, + "end": 268, + "loc": { + "start": { + "line": 9, + "column": 8 + }, + "end": { + "line": 9, + "column": 16 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 275, + "end": 283, + "loc": { + "start": { + "line": 9, + "column": 23 + }, + "end": { + "line": 9, + "column": 31 + } + } + }, + { + "type": "Line", + "value": " The same gap in a for-await-of head", + "start": 298, + "end": 336, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 39 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 418, + "end": 426, + "loc": { + "start": { + "line": 14, + "column": 19 + }, + "end": { + "line": 14, + "column": 27 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 460, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 451, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 17, + "column": 9 + } + }, + "body": [ + { + "type": "FunctionDeclaration", + "start": 100, + "end": 160, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 115, + "end": 121, + "loc": { + "start": { + "line": 3, + "column": 16 + }, + "end": { + "line": 3, + "column": 22 + } + }, + "name": "single" + }, + "expression": false, + "generator": false, + "async": true, + "params": [], + "body": { + "type": "BlockStatement", + "start": 124, + "end": 160, + "loc": { + "start": { + "line": 3, + "column": 25 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 128, + "end": 157, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 31 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 149, + "end": 156, + "loc": { + "start": { + "line": 4, + "column": 23 + }, + "end": { + "line": 4, + "column": 30 + } + }, + "id": { + "type": "Identifier", + "start": 149, + "end": 150, + "loc": { + "start": { + "line": 4, + "column": 23 + }, + "end": { + "line": 4, + "column": 24 + } + }, + "name": "a" + }, + "init": { + "type": "CallExpression", + "start": 153, + "end": 156, + "loc": { + "start": { + "line": 4, + "column": 27 + }, + "end": { + "line": 4, + "column": 30 + } + }, + "callee": { + "type": "Identifier", + "start": 153, + "end": 154, + "loc": { + "start": { + "line": 4, + "column": 27 + }, + "end": { + "line": 4, + "column": 28 + } + }, + "name": "f" + }, + "arguments": [], + "optional": false + }, + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 134, + "end": 142 + } + ] + } + ], + "kind": "await using" + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment between `await` and `using` — stays where the author wrote it", + "start": 20, + "end": 98 + } + ] + }, + { + "type": "FunctionDeclaration", + "start": 228, + "end": 295, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 243, + "end": 247, + "loc": { + "start": { + "line": 8, + "column": 16 + }, + "end": { + "line": 8, + "column": 20 + } + }, + "name": "both" + }, + "expression": false, + "generator": false, + "async": true, + "params": [], + "body": { + "type": "BlockStatement", + "start": 250, + "end": 295, + "loc": { + "start": { + "line": 8, + "column": 23 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 254, + "end": 292, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 40 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 284, + "end": 291, + "loc": { + "start": { + "line": 9, + "column": 32 + }, + "end": { + "line": 9, + "column": 39 + } + }, + "id": { + "type": "Identifier", + "start": 284, + "end": 285, + "loc": { + "start": { + "line": 9, + "column": 32 + }, + "end": { + "line": 9, + "column": 33 + } + }, + "name": "b" + }, + "init": { + "type": "CallExpression", + "start": 288, + "end": 291, + "loc": { + "start": { + "line": 9, + "column": 36 + }, + "end": { + "line": 9, + "column": 39 + } + }, + "callee": { + "type": "Identifier", + "start": 288, + "end": 289, + "loc": { + "start": { + "line": 9, + "column": 36 + }, + "end": { + "line": 9, + "column": 37 + } + }, + "name": "f" + }, + "arguments": [], + "optional": false + }, + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 260, + "end": 268 + }, + { + "type": "Block", + "value": " c3 ", + "start": 275, + "end": 283 + } + ] + } + ], + "kind": "await using" + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Comments on both sides of `using` stay distinct and in order", + "start": 163, + "end": 226 + } + ] + }, + { + "type": "FunctionDeclaration", + "start": 338, + "end": 450, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 16, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 353, + "end": 360, + "loc": { + "start": { + "line": 13, + "column": 16 + }, + "end": { + "line": 13, + "column": 23 + } + }, + "name": "iterate" + }, + "expression": false, + "generator": false, + "async": true, + "params": [ + { + "type": "Identifier", + "start": 361, + "end": 395, + "loc": { + "start": { + "line": 13, + "column": 24 + }, + "end": { + "line": 13, + "column": 58 + } + }, + "name": "it", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 363, + "end": 395, + "loc": { + "start": { + "line": 13, + "column": 26 + }, + "end": { + "line": 13, + "column": 58 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 365, + "end": 395, + "loc": { + "start": { + "line": 13, + "column": 28 + }, + "end": { + "line": 13, + "column": 58 + } + }, + "typeName": { + "type": "Identifier", + "start": 365, + "end": 378, + "loc": { + "start": { + "line": 13, + "column": 28 + }, + "end": { + "line": 13, + "column": 41 + } + }, + "name": "AsyncIterable" + }, + "typeArguments": { + "type": "TSTypeParameterInstantiation", + "start": 378, + "end": 395, + "loc": { + "start": { + "line": 13, + "column": 41 + }, + "end": { + "line": 13, + "column": 58 + } + }, + "params": [ + { + "type": "TSTypeReference", + "start": 379, + "end": 394, + "loc": { + "start": { + "line": 13, + "column": 42 + }, + "end": { + "line": 13, + "column": 57 + } + }, + "typeName": { + "type": "Identifier", + "start": 379, + "end": 394, + "loc": { + "start": { + "line": 13, + "column": 42 + }, + "end": { + "line": 13, + "column": 57 + } + }, + "name": "AsyncDisposable" + } + } + ] + } + } + } + } + ], + "body": { + "type": "BlockStatement", + "start": 397, + "end": 450, + "loc": { + "start": { + "line": 13, + "column": 60 + }, + "end": { + "line": 16, + "column": 2 + } + }, + "body": [ + { + "type": "ForOfStatement", + "start": 401, + "end": 447, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 15, + "column": 3 + } + }, + "await": true, + "left": { + "type": "VariableDeclaration", + "start": 412, + "end": 434, + "loc": { + "start": { + "line": 14, + "column": 13 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 433, + "end": 434, + "loc": { + "start": { + "line": 14, + "column": 34 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "id": { + "type": "Identifier", + "start": 433, + "end": 434, + "loc": { + "start": { + "line": 14, + "column": 34 + }, + "end": { + "line": 14, + "column": 35 + } + }, + "name": "c" + }, + "init": null, + "leadingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 418, + "end": 426 + } + ] + } + ], + "kind": "await using" + }, + "right": { + "type": "Identifier", + "start": 438, + "end": 440, + "loc": { + "start": { + "line": 14, + "column": 39 + }, + "end": { + "line": 14, + "column": 41 + } + }, + "name": "it" + }, + "body": { + "type": "BlockStatement", + "start": 442, + "end": 447, + "loc": { + "start": { + "line": 14, + "column": 43 + }, + "end": { + "line": 15, + "column": 3 + } + }, + "body": [] + } + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " The same gap in a for-await-of head", + "start": 298, + "end": 336 + } + ] + } + ], + "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/using/await_keyword_comment_svelte_prettier_divergence/expected_svelte.json b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_svelte.json new file mode 100644 index 000000000..e7397fec1 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/expected_svelte.json @@ -0,0 +1 @@ +{"error": "failed to parse"} diff --git a/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/input.svelte b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/input.svelte new file mode 100644 index 000000000..f22a78b45 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/input.svelte @@ -0,0 +1,17 @@ + diff --git a/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..263b94a57 --- /dev/null +++ b/tests/fixtures/typescript/typescript_specific/using/await_keyword_comment_svelte_prettier_divergence/output_prettier.svelte @@ -0,0 +1,17 @@ + From 48ef999343dae6abf17ac9ea7bdef78892dc60b3 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Wed, 15 Jul 2026 10:19:00 -0400 Subject: [PATCH 2/4] merge --- crates/tsv_ts/src/printer/analysis.rs | 13 +- .../tsv_ts/src/printer/calls/import_expr.rs | 72 +- .../tsv_ts/src/printer/calls/test_patterns.rs | 43 +- .../src/printer/comments/declarations.rs | 125 ++- crates/tsv_ts/src/printer/comments/lists.rs | 26 + crates/tsv_ts/src/printer/statements/class.rs | 9 +- .../statements/control_flow/loops/for_loop.rs | 11 + .../control_flow/loops/while_loop.rs | 5 +- .../statements/control_flow/try_jump.rs | 5 +- crates/tsv_ts/src/printer/statements/mod.rs | 8 +- .../src/printer/statements/modules/mod.rs | 199 ++-- .../printer/statements/type_declarations.rs | 38 +- docs/conformance_prettier.md | 32 +- .../nested_dot_comment/expected.json | 755 +++++++++++++++ .../namespace/nested_dot_comment/input.svelte | 16 + .../test_function_dot_comment/expected.json | 775 ++++++++++++++++ .../test_function_dot_comment/input.svelte | 11 + .../README.md | 34 + .../audit_signature.txt | 27 + .../expected.json | 523 +++++++++++ .../input.svelte | 19 + .../output_prettier.svelte | 17 + .../expected.json | 315 +++++++ .../equals_require_paren_comment/input.svelte | 7 + .../unformatted_before_paren.svelte | 7 + .../expected.json | 863 ++++++++++++++++++ .../input.svelte | 16 + .../README.md | 31 + .../expected.json | 761 +++++++++++++++ .../input.svelte | 20 + .../output_prettier.svelte | 17 + tests/import_phase.rs | 17 + 32 files changed, 4598 insertions(+), 219 deletions(-) create mode 100644 tests/fixtures/typescript/declarations/namespace/nested_dot_comment/expected.json create mode 100644 tests/fixtures/typescript/declarations/namespace/nested_dot_comment/input.svelte create mode 100644 tests/fixtures/typescript/expressions/calls/test_function_dot_comment/expected.json create mode 100644 tests/fixtures/typescript/expressions/calls/test_function_dot_comment/input.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/audit_signature.txt create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/output_prettier.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_require_paren_comment/expected.json create mode 100644 tests/fixtures/typescript/modules/imports/equals_require_paren_comment/input.svelte create mode 100644 tests/fixtures/typescript/modules/imports/equals_require_paren_comment/unformatted_before_paren.svelte create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_comments/expected.json create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_comments/input.svelte create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/README.md create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/expected.json create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/input.svelte create mode 100644 tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/output_prettier.svelte diff --git a/crates/tsv_ts/src/printer/analysis.rs b/crates/tsv_ts/src/printer/analysis.rs index cf2609f62..4947622c0 100644 --- a/crates/tsv_ts/src/printer/analysis.rs +++ b/crates/tsv_ts/src/printer/analysis.rs @@ -516,13 +516,18 @@ pub(crate) fn build_entity_name_doc( printer: &Printer<'_>, name: &internal::TSEntityName<'_>, ) -> DocId { - let d = printer.d(); match name { internal::TSEntityName::Identifier(id) => printer.identifier_name_doc(id), - internal::TSEntityName::QualifiedName(qn) => d.concat(&[ + // A qualified name is a dotted pair, so both gaps around the `.` are positions an + // author can comment in (`ns /* c */.Type`, `ns./* c */ Type`). The shared printer + // emits them; concatenating the three pieces scans neither and drops what's there. + // One printer serves every qualified name, so this reaches type references, + // interface heritage, and import-equals module references alike. + internal::TSEntityName::QualifiedName(qn) => printer.build_dotted_pair_doc( build_entity_name_doc(printer, &qn.left), - d.text("."), printer.identifier_name_doc(&qn.right), - ]), + qn.left.span().end, + qn.right.span.start, + ), } } diff --git a/crates/tsv_ts/src/printer/calls/import_expr.rs b/crates/tsv_ts/src/printer/calls/import_expr.rs index 8eaa6d16b..af65fdf06 100644 --- a/crates/tsv_ts/src/printer/calls/import_expr.rs +++ b/crates/tsv_ts/src/printer/calls/import_expr.rs @@ -4,7 +4,7 @@ // - Dynamic import: `import('module')`, `import('module', options)` // - Meta properties: `import.meta`, `new.target` -use super::super::{CommentSpacing, Printer}; +use super::super::Printer; use super::arg_comments::{ PartitionedComments, has_blank_line_between_args, should_force_expansion_for_comments, }; @@ -254,68 +254,18 @@ pub(super) fn build_import_expression_doc( /// Build a Doc for a meta property: `import.meta`, `new.target` /// -/// Both gaps around the `.` are real source positions an author can comment in -/// (`new /* c */.target`, `new./* c */ target`), so each is located and emitted. -/// Concatenating the three pieces scans neither gap and drops whatever is in it — the -/// same class as a comment inside a multi-word keyword (see `build_keyword_words_doc` -/// in `printer/comments/declarations.rs`), and the reason that class's usual detector -/// (a `d.text` literal with an *interior* space) is only a proxy: here the pieces are -/// joined by `"."`, which has no space to find. -/// -/// Each side stays where it was authored, which is also what prettier prints: the -/// comment hugs the `.` and keeps its space on the identifier's side. A *line* comment -/// before the `.` ends its line, so `.property` continues one level down -/// (`new // c⏎\t.target`) — the shape a member access already takes. +/// A meta property is a dotted pair of names, so it delegates to the shared +/// [`Printer::build_dotted_pair_doc`] — which emits both gaps around the `.`, the +/// positions an author can comment in (`new /* c */.target`, `new./* c */ target`). +/// A qualified name (`ns.Type`) is the same shape and shares that printer. pub(super) fn build_meta_property_doc( printer: &Printer<'_>, meta: &internal::MetaProperty<'_>, ) -> DocId { - let d = printer.d(); - let meta_doc = printer.identifier_name_doc(&meta.meta); - let prop_doc = printer.identifier_name_doc(&meta.property); - let gap_start = meta.meta.span.end; - let gap_end = meta.property.span.start; - - // `new.target` / `import.meta` with both gaps empty — every ordinary occurrence. - if !printer.has_comments_to_emit_between(gap_start, gap_end) { - return d.concat(&[meta_doc, d.text("."), prop_doc]); - } - - let Some(dot) = printer.find_char_outside_comments(gap_start, gap_end, b'.') else { - debug_assert!( - false, - "a meta property always spells a `.` between its two names" - ); - return d.concat(&[meta_doc, d.text("."), prop_doc]); - }; - - // `.`→property: a block comment keeps its space on the property's side - // (`./* c */ target`); a line comment ends the line, so the property drops a level. - let after_dot = if !printer.has_comments_to_emit_between(dot + 1, gap_end) { - prop_doc - } else if printer.has_line_comments_between(dot + 1, gap_end) { - printer.build_continuation_indent(dot + 1, gap_end, prop_doc) - } else { - d.concat(&[ - printer.build_comments_between(dot + 1, gap_end, CommentSpacing::Trailing), - prop_doc, - ]) - }; - let tail = d.concat(&[d.text("."), after_dot]); - - // meta→`.`: a line comment takes the whole `.property` onto the next line with it. - if printer.has_line_comments_between(gap_start, dot) { - return d.concat(&[ - meta_doc, - printer.build_continuation_indent(gap_start, dot, tail), - ]); - } - if printer.has_comments_to_emit_between(gap_start, dot) { - return d.concat(&[ - meta_doc, - printer.build_comments_between(gap_start, dot, CommentSpacing::Leading), - tail, - ]); - } - d.concat(&[meta_doc, tail]) + printer.build_dotted_pair_doc( + printer.identifier_name_doc(&meta.meta), + printer.identifier_name_doc(&meta.property), + meta.meta.span.end, + meta.property.span.start, + ) } diff --git a/crates/tsv_ts/src/printer/calls/test_patterns.rs b/crates/tsv_ts/src/printer/calls/test_patterns.rs index 7cdf5f712..2eb92ae10 100644 --- a/crates/tsv_ts/src/printer/calls/test_patterns.rs +++ b/crates/tsv_ts/src/printer/calls/test_patterns.rs @@ -45,9 +45,9 @@ pub(super) const TEST_CALL_PATTERNS: &[&str] = &[ ]; /// Get the name channel (+ span start) of an identifier if it's a simple identifier -fn get_identifier_name(expr: &internal::Expression<'_>) -> Option<(IdentName, u32)> { +fn get_identifier_name(expr: &internal::Expression<'_>) -> Option<(IdentName, u32, u32)> { if let internal::Expression::Identifier(id) = expr { - Some((id.ident_name(), id.span.start)) + Some((id.ident_name(), id.span.start, id.span.end)) } else { None } @@ -66,34 +66,45 @@ fn get_identifier_name(expr: &internal::Expression<'_>) -> Option<(IdentName, u3 /// pattern list against the same chain parts directly (also no allocation) via /// the shared [`get_member_chain_parts`], so the two stay in lockstep on which /// callees qualify. +/// +/// Each `.` goes through the shared dotted-pair printer, which emits the gaps +/// around it — the positions an author can comment in (`describe /* c */.only`, +/// `test./* c */ skip`). Joining the parts with a bare `d.text(".")` scans none of +/// them and drops what's there. It stays break-free: with no comment in a gap the +/// pair is the same three text nodes as before. pub(super) fn build_test_callee_flat_doc( expr: &internal::Expression<'_>, printer: &Printer<'_>, ) -> Option { let parts = get_member_chain_parts(expr)?; - let d = printer.d(); - // Parts come out leaf→root; reverse to root→leaf (`test.describe.only`). - let mut doc_parts = DocBuf::new(); - for (i, (name, name_start)) in parts.iter().rev().enumerate() { - if i > 0 { - doc_parts.push(d.text(".")); - } - doc_parts.push(printer.ident_name_doc(*name, *name_start)); + // Parts come out leaf→root; reverse to root→leaf (`test.describe.only`), which is + // also the AST's own association — each pair's left is everything before its dot. + let mut iter = parts.iter().rev(); + // A single-part callee (`it`) is the bare name: the loop never runs. + let &(name, start, mut prev_end) = iter.next()?; + let mut doc = printer.ident_name_doc(name, start); + for &(name, start, end) in iter { + doc = printer.build_dotted_pair_doc( + doc, + printer.ident_name_doc(name, start), + prev_end, + start, + ); + prev_end = end; } - // `concat` short-circuits a single-part callee (`it`) to the bare symbol. - Some(d.concat(&doc_parts)) + Some(doc) } /// Get the member chain parts from an expression /// Returns parts reversed, e.g. `["skip", "test"]` for `test.skip`. fn get_member_chain_parts( expr: &internal::Expression<'_>, -) -> Option> { - let mut parts: SmallVec<[(IdentName, u32); 8]> = SmallVec::new(); +) -> Option> { + let mut parts: SmallVec<[(IdentName, u32, u32); 8]> = SmallVec::new(); match expr { internal::Expression::Identifier(id) => { - parts.push((id.ident_name(), id.span.start)); + parts.push((id.ident_name(), id.span.start, id.span.end)); Some(parts) } internal::Expression::MemberExpression(member) => { @@ -182,7 +193,7 @@ pub(super) fn is_test_call(call: &internal::CallExpression<'_>, printer: &Printe let names: SmallVec<[&str; 8]> = parts .iter() .rev() - .map(|&(name, name_start)| name.resolve(name_start, printer.source, &interner)) + .map(|&(name, name_start, _)| name.resolve(name_start, printer.source, &interner)) .collect(); TEST_CALL_PATTERNS .iter() diff --git a/crates/tsv_ts/src/printer/comments/declarations.rs b/crates/tsv_ts/src/printer/comments/declarations.rs index f65e6b2b5..11673fe4f 100644 --- a/crates/tsv_ts/src/printer/comments/declarations.rs +++ b/crates/tsv_ts/src/printer/comments/declarations.rs @@ -354,6 +354,79 @@ impl<'a> Printer<'a> { ]) } + /// A dotted pair of names, with **both** gaps around the `.` emitted: a meta property + /// (`new` `.` `target`, `import` `.` `meta`) or a qualified name (`ns` `.` `Type`). + /// + /// Concatenating `left` + `"."` + `right` scans neither gap and drops whatever an + /// author wrote in one. That is the punctuator-joined member of the multi-word-keyword + /// class (see [`build_keyword_words_doc`](Self::build_keyword_words_doc)) — and the + /// case that shows why the class's usual detector, a `d.text` literal with an + /// *interior* space, is only a proxy: the joining literal is `"."`, which has no space + /// to find. Both shapes route here so neither can regrow the hole independently. + /// + /// Each side stays where it was authored, which is what prettier prints for a **block** + /// comment: it hugs the `.` and keeps its space on the identifier's side. A **line** + /// comment ends its line, so the tail continues one level down — that half is a + /// divergence (prettier relocates it out of the construct, past the `;`). + /// + /// `gap_start` is `left`'s source end; `gap_end` is `right`'s source start. + pub(crate) fn build_dotted_pair_doc( + &self, + left: DocId, + right: DocId, + gap_start: u32, + gap_end: u32, + ) -> DocId { + let d = self.d(); + // Both gaps empty — every ordinary occurrence, and the only one that is hot. + if !self.has_comments_to_emit_between(gap_start, gap_end) { + return d.concat(&[left, d.text("."), right]); + } + let Some(dot) = self.find_char_outside_comments(gap_start, gap_end, b'.') else { + debug_assert!( + false, + "a dotted pair always spells a `.` between its two names" + ); + return d.concat(&[left, d.text("."), right]); + }; + // `.`→right, then left→`.` wrapping it: the tail is built first so a line comment + // in the left gap takes the whole `.right` down with it. + let tail = d.concat(&[ + d.text("."), + self.build_dot_gap_doc(dot + 1, gap_end, right, CommentSpacing::Trailing), + ]); + d.concat(&[ + left, + self.build_dot_gap_doc(gap_start, dot, tail, CommentSpacing::Leading), + ]) + } + + /// One of the two gaps around a dotted pair's `.`: the comments authored in + /// `[start, end)`, then `tail`. + /// + /// Both sides obey the same rule, so both call this — a *line* comment ends its line + /// and `tail` continues one level down; a block comment stays inline ahead of it. Only + /// the spacing differs, since a gap's comment sits after the `.` on one side and + /// before it on the other. + fn build_dot_gap_doc( + &self, + start: u32, + end: u32, + tail: DocId, + spacing: CommentSpacing, + ) -> DocId { + // The caller established that *some* comment lies between the two names, but not + // which side of the `.` — so each gap still gates, and an empty one adds nothing. + if !self.has_comments_to_emit_between(start, end) { + return tail; + } + if self.has_line_comments_between(start, end) { + return self.build_continuation_indent(start, end, tail); + } + self.d() + .concat(&[self.build_comments_between(start, end, spacing), tail]) + } + /// Build a **multi-word keyword** (`export default`, `await using`, `declare /// const`, `export as namespace`), preserving a comment authored in one of its /// interior gaps. @@ -411,8 +484,20 @@ impl<'a> Printer<'a> { return (d.text(word), start + word.len() as u32); } - let mut starts: SmallVec<[u32; 4]> = SmallVec::new(); + // Left-to-right and FLAT: every gap emits its comments where the author wrote + // them, but none of them indents on its own — the whole tail is wrapped once, + // below. Indenting per gap would compound, and the staircase it builds is not + // just deep, it is wrong: the caller emits the keyword→value gap at the header's + // own level, so a two-broken-gap keyword would leave its last word sitting a + // level *below* the value that follows it. + // + // Locating and emitting ride one pass: a word's gap runs from the previous + // word's end (`cursor`) to this word's start, both of which this loop already + // holds — so nothing needs to remember where the earlier words landed. + let mut tail: DocBuf = DocBuf::new(); + let mut any_line = false; let mut cursor = start; + let mut in_gap = false; for word in words { let Some(pos) = self.find_keyword_in_range(cursor, search_end, word) else { // The source doesn't hold the shape the caller named — only reachable @@ -428,36 +513,29 @@ impl<'a> Printer<'a> { — caller passed a synthetic span or a bad search_end" ); let mut parts: DocBuf = DocBuf::new(); + let mut measured = start; for (i, w) in words.iter().enumerate() { if i > 0 { parts.push(d.text(" ")); + measured += 1; } parts.push(d.text(w)); + measured += w.len() as u32; } - let width: u32 = words.iter().map(|w| w.len() as u32).sum(); - let measured = start + width + words.len() as u32 - 1; return (d.concat(&parts), measured); }; - starts.push(pos); + // The first word is emitted by the caller below, outside the indent — it + // leads the header, so no gap precedes it. + if in_gap { + let (gap_doc, has_line) = self.build_keyword_gap_doc(cursor, pos); + any_line |= has_line; + tail.push(gap_doc); + tail.push(d.text(word)); + } + in_gap = true; cursor = pos + word.len() as u32; } let keyword_end = cursor; - - // Left-to-right and FLAT: every gap emits its comments where the author wrote - // them, but none of them indents on its own — the whole tail is wrapped once, - // below. Indenting per gap would compound, and the staircase it builds is not - // just deep, it is wrong: the caller emits the keyword→value gap at the header's - // own level, so a two-broken-gap keyword would leave its last word sitting a - // level *below* the value that follows it. - let mut tail: DocBuf = DocBuf::new(); - let mut any_line = false; - for i in 0..words.len() - 1 { - let gap_start = starts[i] + words[i].len() as u32; - let (gap_doc, has_line) = self.build_keyword_gap_doc(gap_start, starts[i + 1]); - any_line |= has_line; - tail.push(gap_doc); - tail.push(d.text(words[i + 1])); - } let tail_doc = d.concat(&tail); // One level for the whole header — the same thing the single-gap rule says: a // broken gap reads as one statement continuation, never as N nested ones. With @@ -525,8 +603,13 @@ impl<'a> Printer<'a> { /// so a caller with *several* gaps can emit each one and then decide **once** what /// to indent. Indenting per gap compounds: two broken gaps would put the keyword's /// last word two levels deep, below the value that follows it at one. + /// + /// Two callers: [`build_keyword_words_doc`](Self::build_keyword_words_doc) for a + /// keyword's interior gaps, and the import-equals header — the one multi-gap header + /// whose words aren't contiguous (its name sits between `import` and `=`), so it + /// drives this directly instead. #[inline] - fn build_keyword_gap_doc(&self, start: u32, end: u32) -> (DocId, bool) { + pub(crate) fn build_keyword_gap_doc(&self, start: u32, end: u32) -> (DocId, bool) { let d = self.d(); // One search settles the gap. With no comment there is nothing to emit but the // separator — no empty child, and neither of the per-shape searches below runs. diff --git a/crates/tsv_ts/src/printer/comments/lists.rs b/crates/tsv_ts/src/printer/comments/lists.rs index 0a1b0403e..df5c74f65 100644 --- a/crates/tsv_ts/src/printer/comments/lists.rs +++ b/crates/tsv_ts/src/printer/comments/lists.rs @@ -926,6 +926,32 @@ impl<'a> Printer<'a> { ) } + /// The [`split_separator_gap_comments`](Self::split_separator_gap_comments) caller + /// idiom for a **`;` terminator**, in one call: the gap's pre-`;` comments, the `;`, + /// then the comments that belong after it. + /// + /// The ordering is the reason this exists rather than three lines at each site: the + /// returned docs must be pushed *after* the `;` text, and a site that inlines the + /// idiom can invert it silently — an own-line comment emitted before the separator + /// puts the `;` on the comment's line, where a `//` swallows it outright. + /// + /// `block_after_separator` is the terminator's own axis, not a caller preference: + /// `true` for a statement/member `;` (prettier trails a same-line block past it — + /// `expr; /* c */`), `false` where the operand keeps it (`import =` / `export =` / + /// `export as namespace`). A caller whose split is *conditional* keeps the raw idiom. + pub(crate) fn push_semicolon_with_gap_comments( + &self, + parts: &mut DocBuf, + start: u32, + semicolon_pos: u32, + block_after_separator: bool, + ) { + let after = + self.split_separator_gap_comments(parts, start, semicolon_pos, block_after_separator); + parts.push(self.d().text(";")); + parts.extend(after); + } + /// The **type-member `;`** variant of `split_separator_gap_comments`: a same-line /// block stays *before* the `;` (`a: A /* c */;`, like a list separator) **but** a /// blank line before an own-line comment IS preserved (like a statement terminator). diff --git a/crates/tsv_ts/src/printer/statements/class.rs b/crates/tsv_ts/src/printer/statements/class.rs index 5f3c313c2..c89d16b33 100644 --- a/crates/tsv_ts/src/printer/statements/class.rs +++ b/crates/tsv_ts/src/printer/statements/class.rs @@ -554,9 +554,7 @@ impl<'a> Printer<'a> { .map(|v| v.span().end) .or_else(|| prop.type_annotation.as_ref().map(|ta| ta.span.end)) .unwrap_or(after_modifier); - let after = self.split_separator_gap_comments(&mut parts, content_end, prop.span.end, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, content_end, prop.span.end, true); d.concat(&parts) } @@ -786,10 +784,7 @@ impl<'a> Printer<'a> { |rt| rt.span.end, ); let semicolon_pos = method.span.end.saturating_sub(1); - let after = - self.split_separator_gap_comments(&mut parts, content_end, semicolon_pos, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, content_end, semicolon_pos, true); } else { self.append_body_with_sig_comments(&mut parts, sig_end, &method.value.body); } 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 2983c3314..de7fea4b5 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 @@ -1499,6 +1499,17 @@ impl<'a> Printer<'a> { match left { internal::ForInOfLeft::VariableDeclaration(decl) => { let Some(declarator) = decl.declarations.first() else { + // A for-in/of head always binds something (`for (const of x)` is a + // parse error), so there is no declarator to bound a gap search at + // and nothing to print but the kind. `as_str()` is the joined text — + // the very thing `words()` exists to avoid, since it emits `await + // using`'s interior gap as a fixed space and would drop a comment + // authored there. Safe only because this arm is unreachable; assert + // that rather than let a future caller reach it silently. + debug_assert!( + false, + "a for-in/of variable declaration always has a declarator" + ); return d.concat(&[d.text(decl.kind.as_str()), d.text(" ")]); }; // The keyword→binding gap carries a comment (`for (const /* c */ x of y)`) diff --git a/crates/tsv_ts/src/printer/statements/control_flow/loops/while_loop.rs b/crates/tsv_ts/src/printer/statements/control_flow/loops/while_loop.rs index 0bc0f2596..508011b6c 100644 --- a/crates/tsv_ts/src/printer/statements/control_flow/loops/while_loop.rs +++ b/crates/tsv_ts/src/printer/statements/control_flow/loops/while_loop.rs @@ -212,10 +212,7 @@ impl<'a> Printer<'a> { if let Some(close) = close_paren { parts.push(d.text(")")); let semicolon_pos = stmt.span.end.saturating_sub(1); - let after = - self.split_separator_gap_comments(&mut parts, close + 1, semicolon_pos, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, close + 1, semicolon_pos, true); } else { parts.push(d.text(");")); } diff --git a/crates/tsv_ts/src/printer/statements/control_flow/try_jump.rs b/crates/tsv_ts/src/printer/statements/control_flow/try_jump.rs index f963b3652..02ba7a7e1 100644 --- a/crates/tsv_ts/src/printer/statements/control_flow/try_jump.rs +++ b/crates/tsv_ts/src/printer/statements/control_flow/try_jump.rs @@ -199,10 +199,7 @@ impl<'a> Printer<'a> { // (`break loop; /* c */`, prettier 3.9), a same-line line via `line_suffix`, // an own-line comment on its own line after. See `split_separator_gap_comments`. let semicolon_pos = span.end.saturating_sub(1); - let after = - self.split_separator_gap_comments(&mut parts, label.span.end, semicolon_pos, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, label.span.end, semicolon_pos, true); d.concat(&parts) } else { // No label: a bare keyword closed by `;`. It swallows a following explicit diff --git a/crates/tsv_ts/src/printer/statements/mod.rs b/crates/tsv_ts/src/printer/statements/mod.rs index b148706a9..d4f8c667d 100644 --- a/crates/tsv_ts/src/printer/statements/mod.rs +++ b/crates/tsv_ts/src/printer/statements/mod.rs @@ -294,9 +294,7 @@ impl<'a> Printer<'a> { // `split_separator_gap_comments`. let expr_end = stmt.expression.span().end; let semicolon_pos = stmt.span.end.saturating_sub(1); - let after = self.split_separator_gap_comments(&mut parts, expr_end, semicolon_pos, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, expr_end, semicolon_pos, true); d.concat(&parts) } @@ -359,9 +357,7 @@ impl<'a> Printer<'a> { let keyword_end = span.start + keyword.len() as u32; let semicolon_pos = span.end.saturating_sub(1); let mut parts: DocBuf = smallvec![d.text(keyword)]; - let after = self.split_separator_gap_comments(&mut parts, keyword_end, semicolon_pos, true); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, keyword_end, semicolon_pos, true); d.concat(&parts) } diff --git a/crates/tsv_ts/src/printer/statements/modules/mod.rs b/crates/tsv_ts/src/printer/statements/modules/mod.rs index 2806042e4..cb703d6cd 100644 --- a/crates/tsv_ts/src/printer/statements/modules/mod.rs +++ b/crates/tsv_ts/src/printer/statements/modules/mod.rs @@ -17,6 +17,7 @@ mod specifier_list; pub(super) use super::{Printer, build_entity_name_doc}; use crate::ast::internal; +use crate::printer::calls::PartitionedComments; use crate::printer::needs_parens::export_default_needs_parens; use smallvec::SmallVec; use smallvec::smallvec; @@ -766,26 +767,45 @@ impl<'a> Printer<'a> { self.identifier_name_doc(&decl.id), )); + // One walk of the reference: its span bounds and its doc. Built whole (rather + // than pushed piecemeal) so the `=`→reference gap below can pass it as its + // continuation — a line comment there indents the reference, and every break + // *inside* it has to land at that level too. + let (module_ref_start, ref_end, module_ref_doc) = match &decl.module_reference { + internal::TSModuleReference::ExternalModuleReference(ext) => ( + ext.span.start, + ext.span.end, + self.build_external_module_reference_doc(ext), + ), + internal::TSModuleReference::EntityName(entity) => ( + entity.span().start, + entity.span().end, + build_entity_name_doc(self, entity), + ), + }; + // identifier→`=` and `=`→module-reference gaps. Both are preserved in place // (prettier keeps them there too). Nothing else scans them — a module // reference is not an expression, so a comment glued to `require` is not // owned by any node and would be dropped if this gap didn't emit it. - let module_ref_start = match &decl.module_reference { - internal::TSModuleReference::ExternalModuleReference(ext) => ext.span.start, - internal::TSModuleReference::EntityName(entity) => entity.span().start, - }; match self.find_keyword_in_range(decl.id.span.end, module_ref_start, "=") { Some(eq_start) => { - parts.push(self.build_keyword_to_name_continuation( - decl.id.span.end, - eq_start, - d.text("="), - )); - parts.push(self.build_keyword_to_name_continuation( - eq_start + 1, - module_ref_start, - d.empty(), - )); + // Both gaps FLAT, one `indent` for the whole tail — the multi-word + // keyword rule (`build_keyword_words_doc`), which this header is the + // other instance of. Indenting per gap compounds; and the tail must be + // *inside* the indent, not a sibling after it, or the reference's own + // line breaks resolve at the outer level — leaving `require(`'s + // contents level with it and its `)` a level above. + let (before_eq, line_before) = + self.build_keyword_gap_doc(decl.id.span.end, eq_start); + let (after_eq, line_after) = + self.build_keyword_gap_doc(eq_start + 1, module_ref_start); + let tail = d.concat(&[before_eq, d.text("="), after_eq, module_ref_doc]); + parts.push(if line_before || line_after { + d.indent(tail) + } else { + tail + }); } None => { // Same landmine as `build_keyword_words_doc`'s fallback: this arm scans @@ -799,59 +819,7 @@ impl<'a> Printer<'a> { decl.id.span.end ); parts.push(d.text(" = ")); - } - } - - // module reference - match &decl.module_reference { - internal::TSModuleReference::ExternalModuleReference(ext_ref) => { - // Check for comments inside require() - expand if present - // The require() span includes `require(` at start and `)` at end - // Comments can be between `require(` and the string literal - let require_open_end = ext_ref.span.start + "require(".len() as u32; - let literal_start = ext_ref.expression.span.start; - let has_comments = self.has_line_comments_between(require_open_end, literal_start); - - if has_comments { - // Multi-line format with comments - // Build comments doc: each comment on its own line - let mut comment_parts = DocBuf::new(); - for comment in - comments_to_emit_in_range(self.comments, require_open_end, literal_start) - { - comment_parts.push(self.build_comment_doc(comment)); - comment_parts.push(d.hardline()); - } - - parts.push(d.text("require(")); - parts.push(d.indent(d.concat(&[ - d.hardline(), - d.concat(&comment_parts), - self.build_literal_doc(&ext_ref.expression), - ]))); - parts.push(d.hardline()); - parts.push(d.text(")")); - } else { - // Check for inline block comments - let has_inline_comments = - self.has_comments_to_emit_between(require_open_end, literal_start); - if has_inline_comments { - parts.push(d.text("require(")); - parts.push( - self.build_inline_comments_between_doc(require_open_end, literal_start), - ); - parts.push(self.build_literal_doc(&ext_ref.expression)); - parts.push(d.text(")")); - } else { - // Simple compact format - parts.push(d.text("require(")); - parts.push(self.build_literal_doc(&ext_ref.expression)); - parts.push(d.text(")")); - } - } - } - internal::TSModuleReference::EntityName(entity_name) => { - parts.push(build_entity_name_doc(self, entity_name)); + parts.push(module_ref_doc); } } @@ -860,17 +828,95 @@ impl<'a> Printer<'a> { // *before* the `;` (operand-attached — prettier 3.9 keeps it), while a same-line // **line** comment floats after the `;` via `line_suffix`. So this uses the // comma-style `block_after_separator: false`, not `finish_with_pre_semi`. - let ref_end = match &decl.module_reference { - internal::TSModuleReference::ExternalModuleReference(ext_ref) => ext_ref.span.end, - internal::TSModuleReference::EntityName(entity_name) => entity_name.span().end, - }; let semicolon_pos = decl.span.end.saturating_sub(1); - let after = self.split_separator_gap_comments(&mut parts, ref_end, semicolon_pos, false); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, ref_end, semicolon_pos, false); d.concat(&parts) } + /// The `require('m')` form of an import-equals module reference, as one doc. + /// + /// **Both** in-paren gaps are emitted — `require(`→literal and literal→`)`. Nothing + /// else scans either: a module reference is not an expression, so no node owns a + /// comment written here and a gap this printer skips is a gap whose comment is + /// dropped outright (silent content loss, which is what the literal→`)` gap used to + /// do). + /// + /// The close gap takes the shape a call's **last argument** already has + /// ([`PartitionedComments::emit_last_arg_comments`]): a same-line comment trails the + /// literal, an own-line one dangles above the `)`. There is no trailing comma to + /// split around (`trailingComma: 'none'`), which is exactly that helper's premise. + fn build_external_module_reference_doc( + &self, + ext_ref: &internal::TSExternalModuleReference<'_>, + ) -> DocId { + let d = self.d(); + let require_open_end = ext_ref.span.start + "require(".len() as u32; + let literal_start = ext_ref.expression.span.start; + let close_paren = ext_ref.span.end.saturating_sub(1); + + // The literal plus whatever trails it inside the parens. + let close = PartitionedComments::new( + self.comments, + self.line_breaks, + ext_ref.expression.span.end, + close_paren, + ); + let mut value: DocBuf = smallvec![self.build_literal_doc(&ext_ref.expression)]; + close.emit_last_arg_comments(&mut value, self); + let value_doc = d.concat(&value); + + // A line comment runs to EOL and an own-line comment must keep its own line, so + // either side having one forces the parens open; a lone same-line block stays + // inline. Same rule as the dynamic-import parens (`calls/import_expr.rs`). + let open_has_line = self.has_line_comments_between(require_open_end, literal_start); + let open_has_any = self.has_comments_to_emit_between(require_open_end, literal_start); + let close_forces_break = close.has_trailing_line() || !close.leading.is_empty(); + + if open_has_line || close_forces_break { + let mut inner = DocBuf::new(); + if open_has_line { + // Each open-gap comment on its own line — a line comment there can't + // share one with the literal. + for comment in + comments_to_emit_in_range(self.comments, require_open_end, literal_start) + { + inner.push(self.build_comment_doc(comment)); + inner.push(d.hardline()); + } + } else if open_has_any { + // Only the close gap forced the break, so an open-gap block keeps + // hugging the literal rather than being relocated to its own line. + inner.push(self.build_inline_comments_between_doc_trailing_space( + require_open_end, + literal_start, + )); + } + inner.push(value_doc); + return d.concat(&[ + d.text("require("), + d.indent(d.concat(&[d.hardline(), d.concat(&inner)])), + d.hardline(), + d.text(")"), + ]); + } + if open_has_any { + // The comment hugs the `(` and keeps its space on the literal's side + // (`require(/* c */ 'm')`) — the dotted pair's after-`.` rule, and prettier's. + // An author who wrote it *before* the `(` lands here too: both formatters move + // it inside, so the two authorings converge (`unformatted_before_paren`). + return d.concat(&[ + d.text("require("), + self.build_inline_comments_between_doc_trailing_space( + require_open_end, + literal_start, + ), + value_doc, + d.text(")"), + ]); + } + d.concat(&[d.text("require("), value_doc, d.text(")")]) + } + /// `export as namespace Foo;` — TypeScript UMD global export declaration. pub(super) fn build_namespace_export_declaration_doc( &self, @@ -890,10 +936,7 @@ impl<'a> Printer<'a> { // Trailing comment between the name and `;` (mirrors `export =` / import-equals): // a same-line block comment stays before `;`, a line comment floats after it. let semicolon_pos = decl.span.end.saturating_sub(1); - let after = - self.split_separator_gap_comments(&mut parts, decl.id.span.end, semicolon_pos, false); - parts.push(d.text(";")); - parts.extend(after); + self.push_semicolon_with_gap_comments(&mut parts, decl.id.span.end, semicolon_pos, false); d.concat(&parts) } } diff --git a/crates/tsv_ts/src/printer/statements/type_declarations.rs b/crates/tsv_ts/src/printer/statements/type_declarations.rs index cc8737279..494ebcd08 100644 --- a/crates/tsv_ts/src/printer/statements/type_declarations.rs +++ b/crates/tsv_ts/src/printer/statements/type_declarations.rs @@ -1153,13 +1153,29 @@ impl<'a> Printer<'a> { ), ); } - match &decl.id { - internal::TSModuleName::Identifier(id) => { - parts.push(self.identifier_name_doc(id)); - } - internal::TSModuleName::Literal(lit) => { - parts.push(self.build_literal_doc(lit)); + let name_doc = match &decl.id { + internal::TSModuleName::Identifier(id) => self.identifier_name_doc(id), + internal::TSModuleName::Literal(lit) => self.build_literal_doc(lit), + }; + // A dotted namespace (`namespace Outer.Inner {}`) pairs this name with the + // nested one, and both gaps around that `.` are positions an author can + // comment in — so the shared dotted-pair printer emits the name, the dot and + // the gaps together (it needs the left doc, hence holding it until here). + // Pushing name + `d.text(".")` scans neither gap and drops what's in it. + match &decl.body { + Some(internal::TSModuleDeclarationBody::TSModuleDeclaration(nested)) => { + let name_end = match &decl.id { + internal::TSModuleName::Identifier(id) => id.span.end, + internal::TSModuleName::Literal(lit) => lit.span.end, + }; + parts.push(self.build_dotted_pair_doc( + name_doc, + self.build_module_declaration_doc_inner(nested, false), + name_end, + nested.span.start, + )); } + _ => parts.push(name_doc), } } @@ -1223,11 +1239,11 @@ impl<'a> Printer<'a> { parts.push(d.text("}")); } } - Some(internal::TSModuleDeclarationBody::TSModuleDeclaration(nested)) => { - // Nested namespace: `namespace Outer.Inner { }` - // Print as `Outer.Inner` (dot-separated) - parts.push(d.text(".")); - parts.push(self.build_module_declaration_doc_inner(nested, false)); + Some(internal::TSModuleDeclarationBody::TSModuleDeclaration(_)) => { + // A dotted namespace (`namespace Outer.Inner {}`) is emitted with the + // name above — the `.` and both its gaps belong to that pair, and only + // that path holds the left doc the shared printer needs. A `global` + // namespace can't be dotted, so the name path always ran. } None => { // Shorthand ambient module: `declare module 'name';` diff --git a/docs/conformance_prettier.md b/docs/conformance_prettier.md index 9c9419e66..06c562e86 100644 --- a/docs/conformance_prettier.md +++ b/docs/conformance_prettier.md @@ -767,8 +767,10 @@ Prettier moves comments between syntactic boundaries into adjacent blocks, paren - `export`→`=` (export assignment) → After the `=`, leading the value (`export = /* c */ value`); tsv keeps it after `export`, matching the `export`→`const`/`function` gap both formatters preserve. The operand-side gap agrees in both — [export_equals_keyword_comment](../tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/) - `export as namespace` keyword-interior gaps (`export`→`as`, `as`→`namespace`) → Both relocated past the whole keyword and stacked before the name (`export as namespace /* c1 */ /* c2 */ Foo`), collapsing two distinct positions into one; tsv keeps each on its authored side. The `namespace`→name gap agrees in both (regular [export_as_namespace_name_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_name_comment/)) — [export_as_namespace_keyword_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/) - Meta-property dot gaps (line) → Out of the meta property entirely: past the `;` for `new.target` (`return new.target; // c`), or after `.meta` for `import.meta`; tsv keeps it in the gap and continues `.property` one level down. Block comments in the same gaps agree in both (regular [dot_gap_comments](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_comments/)) — [dot_gap_line_comment](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/) +- Qualified-name dot gaps (line) → Past the `;` (`let a: ns.Type; // c`); tsv keeps it in the gap and continues the rest of the name one level down. The meta property's twin — same `name` `.` `name` shape, one shared printer — so it reaches type references, interface heritage, and import-equals module references alike. Block comments in the same gaps agree in both (regular [qualified_name_dot_gap_comments](../tests/fixtures/typescript/types/qualified_name_dot_gap_comments/)) — [qualified_name_dot_gap_line_comment](../tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/) - `export as namespace` keyword gaps (line) → All relocated past the whole keyword and stacked flush before the name (`export as namespace // c1⏎// c2⏎// c3⏎Foo`); tsv keeps each on its authored side, continuing the header at **one** indent level rather than a staircase — [export_as_namespace_line_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/) - Import-equals `import`→`type` → Binding side of `type` (`import type /* c */ C = require('./c')`, the same rule as a default import); tsv preserves. The **only** diverging gap of that header — the other four agree in both and are pinned by the regular [equals_header_comment](../tests/fixtures/typescript/modules/imports/equals_header_comment/) — [equals_type_keyword_comment](../tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/) +- Import-equals header gaps (line) → Relocated past the `=` to trail the whole statement (`A = require('./a'); // c3`) with the rest stacked flush below it, and the `=`→reference gap's comment onto the `require(` line; tsv keeps each on its authored side, continuing the header at **one** indent level. The module reference is the only such tail that can break *internally*, so this also pins that its own breaks nest **inside** that level — its contents one deeper than `require(`, its `)` back at the reference's level. Prettier is non-idempotent on its own output here (a second pass moves the `require(` comment again; `audit_signature.txt` pins the chain) — [equals_header_line_comment](../tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/) - `export default`→first decorator, comment **glued** to `@dec` → Relocated back past `default` (`export default /* c */`), then one blank line added per pass, saturating at two (Prettier non-idempotent; `output_prettier.*` is its first pass, `audit_signature.txt` pins the chain); tsv keeps it glued to `@dec`, where the parser bound it. The complementary own-line authoring is *not* glued, so it is emitted by the keyword→decorator gap instead — both authorings preserved — [default_decorator_lead_comment](../tests/fixtures/typescript/modules/exports/default_decorator_lead_comment_prettier_divergence/) - Import binding to `from` (line) → After `;` — [from_comment](../tests/fixtures/typescript/modules/imports/from_comment_prettier_divergence/) - Import specifiers to `from` → Into the specifier braces — [from_comment](../tests/fixtures/typescript/modules/imports/from_comment_prettier_divergence/) @@ -1009,6 +1011,14 @@ compounds, and the staircase is not merely deep — the keyword→value gap is e own level, so the keyword's last word would sit *below* the value following it. Only a three-word keyword (`export as namespace`, the import-equals header) can show the difference. +The tail has to be *inside* that one `indent`, not a sibling after it. The distinction is invisible +for a tail that can't break — a bare name renders the same either way, because the gap's hardline +has already written the indent the name lands on. It shows only when the tail breaks **internally**, +and the import-equals module reference is the one that can: as a sibling, `require(`'s own line +breaks resolve at the *outer* level, leaving its contents level with it and its `)` a level above +the `require(` it closes +([equals_header_line_comment](../tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/)). + Not every multi-word keyword admits an interior comment: `declare // c⏎const x` ASI-splits into two statements, and `await /* c⏎ */ using` demotes (the comment *is* a `LineTerminator`, and `await [no LT] using` requires none) — see the exclusions under [Declaration- and module-header @@ -1017,15 +1027,21 @@ line-comment continuation indent](#comment-relocation). The same rule covers a keyword whose second word is a punctuator (`export /* c */ = value`) and multi-gap headers (`export /* c1 */ as /* c2 */ namespace Foo`, the import-equals header). -It reaches past keywords, too: a **meta property** is the same shape — `new` `.` `target` — and both +It reaches past keywords, too. A **meta property** is the same shape — `new` `.` `target` — and both gaps around its dot are positions an author can mean (`new /* c */.target`, `new./* c */ target`). -tsv keeps each on its authored side, which is what prettier does for a block comment; a *line* -comment there is a divergence -([dot_gap_line_comment](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/)). -That case is worth knowing for a different reason: the detector for this class — a `d.text` literal -with an *interior* space — **cannot see it**, because the joining literal is `"."`. The class is +So is a **qualified name** (`ns` `.` `Type`), which is why one printer (`build_dotted_pair_doc`) +serves both: neither can regrow the hole independently, and through the qualified name it covers +type references, interface heritage, and import-equals module references at once. tsv keeps each +comment on its authored side, which is what prettier does for a block comment; a *line* comment +there is a divergence +([dot_gap_line_comment](../tests/fixtures/typescript/expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/), +[qualified_name_dot_gap_line_comment](../tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/)). + +Those two are worth knowing for a different reason: the detector for this class — a `d.text` literal +with an *interior* space — **cannot see them**, because the joining literal is `"."`. The class is "a header that concatenates fixed pieces without scanning the gaps between them"; the interior-space grep is a proxy for it, not a definition of it, so a punctuator-joined header needs finding by hand. +Both were found that way, and both had been **dropping** every comment in both gaps. **Preserving is not the same as diverging, and most of these gaps are neither.** Prettier's treatment is not uniform: it relocates the `export as namespace` keyword-interior gaps, but @@ -1047,7 +1063,9 @@ Catalogued in [§Comment relocation](#comment-relocation): [export_equals_keyword_comment](../tests/fixtures/typescript/modules/exports/export_equals_keyword_comment_prettier_divergence/), [export_as_namespace_keyword_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_keyword_comment_prettier_divergence/), [export_as_namespace_line_comment](../tests/fixtures/typescript/modules/exports/export_as_namespace_line_comment_prettier_divergence/), -[equals_type_keyword_comment](../tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/). +[equals_type_keyword_comment](../tests/fixtures/typescript/modules/imports/equals_type_keyword_comment_prettier_divergence/), +[equals_header_line_comment](../tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/), +[qualified_name_dot_gap_line_comment](../tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/). #### JSDoc / paren semantics diff --git a/tests/fixtures/typescript/declarations/namespace/nested_dot_comment/expected.json b/tests/fixtures/typescript/declarations/namespace/nested_dot_comment/expected.json new file mode 100644 index 000000000..d12a5f43b --- /dev/null +++ b/tests/fixtures/typescript/declarations/namespace/nested_dot_comment/expected.json @@ -0,0 +1,755 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 349, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment before the `.` of a dotted namespace — stays there", + "start": 20, + "end": 87, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 68 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 101, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 13 + }, + "end": { + "line": 3, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 141, + "end": 192, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 52 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 206, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 13 + }, + "end": { + "line": 8, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Both gaps in a deeper chain", + "start": 246, + "end": 276, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 31 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 290, + "end": 298, + "loc": { + "start": { + "line": 13, + "column": 13 + }, + "end": { + "line": 13, + "column": 21 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 301, + "end": 309, + "loc": { + "start": { + "line": 13, + "column": 24 + }, + "end": { + "line": 13, + "column": 32 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 348, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 339, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 16, + "column": 9 + } + }, + "body": [ + { + "type": "TSModuleDeclaration", + "start": 89, + "end": 138, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 99, + "end": 100, + "loc": { + "start": { + "line": 3, + "column": 11 + }, + "end": { + "line": 3, + "column": 12 + } + }, + "name": "A", + "trailingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 101, + "end": 109 + } + ] + }, + "body": { + "type": "TSModuleDeclaration", + "start": 110, + "end": 138, + "loc": { + "start": { + "line": 3, + "column": 22 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 110, + "end": 111, + "loc": { + "start": { + "line": 3, + "column": 22 + }, + "end": { + "line": 3, + "column": 23 + } + }, + "name": "B" + }, + "body": { + "type": "TSModuleBlock", + "start": 112, + "end": 138, + "loc": { + "start": { + "line": 3, + "column": 24 + }, + "end": { + "line": 5, + "column": 2 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 116, + "end": 135, + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 21 + } + }, + "exportKind": "value", + "declaration": { + "type": "VariableDeclaration", + "start": 123, + "end": 135, + "loc": { + "start": { + "line": 4, + "column": 9 + }, + "end": { + "line": 4, + "column": 21 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 129, + "end": 134, + "loc": { + "start": { + "line": 4, + "column": 15 + }, + "end": { + "line": 4, + "column": 20 + } + }, + "id": { + "type": "Identifier", + "start": 129, + "end": 130, + "loc": { + "start": { + "line": 4, + "column": 15 + }, + "end": { + "line": 4, + "column": 16 + } + }, + "name": "a" + }, + "init": { + "type": "Literal", + "start": 133, + "end": 134, + "loc": { + "start": { + "line": 4, + "column": 19 + }, + "end": { + "line": 4, + "column": 20 + } + }, + "value": 1, + "raw": "1" + } + } + ], + "kind": "const" + }, + "specifiers": [], + "source": null + } + ] + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment before the `.` of a dotted namespace — stays there", + "start": 20, + "end": 87 + } + ] + }, + { + "type": "TSModuleDeclaration", + "start": 194, + "end": 243, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 204, + "end": 205, + "loc": { + "start": { + "line": 8, + "column": 11 + }, + "end": { + "line": 8, + "column": 12 + } + }, + "name": "C" + }, + "body": { + "type": "TSModuleDeclaration", + "start": 215, + "end": 243, + "loc": { + "start": { + "line": 8, + "column": 22 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 215, + "end": 216, + "loc": { + "start": { + "line": 8, + "column": 22 + }, + "end": { + "line": 8, + "column": 23 + } + }, + "name": "D" + }, + "body": { + "type": "TSModuleBlock", + "start": 217, + "end": 243, + "loc": { + "start": { + "line": 8, + "column": 24 + }, + "end": { + "line": 10, + "column": 2 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 221, + "end": 240, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 21 + } + }, + "exportKind": "value", + "declaration": { + "type": "VariableDeclaration", + "start": 228, + "end": 240, + "loc": { + "start": { + "line": 9, + "column": 9 + }, + "end": { + "line": 9, + "column": 21 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 234, + "end": 239, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 20 + } + }, + "id": { + "type": "Identifier", + "start": 234, + "end": 235, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 16 + } + }, + "name": "a" + }, + "init": { + "type": "Literal", + "start": 238, + "end": 239, + "loc": { + "start": { + "line": 9, + "column": 19 + }, + "end": { + "line": 9, + "column": 20 + } + }, + "value": 1, + "raw": "1" + } + } + ], + "kind": "const" + }, + "specifiers": [], + "source": null + } + ] + }, + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 206, + "end": 214 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 141, + "end": 192 + } + ] + }, + { + "type": "TSModuleDeclaration", + "start": 278, + "end": 338, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 15, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 288, + "end": 289, + "loc": { + "start": { + "line": 13, + "column": 11 + }, + "end": { + "line": 13, + "column": 12 + } + }, + "name": "E" + }, + "body": { + "type": "TSModuleDeclaration", + "start": 299, + "end": 338, + "loc": { + "start": { + "line": 13, + "column": 22 + }, + "end": { + "line": 15, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 299, + "end": 300, + "loc": { + "start": { + "line": 13, + "column": 22 + }, + "end": { + "line": 13, + "column": 23 + } + }, + "name": "F", + "trailingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 301, + "end": 309 + } + ] + }, + "body": { + "type": "TSModuleDeclaration", + "start": 310, + "end": 338, + "loc": { + "start": { + "line": 13, + "column": 33 + }, + "end": { + "line": 15, + "column": 2 + } + }, + "id": { + "type": "Identifier", + "start": 310, + "end": 311, + "loc": { + "start": { + "line": 13, + "column": 33 + }, + "end": { + "line": 13, + "column": 34 + } + }, + "name": "G" + }, + "body": { + "type": "TSModuleBlock", + "start": 312, + "end": 338, + "loc": { + "start": { + "line": 13, + "column": 35 + }, + "end": { + "line": 15, + "column": 2 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 316, + "end": 335, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 21 + } + }, + "exportKind": "value", + "declaration": { + "type": "VariableDeclaration", + "start": 323, + "end": 335, + "loc": { + "start": { + "line": 14, + "column": 9 + }, + "end": { + "line": 14, + "column": 21 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 329, + "end": 334, + "loc": { + "start": { + "line": 14, + "column": 15 + }, + "end": { + "line": 14, + "column": 20 + } + }, + "id": { + "type": "Identifier", + "start": 329, + "end": 330, + "loc": { + "start": { + "line": 14, + "column": 15 + }, + "end": { + "line": 14, + "column": 16 + } + }, + "name": "a" + }, + "init": { + "type": "Literal", + "start": 333, + "end": 334, + "loc": { + "start": { + "line": 14, + "column": 19 + }, + "end": { + "line": 14, + "column": 20 + } + }, + "value": 1, + "raw": "1" + } + } + ], + "kind": "const" + }, + "specifiers": [], + "source": null + } + ] + } + }, + "leadingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 290, + "end": 298 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Both gaps in a deeper chain", + "start": 246, + "end": 276 + } + ] + } + ], + "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/declarations/namespace/nested_dot_comment/input.svelte b/tests/fixtures/typescript/declarations/namespace/nested_dot_comment/input.svelte new file mode 100644 index 000000000..c4c0a2eda --- /dev/null +++ b/tests/fixtures/typescript/declarations/namespace/nested_dot_comment/input.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/expressions/calls/test_function_dot_comment/expected.json b/tests/fixtures/typescript/expressions/calls/test_function_dot_comment/expected.json new file mode 100644 index 000000000..1e09ecc59 --- /dev/null +++ b/tests/fixtures/typescript/expressions/calls/test_function_dot_comment/expected.json @@ -0,0 +1,775 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 359, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment before the `.` of a test-pattern chain — stays there", + "start": 20, + "end": 89, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 70 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 100, + "end": 108, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 18 + } + } + }, + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 132, + "end": 183, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 52 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 190, + "end": 198, + "loc": { + "start": { + "line": 6, + "column": 6 + }, + "end": { + "line": 6, + "column": 14 + } + } + }, + { + "type": "Line", + "value": " The other recognized variants take the same two gaps", + "start": 222, + "end": 277, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 56 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 284, + "end": 292, + "loc": { + "start": { + "line": 9, + "column": 6 + }, + "end": { + "line": 9, + "column": 14 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 319, + "end": 327, + "loc": { + "start": { + "line": 10, + "column": 4 + }, + "end": { + "line": 10, + "column": 12 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 358, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 349, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 11, + "column": 9 + } + }, + "body": [ + { + "type": "ExpressionStatement", + "start": 91, + "end": 129, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 39 + } + }, + "expression": { + "type": "CallExpression", + "start": 91, + "end": 128, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 38 + } + }, + "callee": { + "type": "MemberExpression", + "start": 91, + "end": 113, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 23 + } + }, + "object": { + "type": "Identifier", + "start": 91, + "end": 99, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 9 + } + }, + "name": "describe", + "trailingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 100, + "end": 108 + } + ] + }, + "property": { + "type": "Identifier", + "start": 109, + "end": 113, + "loc": { + "start": { + "line": 3, + "column": 19 + }, + "end": { + "line": 3, + "column": 23 + } + }, + "name": "only" + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "Literal", + "start": 114, + "end": 117, + "loc": { + "start": { + "line": 3, + "column": 24 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "value": "a", + "raw": "'a'" + }, + { + "type": "ArrowFunctionExpression", + "start": 119, + "end": 127, + "loc": { + "start": { + "line": 3, + "column": 29 + }, + "end": { + "line": 3, + "column": 37 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 125, + "end": 127, + "loc": { + "start": { + "line": 3, + "column": 35 + }, + "end": { + "line": 3, + "column": 37 + } + }, + "body": [] + } + } + ], + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment before the `.` of a test-pattern chain — stays there", + "start": 20, + "end": 89 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 185, + "end": 219, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 35 + } + }, + "expression": { + "type": "CallExpression", + "start": 185, + "end": 218, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 34 + } + }, + "callee": { + "type": "MemberExpression", + "start": 185, + "end": 203, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 19 + } + }, + "object": { + "type": "Identifier", + "start": 185, + "end": 189, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 5 + } + }, + "name": "test" + }, + "property": { + "type": "Identifier", + "start": 199, + "end": 203, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 6, + "column": 19 + } + }, + "name": "skip", + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 190, + "end": 198 + } + ] + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "Literal", + "start": 204, + "end": 207, + "loc": { + "start": { + "line": 6, + "column": 20 + }, + "end": { + "line": 6, + "column": 23 + } + }, + "value": "b", + "raw": "'b'" + }, + { + "type": "ArrowFunctionExpression", + "start": 209, + "end": 217, + "loc": { + "start": { + "line": 6, + "column": 25 + }, + "end": { + "line": 6, + "column": 33 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 215, + "end": 217, + "loc": { + "start": { + "line": 6, + "column": 31 + }, + "end": { + "line": 6, + "column": 33 + } + }, + "body": [] + } + } + ], + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 132, + "end": 183 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 279, + "end": 314, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 36 + } + }, + "expression": { + "type": "CallExpression", + "start": 279, + "end": 313, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 35 + } + }, + "callee": { + "type": "MemberExpression", + "start": 279, + "end": 298, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 20 + } + }, + "object": { + "type": "Identifier", + "start": 279, + "end": 283, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "test" + }, + "property": { + "type": "Identifier", + "start": 293, + "end": 298, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 20 + } + }, + "name": "fixme", + "leadingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 284, + "end": 292 + } + ] + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "Literal", + "start": 299, + "end": 302, + "loc": { + "start": { + "line": 9, + "column": 21 + }, + "end": { + "line": 9, + "column": 24 + } + }, + "value": "c", + "raw": "'c'" + }, + { + "type": "ArrowFunctionExpression", + "start": 304, + "end": 312, + "loc": { + "start": { + "line": 9, + "column": 26 + }, + "end": { + "line": 9, + "column": 34 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 310, + "end": 312, + "loc": { + "start": { + "line": 9, + "column": 32 + }, + "end": { + "line": 9, + "column": 34 + } + }, + "body": [] + } + } + ], + "optional": false + }, + "leadingComments": [ + { + "type": "Line", + "value": " The other recognized variants take the same two gaps", + "start": 222, + "end": 277 + } + ] + }, + { + "type": "ExpressionStatement", + "start": 316, + "end": 348, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 33 + } + }, + "expression": { + "type": "CallExpression", + "start": 316, + "end": 347, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 32 + } + }, + "callee": { + "type": "MemberExpression", + "start": 316, + "end": 332, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 17 + } + }, + "object": { + "type": "Identifier", + "start": 316, + "end": 318, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 3 + } + }, + "name": "it", + "trailingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 319, + "end": 327 + } + ] + }, + "property": { + "type": "Identifier", + "start": 328, + "end": 332, + "loc": { + "start": { + "line": 10, + "column": 13 + }, + "end": { + "line": 10, + "column": 17 + } + }, + "name": "only" + }, + "computed": false, + "optional": false + }, + "arguments": [ + { + "type": "Literal", + "start": 333, + "end": 336, + "loc": { + "start": { + "line": 10, + "column": 18 + }, + "end": { + "line": 10, + "column": 21 + } + }, + "value": "d", + "raw": "'d'" + }, + { + "type": "ArrowFunctionExpression", + "start": 338, + "end": 346, + "loc": { + "start": { + "line": 10, + "column": 23 + }, + "end": { + "line": 10, + "column": 31 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 344, + "end": 346, + "loc": { + "start": { + "line": 10, + "column": 29 + }, + "end": { + "line": 10, + "column": 31 + } + }, + "body": [] + } + } + ], + "optional": false + } + } + ], + "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/expressions/calls/test_function_dot_comment/input.svelte b/tests/fixtures/typescript/expressions/calls/test_function_dot_comment/input.svelte new file mode 100644 index 000000000..dcdcac11a --- /dev/null +++ b/tests/fixtures/typescript/expressions/calls/test_function_dot_comment/input.svelte @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/README.md b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/README.md new file mode 100644 index 000000000..68c54aafe --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/README.md @@ -0,0 +1,34 @@ +# Divergence: import-equals header line comments (preserve, one indent level) + +A *line* comment in the gaps of the import-equals header. tsv keeps each where the author wrote it; +prettier **relocates** them — `// c3` past the `=` to trail the whole statement, `// c4` down onto +its own line, and `// c` in the `=`→module-reference gap onto the `require(` line. + +```ts +// tsv (preserve) // prettier (relocate) +export // c1 export // c1 + import // c2 import // c2 + A // c3 A = require('./a'); // c3 + = // c4 // c4 + require('./a'); +``` + +The line-comment counterpart of [equals_header_comment](../equals_header_comment/) (the +block-comment form, a *regular* fixture — prettier preserves four of the five header gaps, so there +is no opinion to state there). + +Like [export_as_namespace_line_comment](../../exports/export_as_namespace_line_comment_prettier_divergence/), +it pins the **rendering**: a header with several broken gaps continues at **one** indent level, not +a staircase. These two headers are the only three-word ones, so no other keyword can show it. + +What this fixture pins that its `export as namespace` sibling **cannot**: the module reference is +the one such tail that can break *internally*. Its own line breaks have to nest inside the +continuation level the gap established — a reference sitting at one level with its contents at that +same level, and its `)` a level *above* the `require(` it closes, would be wrong. So the gaps are +emitted flat and the whole tail — reference included — is wrapped once. + +Prettier is **non-idempotent** on its own output here: a second pass moves `// c` off the `require(` +line onto its own line, which is why `output_prettier.svelte` carries an `audit_signature.txt`. + +See [conformance_prettier.md §Comments inside a multi-word keyword](../../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +and [§Comment relocation](../../../../../../docs/conformance_prettier.md#comment-relocation). diff --git a/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/audit_signature.txt b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/audit_signature.txt new file mode 100644 index 000000000..8b9f6fb5a --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/audit_signature.txt @@ -0,0 +1,27 @@ +# Auto-generated prettier chain signature. Do not edit manually. +# Regenerate: deno task fixtures:update:formatted +# +# Each %%PASS=N%% section is exactly prettier^N(output_prettier.). +# This file exists when prettier is non-idempotent on output_prettier.* — it pins +# the full chain so the audit recognizes the case and the validator catches drift. +# See docs/fixture_overview.md (rule F4) and crates/tsv_debug/src/fixtures/audit_signature.rs. + +%%PASS=2 (fixed point)%% + diff --git a/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..b556133f1 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/expected.json @@ -0,0 +1,523 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 510, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " A line comment in each gap of the import-equals header — each stays where the", + "start": 20, + "end": 100, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " author wrote it, and the whole header continues at ONE indent level", + "start": 102, + "end": 172, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 71 + } + } + }, + { + "type": "Line", + "value": " c1", + "start": 181, + "end": 186, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 13 + } + } + }, + { + "type": "Line", + "value": " c2", + "start": 196, + "end": 201, + "loc": { + "start": { + "line": 5, + "column": 9 + }, + "end": { + "line": 5, + "column": 14 + } + } + }, + { + "type": "Line", + "value": " c3", + "start": 206, + "end": 211, + "loc": { + "start": { + "line": 6, + "column": 4 + }, + "end": { + "line": 6, + "column": 9 + } + } + }, + { + "type": "Line", + "value": " c4", + "start": 216, + "end": 221, + "loc": { + "start": { + "line": 7, + "column": 4 + }, + "end": { + "line": 7, + "column": 9 + } + } + }, + { + "type": "Line", + "value": " The module reference continues one level down, and its own breaks nest inside", + "start": 242, + "end": 322, + "loc": { + "start": { + "line": 10, + "column": 1 + }, + "end": { + "line": 10, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " that level — its contents one deeper than `require(`, its `)` back at the", + "start": 324, + "end": 400, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 77 + } + } + }, + { + "type": "Line", + "value": " reference's own level", + "start": 402, + "end": 426, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 25 + } + } + }, + { + "type": "Line", + "value": " c5", + "start": 439, + "end": 444, + "loc": { + "start": { + "line": 13, + "column": 12 + }, + "end": { + "line": 13, + "column": 17 + } + } + }, + { + "type": "Line", + "value": " c6", + "start": 459, + "end": 464, + "loc": { + "start": { + "line": 15, + "column": 3 + }, + "end": { + "line": 15, + "column": 8 + } + } + }, + { + "type": "Line", + "value": " c7", + "start": 474, + "end": 479, + "loc": { + "start": { + "line": 16, + "column": 9 + }, + "end": { + "line": 16, + "column": 14 + } + } + }, + { + "type": "Line", + "value": " c8", + "start": 483, + "end": 488, + "loc": { + "start": { + "line": 17, + "column": 3 + }, + "end": { + "line": 17, + "column": 8 + } + } + }, + { + "type": "Line", + "value": " c9", + "start": 494, + "end": 499, + "loc": { + "start": { + "line": 18, + "column": 5 + }, + "end": { + "line": 18, + "column": 10 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 509, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 500, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 19, + "column": 9 + } + }, + "body": [ + { + "type": "TSImportEqualsDeclaration", + "start": 174, + "end": 239, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 8, + "column": 17 + } + }, + "importKind": "value", + "isExport": true, + "id": { + "type": "Identifier", + "start": 204, + "end": 205, + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 3 + } + }, + "name": "A", + "leadingComments": [ + { + "type": "Line", + "value": " c1", + "start": 181, + "end": 186 + }, + { + "type": "Line", + "value": " c2", + "start": 196, + "end": 201 + } + ], + "trailingComments": [ + { + "type": "Line", + "value": " c3", + "start": 206, + "end": 211 + } + ] + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 224, + "end": 238, + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 16 + } + }, + "expression": { + "type": "Literal", + "start": 232, + "end": 237, + "loc": { + "start": { + "line": 8, + "column": 10 + }, + "end": { + "line": 8, + "column": 15 + } + }, + "value": "./a", + "raw": "'./a'" + }, + "leadingComments": [ + { + "type": "Line", + "value": " c4", + "start": 216, + "end": 221 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " A line comment in each gap of the import-equals header — each stays where the", + "start": 20, + "end": 100 + }, + { + "type": "Line", + "value": " author wrote it, and the whole header continues at ONE indent level", + "start": 102, + "end": 172 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 428, + "end": 493, + "loc": { + "start": { + "line": 13, + "column": 1 + }, + "end": { + "line": 18, + "column": 4 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 435, + "end": 436, + "loc": { + "start": { + "line": 13, + "column": 8 + }, + "end": { + "line": 13, + "column": 9 + } + }, + "name": "B" + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 447, + "end": 492, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 18, + "column": 3 + } + }, + "expression": { + "type": "Literal", + "start": 468, + "end": 473, + "loc": { + "start": { + "line": 16, + "column": 3 + }, + "end": { + "line": 16, + "column": 8 + } + }, + "value": "./b", + "raw": "'./b'", + "leadingComments": [ + { + "type": "Line", + "value": " c6", + "start": 459, + "end": 464 + } + ], + "trailingComments": [ + { + "type": "Line", + "value": " c7", + "start": 474, + "end": 479 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " c5", + "start": 439, + "end": 444 + } + ] + }, + "leadingComments": [ + { + "type": "Line", + "value": " The module reference continues one level down, and its own breaks nest inside", + "start": 242, + "end": 322 + }, + { + "type": "Line", + "value": " that level — its contents one deeper than `require(`, its `)` back at the", + "start": 324, + "end": 400 + }, + { + "type": "Line", + "value": " reference's own level", + "start": 402, + "end": 426 + } + ], + "trailingComments": [ + { + "type": "Line", + "value": " c8", + "start": 483, + "end": 488 + }, + { + "type": "Line", + "value": " c9", + "start": 494, + "end": 499 + } + ] + } + ], + "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/imports/equals_header_line_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..d2fdc480a --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/input.svelte @@ -0,0 +1,19 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..c0ba94ce0 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_header_line_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,17 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/expected.json b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/expected.json new file mode 100644 index 000000000..9104e1d93 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/expected.json @@ -0,0 +1,315 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 255, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment inside `require(` — hugs the paren, keeps its space on the literal's side", + "start": 20, + "end": 110, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 91 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 131, + "end": 139, + "loc": { + "start": { + "line": 3, + "column": 20 + }, + "end": { + "line": 3, + "column": 28 + } + } + }, + { + "type": "Line", + "value": " Two comments in the same gap keep their order", + "start": 150, + "end": 198, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 49 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 219, + "end": 227, + "loc": { + "start": { + "line": 6, + "column": 20 + }, + "end": { + "line": 6, + "column": 28 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 228, + "end": 236, + "loc": { + "start": { + "line": 6, + "column": 29 + }, + "end": { + "line": 6, + "column": 37 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 254, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 245, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 7, + "column": 9 + } + }, + "body": [ + { + "type": "TSImportEqualsDeclaration", + "start": 112, + "end": 147, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 36 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 119, + "end": 120, + "loc": { + "start": { + "line": 3, + "column": 8 + }, + "end": { + "line": 3, + "column": 9 + } + }, + "name": "A" + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 123, + "end": 146, + "loc": { + "start": { + "line": 3, + "column": 12 + }, + "end": { + "line": 3, + "column": 35 + } + }, + "expression": { + "type": "Literal", + "start": 140, + "end": 145, + "loc": { + "start": { + "line": 3, + "column": 29 + }, + "end": { + "line": 3, + "column": 34 + } + }, + "value": "./a", + "raw": "'./a'", + "leadingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 131, + "end": 139 + } + ] + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment inside `require(` — hugs the paren, keeps its space on the literal's side", + "start": 20, + "end": 110 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 200, + "end": 244, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 45 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 207, + "end": 208, + "loc": { + "start": { + "line": 6, + "column": 8 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "name": "B" + }, + "moduleReference": { + "type": "TSExternalModuleReference", + "start": 211, + "end": 243, + "loc": { + "start": { + "line": 6, + "column": 12 + }, + "end": { + "line": 6, + "column": 44 + } + }, + "expression": { + "type": "Literal", + "start": 237, + "end": 242, + "loc": { + "start": { + "line": 6, + "column": 38 + }, + "end": { + "line": 6, + "column": 43 + } + }, + "value": "./b", + "raw": "'./b'", + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 219, + "end": 227 + }, + { + "type": "Block", + "value": " c3 ", + "start": 228, + "end": 236 + } + ] + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Two comments in the same gap keep their order", + "start": 150, + "end": 198 + } + ] + } + ], + "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/imports/equals_require_paren_comment/input.svelte b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/input.svelte new file mode 100644 index 000000000..2030660cd --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/input.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/unformatted_before_paren.svelte b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/unformatted_before_paren.svelte new file mode 100644 index 000000000..903847064 --- /dev/null +++ b/tests/fixtures/typescript/modules/imports/equals_require_paren_comment/unformatted_before_paren.svelte @@ -0,0 +1,7 @@ + diff --git a/tests/fixtures/typescript/types/qualified_name_dot_gap_comments/expected.json b/tests/fixtures/typescript/types/qualified_name_dot_gap_comments/expected.json new file mode 100644 index 000000000..bba4ee7c6 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_comments/expected.json @@ -0,0 +1,863 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 459, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " Block comment before the `.` — stays there, hugging the dot", + "start": 20, + "end": 82, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 63 + } + } + }, + { + "type": "Block", + "value": " c1 ", + "start": 96, + "end": 104, + "loc": { + "start": { + "line": 3, + "column": 13 + }, + "end": { + "line": 3, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 113, + "end": 164, + "loc": { + "start": { + "line": 5, + "column": 1 + }, + "end": { + "line": 5, + "column": 52 + } + } + }, + { + "type": "Block", + "value": " c2 ", + "start": 178, + "end": 186, + "loc": { + "start": { + "line": 6, + "column": 13 + }, + "end": { + "line": 6, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " Both gaps in a deeper chain", + "start": 195, + "end": 225, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 8, + "column": 31 + } + } + }, + { + "type": "Block", + "value": " c3 ", + "start": 240, + "end": 248, + "loc": { + "start": { + "line": 9, + "column": 14 + }, + "end": { + "line": 9, + "column": 22 + } + } + }, + { + "type": "Block", + "value": " c4 ", + "start": 251, + "end": 259, + "loc": { + "start": { + "line": 9, + "column": 25 + }, + "end": { + "line": 9, + "column": 33 + } + } + }, + { + "type": "Line", + "value": " Interface heritage shares the qualified-name printer", + "start": 268, + "end": 323, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 56 + } + } + }, + { + "type": "Block", + "value": " c5 ", + "start": 354, + "end": 362, + "loc": { + "start": { + "line": 12, + "column": 30 + }, + "end": { + "line": 12, + "column": 38 + } + } + }, + { + "type": "Line", + "value": " So does an import-equals module reference", + "start": 373, + "end": 417, + "loc": { + "start": { + "line": 14, + "column": 1 + }, + "end": { + "line": 14, + "column": 45 + } + } + }, + { + "type": "Block", + "value": " c6 ", + "start": 435, + "end": 443, + "loc": { + "start": { + "line": 15, + "column": 17 + }, + "end": { + "line": 15, + "column": 25 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 458, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 449, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 16, + "column": 9 + } + }, + "body": [ + { + "type": "TSTypeAliasDeclaration", + "start": 84, + "end": 110, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 27 + } + }, + "id": { + "type": "Identifier", + "start": 89, + "end": 90, + "loc": { + "start": { + "line": 3, + "column": 6 + }, + "end": { + "line": 3, + "column": 7 + } + }, + "name": "A" + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 93, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 26 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 93, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 26 + } + }, + "left": { + "type": "Identifier", + "start": 93, + "end": 95, + "loc": { + "start": { + "line": 3, + "column": 10 + }, + "end": { + "line": 3, + "column": 12 + } + }, + "name": "ns", + "trailingComments": [ + { + "type": "Block", + "value": " c1 ", + "start": 96, + "end": 104 + } + ] + }, + "right": { + "type": "Identifier", + "start": 105, + "end": 109, + "loc": { + "start": { + "line": 3, + "column": 22 + }, + "end": { + "line": 3, + "column": 26 + } + }, + "name": "Type" + } + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment before the `.` — stays there, hugging the dot", + "start": 20, + "end": 82 + } + ] + }, + { + "type": "TSTypeAliasDeclaration", + "start": 166, + "end": 192, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 27 + } + }, + "id": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 6, + "column": 6 + }, + "end": { + "line": 6, + "column": 7 + } + }, + "name": "B" + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 175, + "end": 191, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 26 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 175, + "end": 191, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 26 + } + }, + "left": { + "type": "Identifier", + "start": 175, + "end": 177, + "loc": { + "start": { + "line": 6, + "column": 10 + }, + "end": { + "line": 6, + "column": 12 + } + }, + "name": "ns" + }, + "right": { + "type": "Identifier", + "start": 187, + "end": 191, + "loc": { + "start": { + "line": 6, + "column": 22 + }, + "end": { + "line": 6, + "column": 26 + } + }, + "name": "Type", + "leadingComments": [ + { + "type": "Block", + "value": " c2 ", + "start": 178, + "end": 186 + } + ] + } + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Block comment after the `.` — stays on that side", + "start": 113, + "end": 164 + } + ] + }, + { + "type": "TSTypeAliasDeclaration", + "start": 227, + "end": 265, + "loc": { + "start": { + "line": 9, + "column": 1 + }, + "end": { + "line": 9, + "column": 39 + } + }, + "id": { + "type": "Identifier", + "start": 232, + "end": 233, + "loc": { + "start": { + "line": 9, + "column": 6 + }, + "end": { + "line": 9, + "column": 7 + } + }, + "name": "C" + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 236, + "end": 264, + "loc": { + "start": { + "line": 9, + "column": 10 + }, + "end": { + "line": 9, + "column": 38 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 236, + "end": 264, + "loc": { + "start": { + "line": 9, + "column": 10 + }, + "end": { + "line": 9, + "column": 38 + } + }, + "left": { + "type": "TSQualifiedName", + "start": 236, + "end": 250, + "loc": { + "start": { + "line": 9, + "column": 10 + }, + "end": { + "line": 9, + "column": 24 + } + }, + "left": { + "type": "TSQualifiedName", + "start": 236, + "end": 239, + "loc": { + "start": { + "line": 9, + "column": 10 + }, + "end": { + "line": 9, + "column": 13 + } + }, + "left": { + "type": "Identifier", + "start": 236, + "end": 237, + "loc": { + "start": { + "line": 9, + "column": 10 + }, + "end": { + "line": 9, + "column": 11 + } + }, + "name": "a" + }, + "right": { + "type": "Identifier", + "start": 238, + "end": 239, + "loc": { + "start": { + "line": 9, + "column": 12 + }, + "end": { + "line": 9, + "column": 13 + } + }, + "name": "b" + }, + "trailingComments": [ + { + "type": "Block", + "value": " c3 ", + "start": 240, + "end": 248 + } + ] + }, + "right": { + "type": "Identifier", + "start": 249, + "end": 250, + "loc": { + "start": { + "line": 9, + "column": 23 + }, + "end": { + "line": 9, + "column": 24 + } + }, + "name": "c" + } + }, + "right": { + "type": "Identifier", + "start": 260, + "end": 264, + "loc": { + "start": { + "line": 9, + "column": 34 + }, + "end": { + "line": 9, + "column": 38 + } + }, + "name": "Type", + "leadingComments": [ + { + "type": "Block", + "value": " c4 ", + "start": 251, + "end": 259 + } + ] + } + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " Both gaps in a deeper chain", + "start": 195, + "end": 225 + } + ] + }, + { + "type": "TSInterfaceDeclaration", + "start": 325, + "end": 370, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 12, + "column": 46 + } + }, + "id": { + "type": "Identifier", + "start": 335, + "end": 342, + "loc": { + "start": { + "line": 12, + "column": 11 + }, + "end": { + "line": 12, + "column": 18 + } + }, + "name": "Extends" + }, + "extends": [ + { + "type": "TSExpressionWithTypeArguments", + "start": 351, + "end": 367, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 12, + "column": 43 + } + }, + "expression": { + "type": "TSQualifiedName", + "start": 351, + "end": 367, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 12, + "column": 43 + } + }, + "left": { + "type": "Identifier", + "start": 351, + "end": 353, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 12, + "column": 29 + } + }, + "name": "ns" + }, + "right": { + "type": "Identifier", + "start": 363, + "end": 367, + "loc": { + "start": { + "line": 12, + "column": 39 + }, + "end": { + "line": 12, + "column": 43 + } + }, + "name": "Base", + "leadingComments": [ + { + "type": "Block", + "value": " c5 ", + "start": 354, + "end": 362 + } + ] + } + } + } + ], + "body": { + "type": "TSInterfaceBody", + "start": 368, + "end": 370, + "loc": { + "start": { + "line": 12, + "column": 44 + }, + "end": { + "line": 12, + "column": 46 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Interface heritage shares the qualified-name printer", + "start": 268, + "end": 323 + } + ] + }, + { + "type": "TSImportEqualsDeclaration", + "start": 419, + "end": 448, + "loc": { + "start": { + "line": 15, + "column": 1 + }, + "end": { + "line": 15, + "column": 30 + } + }, + "importKind": "value", + "isExport": false, + "id": { + "type": "Identifier", + "start": 426, + "end": 429, + "loc": { + "start": { + "line": 15, + "column": 8 + }, + "end": { + "line": 15, + "column": 11 + } + }, + "name": "Ref" + }, + "moduleReference": { + "type": "TSQualifiedName", + "start": 432, + "end": 447, + "loc": { + "start": { + "line": 15, + "column": 14 + }, + "end": { + "line": 15, + "column": 29 + } + }, + "left": { + "type": "Identifier", + "start": 432, + "end": 434, + "loc": { + "start": { + "line": 15, + "column": 14 + }, + "end": { + "line": 15, + "column": 16 + } + }, + "name": "ns" + }, + "right": { + "type": "Identifier", + "start": 444, + "end": 447, + "loc": { + "start": { + "line": 15, + "column": 26 + }, + "end": { + "line": 15, + "column": 29 + } + }, + "name": "Sub", + "leadingComments": [ + { + "type": "Block", + "value": " c6 ", + "start": 435, + "end": 443 + } + ] + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " So does an import-equals module reference", + "start": 373, + "end": 417 + } + ] + } + ], + "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/types/qualified_name_dot_gap_comments/input.svelte b/tests/fixtures/typescript/types/qualified_name_dot_gap_comments/input.svelte new file mode 100644 index 000000000..9b3875a42 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_comments/input.svelte @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/README.md b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/README.md new file mode 100644 index 000000000..d46ccf739 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/README.md @@ -0,0 +1,31 @@ +# Divergence: qualified-name dot-gap line comments (preserve) + +A *line* comment in either gap around a qualified name's `.` (`ns // c⏎.Type`, `ns. // c⏎Type`). +tsv keeps it where the author wrote it and continues the rest of the name one level down; prettier +**relocates** it past the `;`, where it no longer reads as being about the name at all. + +```ts +// tsv (preserve) // prettier (relocate) +let a: ns // c1 let a: ns.Type; // c1 + .Type; +``` + +The qualified-name twin of +[meta_property/dot_gap_line_comment](../../expressions/misc/meta_property/dot_gap_line_comment_prettier_divergence/): +same shape (`name` `.` `name`), same two gaps, same rule — and, before the fix, the same bug. Both +route through one printer, so neither can drift. + +Block comments in these gaps are **not** a divergence: prettier keeps each on its authored side of +the dot, and so does tsv — pinned by the regular sibling +[qualified_name_dot_gap_comments](../qualified_name_dot_gap_comments/). + +The type alias is the one nested shape: a breaking right-hand side goes below the `=` (prettier's +own convention for a multiline RHS — a broken union takes the same form), so the name's `+1` +continuation nests inside that, not against the statement. The annotation and heritage cases show +the rule flat. Prettier isn't an oracle for any of it — it relocates the comment out of the name +first, and on the heritage case leaves `.Base` sitting level with the `extends` it belongs to. + +See [conformance_prettier.md §Comment relocation](../../../../../docs/conformance_prettier.md#comment-relocation) +and [§Comments inside a multi-word keyword](../../../../../docs/conformance_prettier.md#comments-inside-a-multi-word-keyword) +(the punctuator-joined member of that class — the reason its usual detector, a `d.text` literal with +an *interior* space, cannot see it: the joining literal is `"."`). diff --git a/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/expected.json b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/expected.json new file mode 100644 index 000000000..ab07a23d9 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/expected.json @@ -0,0 +1,761 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 480, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [] + }, + "options": null, + "comments": [ + { + "type": "Line", + "value": " A line comment before the `.` — stays there, and the rest of the name continues", + "start": 20, + "end": 102, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 83 + } + } + }, + { + "type": "Line", + "value": " one level down", + "start": 104, + "end": 121, + "loc": { + "start": { + "line": 3, + "column": 1 + }, + "end": { + "line": 3, + "column": 18 + } + } + }, + { + "type": "Line", + "value": " c1", + "start": 133, + "end": 138, + "loc": { + "start": { + "line": 4, + "column": 11 + }, + "end": { + "line": 4, + "column": 16 + } + } + }, + { + "type": "Line", + "value": " The gap on the other side of the dot", + "start": 150, + "end": 189, + "loc": { + "start": { + "line": 7, + "column": 1 + }, + "end": { + "line": 7, + "column": 40 + } + } + }, + { + "type": "Line", + "value": " c2", + "start": 202, + "end": 207, + "loc": { + "start": { + "line": 8, + "column": 12 + }, + "end": { + "line": 8, + "column": 17 + } + } + }, + { + "type": "Line", + "value": " Interface heritage shares the qualified-name printer", + "start": 218, + "end": 273, + "loc": { + "start": { + "line": 11, + "column": 1 + }, + "end": { + "line": 11, + "column": 56 + } + } + }, + { + "type": "Line", + "value": " c3", + "start": 304, + "end": 309, + "loc": { + "start": { + "line": 12, + "column": 30 + }, + "end": { + "line": 12, + "column": 35 + } + } + }, + { + "type": "Line", + "value": " A type alias puts a breaking right-hand side below the `=`, so the name's own", + "start": 323, + "end": 403, + "loc": { + "start": { + "line": 15, + "column": 1 + }, + "end": { + "line": 15, + "column": 81 + } + } + }, + { + "type": "Line", + "value": " continuation nests inside that", + "start": 405, + "end": 438, + "loc": { + "start": { + "line": 16, + "column": 1 + }, + "end": { + "line": 16, + "column": 34 + } + } + }, + { + "type": "Line", + "value": " c4", + "start": 454, + "end": 459, + "loc": { + "start": { + "line": 18, + "column": 5 + }, + "end": { + "line": 18, + "column": 10 + } + } + } + ], + "instance": { + "type": "Script", + "start": 0, + "end": 479, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 470, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 20, + "column": 9 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 123, + "end": 147, + "loc": { + "start": { + "line": 4, + "column": 1 + }, + "end": { + "line": 5, + "column": 8 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 127, + "end": 146, + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "id": { + "type": "Identifier", + "start": 127, + "end": 146, + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "name": "a", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 128, + "end": 146, + "loc": { + "start": { + "line": 4, + "column": 6 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 130, + "end": 146, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 130, + "end": 146, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "left": { + "type": "Identifier", + "start": 130, + "end": 132, + "loc": { + "start": { + "line": 4, + "column": 8 + }, + "end": { + "line": 4, + "column": 10 + } + }, + "name": "ns", + "trailingComments": [ + { + "type": "Line", + "value": " c1", + "start": 133, + "end": 138 + } + ] + }, + "right": { + "type": "Identifier", + "start": 142, + "end": 146, + "loc": { + "start": { + "line": 5, + "column": 3 + }, + "end": { + "line": 5, + "column": 7 + } + }, + "name": "Type" + } + } + } + } + }, + "init": null + } + ], + "kind": "let", + "leadingComments": [ + { + "type": "Line", + "value": " A line comment before the `.` — stays there, and the rest of the name continues", + "start": 20, + "end": 102 + }, + { + "type": "Line", + "value": " one level down", + "start": 104, + "end": 121 + } + ] + }, + { + "type": "VariableDeclaration", + "start": 191, + "end": 215, + "loc": { + "start": { + "line": 8, + "column": 1 + }, + "end": { + "line": 9, + "column": 7 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 195, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 5 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "id": { + "type": "Identifier", + "start": 195, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 5 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "name": "b", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 196, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 6 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 198, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 198, + "end": 214, + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "left": { + "type": "Identifier", + "start": 198, + "end": 200, + "loc": { + "start": { + "line": 8, + "column": 8 + }, + "end": { + "line": 8, + "column": 10 + } + }, + "name": "ns" + }, + "right": { + "type": "Identifier", + "start": 210, + "end": 214, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 6 + } + }, + "name": "Type", + "leadingComments": [ + { + "type": "Line", + "value": " c2", + "start": 202, + "end": 207 + } + ] + } + } + } + } + }, + "init": null + } + ], + "kind": "let", + "leadingComments": [ + { + "type": "Line", + "value": " The gap on the other side of the dot", + "start": 150, + "end": 189 + } + ] + }, + { + "type": "TSInterfaceDeclaration", + "start": 275, + "end": 320, + "loc": { + "start": { + "line": 12, + "column": 1 + }, + "end": { + "line": 13, + "column": 10 + } + }, + "id": { + "type": "Identifier", + "start": 285, + "end": 292, + "loc": { + "start": { + "line": 12, + "column": 11 + }, + "end": { + "line": 12, + "column": 18 + } + }, + "name": "Extends" + }, + "extends": [ + { + "type": "TSExpressionWithTypeArguments", + "start": 301, + "end": 317, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 13, + "column": 7 + } + }, + "expression": { + "type": "TSQualifiedName", + "start": 301, + "end": 317, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 13, + "column": 7 + } + }, + "left": { + "type": "Identifier", + "start": 301, + "end": 303, + "loc": { + "start": { + "line": 12, + "column": 27 + }, + "end": { + "line": 12, + "column": 29 + } + }, + "name": "ns", + "trailingComments": [ + { + "type": "Line", + "value": " c3", + "start": 304, + "end": 309 + } + ] + }, + "right": { + "type": "Identifier", + "start": 313, + "end": 317, + "loc": { + "start": { + "line": 13, + "column": 3 + }, + "end": { + "line": 13, + "column": 7 + } + }, + "name": "Base" + } + } + } + ], + "body": { + "type": "TSInterfaceBody", + "start": 318, + "end": 320, + "loc": { + "start": { + "line": 13, + "column": 8 + }, + "end": { + "line": 13, + "column": 10 + } + }, + "body": [] + }, + "leadingComments": [ + { + "type": "Line", + "value": " Interface heritage shares the qualified-name printer", + "start": 218, + "end": 273 + } + ] + }, + { + "type": "TSTypeAliasDeclaration", + "start": 440, + "end": 469, + "loc": { + "start": { + "line": 17, + "column": 1 + }, + "end": { + "line": 19, + "column": 9 + } + }, + "id": { + "type": "Identifier", + "start": 445, + "end": 446, + "loc": { + "start": { + "line": 17, + "column": 6 + }, + "end": { + "line": 17, + "column": 7 + } + }, + "name": "A" + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 451, + "end": 468, + "loc": { + "start": { + "line": 18, + "column": 2 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "typeName": { + "type": "TSQualifiedName", + "start": 451, + "end": 468, + "loc": { + "start": { + "line": 18, + "column": 2 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "left": { + "type": "Identifier", + "start": 451, + "end": 453, + "loc": { + "start": { + "line": 18, + "column": 2 + }, + "end": { + "line": 18, + "column": 4 + } + }, + "name": "ns", + "trailingComments": [ + { + "type": "Line", + "value": " c4", + "start": 454, + "end": 459 + } + ] + }, + "right": { + "type": "Identifier", + "start": 464, + "end": 468, + "loc": { + "start": { + "line": 19, + "column": 4 + }, + "end": { + "line": 19, + "column": 8 + } + }, + "name": "Type" + } + } + }, + "leadingComments": [ + { + "type": "Line", + "value": " A type alias puts a breaking right-hand side below the `=`, so the name's own", + "start": 323, + "end": 403 + }, + { + "type": "Line", + "value": " continuation nests inside that", + "start": 405, + "end": 438 + } + ] + } + ], + "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/types/qualified_name_dot_gap_line_comment_prettier_divergence/input.svelte b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/input.svelte new file mode 100644 index 000000000..337f05108 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/input.svelte @@ -0,0 +1,20 @@ + diff --git a/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/output_prettier.svelte b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/output_prettier.svelte new file mode 100644 index 000000000..084d49444 --- /dev/null +++ b/tests/fixtures/typescript/types/qualified_name_dot_gap_line_comment_prettier_divergence/output_prettier.svelte @@ -0,0 +1,17 @@ + diff --git a/tests/import_phase.rs b/tests/import_phase.rs index eccd621d5..bff454eca 100644 --- a/tests/import_phase.rs +++ b/tests/import_phase.rs @@ -129,3 +129,20 @@ fn static_import_source_keyword_binding_rejected() { assert_ours_rejects("\n"); assert_ours_stable("\n"); } + +/// A comment in either gap around the phase dot survives. +/// +/// `import.source` / `import.defer` are a dotted pair (`import` `.` `source`), so both +/// gaps are positions an author can comment in. Emitting the pair as one fixed text +/// (`"import.source("`) scans neither and drops what's there — the punctuator-joined +/// member of the multi-word-keyword class (`docs/conformance_prettier.md` §Comments +/// inside a multi-word keyword). Prettier preserves these, so the "no divergence on the +/// dynamic forms" claim in that doc depends on tsv preserving them too; there is no +/// fixture to pin it with, because acorn rejects the syntax outright. +#[test] +fn dynamic_import_phase_dot_gap_comments_stable() { + assert_ours_stable("\n"); + assert_ours_stable("\n"); + assert_ours_stable("\n"); + assert_ours_stable("\n"); +} From 05ac09773752c3568b48d3901146c1222298cd23 Mon Sep 17 00:00:00 2001 From: Ryan Atkinson Date: Wed, 15 Jul 2026 13:26:35 -0400 Subject: [PATCH 3/4] test: add gaps audit --- CLAUDE.md | 26 + .../tsv_debug/src/cli/commands/gap_audit.rs | 1937 +++++++++++++++++ .../src/cli/commands/gap_audit_known.txt | 715 ++++++ crates/tsv_debug/src/cli/commands/mod.rs | 2 + crates/tsv_debug/src/cli/mod.rs | 8 + crates/tsv_lang/src/doc/arena.rs | 128 +- crates/tsv_lang/src/doc/mod.rs | 45 +- .../src/printer/calls/call_formatting.rs | 18 +- crates/tsv_ts/src/printer/calls/chain_args.rs | 18 +- .../src/printer/expressions/functions.rs | 35 + .../printer/expressions/template_literal.rs | 14 +- deno.json | 4 +- docs/gap_audit.md | 172 ++ .../expected.json | 811 +++++++ .../input.svelte | 19 + 15 files changed, 3912 insertions(+), 40 deletions(-) create mode 100644 crates/tsv_debug/src/cli/commands/gap_audit.rs create mode 100644 crates/tsv_debug/src/cli/commands/gap_audit_known.txt create mode 100644 docs/gap_audit.md create mode 100644 tests/fixtures/typescript/expressions/calls/arrow_array_return_multiline_comment/expected.json create mode 100644 tests/fixtures/typescript/expressions/calls/arrow_array_return_multiline_comment/input.svelte diff --git a/CLAUDE.md b/CLAUDE.md index 463bf9ca1..541cf9750 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,6 +185,8 @@ deno task binding:audit # comment↔token binding audit: does forma deno task authoring:audit # authoring-independence over Svelte boundary whitespace: every render-equivalent authoring of one document (hug ↔ space ↔ newline at a tag's content boundary; space ↔ newline between siblings) must reach ONE tsv fixed point (pure Rust, no sidecar; gated in `deno task check`) — exits 1 on any non-idempotency, site-level or a base-non-idempotent FILE; see Debug Tooling deno task fuzz:audit # seeded mutational fuzzer over tests/fixtures (fixed --seed 0 --iterations 5000; pure Rust, no sidecar; gated in `deno task check`) — asserts no-panic + idempotency + structural-reparse, on every seed file AS AUTHORED and then on mutated input; see Debug Tooling deno task comments:audit # print-once comment ledger: every comment a document PARSES must be EMITTED exactly once (pure Rust, no sidecar; gated in `deno task check`) — reports DROPPED (silent content loss) and DOUBLE-PRINTED; the structural guard on the detached comment model, tsv's `ensureAllCommentsPrinted`; see Debug Tooling +deno task gaps:audit # gap-injection audit: inject a comment into EVERY gap (five payloads, one per ownership path) and re-run the ledger — the discovery arm `comments:audit` can't be, since it only formats each file AS AUTHORED and no fixture covers most positions (eight such drops were found BY HAND, all green on every gate). Pure Rust, no sidecar; gated in `deno task check` as a RATCHET over a generated shape snapshot (`gap_audit_known.txt`): every line is a known bug and the file shrinking is the goal, so a shape not on the list, one on it that no longer fires, or any PANIC, FAILS. ~17 s. Full reference: ./docs/gap_audit.md +deno task gaps:audit:update # regenerate that snapshot after fixing a shape (or when a new fixture merely REACHES a pre-existing one); refuses a narrowed run deno task idempotency:sweep # F1 (idempotency) sweep over the real-code corpus (the `perf` view — sibling dev repos + upstream framework source). NOT in `deno task check`: machine-dependent corpus, minutes not seconds. Run at conformance cadence or after a printer change; see Debug Tooling ``` @@ -882,6 +884,30 @@ cargo run -p tsv_debug --features comment_check comment_audit ~/dev/zzz/src # # source), so they are outside the model by construction. ``` +**Gap-Injection Audit (dropped-comment discovery):** + +```bash +# gap_audit - inject a comment into EVERY gap and re-run the print-once ledger. The +# DISCOVERY arm `comments:audit` can't be: the ledger only ever sees a document AS +# AUTHORED, so a gap no fixture puts a comment in is one it never checks (eight such +# drops were found BY HAND, all green on every gate). Pure Rust, no sidecar. +cargo run --profile corpus -p tsv_debug --features comment_check gap_audit # tests/fixtures +cargo run --profile corpus -p tsv_debug --features comment_check gap_audit ~/dev/zzz/src +# Also: --json, --jobs N, --limit N, --payload , --all-bytes, --update. +# Build with `--profile corpus` (release + panic=unwind): plain `--release` is +# panic=abort, so a formatter panic kills the process instead of being caught + reported. +# +# GATED as a RATCHET, not a green gate: `gap_audit_known.txt` is a machine-generated +# snapshot of the ~717 shapes tests/fixtures produces, every line a KNOWN BUG, the file +# shrinking is the goal. A shape not on the list, one on it that no longer fires, or any +# PANIC, FAILS. `--limit`/`--payload`/`--all-bytes`/a path narrow a run, so they skip the +# ratchet and refuse `--update`. ~17 s. +``` + +Full reference — flags, the ratchet, reading a finding, triage + re-pin workflow, +scope: ./docs/gap_audit.md. Design rationale (why byte offsets and not tokens, why the +ledger is the oracle, why five payloads) lives in the `gap_audit` module docs. + **Build-Fanout Audit (exponential-rebuild regression guard):** ```bash diff --git a/crates/tsv_debug/src/cli/commands/gap_audit.rs b/crates/tsv_debug/src/cli/commands/gap_audit.rs new file mode 100644 index 000000000..2ece89117 --- /dev/null +++ b/crates/tsv_debug/src/cli/commands/gap_audit.rs @@ -0,0 +1,1937 @@ +//! Gap-injection audit — the mechanized form of hunting the dropped-comment class. +//! +//! ## Why this exists +//! +//! One recurring bug class: **a printer that concatenates fixed pieces without scanning +//! the gaps between them silently DROPS any comment an author wrote in a gap.** A header +//! built as `d.text("import.source(")` scans neither of its two dot gaps, so +//! `import./* c */source(x)` loses the comment — no error, no diff, just gone. +//! +//! The print-once ledger ([`comment_ledger`]) would catch every one of these. It just +//! never sees them: [`comment_audit`](super::comment_audit) formats each file **as +//! authored**, and a gap only becomes a finding once a comment is actually *in* it. Eight +//! such sites were found BY HAND, and every one was green on every gate — `cargo test`, +//! `comments:audit`, `roundtrip:audit`, the corpus diff — purely because no fixture +//! happened to put a comment in that position. The gates were not wrong; the corpus was +//! silent. +//! +//! This audit closes that hole: for each seed file it injects a comment into **every** +//! candidate gap, one at a time, formats, and runs the ledger over the result. +//! +//! ## Design +//! +//! Pure Rust, no sidecar, no new deps — the [`fuzz`](super::fuzz) / +//! [`comment_audit`](super::comment_audit) direction. Deliberately **targeted, not +//! random**: byte mutation would essentially never synthesize a valid comment in a dot +//! gap, which is the whole point of the class. +//! +//! **Sites are byte offsets, not tokens.** A token-stream enumeration would need a flat +//! Svelte token contract that doesn't exist — and `.svelte` is where this class lives, +//! since TS-only syntax is fixtured as `.svelte` + `lang="ts"` (a TS-only audit reaches 53 +//! of 6,689 fixture files). Worse, it would carry exactly the blind spot the class +//! exploits: a punctuator-joined header is a **zero-width** gap, the first thing a +//! "between two tokens" abstraction elides. A byte offset has no such notion, so it cannot +//! miss one. +//! +//! **But an offset must first be somewhere the payload IS a comment**, and tsv's own parser +//! cannot answer that. It is deliberately more permissive than the canonical one, so +//! "tsv accepted it" does not mean "an author could write it": tsv parses +//! `` region), +/// and an `ExpressionTag`'s brace interior (`{ /* c */ x.y }` is legal). +/// +/// TODO: `