1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// try/catch/finally, throw, break/continue, and labeled statement printing
use super::{HeadChainGrouping, OpenParenLineBlockComment};
use crate::ast::{self, StatementKind};
use crate::printer::statements::StatementContext;
use crate::printer::statements::TerminatorGap;
use crate::printer::{CommentVec, LeadingGlue, Printer};
use smallvec::smallvec;
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::source_scan::find_char_skipping_comments;
impl<'a> Printer<'a> {
/// Append a space (or comments + space/hardline) between a keyword/token end and body start.
///
/// Used for `try /* c */ {`, `catch (e) /* c */ {`, `catch /* c */ {`, `finally /* c */ {`.
///
/// The keyword anchor for `push_header_to_body_gap` — see there for the gap's comment
/// rules. Emitting this gap inline instead would relocate an
/// own-line `//` up onto the keyword line (`try⏎// c⏎{` → `try // c⏎{`), which the
/// `if`/`while` `)`→`{` siblings never do.
fn append_keyword_to_body_comments(&self, parts: &mut DocBuf, token_end: u32, body_start: u32) {
if self.has_comments_to_emit_between(token_end, body_start) {
self.push_header_to_body_gap(parts, token_end, body_start);
} else {
parts.push(self.d().text(" "));
}
}
/// Append the `}`→continuation-keyword gap and then the clause it introduces — `catch` or
/// `finally`. Comments in the gap keep where the author put them: one that trailed the
/// previous `}` stays trailing, one on its own line keeps its own line. The
/// keyword-preceding mirror of [`Self::append_keyword_to_body_comments`].
///
/// The gap partitions by line exactly as the `}`→`else` path does — the authored position is
/// the whole signal, and a blank line above an own-line comment is authoring intent
/// ([`Self::push_block_to_keyword_gap`]). Prettier is no oracle here: it relocates these
/// comments into the following block's body, which it does *not* do at `else`. See
/// `conformance_prettier_ts_comments.md` §Comment relocation.
///
/// An own-line directive in the gap freezes the WHOLE clause — keyword, any binding and the
/// body all ride inside the verbatim slice — and the return is then `true`, telling the
/// caller to emit nothing further for this clause. `clause_end` rather than a span because
/// the two clauses anchor differently: a `CatchClause`'s span already starts at its keyword,
/// while a finalizer's starts at its `{` (the keyword sits in the gap), so the frozen slice
/// is keyed on `keyword_pos` for both. Prettier instead relocates the directive into the
/// clause body and freezes the first statement there
/// (`handler_prettier_ignore_head_prettier_divergence`).
fn append_clause_head(
&self,
parts: &mut DocBuf,
gap_start: u32,
keyword_pos: u32,
keyword: &'static str,
clause_end: u32,
) -> bool {
// A `try`/`catch` body is always a block, so the keyword can hug `}`.
self.push_block_to_keyword_gap(parts, gap_start, keyword_pos, true, false);
match self.gap_frozen_span(gap_start, Span::new(keyword_pos, clause_end)) {
Some(frozen) => {
parts.push(self.build_frozen_span_doc(frozen));
true
}
None => {
parts.push(self.d().text(keyword));
false
}
}
}
/// Append a `catch` clause's binding and body — everything after the keyword, which
/// [`Self::append_clause_head`] has already emitted. Split out of
/// `build_try_statement_doc` so the freeze guard reads as one branch rather than wrapping
/// this whole run.
fn append_catch_clause_body(
&self,
parts: &mut DocBuf,
stmt: &ast::TryStatement<'_>,
handler: &ast::CatchClause<'_>,
) {
let d = self.d();
let catch_keyword_end = handler.span.start + "catch".len() as u32;
if let Some(param) = &handler.param {
// Find paren positions for comment handling
let open_paren = self.find_open_paren_after(stmt.block.span.end);
let close_paren = open_paren.and_then(|o| self.matching_close_paren(o));
// Preserve comments between catch keyword and ( in place:
// catch/* comment */(e) → catch /* comment */ (e)
// The keyword itself was emitted by `append_clause_head` (possibly as a
// frozen span), so this takes the opener's `(` half.
let keyword_comments = self.build_keyword_paren_comments(catch_keyword_end, open_paren);
self.push_open_paren_after_keyword(parts, keyword_comments);
// Check for comments in catch parameter
if let (Some(open), Some(close)) = (open_paren, close_paren)
&& self.header_parens_hold_comments(open, close, param)
{
parts.push(self.build_condition_group_with_comments(
param,
open,
close,
OpenParenLineBlockComment::JoinsRun,
HeadChainGrouping::ParenGroupDrives,
));
} else {
parts.push(self.build_expression_doc(param));
}
parts.push(d.text(")"));
// Comments between ) and body: `catch (e) /* comment */ {`
let paren_end = close_paren.unwrap_or_else(|| param.span().end) + 1;
self.append_keyword_to_body_comments(parts, paren_end, handler.body.span.start);
} else {
// No param: comments between catch keyword and body: `catch /* comment */ {`
self.append_keyword_to_body_comments(parts, catch_keyword_end, handler.body.span.start);
}
// Catch block stays inline (`catch (e) {}`) UNLESS a `finally` follows, in which case
// it expands empty like `try`/`finally` do (Prettier's `block.js`:
// `parent.type === "CatchClause" && !parentParent.finalizer` is the only case that
// stays collapsed).
if stmt.finalizer.is_some() {
parts.push(self.build_block_statement_expand_empty_doc(&handler.body));
} else {
parts.push(self.build_block_statement_doc(&handler.body));
}
}
pub(in crate::printer::statements) fn build_try_statement_doc(
&self,
stmt: &ast::TryStatement<'_>,
span: Span,
) -> DocId {
let d = self.d();
// try keyword to block: `try /* comment */ {`
let try_keyword_end = span.start + "try".len() as u32;
let block_start = stmt.block.span.start;
let mut parts = d.pooled_docbuf();
parts.push(d.text("try"));
self.append_keyword_to_body_comments(&mut parts, try_keyword_end, block_start);
// Try block expands empty: `try {\n}` not `try {}`
parts.push(self.build_block_statement_expand_empty_doc(&stmt.block));
if let Some(handler) = &stmt.handler {
// `handler.span.start` is the position of the "catch" keyword. A frozen clause
// rides out whole in the verbatim slice — keyword, binding and body — so the
// per-part emission is skipped entirely for it.
let try_end = stmt.block.span.end;
if !self.append_clause_head(
&mut parts,
try_end,
handler.span.start,
"catch",
handler.span.end,
) {
self.append_catch_clause_body(&mut parts, stmt, handler);
}
}
if let Some(finalizer) = &stmt.finalizer {
// Check for comments before finally (after catch block or try block)
let prev_end = stmt
.handler
.as_ref()
.map_or(stmt.block.span.end, |h| h.body.span.end);
// The finalizer span starts at the "finally" block `{`; the keyword sits
// in the gap after the previous block. It's the only real keyword there,
// so the first whole-word match wins — trivia-aware so a `/* finally */`
// comment before or after the keyword can't be mistaken for it (a raw
// `rfind` matched the one inside such a comment and dropped it).
let finally_keyword_pos = self
.find_keyword_in_range(prev_end, finalizer.span.start, "finally")
.unwrap_or(finalizer.span.start);
if !self.append_clause_head(
&mut parts,
prev_end,
finally_keyword_pos,
"finally",
finalizer.span.end,
) {
// Comments between finally keyword and body: `finally /* comment */ {`
let finally_keyword_end = finally_keyword_pos + "finally".len() as u32;
self.append_keyword_to_body_comments(
&mut parts,
finally_keyword_end,
finalizer.span.start,
);
// Finally block expands empty: `finally {\n}` not `finally {}`
parts.push(self.build_block_statement_expand_empty_doc(finalizer));
}
}
d.concat(&parts)
}
pub(in crate::printer::statements) fn build_throw_statement_doc(
&self,
stmt: &ast::ThrowStatement<'_>,
span: Span,
gap: TerminatorGap,
) -> DocId {
self.build_keyword_argument_doc("throw", span.start, span.end, stmt.argument, gap)
}
pub(in crate::printer::statements) fn build_break_statement_doc(
&self,
stmt: &ast::BreakStatement<'_>,
span: Span,
gap: TerminatorGap,
) -> DocId {
self.build_jump_statement_doc("break", span, stmt.label.as_ref(), gap)
}
pub(in crate::printer::statements) fn build_continue_statement_doc(
&self,
stmt: &ast::ContinueStatement<'_>,
span: Span,
gap: TerminatorGap,
) -> DocId {
self.build_jump_statement_doc("continue", span, stmt.label.as_ref(), gap)
}
/// Shared builder for break/continue statements with optional label and trailing comments.
fn build_jump_statement_doc(
&self,
keyword: &'static str,
span: Span,
label: Option<&ast::Identifier<'_>>,
gap: TerminatorGap,
) -> DocId {
let d = self.d();
if let Some(label) = label {
let keyword_end = span.start + keyword.len() as u32;
// Comments between keyword and label (e.g., `break /* c */ loop;`)
let pre_label_comment =
self.build_inline_comments_between_doc_opt(keyword_end, label.span.start);
let mut parts = DocBuf::new();
parts.push(d.text(keyword));
if let Some(comment_doc) = pre_label_comment {
parts.push(comment_doc);
}
parts.push(d.text(" "));
parts.push(self.identifier_name_doc(label));
// Comments between label and `;`: a same-line block trails *after* the `;`
// (`break loop; /* c */`, prettier 3.9), a same-line line via `line_suffix`,
// an own-line comment on its own line after. See `push_semicolon_with_gap_comments`.
self.push_statement_semicolon(&mut parts, label.span.end, span.end, gap);
d.concat(&parts)
} else {
// No label: a bare keyword closed by `;`. It swallows a following explicit
// `;` as its terminator (no `[no LineTerminator]` issue once the label is
// absent), so any comment between the keyword and that `;` is interior to the
// span — the shared helper preserves it (own-line aware, blank line kept). The
// previous inline-only emission merged consecutive own-line comments onto one
// line (`break; // c1 // c2`, swallowing the second).
self.build_bare_keyword_terminator_doc(keyword, span, gap)
}
}
pub(in crate::printer::statements) fn build_labeled_statement_doc(
&self,
stmt: &ast::LabeledStatement<'_>,
ctx: StatementContext,
) -> DocId {
let d = self.d();
let label_end = stmt.label.span.end;
let body_start = stmt.body.span().start;
// Find actual colon position (skip comments between label and colon)
let colon_pos = find_char_skipping_comments(
self.source.as_bytes(),
label_end as usize,
body_start as usize,
b':',
)
.unwrap_or(label_end as usize);
let colon_end = colon_pos as u32 + 1;
// Build the `: body` tail (including any colon→body comments).
let mut tail_parts: DocBuf = smallvec![];
if self.gap_frozen_span(colon_end, stmt.body.span()).is_some() {
// An own-line directive in the `:`→body gap freezes the body. The inline
// emission below would trail the run on the label's line (`lll: // c`), an
// inert placement that loses the freeze on the second pass, so route through
// the own-line-preserving header→body emitter instead — the declaration-header
// rule of `conformance_prettier_ignore.md` §On module and declarator lists.
// The statement-aware emitter restores the `;` an ASI-reliant body owes
// ([`Printer::build_frozen_statement_doc`]).
tail_parts.push(d.text(":"));
self.push_header_to_body_gap(&mut tail_parts, colon_end, body_start);
tail_parts.push(self.build_frozen_statement_doc(stmt.body));
} else if self.has_comments_to_emit_between(colon_end, body_start) {
// The run is pinned to the `:` line (prettier hoists it above the whole labeled
// statement instead — the sanctioned position divergence), but its INTERNAL
// shape is not part of that licence: the lines the author gave the run survive,
// as they do in every other run. Emitting it with one separator keyed on
// `has_line_comments_between` WELDED them (`l: /* c1 */⏎// c2` → one line), a
// divergence riding inside the cataloged one and matching neither formatter —
// prettier keeps those breaks in its own position.
//
// `AdjacentAnchorLine` for the same reason the empty-body gap uses it: the run's
// first comment shares the `:` line by construction, so reading its authored
// newline-before would force a break the next pass removes.
//
// The group holds the run ALONE, not the body: with no oracle here (prettier is
// somewhere else entirely), the body's width must not decide where a comment
// separator breaks. The run's own hardline still opens it.
tail_parts.push(d.text(":"));
tail_parts.push(d.text(" "));
let mut run = DocBuf::new();
self.push_leading_comment_run(
&mut run,
self.comments_to_emit_between(colon_end, body_start),
body_start,
LeadingGlue::AdjacentAnchorLine,
);
tail_parts.push(d.group(d.concat(&run)));
tail_parts.push(self.build_statement_doc(stmt.body, ctx.labeled_body()));
} else {
// No space before empty statement: `label:;` not `label: ;`
let separator = if matches!(&stmt.body.kind, StatementKind::EmptyStatement(_)) {
":"
} else {
": "
};
tail_parts.push(d.text(separator));
tail_parts.push(self.build_statement_doc(stmt.body, ctx.labeled_body()));
}
let tail = d.concat(&tail_parts);
// An **own-line** comment in the label→`:` gap — a line comment, or a block
// comment the author put on its own line — is relocated onto its own line(s)
// before the label (matching prettier). A line comment must move (emitting it
// inline would let the `//` swallow the `:` + body); an own-line block follows
// the same rule rather than reflowing inline. A purely **same-line** block
// stays inline before `:` (`label /* c */: body`), matching prettier.
// **to emit**: this set is printed below, and `relocate` is derived from it — so the
// two agree by construction. Nothing can be owned here anyway: an owned comment binds
// to the token that follows it, and `:` begins no node.
let gap_comments: CommentVec<'_> = self
.comments_to_emit_between(label_end, colon_pos as u32)
.collect();
let relocate = gap_comments.iter().any(|c| self.is_own_line_comment(c));
let mut parts: DocBuf = smallvec![];
if relocate {
for (i, comment) in gap_comments.iter().enumerate() {
parts.push(self.build_comment_doc(comment));
// A space keeps a pair the author glued together (`/* c */ // d`);
// otherwise break. The glue question is `comment_hugs_next`, the single
// glue test — not an `is_same_line` restatement of it, which only
// coincides here because this gap holds nothing but trivia between two
// comments (`docs/comments.md` §Own-line-ness is a SOURCE question).
//
// ⚠️ The **last** comment always breaks, whatever the source says: it is
// followed by the label the run was relocated off, so hugging it would
// undo the relocation this arm exists for.
match gap_comments.get(i + 1) {
Some(_) if self.comment_hugs_next(comment) => parts.push(d.text(" ")),
_ => parts.push(d.hardline()),
}
}
parts.push(self.identifier_name_doc(&stmt.label));
parts.push(tail);
} else {
parts.push(self.identifier_name_doc(&stmt.label));
parts.push(self.build_inline_comments_between_doc(label_end, colon_pos as u32));
parts.push(tail);
}
d.concat(&parts)
}
}