@tishlang/tish 1.12.0 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +1201 -132
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -1,44 +1,177 @@
|
|
|
1
1
|
//! Pretty-print Tish AST to source. Style: 2-space indent, braces for blocks, trailing newline.
|
|
2
2
|
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
3
5
|
use tishlang_ast::{
|
|
4
6
|
ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern,
|
|
5
7
|
ExportDeclaration, Expr, FunParam, ImportSpecifier, JsxAttrValue, JsxChild, JsxProp, Literal,
|
|
6
|
-
LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, TypeAnnotation, TypedParam,
|
|
8
|
+
LogicalAssignOp, MemberProp, ObjectProp, Program, Span, Statement, TypeAnnotation, TypedParam,
|
|
7
9
|
UnaryOp,
|
|
8
10
|
};
|
|
9
11
|
|
|
12
|
+
/// A comment recovered from source by [`scan_comments`]. The lexer discards
|
|
13
|
+
/// comments, so the parsed AST has none; the formatter re-inserts these by source
|
|
14
|
+
/// position so they survive a format pass.
|
|
15
|
+
#[derive(Clone)]
|
|
16
|
+
struct CommentTok {
|
|
17
|
+
/// 1-based (line, col) of the opening `/`, matching `ast::Span`.
|
|
18
|
+
start: (usize, usize),
|
|
19
|
+
/// Verbatim comment text, including the `//` or `/* */` delimiters.
|
|
20
|
+
text: String,
|
|
21
|
+
/// True when only whitespace precedes the comment on its line (a leading,
|
|
22
|
+
/// own-line comment); false for a trailing `code // note` comment.
|
|
23
|
+
own_line: bool,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Maps a `{`'s 1-based (line, col) to its matching `}`'s (line, col). Used to
|
|
27
|
+
/// bound a block's dangling-comment flush to the real closing brace, since the
|
|
28
|
+
/// parser's `Block` `span.end` overshoots to the next token.
|
|
29
|
+
type BraceMap = HashMap<(usize, usize), (usize, usize)>;
|
|
30
|
+
|
|
10
31
|
/// Format Tish source. On parse error, returns the parser message.
|
|
11
32
|
pub fn format_source(source: &str) -> Result<String, String> {
|
|
12
33
|
let program = tishlang_parser::parse(source)?;
|
|
13
|
-
|
|
34
|
+
let (comments, braces, bracket_spans) = scan_comments(source);
|
|
35
|
+
Ok(format_with_comments(
|
|
36
|
+
&program,
|
|
37
|
+
comments,
|
|
38
|
+
blank_line_map(source),
|
|
39
|
+
braces,
|
|
40
|
+
bracket_spans,
|
|
41
|
+
source,
|
|
42
|
+
))
|
|
14
43
|
}
|
|
15
44
|
|
|
45
|
+
/// Format an already-parsed program. Comments and blank lines are unavailable
|
|
46
|
+
/// here (the AST carries neither), so the output is comment-free and dense and
|
|
47
|
+
/// `// tish-fmt-ignore` has no effect; use [`format_source`] when the original
|
|
48
|
+
/// text is available.
|
|
16
49
|
pub fn format_program(program: &Program) -> String {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
format_with_comments(
|
|
51
|
+
program,
|
|
52
|
+
Vec::new(),
|
|
53
|
+
Vec::new(),
|
|
54
|
+
BraceMap::new(),
|
|
55
|
+
Vec::new(),
|
|
56
|
+
"",
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn format_with_comments(
|
|
61
|
+
program: &Program,
|
|
62
|
+
comments: Vec<CommentTok>,
|
|
63
|
+
blank_lines: Vec<bool>,
|
|
64
|
+
braces: BraceMap,
|
|
65
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
66
|
+
source: &str,
|
|
67
|
+
) -> String {
|
|
68
|
+
let mut p = Printer::new(comments, blank_lines, braces, bracket_spans, source);
|
|
69
|
+
p.print_seq(&program.statements, 0);
|
|
70
|
+
// Comments after the last statement (trailing file comments).
|
|
71
|
+
p.emit_leading_comments((usize::MAX, usize::MAX), 0);
|
|
72
|
+
// Exactly one trailing newline.
|
|
73
|
+
while p.buf.ends_with('\n') {
|
|
74
|
+
p.buf.pop();
|
|
29
75
|
}
|
|
76
|
+
p.buf.push('\n');
|
|
30
77
|
p.buf
|
|
31
78
|
}
|
|
32
79
|
|
|
80
|
+
/// `out[line]` is true when 1-based source `line` is blank (empty or whitespace
|
|
81
|
+
/// only). Index 0 is unused so callers can index by 1-based line number.
|
|
82
|
+
fn blank_line_map(source: &str) -> Vec<bool> {
|
|
83
|
+
let mut v = vec![false];
|
|
84
|
+
v.extend(source.lines().map(|l| l.trim().is_empty()));
|
|
85
|
+
v
|
|
86
|
+
}
|
|
87
|
+
|
|
33
88
|
struct Printer {
|
|
34
89
|
buf: String,
|
|
90
|
+
/// Comments in source order; `ci` is the next one not yet emitted.
|
|
91
|
+
comments: Vec<CommentTok>,
|
|
92
|
+
ci: usize,
|
|
93
|
+
/// 1-indexed: `blank_lines[n]` is true when source line `n` is blank. Empty
|
|
94
|
+
/// when no source is available (`format_program`).
|
|
95
|
+
blank_lines: Vec<bool>,
|
|
96
|
+
/// `{`→`}` position map for bounding block dangling-comment flushes.
|
|
97
|
+
braces: BraceMap,
|
|
98
|
+
/// Nonzero once anything has been emitted in the current sequence; used only
|
|
99
|
+
/// to suppress a leading blank line at the very start of a file or block.
|
|
100
|
+
emitted: usize,
|
|
101
|
+
/// Current structural indentation level for expression layout — set to the
|
|
102
|
+
/// enclosing statement's level and bumped as broken containers nest. Continuation
|
|
103
|
+
/// lines of a broken object/array/argument-list indent to `depth + 1`.
|
|
104
|
+
depth: usize,
|
|
105
|
+
/// When set, containers render on one line regardless of width. Used to measure
|
|
106
|
+
/// a container's flat width before deciding whether to break it.
|
|
107
|
+
force_flat: bool,
|
|
108
|
+
/// Source as chars, plus 1-based line → starting char-index, for slicing the
|
|
109
|
+
/// original text of a `// tish-fmt-ignore`-d statement verbatim. Empty when no
|
|
110
|
+
/// source is available (`format_program`).
|
|
111
|
+
src: Vec<char>,
|
|
112
|
+
line_start: Vec<usize>,
|
|
113
|
+
/// Set by [`Printer::emit_leading_comments`] when the comment directly above the
|
|
114
|
+
/// next statement is `// tish-fmt-ignore`; consumed by [`Printer::print_seq`].
|
|
115
|
+
ignore_next: bool,
|
|
116
|
+
/// `(open_line, close_line)` for every bracket pair, used to bound an ignored
|
|
117
|
+
/// statement's verbatim slice to its own full extent.
|
|
118
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
35
119
|
}
|
|
36
120
|
|
|
121
|
+
/// Target line width: objects/arrays/argument lists that fit within this stay on
|
|
122
|
+
/// one line; longer ones break one item per line.
|
|
123
|
+
const WIDTH: usize = 100;
|
|
124
|
+
|
|
125
|
+
/// The escape-hatch marker (à la Prettier's `// prettier-ignore`): a comment with
|
|
126
|
+
/// exactly this content leaves the next statement's original source untouched.
|
|
127
|
+
const IGNORE_MARKER: &str = "tish-fmt-ignore";
|
|
128
|
+
|
|
37
129
|
impl Printer {
|
|
38
|
-
fn new(
|
|
130
|
+
fn new(
|
|
131
|
+
comments: Vec<CommentTok>,
|
|
132
|
+
blank_lines: Vec<bool>,
|
|
133
|
+
braces: BraceMap,
|
|
134
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
135
|
+
source: &str,
|
|
136
|
+
) -> Self {
|
|
137
|
+
let src: Vec<char> = source.chars().collect();
|
|
138
|
+
// line_start[L] = char index where 1-based line L begins (index 0 unused).
|
|
139
|
+
let mut line_start = vec![0usize, 0usize];
|
|
140
|
+
for (i, &c) in src.iter().enumerate() {
|
|
141
|
+
if c == '\n' {
|
|
142
|
+
line_start.push(i + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
39
145
|
Self {
|
|
40
146
|
buf: String::with_capacity(4096),
|
|
147
|
+
comments,
|
|
148
|
+
ci: 0,
|
|
149
|
+
blank_lines,
|
|
150
|
+
braces,
|
|
151
|
+
emitted: 0,
|
|
152
|
+
depth: 0,
|
|
153
|
+
force_flat: false,
|
|
154
|
+
src,
|
|
155
|
+
line_start,
|
|
156
|
+
ignore_next: false,
|
|
157
|
+
bracket_spans,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Char index of a 1-based (line, col) position, clamped to the source length.
|
|
162
|
+
fn char_pos(&self, (line, col): (usize, usize)) -> usize {
|
|
163
|
+
if line >= self.line_start.len() {
|
|
164
|
+
return self.src.len();
|
|
41
165
|
}
|
|
166
|
+
(self.line_start[line] + col - 1).min(self.src.len())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// The original source spanning `[from, to)`, with trailing whitespace trimmed.
|
|
170
|
+
fn verbatim(&self, from: (usize, usize), to: (usize, usize)) -> String {
|
|
171
|
+
let a = self.char_pos(from);
|
|
172
|
+
let b = self.char_pos(to).max(a);
|
|
173
|
+
let s: String = self.src[a..b].iter().collect();
|
|
174
|
+
s.trim_end().to_string()
|
|
42
175
|
}
|
|
43
176
|
|
|
44
177
|
fn indent(&mut self, level: usize) {
|
|
@@ -47,18 +180,334 @@ impl Printer {
|
|
|
47
180
|
}
|
|
48
181
|
}
|
|
49
182
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
183
|
+
/// Current column (chars since the last newline) — the start position for the
|
|
184
|
+
/// next thing to be printed.
|
|
185
|
+
fn col(&self) -> usize {
|
|
186
|
+
let line_start = self.buf.rfind('\n').map(|i| i + 1).unwrap_or(0);
|
|
187
|
+
self.buf[line_start..].chars().count()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Render a container flat into `buf`; if it fits in the remaining width keep it,
|
|
191
|
+
/// otherwise roll back and render the broken form. While measuring (and inside an
|
|
192
|
+
/// already-flat context) nested containers stay inline too, so the measured width
|
|
193
|
+
/// is the true single-line length. This is the layout decision in miniature — the
|
|
194
|
+
/// `fits` check of a Wadler/Prettier-style pretty-printer, done by rendering.
|
|
195
|
+
fn fit(&mut self, inline: impl Fn(&mut Self), broken: impl Fn(&mut Self)) {
|
|
196
|
+
if self.force_flat {
|
|
197
|
+
inline(self);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let mark = self.buf.len();
|
|
201
|
+
let col = self.col();
|
|
202
|
+
self.force_flat = true;
|
|
203
|
+
inline(self);
|
|
204
|
+
self.force_flat = false;
|
|
205
|
+
if col + (self.buf.len() - mark) <= WIDTH {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
self.buf.truncate(mark);
|
|
209
|
+
broken(self);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- Width-aware containers: inline when they fit, else one item per line. ----
|
|
213
|
+
|
|
214
|
+
fn emit_object(&mut self, props: &[ObjectProp]) {
|
|
215
|
+
if props.is_empty() {
|
|
216
|
+
self.buf.push_str("{}");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
self.fit(|s| s.object_inline(props), |s| s.object_broken(props));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn object_inline(&mut self, props: &[ObjectProp]) {
|
|
223
|
+
self.buf.push_str("{ ");
|
|
224
|
+
for (i, pr) in props.iter().enumerate() {
|
|
225
|
+
if i > 0 {
|
|
226
|
+
self.buf.push_str(", ");
|
|
227
|
+
}
|
|
228
|
+
self.object_prop(pr);
|
|
229
|
+
}
|
|
230
|
+
self.buf.push_str(" }");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn object_broken(&mut self, props: &[ObjectProp]) {
|
|
234
|
+
self.buf.push_str("{\n");
|
|
235
|
+
self.depth += 1;
|
|
236
|
+
for (i, pr) in props.iter().enumerate() {
|
|
237
|
+
if i > 0 {
|
|
238
|
+
self.buf.push_str(",\n");
|
|
239
|
+
}
|
|
240
|
+
self.indent(self.depth);
|
|
241
|
+
self.object_prop(pr);
|
|
242
|
+
}
|
|
243
|
+
self.depth -= 1;
|
|
244
|
+
self.buf.push('\n');
|
|
245
|
+
self.indent(self.depth);
|
|
246
|
+
self.buf.push('}');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn object_prop(&mut self, pr: &ObjectProp) {
|
|
250
|
+
match pr {
|
|
251
|
+
ObjectProp::KeyValue(k, v) => {
|
|
252
|
+
self.buf.push_str(k.as_ref());
|
|
253
|
+
self.buf.push_str(": ");
|
|
254
|
+
self.expr(v);
|
|
255
|
+
}
|
|
256
|
+
ObjectProp::Spread(ex) => {
|
|
257
|
+
self.buf.push_str("...");
|
|
258
|
+
self.expr(ex);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn emit_array(&mut self, elems: &[ArrayElement]) {
|
|
264
|
+
if elems.is_empty() {
|
|
265
|
+
self.buf.push_str("[]");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
self.fit(|s| s.array_inline(elems), |s| s.array_broken(elems));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn array_inline(&mut self, elems: &[ArrayElement]) {
|
|
272
|
+
self.buf.push('[');
|
|
273
|
+
for (i, el) in elems.iter().enumerate() {
|
|
274
|
+
if i > 0 {
|
|
275
|
+
self.buf.push_str(", ");
|
|
276
|
+
}
|
|
277
|
+
self.array_elem(el);
|
|
278
|
+
}
|
|
279
|
+
self.buf.push(']');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn array_broken(&mut self, elems: &[ArrayElement]) {
|
|
283
|
+
self.buf.push_str("[\n");
|
|
284
|
+
self.depth += 1;
|
|
285
|
+
for (i, el) in elems.iter().enumerate() {
|
|
286
|
+
if i > 0 {
|
|
287
|
+
self.buf.push_str(",\n");
|
|
288
|
+
}
|
|
289
|
+
self.indent(self.depth);
|
|
290
|
+
self.array_elem(el);
|
|
291
|
+
}
|
|
292
|
+
self.depth -= 1;
|
|
293
|
+
self.buf.push('\n');
|
|
294
|
+
self.indent(self.depth);
|
|
295
|
+
self.buf.push(']');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn array_elem(&mut self, el: &ArrayElement) {
|
|
299
|
+
match el {
|
|
300
|
+
ArrayElement::Expr(ex) => self.expr(ex),
|
|
301
|
+
ArrayElement::Spread(ex) => {
|
|
302
|
+
self.buf.push_str("...");
|
|
303
|
+
self.expr(ex);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Argument list `(...)`. A sole/last object|array|arrow argument "hugs" the
|
|
309
|
+
/// parens — it breaks itself rather than forcing the whole list to break, e.g.
|
|
310
|
+
/// `f(layout, [\n …\n])`. Otherwise the list breaks as a unit when too wide.
|
|
311
|
+
fn emit_args(&mut self, args: &[CallArg]) {
|
|
312
|
+
if args.is_empty() {
|
|
313
|
+
self.buf.push_str("()");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if !self.force_flat && self.last_arg_huggable(args) {
|
|
317
|
+
self.buf.push('(');
|
|
318
|
+
for (i, a) in args.iter().enumerate() {
|
|
319
|
+
if i > 0 {
|
|
320
|
+
self.buf.push_str(", ");
|
|
58
321
|
}
|
|
59
|
-
self.
|
|
60
|
-
|
|
322
|
+
self.call_arg(a);
|
|
323
|
+
}
|
|
324
|
+
self.buf.push(')');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
self.fit(|s| s.args_inline(args), |s| s.args_broken(args));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn last_arg_huggable(&self, args: &[CallArg]) -> bool {
|
|
331
|
+
let huggable = |a: &CallArg| {
|
|
332
|
+
matches!(
|
|
333
|
+
a,
|
|
334
|
+
CallArg::Expr(Expr::Object { .. })
|
|
335
|
+
| CallArg::Expr(Expr::Array { .. })
|
|
336
|
+
| CallArg::Expr(Expr::ArrowFunction { .. })
|
|
337
|
+
)
|
|
338
|
+
};
|
|
339
|
+
let collection =
|
|
340
|
+
|a: &CallArg| matches!(a, CallArg::Expr(Expr::Object { .. } | Expr::Array { .. }));
|
|
341
|
+
match args.split_last() {
|
|
342
|
+
Some((last, rest)) => huggable(last) && !rest.iter().any(collection),
|
|
343
|
+
None => false,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn args_inline(&mut self, args: &[CallArg]) {
|
|
348
|
+
self.buf.push('(');
|
|
349
|
+
for (i, a) in args.iter().enumerate() {
|
|
350
|
+
if i > 0 {
|
|
351
|
+
self.buf.push_str(", ");
|
|
61
352
|
}
|
|
353
|
+
self.call_arg(a);
|
|
354
|
+
}
|
|
355
|
+
self.buf.push(')');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn args_broken(&mut self, args: &[CallArg]) {
|
|
359
|
+
self.buf.push_str("(\n");
|
|
360
|
+
self.depth += 1;
|
|
361
|
+
for (i, a) in args.iter().enumerate() {
|
|
362
|
+
if i > 0 {
|
|
363
|
+
self.buf.push_str(",\n");
|
|
364
|
+
}
|
|
365
|
+
self.indent(self.depth);
|
|
366
|
+
self.call_arg(a);
|
|
367
|
+
}
|
|
368
|
+
self.depth -= 1;
|
|
369
|
+
self.buf.push('\n');
|
|
370
|
+
self.indent(self.depth);
|
|
371
|
+
self.buf.push(')');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn call_arg(&mut self, a: &CallArg) {
|
|
375
|
+
match a {
|
|
376
|
+
CallArg::Expr(ex) => self.expr(ex),
|
|
377
|
+
CallArg::Spread(ex) => {
|
|
378
|
+
self.buf.push_str("...");
|
|
379
|
+
self.expr(ex);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn is_blank(&self, line: usize) -> bool {
|
|
385
|
+
self.blank_lines.get(line).copied().unwrap_or(false)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Preserve a single blank line before the item starting at `next_line` when
|
|
389
|
+
/// the source line directly above it was blank. Statement `span.end` is
|
|
390
|
+
/// unreliable (it points at the next token), so spacing keys off the reliable
|
|
391
|
+
/// start line and the source blank-line map rather than on span ranges.
|
|
392
|
+
fn vspace(&mut self, next_line: usize) {
|
|
393
|
+
if self.emitted != 0
|
|
394
|
+
&& self.is_blank(next_line.saturating_sub(1))
|
|
395
|
+
&& !self.buf.ends_with("\n\n")
|
|
396
|
+
{
|
|
397
|
+
self.buf.push('\n');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Emit every pending comment positioned before `before`, each on its own line
|
|
402
|
+
/// at `level` indentation, preserving source blank-line separation. Used at
|
|
403
|
+
/// statement-sequence boundaries and before a block's closing brace.
|
|
404
|
+
fn emit_leading_comments(&mut self, before: (usize, usize), level: usize) {
|
|
405
|
+
while self.ci < self.comments.len() && self.comments[self.ci].start < before {
|
|
406
|
+
let c = self.comments[self.ci].clone();
|
|
407
|
+
self.ci += 1;
|
|
408
|
+
self.vspace(c.start.0);
|
|
409
|
+
self.indent(level);
|
|
410
|
+
self.buf.push_str(&c.text);
|
|
411
|
+
self.buf.push('\n');
|
|
412
|
+
self.emitted = c.start.0;
|
|
413
|
+
// A marker anywhere in the statement's leading comment group ignores it.
|
|
414
|
+
if is_ignore_marker(&c.text) {
|
|
415
|
+
self.ignore_next = true;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// Emit trailing (same-line) comments sitting on source line `line`, inline
|
|
421
|
+
/// after the just-printed statement text.
|
|
422
|
+
fn emit_trailing_comments(&mut self, line: usize) {
|
|
423
|
+
while self.ci < self.comments.len() {
|
|
424
|
+
let c = &self.comments[self.ci];
|
|
425
|
+
if c.own_line || c.start.0 != line {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
let text = c.text.clone();
|
|
429
|
+
self.ci += 1;
|
|
430
|
+
self.buf.push(' ');
|
|
431
|
+
self.buf.push_str(&text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Print a run of statements (top level, block body, or switch case body),
|
|
436
|
+
/// interleaving recovered comments and preserving blank-line grouping. Trailing
|
|
437
|
+
/// comments attach by the statement's reliable start line (single-line case);
|
|
438
|
+
/// a trailing comment on a multi-line statement's last line simply migrates to
|
|
439
|
+
/// a leading comment of the next item — preserved, never dropped.
|
|
440
|
+
fn print_seq(&mut self, stmts: &[Statement], level: usize) {
|
|
441
|
+
for (i, s) in stmts.iter().enumerate() {
|
|
442
|
+
let sp = s.span();
|
|
443
|
+
self.ignore_next = false;
|
|
444
|
+
self.emit_leading_comments(sp.start, level);
|
|
445
|
+
self.vspace(sp.start.0);
|
|
446
|
+
if self.ignore_next {
|
|
447
|
+
self.emit_ignored(s, i, stmts, level);
|
|
448
|
+
} else {
|
|
449
|
+
self.stmt(s, level);
|
|
450
|
+
}
|
|
451
|
+
self.emitted = sp.start.0;
|
|
452
|
+
self.emit_trailing_comments(sp.start.0);
|
|
453
|
+
self.buf.push('\n');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/// Last source line covered by the statement at `start` — its start line, grown
|
|
458
|
+
/// over the full lines of any bracket (`{}`/`[]`/`()`) it transitively opens. This
|
|
459
|
+
/// bounds an ignored statement to its own extent regardless of what follows (next
|
|
460
|
+
/// sibling, a `switch` case label, the enclosing `}`), so verbatim never overruns.
|
|
461
|
+
fn ignored_last_line(&self, start: (usize, usize)) -> usize {
|
|
462
|
+
let mut last = start.0;
|
|
463
|
+
loop {
|
|
464
|
+
let mut grew = false;
|
|
465
|
+
for &(open_line, close_line) in &self.bracket_spans {
|
|
466
|
+
if open_line >= start.0 && open_line <= last && close_line > last {
|
|
467
|
+
last = close_line;
|
|
468
|
+
grew = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if !grew {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
last
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Emit a `// tish-fmt-ignore`-d statement as its original source, verbatim. The
|
|
479
|
+
/// slice ends at the smallest of: the statement's own bracket extent, the next
|
|
480
|
+
/// sibling, and the next own-line comment — so a same-line trailing comment is
|
|
481
|
+
/// kept but neither the next statement's leading comments nor anything past this
|
|
482
|
+
/// statement leaks in. Captured comments are skipped so they aren't re-emitted.
|
|
483
|
+
fn emit_ignored(&mut self, s: &Statement, i: usize, stmts: &[Statement], level: usize) {
|
|
484
|
+
let start = s.span().start;
|
|
485
|
+
let mut boundary = (self.ignored_last_line(start) + 1, 1);
|
|
486
|
+
if let Some(next) = stmts.get(i + 1) {
|
|
487
|
+
boundary = boundary.min(next.span().start);
|
|
488
|
+
}
|
|
489
|
+
let mut j = self.ci;
|
|
490
|
+
while j < self.comments.len() && self.comments[j].start < boundary {
|
|
491
|
+
let c = &self.comments[j];
|
|
492
|
+
if c.start > start && c.own_line {
|
|
493
|
+
boundary = c.start;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
j += 1;
|
|
497
|
+
}
|
|
498
|
+
self.indent(level);
|
|
499
|
+
let text = self.verbatim(start, boundary);
|
|
500
|
+
self.buf.push_str(&text);
|
|
501
|
+
while self.ci < self.comments.len() && self.comments[self.ci].start < boundary {
|
|
502
|
+
self.ci += 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn stmt(&mut self, s: &Statement, level: usize) {
|
|
507
|
+
// Expression layout indents relative to this statement's level.
|
|
508
|
+
self.depth = level;
|
|
509
|
+
match s {
|
|
510
|
+
Statement::Block { statements, span } => self.block(statements, *span, level, true),
|
|
62
511
|
Statement::VarDecl {
|
|
63
512
|
name,
|
|
64
513
|
mutable,
|
|
@@ -197,7 +646,7 @@ impl Printer {
|
|
|
197
646
|
self.expr(expr);
|
|
198
647
|
} else {
|
|
199
648
|
self.buf.push(' ');
|
|
200
|
-
self.
|
|
649
|
+
self.stmt_inline_or_block(body, level);
|
|
201
650
|
}
|
|
202
651
|
}
|
|
203
652
|
Statement::Switch {
|
|
@@ -220,18 +669,14 @@ impl Printer {
|
|
|
220
669
|
}
|
|
221
670
|
None => self.buf.push_str("default:\n"),
|
|
222
671
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
self.buf.push('\n');
|
|
226
|
-
}
|
|
672
|
+
self.emitted = 0;
|
|
673
|
+
self.print_seq(stmts, level + 2);
|
|
227
674
|
}
|
|
228
675
|
if let Some(def) = default_body {
|
|
229
676
|
self.indent(level + 1);
|
|
230
677
|
self.buf.push_str("default:\n");
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self.buf.push('\n');
|
|
234
|
-
}
|
|
678
|
+
self.emitted = 0;
|
|
679
|
+
self.print_seq(def, level + 2);
|
|
235
680
|
}
|
|
236
681
|
self.indent(level);
|
|
237
682
|
self.buf.push('}');
|
|
@@ -240,6 +685,7 @@ impl Printer {
|
|
|
240
685
|
self.indent(level);
|
|
241
686
|
self.buf.push_str("do ");
|
|
242
687
|
self.stmt_inline_or_block(body, level);
|
|
688
|
+
self.depth = level; // body recursion moved depth; restore for cond
|
|
243
689
|
self.buf.push_str(" while (");
|
|
244
690
|
self.expr(cond);
|
|
245
691
|
self.buf.push(')');
|
|
@@ -352,7 +798,7 @@ impl Printer {
|
|
|
352
798
|
self.type_ann(rt);
|
|
353
799
|
}
|
|
354
800
|
self.buf.push(' ');
|
|
355
|
-
self.
|
|
801
|
+
self.stmt_inline_or_block(body, level);
|
|
356
802
|
} else {
|
|
357
803
|
self.stmt(inner, level);
|
|
358
804
|
}
|
|
@@ -391,15 +837,43 @@ impl Printer {
|
|
|
391
837
|
}
|
|
392
838
|
}
|
|
393
839
|
|
|
840
|
+
/// Print a `{ … }` block. `lead_indent` controls whether the opening brace is
|
|
841
|
+
/// indented (true for a standalone block statement) or written at the current
|
|
842
|
+
/// position (false when it follows `if (…) `, `fn f() `, `else `, etc.).
|
|
843
|
+
fn block(&mut self, statements: &[Statement], span: Span, level: usize, lead_indent: bool) {
|
|
844
|
+
if lead_indent {
|
|
845
|
+
self.indent(level);
|
|
846
|
+
}
|
|
847
|
+
self.buf.push_str("{\n");
|
|
848
|
+
// Fresh sequence: suppress any blank line immediately after `{`.
|
|
849
|
+
self.emitted = 0;
|
|
850
|
+
self.print_seq(statements, level + 1);
|
|
851
|
+
// Dangling comments between the last statement and `}` (e.g. a note inside an
|
|
852
|
+
// otherwise-empty block). Bound by the real closing brace — `span.end`
|
|
853
|
+
// overshoots to the next token. Brace-less (indent) blocks have no map entry,
|
|
854
|
+
// so `span.start` flushes nothing and the comment migrates to the next sibling.
|
|
855
|
+
let bound = self.braces.get(&span.start).copied().unwrap_or(span.start);
|
|
856
|
+
self.emit_leading_comments(bound, level + 1);
|
|
857
|
+
self.indent(level);
|
|
858
|
+
self.buf.push('}');
|
|
859
|
+
self.emitted = span.start.0;
|
|
860
|
+
}
|
|
861
|
+
|
|
394
862
|
fn stmt_inline_or_block(&mut self, s: &Statement, level: usize) {
|
|
395
|
-
if let Statement::Block {
|
|
396
|
-
self.
|
|
863
|
+
if let Statement::Block { statements, span } = s {
|
|
864
|
+
self.block(statements, *span, level, false);
|
|
397
865
|
} else {
|
|
866
|
+
let sp = s.span();
|
|
398
867
|
self.buf.push_str("{\n");
|
|
868
|
+
self.emitted = 0;
|
|
869
|
+
self.emit_leading_comments(sp.start, level + 1);
|
|
399
870
|
self.stmt(s, level + 1);
|
|
871
|
+
self.emitted = sp.start.0;
|
|
872
|
+
self.emit_trailing_comments(sp.start.0);
|
|
400
873
|
self.buf.push('\n');
|
|
401
874
|
self.indent(level);
|
|
402
875
|
self.buf.push('}');
|
|
876
|
+
self.emitted = sp.start.0;
|
|
403
877
|
}
|
|
404
878
|
}
|
|
405
879
|
|
|
@@ -413,35 +887,58 @@ impl Printer {
|
|
|
413
887
|
}
|
|
414
888
|
ImportSpecifier::Named { name, alias, .. } => {
|
|
415
889
|
self.buf.push_str("{ ");
|
|
416
|
-
self.
|
|
417
|
-
if let Some(a) = alias {
|
|
418
|
-
self.buf.push_str(" as ");
|
|
419
|
-
self.buf.push_str(a.as_ref());
|
|
420
|
-
}
|
|
890
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
421
891
|
self.buf.push_str(" }");
|
|
422
892
|
}
|
|
423
893
|
}
|
|
424
894
|
return;
|
|
425
895
|
}
|
|
896
|
+
// A long named-import list wraps one name per line.
|
|
897
|
+
self.fit(
|
|
898
|
+
|s| s.import_list_inline(specs),
|
|
899
|
+
|s| s.import_list_broken(specs),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
fn import_named(&mut self, name: &str, alias: Option<&str>) {
|
|
904
|
+
self.buf.push_str(name);
|
|
905
|
+
if let Some(a) = alias {
|
|
906
|
+
self.buf.push_str(" as ");
|
|
907
|
+
self.buf.push_str(a);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
fn import_list_inline(&mut self, specs: &[ImportSpecifier]) {
|
|
426
912
|
self.buf.push_str("{ ");
|
|
427
913
|
for (i, sp) in specs.iter().enumerate() {
|
|
428
914
|
if i > 0 {
|
|
429
915
|
self.buf.push_str(", ");
|
|
430
916
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
self.buf.push_str(name.as_ref());
|
|
434
|
-
if let Some(a) = alias {
|
|
435
|
-
self.buf.push_str(" as ");
|
|
436
|
-
self.buf.push_str(a.as_ref());
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
_ => {}
|
|
917
|
+
if let ImportSpecifier::Named { name, alias, .. } = sp {
|
|
918
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
440
919
|
}
|
|
441
920
|
}
|
|
442
921
|
self.buf.push_str(" }");
|
|
443
922
|
}
|
|
444
923
|
|
|
924
|
+
fn import_list_broken(&mut self, specs: &[ImportSpecifier]) {
|
|
925
|
+
self.buf.push_str("{\n");
|
|
926
|
+
self.depth += 1;
|
|
927
|
+
for (i, sp) in specs.iter().enumerate() {
|
|
928
|
+
if i > 0 {
|
|
929
|
+
self.buf.push_str(",\n");
|
|
930
|
+
}
|
|
931
|
+
self.indent(self.depth);
|
|
932
|
+
if let ImportSpecifier::Named { name, alias, .. } = sp {
|
|
933
|
+
self.import_named(name.as_ref(), alias.as_deref());
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
self.depth -= 1;
|
|
937
|
+
self.buf.push('\n');
|
|
938
|
+
self.indent(self.depth);
|
|
939
|
+
self.buf.push('}');
|
|
940
|
+
}
|
|
941
|
+
|
|
445
942
|
fn param_list(&mut self, params: &[FunParam], rest: &Option<TypedParam>) {
|
|
446
943
|
for (i, p) in params.iter().enumerate() {
|
|
447
944
|
if i > 0 {
|
|
@@ -578,6 +1075,18 @@ impl Printer {
|
|
|
578
1075
|
}
|
|
579
1076
|
}
|
|
580
1077
|
|
|
1078
|
+
/// Print `e` as a sub-expression, wrapping it in parentheses when its operator
|
|
1079
|
+
/// binds looser than `min_prec` (so the printed form re-parses to the same AST).
|
|
1080
|
+
fn child(&mut self, e: &Expr, min_prec: u8) {
|
|
1081
|
+
if expr_prec(e) < min_prec {
|
|
1082
|
+
self.buf.push('(');
|
|
1083
|
+
self.expr(e);
|
|
1084
|
+
self.buf.push(')');
|
|
1085
|
+
} else {
|
|
1086
|
+
self.expr(e);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
581
1090
|
fn expr(&mut self, e: &Expr) {
|
|
582
1091
|
match e {
|
|
583
1092
|
Expr::Literal { value, .. } => match value {
|
|
@@ -596,11 +1105,16 @@ impl Printer {
|
|
|
596
1105
|
Expr::Binary {
|
|
597
1106
|
left, op, right, ..
|
|
598
1107
|
} => {
|
|
599
|
-
|
|
1108
|
+
// Parenthesize operands by precedence/associativity so the printed
|
|
1109
|
+
// grouping re-parses to the same tree (the AST has no paren nodes).
|
|
1110
|
+
let p = binop_prec(*op);
|
|
1111
|
+
let right_assoc = matches!(op, BinOp::Pow);
|
|
1112
|
+
let (lmin, rmin) = if right_assoc { (p + 1, p) } else { (p, p + 1) };
|
|
1113
|
+
self.child(left, lmin);
|
|
600
1114
|
self.buf.push(' ');
|
|
601
1115
|
self.buf.push_str(binop(*op));
|
|
602
1116
|
self.buf.push(' ');
|
|
603
|
-
self.
|
|
1117
|
+
self.child(right, rmin);
|
|
604
1118
|
}
|
|
605
1119
|
Expr::Unary { op, operand, .. } => {
|
|
606
1120
|
match op {
|
|
@@ -610,43 +1124,17 @@ impl Printer {
|
|
|
610
1124
|
UnaryOp::BitNot => self.buf.push_str("~"),
|
|
611
1125
|
UnaryOp::Void => self.buf.push_str("void "),
|
|
612
1126
|
}
|
|
613
|
-
self.
|
|
1127
|
+
self.child(operand, PREC_POSTFIX);
|
|
614
1128
|
}
|
|
615
1129
|
Expr::Call { callee, args, .. } => {
|
|
616
|
-
self.
|
|
617
|
-
self.
|
|
618
|
-
for (i, a) in args.iter().enumerate() {
|
|
619
|
-
if i > 0 {
|
|
620
|
-
self.buf.push_str(", ");
|
|
621
|
-
}
|
|
622
|
-
match a {
|
|
623
|
-
CallArg::Expr(ex) => self.expr(ex),
|
|
624
|
-
CallArg::Spread(ex) => {
|
|
625
|
-
self.buf.push_str("...");
|
|
626
|
-
self.expr(ex);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
self.buf.push(')');
|
|
1130
|
+
self.child(callee, PREC_POSTFIX);
|
|
1131
|
+
self.emit_args(args);
|
|
631
1132
|
}
|
|
632
1133
|
Expr::New { callee, args, .. } => {
|
|
633
1134
|
self.buf.push_str("new ");
|
|
634
|
-
self.
|
|
1135
|
+
self.child(callee, PREC_POSTFIX);
|
|
635
1136
|
if !args.is_empty() {
|
|
636
|
-
self.
|
|
637
|
-
for (i, a) in args.iter().enumerate() {
|
|
638
|
-
if i > 0 {
|
|
639
|
-
self.buf.push_str(", ");
|
|
640
|
-
}
|
|
641
|
-
match a {
|
|
642
|
-
CallArg::Expr(ex) => self.expr(ex),
|
|
643
|
-
CallArg::Spread(ex) => {
|
|
644
|
-
self.buf.push_str("...");
|
|
645
|
-
self.expr(ex);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
self.buf.push(')');
|
|
1137
|
+
self.emit_args(args);
|
|
650
1138
|
}
|
|
651
1139
|
}
|
|
652
1140
|
Expr::Member {
|
|
@@ -655,7 +1143,7 @@ impl Printer {
|
|
|
655
1143
|
optional,
|
|
656
1144
|
..
|
|
657
1145
|
} => {
|
|
658
|
-
self.
|
|
1146
|
+
self.child(object, PREC_POSTFIX);
|
|
659
1147
|
if *optional {
|
|
660
1148
|
self.buf.push_str("?.");
|
|
661
1149
|
} else {
|
|
@@ -676,7 +1164,7 @@ impl Printer {
|
|
|
676
1164
|
optional,
|
|
677
1165
|
..
|
|
678
1166
|
} => {
|
|
679
|
-
self.
|
|
1167
|
+
self.child(object, PREC_POSTFIX);
|
|
680
1168
|
if *optional {
|
|
681
1169
|
self.buf.push_str("?.[");
|
|
682
1170
|
} else {
|
|
@@ -691,53 +1179,20 @@ impl Printer {
|
|
|
691
1179
|
else_branch,
|
|
692
1180
|
..
|
|
693
1181
|
} => {
|
|
694
|
-
|
|
1182
|
+
// cond binds tighter than `?:`; the else chains (right-assoc).
|
|
1183
|
+
self.child(cond, PREC_NULLISH);
|
|
695
1184
|
self.buf.push_str(" ? ");
|
|
696
1185
|
self.expr(then_branch);
|
|
697
1186
|
self.buf.push_str(" : ");
|
|
698
|
-
self.
|
|
1187
|
+
self.child(else_branch, PREC_CONDITIONAL);
|
|
699
1188
|
}
|
|
700
1189
|
Expr::NullishCoalesce { left, right, .. } => {
|
|
701
|
-
self.
|
|
1190
|
+
self.child(left, PREC_NULLISH);
|
|
702
1191
|
self.buf.push_str(" ?? ");
|
|
703
|
-
self.
|
|
704
|
-
}
|
|
705
|
-
Expr::Array { elements, .. } => {
|
|
706
|
-
self.buf.push('[');
|
|
707
|
-
for (i, el) in elements.iter().enumerate() {
|
|
708
|
-
if i > 0 {
|
|
709
|
-
self.buf.push_str(", ");
|
|
710
|
-
}
|
|
711
|
-
match el {
|
|
712
|
-
ArrayElement::Expr(ex) => self.expr(ex),
|
|
713
|
-
ArrayElement::Spread(ex) => {
|
|
714
|
-
self.buf.push_str("...");
|
|
715
|
-
self.expr(ex);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
self.buf.push(']');
|
|
720
|
-
}
|
|
721
|
-
Expr::Object { props, .. } => {
|
|
722
|
-
self.buf.push_str("{ ");
|
|
723
|
-
for (i, pr) in props.iter().enumerate() {
|
|
724
|
-
if i > 0 {
|
|
725
|
-
self.buf.push_str(", ");
|
|
726
|
-
}
|
|
727
|
-
match pr {
|
|
728
|
-
ObjectProp::KeyValue(k, v) => {
|
|
729
|
-
self.buf.push_str(k.as_ref());
|
|
730
|
-
self.buf.push_str(": ");
|
|
731
|
-
self.expr(v);
|
|
732
|
-
}
|
|
733
|
-
ObjectProp::Spread(ex) => {
|
|
734
|
-
self.buf.push_str("...");
|
|
735
|
-
self.expr(ex);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
self.buf.push_str(" }");
|
|
1192
|
+
self.child(right, PREC_NULLISH + 1);
|
|
740
1193
|
}
|
|
1194
|
+
Expr::Array { elements, .. } => self.emit_array(elements),
|
|
1195
|
+
Expr::Object { props, .. } => self.emit_object(props),
|
|
741
1196
|
Expr::Assign { name, value, .. } => {
|
|
742
1197
|
self.buf.push_str(name.as_ref());
|
|
743
1198
|
self.buf.push_str(" = ");
|
|
@@ -745,7 +1200,7 @@ impl Printer {
|
|
|
745
1200
|
}
|
|
746
1201
|
Expr::TypeOf { operand, .. } => {
|
|
747
1202
|
self.buf.push_str("typeof ");
|
|
748
|
-
self.
|
|
1203
|
+
self.child(operand, PREC_POSTFIX);
|
|
749
1204
|
}
|
|
750
1205
|
Expr::PostfixInc { name, .. } => {
|
|
751
1206
|
self.buf.push_str(name.as_ref());
|
|
@@ -783,7 +1238,7 @@ impl Printer {
|
|
|
783
1238
|
value,
|
|
784
1239
|
..
|
|
785
1240
|
} => {
|
|
786
|
-
self.
|
|
1241
|
+
self.child(object, PREC_POSTFIX);
|
|
787
1242
|
self.buf.push('.');
|
|
788
1243
|
self.buf.push_str(prop.as_ref());
|
|
789
1244
|
self.buf.push_str(" = ");
|
|
@@ -795,7 +1250,7 @@ impl Printer {
|
|
|
795
1250
|
value,
|
|
796
1251
|
..
|
|
797
1252
|
} => {
|
|
798
|
-
self.
|
|
1253
|
+
self.child(object, PREC_POSTFIX);
|
|
799
1254
|
self.buf.push('[');
|
|
800
1255
|
self.expr(index);
|
|
801
1256
|
self.buf.push_str("] = ");
|
|
@@ -806,8 +1261,20 @@ impl Printer {
|
|
|
806
1261
|
self.param_list(params, &None);
|
|
807
1262
|
self.buf.push_str(") => ");
|
|
808
1263
|
match body {
|
|
809
|
-
|
|
810
|
-
|
|
1264
|
+
// A bare object-literal body must be parenthesized, else `=> {`
|
|
1265
|
+
// re-parses as a block.
|
|
1266
|
+
ArrowBody::Expr(e) => {
|
|
1267
|
+
if matches!(e.as_ref(), Expr::Object { .. }) {
|
|
1268
|
+
self.buf.push('(');
|
|
1269
|
+
self.expr(e);
|
|
1270
|
+
self.buf.push(')');
|
|
1271
|
+
} else {
|
|
1272
|
+
self.expr(e);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
// Indent the block body relative to the arrow's own line
|
|
1276
|
+
// (`self.depth`), printed inline after `=> ` (no leading indent).
|
|
1277
|
+
ArrowBody::Block(b) => self.stmt_inline_or_block(b, self.depth),
|
|
811
1278
|
}
|
|
812
1279
|
}
|
|
813
1280
|
Expr::TemplateLiteral { quasis, exprs, .. } => {
|
|
@@ -824,7 +1291,7 @@ impl Printer {
|
|
|
824
1291
|
}
|
|
825
1292
|
Expr::Await { operand, .. } => {
|
|
826
1293
|
self.buf.push_str("await ");
|
|
827
|
-
self.
|
|
1294
|
+
self.child(operand, PREC_POSTFIX);
|
|
828
1295
|
}
|
|
829
1296
|
Expr::JsxElement {
|
|
830
1297
|
tag,
|
|
@@ -963,6 +1430,72 @@ fn escape_template(s: &str) -> String {
|
|
|
963
1430
|
.replace('$', "\\$")
|
|
964
1431
|
}
|
|
965
1432
|
|
|
1433
|
+
// Expression precedence levels (higher binds tighter), mirroring the parser's
|
|
1434
|
+
// descent chain (parse_conditional → … → parse_unary → primary). Used to decide
|
|
1435
|
+
// when a sub-expression needs parentheses.
|
|
1436
|
+
const PREC_CONDITIONAL: u8 = 1;
|
|
1437
|
+
const PREC_NULLISH: u8 = 2;
|
|
1438
|
+
const PREC_POSTFIX: u8 = 15; // call / member / index — tighter than any operator
|
|
1439
|
+
const PREC_ATOM: u8 = 16; // literals, identifiers, array/object/template/jsx
|
|
1440
|
+
|
|
1441
|
+
/// True when a comment is exactly the ignore marker — `// tish-fmt-ignore` or the
|
|
1442
|
+
/// `/* tish-fmt-ignore */` block form.
|
|
1443
|
+
fn is_ignore_marker(text: &str) -> bool {
|
|
1444
|
+
let t = text.trim();
|
|
1445
|
+
let inner = if let Some(r) = t.strip_prefix("//") {
|
|
1446
|
+
r
|
|
1447
|
+
} else if let Some(r) = t.strip_prefix("/*").and_then(|x| x.strip_suffix("*/")) {
|
|
1448
|
+
r
|
|
1449
|
+
} else {
|
|
1450
|
+
return false;
|
|
1451
|
+
};
|
|
1452
|
+
inner.trim() == IGNORE_MARKER
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/// Precedence of a binary operator, matching the parser (parser.rs `parse_*`).
|
|
1456
|
+
fn binop_prec(op: BinOp) -> u8 {
|
|
1457
|
+
match op {
|
|
1458
|
+
BinOp::Or => 3,
|
|
1459
|
+
BinOp::And => 4,
|
|
1460
|
+
BinOp::BitOr => 5,
|
|
1461
|
+
BinOp::BitXor => 6,
|
|
1462
|
+
BinOp::BitAnd => 7,
|
|
1463
|
+
BinOp::Shl | BinOp::Shr => 8,
|
|
1464
|
+
BinOp::Eq | BinOp::Ne | BinOp::StrictEq | BinOp::StrictNe => 9,
|
|
1465
|
+
BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::In => 10,
|
|
1466
|
+
BinOp::Add | BinOp::Sub => 11,
|
|
1467
|
+
BinOp::Mul | BinOp::Div | BinOp::Mod => 12,
|
|
1468
|
+
BinOp::Pow => 13,
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/// Precedence of an expression's outermost operator (atoms bind tightest).
|
|
1473
|
+
fn expr_prec(e: &Expr) -> u8 {
|
|
1474
|
+
match e {
|
|
1475
|
+
Expr::Assign { .. }
|
|
1476
|
+
| Expr::MemberAssign { .. }
|
|
1477
|
+
| Expr::IndexAssign { .. }
|
|
1478
|
+
| Expr::CompoundAssign { .. }
|
|
1479
|
+
| Expr::LogicalAssign { .. } => 0,
|
|
1480
|
+
Expr::Conditional { .. } => PREC_CONDITIONAL,
|
|
1481
|
+
Expr::NullishCoalesce { .. } => PREC_NULLISH,
|
|
1482
|
+
Expr::Binary { op, .. } => binop_prec(*op),
|
|
1483
|
+
Expr::Unary { .. }
|
|
1484
|
+
| Expr::TypeOf { .. }
|
|
1485
|
+
| Expr::Await { .. }
|
|
1486
|
+
| Expr::PrefixInc { .. }
|
|
1487
|
+
| Expr::PrefixDec { .. } => 14,
|
|
1488
|
+
Expr::Call { .. }
|
|
1489
|
+
| Expr::New { .. }
|
|
1490
|
+
| Expr::Member { .. }
|
|
1491
|
+
| Expr::Index { .. }
|
|
1492
|
+
| Expr::PostfixInc { .. }
|
|
1493
|
+
| Expr::PostfixDec { .. } => PREC_POSTFIX,
|
|
1494
|
+
Expr::ArrowFunction { .. } => PREC_ATOM,
|
|
1495
|
+
_ => PREC_ATOM,
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
966
1499
|
fn binop(op: BinOp) -> &'static str {
|
|
967
1500
|
match op {
|
|
968
1501
|
BinOp::Add => "+",
|
|
@@ -1008,6 +1541,224 @@ fn logical_assign(op: LogicalAssignOp) -> &'static str {
|
|
|
1008
1541
|
}
|
|
1009
1542
|
}
|
|
1010
1543
|
|
|
1544
|
+
/// Recover `//` and `/* */` comments from source, in order, with their 1-based
|
|
1545
|
+
/// (line, col) positions — matching `ast::Span`'s convention so the printer can
|
|
1546
|
+
/// re-insert them by position. The lexer discards comments, so this is a separate
|
|
1547
|
+
/// pass; it skips string and template literals so a `//` inside `"…"` or `` `…` ``
|
|
1548
|
+
/// is never mistaken for a comment. (Tish has no regex literals, so a bare `/` is
|
|
1549
|
+
/// only division or a comment opener — no further disambiguation is needed.)
|
|
1550
|
+
fn scan_comments(source: &str) -> (Vec<CommentTok>, BraceMap, Vec<(usize, usize)>) {
|
|
1551
|
+
let mut s = Scanner {
|
|
1552
|
+
chars: source.chars().collect(),
|
|
1553
|
+
i: 0,
|
|
1554
|
+
line: 1,
|
|
1555
|
+
col: 1,
|
|
1556
|
+
seen_nonws: false,
|
|
1557
|
+
out: Vec::new(),
|
|
1558
|
+
brace_stack: Vec::new(),
|
|
1559
|
+
braces: BraceMap::new(),
|
|
1560
|
+
bracket_open: Vec::new(),
|
|
1561
|
+
bracket_spans: Vec::new(),
|
|
1562
|
+
};
|
|
1563
|
+
s.scan_code(false);
|
|
1564
|
+
(s.out, s.braces, s.bracket_spans)
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
struct Scanner {
|
|
1568
|
+
chars: Vec<char>,
|
|
1569
|
+
i: usize,
|
|
1570
|
+
line: usize,
|
|
1571
|
+
col: usize,
|
|
1572
|
+
/// Whether any non-whitespace has appeared on the current line yet.
|
|
1573
|
+
seen_nonws: bool,
|
|
1574
|
+
out: Vec<CommentTok>,
|
|
1575
|
+
/// Open `{` positions awaiting their match, for building `braces`.
|
|
1576
|
+
brace_stack: Vec<(usize, usize)>,
|
|
1577
|
+
/// `{`→`}` position pairs (see [`BraceMap`]).
|
|
1578
|
+
braces: BraceMap,
|
|
1579
|
+
/// Open lines of every bracket (`{ [ (`) awaiting its match.
|
|
1580
|
+
bracket_open: Vec<usize>,
|
|
1581
|
+
/// `(open_line, close_line)` for every bracket pair — lets an ignored statement's
|
|
1582
|
+
/// verbatim slice extend over the full lines of any `{}`/`[]`/`()` it opens.
|
|
1583
|
+
bracket_spans: Vec<(usize, usize)>,
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
impl Scanner {
|
|
1587
|
+
fn peek(&self) -> Option<char> {
|
|
1588
|
+
self.chars.get(self.i).copied()
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
fn peek2(&self) -> Option<char> {
|
|
1592
|
+
self.chars.get(self.i + 1).copied()
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/// Consume one char, tracking line/col like the lexer (col resets after `\n`).
|
|
1596
|
+
fn bump(&mut self) -> Option<char> {
|
|
1597
|
+
let c = *self.chars.get(self.i)?;
|
|
1598
|
+
self.i += 1;
|
|
1599
|
+
if c == '\n' {
|
|
1600
|
+
self.line += 1;
|
|
1601
|
+
self.col = 1;
|
|
1602
|
+
self.seen_nonws = false;
|
|
1603
|
+
} else {
|
|
1604
|
+
self.col += 1;
|
|
1605
|
+
}
|
|
1606
|
+
Some(c)
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/// Scan ordinary code, collecting comments. When `stop_at_close_brace` is set
|
|
1610
|
+
/// (inside a `${ … }` template interpolation), returns at the matching `}`
|
|
1611
|
+
/// without consuming it.
|
|
1612
|
+
fn scan_code(&mut self, stop_at_close_brace: bool) {
|
|
1613
|
+
let mut depth = 0usize;
|
|
1614
|
+
while let Some(c) = self.peek() {
|
|
1615
|
+
match c {
|
|
1616
|
+
'}' if stop_at_close_brace && depth == 0 => return,
|
|
1617
|
+
'{' => {
|
|
1618
|
+
self.brace_stack.push((self.line, self.col));
|
|
1619
|
+
self.bracket_open.push(self.line);
|
|
1620
|
+
depth += 1;
|
|
1621
|
+
self.bump();
|
|
1622
|
+
self.seen_nonws = true;
|
|
1623
|
+
}
|
|
1624
|
+
'}' => {
|
|
1625
|
+
let close = (self.line, self.col);
|
|
1626
|
+
if let Some(open) = self.brace_stack.pop() {
|
|
1627
|
+
self.braces.insert(open, close);
|
|
1628
|
+
}
|
|
1629
|
+
if let Some(open_line) = self.bracket_open.pop() {
|
|
1630
|
+
self.bracket_spans.push((open_line, self.line));
|
|
1631
|
+
}
|
|
1632
|
+
depth = depth.saturating_sub(1);
|
|
1633
|
+
self.bump();
|
|
1634
|
+
self.seen_nonws = true;
|
|
1635
|
+
}
|
|
1636
|
+
'(' | '[' => {
|
|
1637
|
+
self.bracket_open.push(self.line);
|
|
1638
|
+
self.bump();
|
|
1639
|
+
self.seen_nonws = true;
|
|
1640
|
+
}
|
|
1641
|
+
')' | ']' => {
|
|
1642
|
+
if let Some(open_line) = self.bracket_open.pop() {
|
|
1643
|
+
self.bracket_spans.push((open_line, self.line));
|
|
1644
|
+
}
|
|
1645
|
+
self.bump();
|
|
1646
|
+
self.seen_nonws = true;
|
|
1647
|
+
}
|
|
1648
|
+
'"' | '\'' => self.scan_string(c),
|
|
1649
|
+
'`' => {
|
|
1650
|
+
self.bump();
|
|
1651
|
+
self.seen_nonws = true;
|
|
1652
|
+
self.scan_template();
|
|
1653
|
+
}
|
|
1654
|
+
'/' if self.peek2() == Some('/') => self.scan_line_comment(),
|
|
1655
|
+
'/' if self.peek2() == Some('*') => self.scan_block_comment(),
|
|
1656
|
+
' ' | '\t' | '\r' | '\n' => {
|
|
1657
|
+
self.bump();
|
|
1658
|
+
}
|
|
1659
|
+
_ => {
|
|
1660
|
+
self.bump();
|
|
1661
|
+
self.seen_nonws = true;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
fn scan_string(&mut self, quote: char) {
|
|
1668
|
+
self.bump(); // opening quote
|
|
1669
|
+
self.seen_nonws = true;
|
|
1670
|
+
while let Some(c) = self.bump() {
|
|
1671
|
+
if c == '\\' {
|
|
1672
|
+
self.bump(); // escaped char
|
|
1673
|
+
} else if c == quote {
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/// Scan a template literal body (opening backtick already consumed), recursing
|
|
1680
|
+
/// into `${ … }` interpolations so their strings/comments are handled too.
|
|
1681
|
+
fn scan_template(&mut self) {
|
|
1682
|
+
while let Some(c) = self.peek() {
|
|
1683
|
+
match c {
|
|
1684
|
+
'`' => {
|
|
1685
|
+
self.bump();
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
'\\' => {
|
|
1689
|
+
self.bump();
|
|
1690
|
+
self.bump();
|
|
1691
|
+
}
|
|
1692
|
+
'$' if self.peek2() == Some('{') => {
|
|
1693
|
+
self.bump();
|
|
1694
|
+
self.bump();
|
|
1695
|
+
self.scan_code(true);
|
|
1696
|
+
if self.peek() == Some('}') {
|
|
1697
|
+
self.bump();
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
_ => {
|
|
1701
|
+
self.bump();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
fn scan_line_comment(&mut self) {
|
|
1708
|
+
let start = (self.line, self.col);
|
|
1709
|
+
let own_line = !self.seen_nonws;
|
|
1710
|
+
let mut text = String::new();
|
|
1711
|
+
while let Some(c) = self.peek() {
|
|
1712
|
+
if c == '\n' {
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
text.push(c);
|
|
1716
|
+
self.bump();
|
|
1717
|
+
}
|
|
1718
|
+
self.out.push(CommentTok {
|
|
1719
|
+
start,
|
|
1720
|
+
text: text.trim_end().to_string(),
|
|
1721
|
+
own_line,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
fn scan_block_comment(&mut self) {
|
|
1726
|
+
let start = (self.line, self.col);
|
|
1727
|
+
let own_line = !self.seen_nonws;
|
|
1728
|
+
let mut text = String::new();
|
|
1729
|
+
let mut depth = 0usize;
|
|
1730
|
+
loop {
|
|
1731
|
+
if self.peek() == Some('/') && self.peek2() == Some('*') {
|
|
1732
|
+
text.push('/');
|
|
1733
|
+
text.push('*');
|
|
1734
|
+
self.bump();
|
|
1735
|
+
self.bump();
|
|
1736
|
+
depth += 1;
|
|
1737
|
+
} else if self.peek() == Some('*') && self.peek2() == Some('/') {
|
|
1738
|
+
text.push('*');
|
|
1739
|
+
text.push('/');
|
|
1740
|
+
self.bump();
|
|
1741
|
+
self.bump();
|
|
1742
|
+
depth -= 1;
|
|
1743
|
+
if depth == 0 {
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
} else {
|
|
1747
|
+
match self.bump() {
|
|
1748
|
+
Some(c) => text.push(c),
|
|
1749
|
+
None => break,
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
self.out.push(CommentTok {
|
|
1754
|
+
start,
|
|
1755
|
+
text,
|
|
1756
|
+
own_line,
|
|
1757
|
+
});
|
|
1758
|
+
self.seen_nonws = true;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1011
1762
|
#[cfg(test)]
|
|
1012
1763
|
mod tests {
|
|
1013
1764
|
use super::*;
|
|
@@ -1029,4 +1780,322 @@ mod tests {
|
|
|
1029
1780
|
);
|
|
1030
1781
|
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1031
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
#[test]
|
|
1785
|
+
fn preserves_leading_and_section_comments() {
|
|
1786
|
+
let src = "\
|
|
1787
|
+
// file header
|
|
1788
|
+
// second line
|
|
1789
|
+
let a = 1
|
|
1790
|
+
|
|
1791
|
+
// a section
|
|
1792
|
+
let b = 2
|
|
1793
|
+
";
|
|
1794
|
+
let out = format_source(src).unwrap();
|
|
1795
|
+
assert!(out.contains("// file header"), "{out:?}");
|
|
1796
|
+
assert!(out.contains("// second line"), "{out:?}");
|
|
1797
|
+
assert!(out.contains("// a section"), "{out:?}");
|
|
1798
|
+
// header hugs the statement it documents; blank line before the section.
|
|
1799
|
+
assert!(out.contains("// a section\nlet b = 2"), "{out:?}");
|
|
1800
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
#[test]
|
|
1804
|
+
fn preserves_trailing_comment() {
|
|
1805
|
+
let src = "let a = 1 // inline note\n";
|
|
1806
|
+
let out = format_source(src).unwrap();
|
|
1807
|
+
assert!(out.contains("let a = 1 // inline note"), "{out:?}");
|
|
1808
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
#[test]
|
|
1812
|
+
fn preserves_comments_inside_block() {
|
|
1813
|
+
let src = "\
|
|
1814
|
+
fn f() {
|
|
1815
|
+
// step one
|
|
1816
|
+
let a = 1
|
|
1817
|
+
// step two
|
|
1818
|
+
return a
|
|
1819
|
+
}
|
|
1820
|
+
";
|
|
1821
|
+
let out = format_source(src).unwrap();
|
|
1822
|
+
assert!(out.contains(" // step one"), "{out:?}");
|
|
1823
|
+
assert!(out.contains(" // step two"), "{out:?}");
|
|
1824
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
#[test]
|
|
1828
|
+
fn preserves_dangling_comment_in_empty_block() {
|
|
1829
|
+
let src = "fn f() {\n // nothing yet\n}\n";
|
|
1830
|
+
let out = format_source(src).unwrap();
|
|
1831
|
+
assert!(out.contains("// nothing yet"), "{out:?}");
|
|
1832
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
#[test]
|
|
1836
|
+
fn preserves_block_comment() {
|
|
1837
|
+
let src = "/* a block comment */\nlet a = 1\n";
|
|
1838
|
+
let out = format_source(src).unwrap();
|
|
1839
|
+
assert!(out.contains("/* a block comment */"), "{out:?}");
|
|
1840
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
#[test]
|
|
1844
|
+
fn double_slash_in_string_is_not_a_comment() {
|
|
1845
|
+
let src = "let url = \"http://example.com\"\n";
|
|
1846
|
+
let out = format_source(src).unwrap();
|
|
1847
|
+
assert_eq!(out, "let url = \"http://example.com\"\n", "{out:?}");
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
#[test]
|
|
1851
|
+
fn idempotent_with_comments() {
|
|
1852
|
+
let src = "\
|
|
1853
|
+
// header
|
|
1854
|
+
let a = 1
|
|
1855
|
+
|
|
1856
|
+
fn f() {
|
|
1857
|
+
// body note
|
|
1858
|
+
let b = 2 // trailing
|
|
1859
|
+
return b
|
|
1860
|
+
}
|
|
1861
|
+
";
|
|
1862
|
+
let once = format_source(src).unwrap();
|
|
1863
|
+
let twice = format_source(&once).unwrap();
|
|
1864
|
+
assert_eq!(
|
|
1865
|
+
once, twice,
|
|
1866
|
+
"formatting is not idempotent:\n{once}\n---\n{twice}"
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
#[test]
|
|
1871
|
+
fn collapses_multiple_blank_lines_to_one() {
|
|
1872
|
+
let src = "let a = 1\n\n\n\nlet b = 2\n";
|
|
1873
|
+
let out = format_source(src).unwrap();
|
|
1874
|
+
assert_eq!(out, "let a = 1\n\nlet b = 2\n", "{out:?}");
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
#[test]
|
|
1878
|
+
fn comment_after_inner_block_stays_at_outer_level() {
|
|
1879
|
+
// Regression: a comment after an inner block's `}` belongs to the outer
|
|
1880
|
+
// scope, not inside the inner block (the parser's block span.end overshoots
|
|
1881
|
+
// past `}`, which previously pulled this comment in and broke idempotency).
|
|
1882
|
+
let src = "\
|
|
1883
|
+
fn f() {
|
|
1884
|
+
if (x) {
|
|
1885
|
+
a()
|
|
1886
|
+
}
|
|
1887
|
+
// after the if
|
|
1888
|
+
b()
|
|
1889
|
+
}
|
|
1890
|
+
";
|
|
1891
|
+
let out = format_source(src).unwrap();
|
|
1892
|
+
assert!(out.contains(" // after the if\n b()"), "{out:?}");
|
|
1893
|
+
let twice = format_source(&out).unwrap();
|
|
1894
|
+
assert_eq!(out, twice, "not idempotent:\n{out}\n---\n{twice}");
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
#[test]
|
|
1898
|
+
fn trailing_comment_inside_block_stays_inside() {
|
|
1899
|
+
let src = "\
|
|
1900
|
+
fn f() {
|
|
1901
|
+
a()
|
|
1902
|
+
// last note
|
|
1903
|
+
}
|
|
1904
|
+
";
|
|
1905
|
+
let out = format_source(src).unwrap();
|
|
1906
|
+
assert!(out.contains(" // last note\n}"), "{out:?}");
|
|
1907
|
+
let twice = format_source(&out).unwrap();
|
|
1908
|
+
assert_eq!(out, twice, "{out:?}");
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
#[test]
|
|
1912
|
+
fn preserves_operator_grouping() {
|
|
1913
|
+
// The AST has no parenthesis nodes, so the printer must re-derive parens
|
|
1914
|
+
// from precedence. Each `want` must re-parse to the same tree.
|
|
1915
|
+
let cases = [
|
|
1916
|
+
("let a = 1 / (b - c)\n", "1 / (b - c)"),
|
|
1917
|
+
("let a = 0 - (x + y + z)\n", "0 - (x + y + z)"),
|
|
1918
|
+
("let a = (1 - (p + q)) * s\n", "(1 - (p + q)) * s"),
|
|
1919
|
+
("let a = b * c + d\n", "b * c + d"),
|
|
1920
|
+
("let a = b + c * d\n", "b + c * d"),
|
|
1921
|
+
("let a = (b + c) * d\n", "(b + c) * d"),
|
|
1922
|
+
("let a = 1 - 2 - 3\n", "1 - 2 - 3"),
|
|
1923
|
+
("let a = 1 - (2 - 3)\n", "1 - (2 - 3)"),
|
|
1924
|
+
("let a = -(b + c)\n", "-(b + c)"),
|
|
1925
|
+
("let a = !(b && c)\n", "!(b && c)"),
|
|
1926
|
+
("let a = (a | b) & c\n", "(a | b) & c"),
|
|
1927
|
+
];
|
|
1928
|
+
for (src, want) in cases {
|
|
1929
|
+
let out = format_source(src).unwrap();
|
|
1930
|
+
assert!(
|
|
1931
|
+
out.contains(want),
|
|
1932
|
+
"for {src:?} expected to contain {want:?}, got {out:?}"
|
|
1933
|
+
);
|
|
1934
|
+
let twice = format_source(&out).unwrap();
|
|
1935
|
+
assert_eq!(out, twice, "not idempotent for {src:?}: {out:?}");
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
#[test]
|
|
1940
|
+
fn nested_control_flow_brace_spacing() {
|
|
1941
|
+
let src = "fn f() {\n if (a) {\n b()\n }\n}\n";
|
|
1942
|
+
let out = format_source(src).unwrap();
|
|
1943
|
+
assert!(!out.contains(") {"), "double-space before brace: {out:?}");
|
|
1944
|
+
assert!(out.contains(" if (a) {\n"), "{out:?}");
|
|
1945
|
+
let twice = format_source(&out).unwrap();
|
|
1946
|
+
assert_eq!(out, twice, "{out:?}");
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
#[test]
|
|
1950
|
+
fn short_object_stays_inline() {
|
|
1951
|
+
let src = "let a = { x: 1, y: 2 }\n";
|
|
1952
|
+
assert_eq!(format_source(src).unwrap(), src);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
#[test]
|
|
1956
|
+
fn long_object_breaks_one_per_line() {
|
|
1957
|
+
let props: Vec<String> = (0..12).map(|i| format!("key{i}: {i}")).collect();
|
|
1958
|
+
let src = format!("let a = {{ {} }}\n", props.join(", "));
|
|
1959
|
+
let out = format_source(&src).unwrap();
|
|
1960
|
+
assert!(
|
|
1961
|
+
out.contains("let a = {\n key0: 0,\n"),
|
|
1962
|
+
"expected broken object:\n{out}"
|
|
1963
|
+
);
|
|
1964
|
+
assert!(out.ends_with("\n}\n"), "{out:?}");
|
|
1965
|
+
// idempotent and re-parses
|
|
1966
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
1967
|
+
tishlang_parser::parse(&out).unwrap();
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
#[test]
|
|
1971
|
+
fn long_array_breaks_one_per_line() {
|
|
1972
|
+
let elems: Vec<String> = (0..40).map(|i| i.to_string()).collect();
|
|
1973
|
+
let src = format!("let a = [{}]\n", elems.join(", "));
|
|
1974
|
+
let out = format_source(&src).unwrap();
|
|
1975
|
+
assert!(
|
|
1976
|
+
out.contains("[\n 0,\n 1,\n"),
|
|
1977
|
+
"expected broken array:\n{out}"
|
|
1978
|
+
);
|
|
1979
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
#[test]
|
|
1983
|
+
fn last_arg_object_hugs_parens() {
|
|
1984
|
+
let props: Vec<String> = (0..20).map(|i| format!("k{i}: {i}")).collect();
|
|
1985
|
+
let src = format!("f(a, {{ {} }})\n", props.join(", "));
|
|
1986
|
+
let out = format_source(&src).unwrap();
|
|
1987
|
+
assert!(
|
|
1988
|
+
out.starts_with("f(a, {\n"),
|
|
1989
|
+
"expected hugged object:\n{out}"
|
|
1990
|
+
);
|
|
1991
|
+
assert!(out.contains("\n})\n"), "expected hugged close:\n{out}");
|
|
1992
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
1993
|
+
tishlang_parser::parse(&out).unwrap();
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
#[test]
|
|
1997
|
+
fn nested_containers_indent_progressively() {
|
|
1998
|
+
let inner: Vec<String> = (0..16).map(|i| format!("p{i}: {i}")).collect();
|
|
1999
|
+
let src = format!("let a = {{ outer: {{ {} }} }}\n", inner.join(", "));
|
|
2000
|
+
let out = format_source(&src).unwrap();
|
|
2001
|
+
// outer object at 2 spaces, inner props at 4 spaces
|
|
2002
|
+
assert!(
|
|
2003
|
+
out.contains("\n outer: {\n p0: 0,"),
|
|
2004
|
+
"expected nested indent:\n{out}"
|
|
2005
|
+
);
|
|
2006
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
#[test]
|
|
2010
|
+
fn arrow_block_body_indents_to_context() {
|
|
2011
|
+
let src =
|
|
2012
|
+
"export fn make() {\n let s = {}\n s.go = (x) => {\n foo(x)\n }\n return s\n}\n";
|
|
2013
|
+
let out = format_source(src).unwrap();
|
|
2014
|
+
// Arrow body one level past its `s.go` line (4 spaces); closing `}` at 2.
|
|
2015
|
+
assert!(
|
|
2016
|
+
out.contains(" s.go = (x) => {\n foo(x)\n }\n"),
|
|
2017
|
+
"arrow body mis-indented:\n{out}"
|
|
2018
|
+
);
|
|
2019
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
#[test]
|
|
2023
|
+
fn ignore_marker_preserves_statement_verbatim() {
|
|
2024
|
+
let src = "// tish-fmt-ignore\nexport fn m(out) {\n out[0]=1; out[1]=0\n out[2]=0; out[3]=1\n}\n\nlet x = {a:1,b:2}\n";
|
|
2025
|
+
let out = format_source(src).unwrap();
|
|
2026
|
+
// The ignored function keeps its exact source (aligned, no spaces around `=`).
|
|
2027
|
+
assert!(
|
|
2028
|
+
out.contains("export fn m(out) {\n out[0]=1; out[1]=0\n out[2]=0; out[3]=1\n}"),
|
|
2029
|
+
"ignored block not verbatim:\n{out}"
|
|
2030
|
+
);
|
|
2031
|
+
// Surrounding code is still formatted normally.
|
|
2032
|
+
assert!(
|
|
2033
|
+
out.contains("let x = { a: 1, b: 2 }"),
|
|
2034
|
+
"neighbour not formatted:\n{out}"
|
|
2035
|
+
);
|
|
2036
|
+
// The marker itself is kept.
|
|
2037
|
+
assert!(out.contains("// tish-fmt-ignore\n"), "{out}");
|
|
2038
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2039
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
#[test]
|
|
2043
|
+
fn ignore_marker_block_comment_form() {
|
|
2044
|
+
let src = "/* tish-fmt-ignore */\nlet a = [1,2, 3]\n";
|
|
2045
|
+
let out = format_source(src).unwrap();
|
|
2046
|
+
assert!(
|
|
2047
|
+
out.contains("let a = [1,2, 3]"),
|
|
2048
|
+
"expected verbatim array:\n{out}"
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
#[test]
|
|
2053
|
+
fn ignore_in_switch_case_does_not_overrun() {
|
|
2054
|
+
// Regression: an ignored last statement of a non-final case must not swallow
|
|
2055
|
+
// the following cases / closing brace / trailing code.
|
|
2056
|
+
let src = "switch (x) {\n case 1:\n // tish-fmt-ignore\n foo( a,b )\n case 2:\n bar()\n}\nlet after = 1\n";
|
|
2057
|
+
let out = format_source(src).unwrap();
|
|
2058
|
+
assert!(out.contains("foo( a,b )"), "ignored not verbatim:\n{out}");
|
|
2059
|
+
assert!(out.contains("case 2:"), "case 2 was swallowed:\n{out}");
|
|
2060
|
+
assert!(out.contains("bar()"), "case 2 body swallowed:\n{out}");
|
|
2061
|
+
assert!(
|
|
2062
|
+
out.contains("let after = 1"),
|
|
2063
|
+
"trailing code swallowed:\n{out}"
|
|
2064
|
+
);
|
|
2065
|
+
tishlang_parser::parse(&out).unwrap();
|
|
2066
|
+
assert_eq!(format_source(&out).unwrap(), out, "not idempotent:\n{out}");
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
#[test]
|
|
2070
|
+
fn ignore_preserves_multiline_bracket_statement() {
|
|
2071
|
+
// The verbatim extent must follow `[]`/`()`, not just `{}`.
|
|
2072
|
+
let src = "// tish-fmt-ignore\nlet m = [\n 1,2,\n 3,4\n]\nlet n = 5\n";
|
|
2073
|
+
let out = format_source(src).unwrap();
|
|
2074
|
+
assert!(
|
|
2075
|
+
out.contains("let m = [\n 1,2,\n 3,4\n]"),
|
|
2076
|
+
"multiline array truncated:\n{out}"
|
|
2077
|
+
);
|
|
2078
|
+
assert!(out.contains("let n = 5"), "{out}");
|
|
2079
|
+
assert_eq!(format_source(&out).unwrap(), out);
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
#[test]
|
|
2083
|
+
fn without_marker_is_formatted_normally() {
|
|
2084
|
+
let src = "let a = [1,2, 3]\n";
|
|
2085
|
+
let out = format_source(src).unwrap();
|
|
2086
|
+
assert_eq!(out, "let a = [1, 2, 3]\n", "{out:?}");
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
#[test]
|
|
2090
|
+
fn no_comments_round_trips_without_loss() {
|
|
2091
|
+
let src = "\
|
|
2092
|
+
fn add(a, b) {
|
|
2093
|
+
return a + b
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
let x = add(1, 2)
|
|
2097
|
+
";
|
|
2098
|
+
let out = format_source(src).unwrap();
|
|
2099
|
+
assert_eq!(out, src, "{out:?}");
|
|
2100
|
+
}
|
|
1032
2101
|
}
|