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
// The paren-head statement family: while, with and do-while
//
// The statements printed off one `(head)` + body pair — `while` and `with` share the
// layout outright (`ParenHeadKind` names what differs), and do-while is the same pair
// inverted. Condition-group layout and body handling, including the do-while
// comment-preservation divergence from Prettier. `with` is not a loop, which is why
// this file sits beside `loops/` rather than in it.
use super::{HeadChainGrouping, OpenParenLineBlockComment};
use crate::ast::{self, Statement, StatementKind};
use crate::printer::Printer;
use crate::printer::statements::StatementContext;
use smallvec::smallvec;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;
/// What parts `with` from `while` in the layout they otherwise share: the keyword, whether
/// an empty block body collapses (prettier's parent allowlist in `print/block.js`), and how
/// a logical head chain groups (prettier's `isInsideParenthesis`, [`HeadChainGrouping`]).
#[derive(Clone, Copy)]
struct ParenHeadKind {
keyword: &'static str,
collapses_empty_block: bool,
grouping: HeadChainGrouping,
}
impl ParenHeadKind {
const WHILE: Self = Self {
keyword: "while",
collapses_empty_block: true,
grouping: HeadChainGrouping::ParenGroupDrives,
};
const WITH: Self = Self {
keyword: "with",
collapses_empty_block: false,
grouping: HeadChainGrouping::ChainGroupsItself,
};
}
impl<'a> Printer<'a> {
/// Build a doc for a while statement with proper line-width wrapping
///
/// Matches Prettier's architecture: the condition wraps to multiple lines
/// when the `while (condition)` line exceeds print width.
pub(in crate::printer::statements) fn build_while_statement_doc(
&self,
stmt: &ast::WhileStatement<'_>,
span: Span,
ctx: StatementContext,
) -> DocId {
self.build_paren_head_statement_doc(
ParenHeadKind::WHILE,
span.start,
stmt.test,
stmt.body,
ctx,
)
}
/// Build a doc for a `with` statement — `while`'s layout, which is also how prettier
/// prints it: one printer serves both, keying the keyword on the node type
/// (`print/while-statement.js`).
///
/// Two axes part them, both carried by [`ParenHeadKind`]:
///
/// - the EMPTY BLOCK body — prettier's block printer collapses `{}` only for a listed
/// set of parents, and `WithStatement` is not in it, so `with (o) {}` prints
/// `with (o) {⏎}` where `while (c) {}` stays collapsed (`print/block.js`);
/// - the HEAD CHAIN grouping — `WithStatement` is not on prettier's
/// `isInsideParenthesis` parent list, so a logical head chain groups itself instead
/// of handing its breaking to the head's paren group ([`HeadChainGrouping`],
/// `print/binaryish.js`).
pub(in crate::printer::statements) fn build_with_statement_doc(
&self,
stmt: &ast::WithStatement<'_>,
span: Span,
ctx: StatementContext,
) -> DocId {
self.build_paren_head_statement_doc(
ParenHeadKind::WITH,
span.start,
stmt.object,
stmt.body,
ctx,
)
}
/// The layout `while` and `with` share: a `keyword (head)` group, then the body in
/// one of three arms (block / empty statement / `adjustClause`).
///
/// [`ParenHeadKind`] carries the three facts the two constructs part on.
fn build_paren_head_statement_doc(
&self,
kind: ParenHeadKind,
stmt_start: u32,
head: &ast::Expression<'_>,
body: &Statement<'_>,
ctx: StatementContext,
) -> DocId {
let d = self.d();
// The head every arm below shares, built once — the same one the `if` printer
// takes, so the paren-headed statements cannot drift apart.
let (mut parts, paren_end) =
self.build_paren_condition_head(kind.keyword, stmt_start, head, kind.grouping);
if let StatementKind::BlockStatement(block) = &body.kind {
// Block body: while (cond) { ... }
// Uses append_close_paren_with_comments for consistency with if/for-in/for-of:
// block comments stay inline, line comments become trailing.
self.append_close_paren_with_comments(&mut parts, paren_end, block.span.start);
parts.push(self.build_statement_head_doc(paren_end, body, || {
if kind.collapses_empty_block {
self.build_block_statement_doc(block)
} else {
self.build_block_statement_expand_empty_doc(block)
}
}));
d.group(d.concat(&parts))
} else if matches!(&body.kind, StatementKind::EmptyStatement(_)) {
// Empty statement: `while (cond);` or `while (cond) /* comment */ ;`
let empty_start = body.span().start;
self.append_close_paren_empty_stmt_with_comments(&mut parts, paren_end, empty_start);
d.group(d.concat(&parts))
} else {
// Non-block body: use adjustClause equivalent
// - When flat: line becomes space -> `while (cond) a;`
// - When broken: line becomes newline + indent -> `while (cond)\n\ta;`
let body_start = body.span().start;
let body_doc = self.build_statement_head_doc(paren_end, body, || {
// `adjustClause` wraps the body in one indent in every arm; nothing
// continues on the tail's line, so the tail falls through to whatever
// flushes the statement's own position.
self.build_statement_doc(body, ctx.clause_body(false, true))
});
self.build_adjust_clause_with_comments(&parts, paren_end, body_start, body_doc)
}
}
pub(in crate::printer::statements) fn build_do_while_statement_doc(
&self,
stmt: &ast::DoWhileStatement<'_>,
span: Span,
ctx: StatementContext,
) -> DocId {
let d = self.d();
let is_block = matches!(&stmt.body.kind, StatementKind::BlockStatement(_));
// Check for comments between `do` keyword and body
let do_end = span.start + "do".len() as u32;
let body_start = stmt.body.span().start;
// The body's container facts: the `while` CONTINUES on the tail's flush line,
// and the body is wrapped in an indent by the broken header→body arm and by the
// comment-free clause arm (a non-block, non-empty body) below — a block body and
// a non-breaking comment gap print `do body` inline, so their tail dedents zero
// levels.
let gap_has_comments = self.has_comments_to_emit_between(do_end, body_start);
let gap_breaks_body =
gap_has_comments && self.header_to_body_gap_breaks(do_end, body_start) && !is_block;
let is_empty = matches!(&stmt.body.kind, StatementKind::EmptyStatement(_));
let is_clause = !gap_has_comments && !is_block && !is_empty;
let body_ctx = ctx.clause_body(true, gap_breaks_body || is_clause);
// A loop body collapses its empty block form (`do {} while (cond)`) — unless an
// own-line directive in the `do`→body gap freezes it.
let body_doc = self.build_statement_head_doc(do_end, stmt.body, || {
self.build_collapsing_body_doc(stmt.body, body_ctx)
});
let mut parts = if gap_has_comments {
let mut p = smallvec![d.text("do")];
if gap_breaks_body {
// Non-block body whose run breaks: the run shares the body's indent, with
// a `//` normalized onto its own line — prettier does the same here, so
// there is nothing to preserve.
self.push_indented_header_to_body_gap(&mut p, do_end, body_start, body_doc);
} else {
// Everything else — the shared header→body gap, exactly as `try`/`catch`/
// `finally` use it: a comment trailing `do` stays trailing, one on its own
// line keeps it. Emitting the run inline after a bare space relocated an
// own-line comment up onto the `do` line (`do // c⏎{`), the same defect
// the `try` family had; prettier preserves here, so it is a clean oracle
// rather than a stance.
//
// A non-breaking run (block comments trailing `do`) took a hand-rolled
// third arm here until it was measured against this one: that arm's
// `" "` + run + `" "` is exactly what this emitter emits when
// `header_to_body_gap_breaks` is false, so it was a second spelling of
// this gap's own no-break case rather than a case this one lacks.
self.push_header_to_body_gap(&mut p, do_end, body_start);
p.push(body_doc);
}
p
} else if is_empty {
// Prettier's `adjustClause` returns `";"` directly for an empty body
// → `do;`, not `do ;`.
smallvec![d.text("do"), body_doc]
} else if is_clause {
// A non-block body takes `adjustClause` (`group(indent([line, clause]))`), as
// an `if` consequent and an `else` alternate do: on the `do`'s line when it
// fits flat, dropped below it and indented when it breaks.
smallvec![d.text("do"), d.group(d.indent_line(body_doc))]
} else {
smallvec![d.text("do "), body_doc]
};
// Find the while keyword position for comment handling
// Search forward from body end, skipping over comments to find the actual keyword
let body_end = stmt.body.span().end;
let test_start = stmt.test.span().start;
let while_pos = self.find_keyword_in_range(body_end, test_start, "while");
// The `}`→`while` gap: its comments and the separator before the keyword.
// Emitted here, ahead of the paren bookkeeping below, which computes without
// pushing; the `while` keyword itself follows it.
if let Some(while_start) = while_pos {
self.push_block_to_keyword_gap(
&mut parts,
body_end,
while_start,
is_block,
self.terminator_defers_comment(stmt.body.span().start, body_end),
);
} else {
parts.push(if is_block { d.text(" ") } else { d.hardline() });
}
// Find paren positions for comment handling
let open_paren = while_pos.and_then(|p| self.find_open_paren_after(p));
let close_paren = open_paren.and_then(|o| self.matching_close_paren(o));
// Preserve comments between `while` keyword and `(` in place:
// do{}while/* c */(a); → do {} while /* c */ (a);
let keyword_comments = while_pos.and_then(|wp| {
self.build_keyword_paren_comments(wp + "while".len() as u32, open_paren)
});
// The keyword is pushed here rather than by `push_keyword_open_paren`: the
// `}`→`while` gap above already owns everything ahead of it.
parts.push(d.text("while"));
self.push_open_paren_after_keyword(&mut parts, keyword_comments);
// The same entry point `if` / `while` use, as in prettier (whose
// `printDoWhileStatementCondition` is `printIfStatementCondition` under another
// name): the width-driven condition group, the `!(…)` negation hug, and the
// clarity parens an assignment takes (`do {} while ((x = y))`). The one axis
// that parts do-while from the rest is the `(`-line comment policy — a comment
// after `while (` stays in place, where prettier relocates it outside the parens
// (`OpenParenLineBlockComment::Preserve`).
parts.push(self.build_statement_condition_doc(
stmt.test,
open_paren,
close_paren,
OpenParenLineBlockComment::Preserve,
HeadChainGrouping::ParenGroupDrives,
));
// Comments between the condition's `)` and the do-while's terminating `;`,
// with the `;` bound to the statement: a same-line block trails *after* it
// (`} while (x) /* c */;` → `} while (x); /* c */`, prettier 3.9), a same-line
// line via `line_suffix`, an own-line comment on its own line after. (Unlike
// an empty *body* `;` — `if (a) /* c */ ;` — which keeps the comment inline;
// the do-while `;` is the statement terminator.) See
// `push_semicolon_with_gap_comments`.
if let Some(close) = close_paren {
parts.push(d.text(")"));
self.push_statement_semicolon(&mut parts, close + 1, span.end, ctx.terminator_gap());
} else {
parts.push(d.text(");"));
}
d.concat(&parts)
}
}