@ttsc/factory 0.19.3 → 0.20.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/README.md +2 -0
- package/lib/TsPrinter.d.ts +124 -17
- package/lib/TsPrinter.js +414 -106
- package/lib/TsPrinter.js.map +1 -1
- package/lib/TsPrinter.mjs +413 -107
- package/lib/TsPrinter.mjs.map +1 -1
- package/lib/ast/expressions/Expression.d.ts +5 -1
- package/lib/ast/imports/ImportClause.d.ts +10 -2
- package/lib/ast/jsdoc/JSDocImportTag.d.ts +3 -0
- package/lib/ast/types/ImportTypeNode.d.ts +3 -0
- package/lib/factory/expressions/createComma.d.ts +3 -3
- package/lib/factory/expressions/createComma.js +3 -3
- package/lib/factory/expressions/createComma.mjs +3 -3
- package/lib/factory/imports/createImportClause.d.ts +9 -3
- package/lib/factory/imports/createImportClause.js +8 -3
- package/lib/factory/imports/createImportClause.js.map +1 -1
- package/lib/factory/imports/createImportClause.mjs +8 -3
- package/lib/factory/imports/createImportClause.mjs.map +1 -1
- package/lib/factory/jsdoc/createJSDocImportTag.d.ts +3 -2
- package/lib/factory/jsdoc/createJSDocImportTag.js +3 -1
- package/lib/factory/jsdoc/createJSDocImportTag.js.map +1 -1
- package/lib/factory/jsdoc/createJSDocImportTag.mjs +3 -1
- package/lib/factory/jsdoc/createJSDocImportTag.mjs.map +1 -1
- package/lib/factory/types/createImportTypeNode.d.ts +8 -2
- package/lib/factory/types/createImportTypeNode.js +13 -1
- package/lib/factory/types/createImportTypeNode.js.map +1 -1
- package/lib/factory/types/createImportTypeNode.mjs +13 -1
- package/lib/factory/types/createImportTypeNode.mjs.map +1 -1
- package/lib/internal/doc.d.ts +13 -0
- package/lib/internal/doc.js +31 -4
- package/lib/internal/doc.js.map +1 -1
- package/lib/internal/doc.mjs +30 -4
- package/lib/internal/doc.mjs.map +1 -1
- package/lib/syntax/NodeFlags.d.ts +6 -4
- package/lib/syntax/NodeFlags.js +6 -4
- package/lib/syntax/NodeFlags.js.map +1 -1
- package/lib/syntax/NodeFlags.mjs +6 -4
- package/lib/syntax/NodeFlags.mjs.map +1 -1
- package/lib/syntax/SyntaxKind.d.ts +2 -0
- package/lib/syntax/SyntaxKind.js +6 -0
- package/lib/syntax/SyntaxKind.js.map +1 -1
- package/lib/syntax/SyntaxKind.mjs +6 -0
- package/lib/syntax/SyntaxKind.mjs.map +1 -1
- package/package.json +1 -1
- package/src/TsPrinter.ts +489 -121
- package/src/ast/expressions/Expression.ts +8 -0
- package/src/ast/imports/ImportClause.ts +10 -2
- package/src/ast/jsdoc/JSDocImportTag.ts +4 -0
- package/src/ast/types/ImportTypeNode.ts +4 -0
- package/src/factory/expressions/createComma.ts +3 -3
- package/src/factory/imports/createImportClause.ts +12 -6
- package/src/factory/jsdoc/createJSDocImportTag.ts +5 -1
- package/src/factory/types/createImportTypeNode.ts +21 -3
- package/src/internal/doc.ts +30 -3
- package/src/syntax/NodeFlags.ts +6 -4
- package/src/syntax/SyntaxKind.ts +7 -0
package/src/TsPrinter.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
join,
|
|
23
23
|
line,
|
|
24
24
|
printDocToString,
|
|
25
|
+
raw,
|
|
25
26
|
softline,
|
|
26
27
|
} from "./internal/doc";
|
|
27
28
|
import { NodeFlags, SyntaxKind } from "./syntax";
|
|
@@ -116,7 +117,7 @@ export class TsPrinter {
|
|
|
116
117
|
close: string,
|
|
117
118
|
opts: {
|
|
118
119
|
space?: boolean;
|
|
119
|
-
trailingComma?:
|
|
120
|
+
trailingComma?: TrailingComma;
|
|
120
121
|
forceBreak?: boolean;
|
|
121
122
|
} = {},
|
|
122
123
|
): Doc {
|
|
@@ -126,7 +127,11 @@ export class TsPrinter {
|
|
|
126
127
|
concat([
|
|
127
128
|
open,
|
|
128
129
|
indent(concat([ln, join(concat([",", line]), items)])),
|
|
129
|
-
opts.trailingComma
|
|
130
|
+
opts.trailingComma === "always"
|
|
131
|
+
? ","
|
|
132
|
+
: opts.trailingComma === "onBreak"
|
|
133
|
+
? ifBreak(",")
|
|
134
|
+
: "",
|
|
130
135
|
ln,
|
|
131
136
|
close,
|
|
132
137
|
]),
|
|
@@ -191,28 +196,78 @@ export class TsPrinter {
|
|
|
191
196
|
args.map((a) => this.emit(a)),
|
|
192
197
|
">",
|
|
193
198
|
{
|
|
194
|
-
trailingComma:
|
|
199
|
+
trailingComma: "never",
|
|
195
200
|
},
|
|
196
201
|
)
|
|
197
202
|
: "";
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
/**
|
|
201
|
-
*
|
|
202
|
-
* trailing comma after its last element.
|
|
206
|
+
* Trailing-comma policy for a parameter list or binding pattern.
|
|
203
207
|
*
|
|
204
|
-
* A
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
208
|
+
* A comma the printer adds only because a group broke must never change
|
|
209
|
+
* whether the text parses, nor what it parses to. After a rest element
|
|
210
|
+
* (`...rest`) it changes the first: a trailing comma there is a syntax error
|
|
211
|
+
* (TS1013 / V8 `SyntaxError`). After a trailing elision it changes the
|
|
212
|
+
* second: `[a, ,]` has one more hole than `[a, ]`, so the flat and broken
|
|
213
|
+
* layouts of the same node would disagree. A binding pattern is the one place
|
|
214
|
+
* where dropping that hole is lossless, since a trailing hole binds nothing;
|
|
215
|
+
* {@link literalTrailingComma} materializes it instead, because in an array
|
|
216
|
+
* literal the hole is a value.
|
|
210
217
|
*/
|
|
211
|
-
private listTrailingComma(nodes: readonly Node[]):
|
|
218
|
+
private listTrailingComma(nodes: readonly Node[]): TrailingComma {
|
|
212
219
|
const last: Node | undefined = nodes[nodes.length - 1];
|
|
213
|
-
if (last === undefined) return
|
|
214
|
-
if (last.kind === "OmittedExpression") return
|
|
215
|
-
return
|
|
220
|
+
if (last === undefined) return "onBreak";
|
|
221
|
+
if (last.kind === "OmittedExpression") return "never";
|
|
222
|
+
return "dotDotDotToken" in last && last.dotDotDotToken !== undefined
|
|
223
|
+
? "never"
|
|
224
|
+
: "onBreak";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Trailing-comma policy for a call or `new` argument list.
|
|
229
|
+
*
|
|
230
|
+
* A trailing `OmittedExpression` prints as nothing, so the list already ends
|
|
231
|
+
* in the separator comma of its last real argument: `f(a, )`, which is what
|
|
232
|
+
* the legacy printer emits too and parses as one argument. Adding the break
|
|
233
|
+
* comma on top produces `f(a, ,)`, which is a syntax error. A trailing spread
|
|
234
|
+
* is unaffected — a comma after it is legal in an argument list.
|
|
235
|
+
*/
|
|
236
|
+
private argsTrailingComma(args: readonly Expression[]): TrailingComma {
|
|
237
|
+
const last: Expression | undefined = args[args.length - 1];
|
|
238
|
+
return last !== undefined && last.kind === "OmittedExpression"
|
|
239
|
+
? "never"
|
|
240
|
+
: "onBreak";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Trailing-comma policy for an array or object literal.
|
|
245
|
+
*
|
|
246
|
+
* Two positions make the comma load-bearing rather than cosmetic.
|
|
247
|
+
*
|
|
248
|
+
* A trailing elision is a **value**: the comma is the token that materializes
|
|
249
|
+
* the hole, so `["a", ]` has one element and `["a", ,]` has two. The legacy
|
|
250
|
+
* printer emits it in every layout, so this printer emits it in every layout
|
|
251
|
+
* too; leaving it to the break would make the same node mean different things
|
|
252
|
+
* at different widths.
|
|
253
|
+
*
|
|
254
|
+
* A destructuring **assignment target** is the same node kind as an rvalue
|
|
255
|
+
* literal, but ECMAScript forbids a comma after its `AssignmentRestElement` /
|
|
256
|
+
* `AssignmentRestProperty`: `[a, ...rest,] = source` is a syntax error, while
|
|
257
|
+
* the identical rvalue `[a, ...rest,]` is legal. Only the target position
|
|
258
|
+
* suppresses it, so the rvalue twin keeps its break comma.
|
|
259
|
+
*/
|
|
260
|
+
private literalTrailingComma(
|
|
261
|
+
elements: readonly Node[],
|
|
262
|
+
assignmentTarget: boolean,
|
|
263
|
+
): TrailingComma {
|
|
264
|
+
const last: Node | undefined = elements[elements.length - 1];
|
|
265
|
+
if (last === undefined) return "onBreak";
|
|
266
|
+
if (last.kind === "OmittedExpression") return "always";
|
|
267
|
+
return assignmentTarget &&
|
|
268
|
+
(last.kind === "SpreadElement" || last.kind === "SpreadAssignment")
|
|
269
|
+
? "never"
|
|
270
|
+
: "onBreak";
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
private params(params: readonly Node[]): Doc {
|
|
@@ -232,7 +287,7 @@ export class TsPrinter {
|
|
|
232
287
|
args.map((a) => this.expressionForDisallowedComma(a)),
|
|
233
288
|
")",
|
|
234
289
|
{
|
|
235
|
-
trailingComma:
|
|
290
|
+
trailingComma: this.argsTrailingComma(args),
|
|
236
291
|
},
|
|
237
292
|
);
|
|
238
293
|
}
|
|
@@ -277,6 +332,47 @@ export class TsPrinter {
|
|
|
277
332
|
: "";
|
|
278
333
|
}
|
|
279
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Lay out a JSX element's or fragment's children.
|
|
337
|
+
*
|
|
338
|
+
* A line break between JSX children is not cosmetic. JSX deletes a
|
|
339
|
+
* whitespace-only text child that contains a newline and trims
|
|
340
|
+
* whitespace-carrying-a-newline off both edges of every other text child, so
|
|
341
|
+
* a break introduced only because the group did not fit changes what the
|
|
342
|
+
* component renders: `<div>Hello there, {name}!</div>` becomes `Hello
|
|
343
|
+
* there,NAME!`, and the separator in `<div>{a} {b}</div>` disappears
|
|
344
|
+
* outright.
|
|
345
|
+
*
|
|
346
|
+
* Children are therefore laid out across lines only when the break survives
|
|
347
|
+
* that transformation unchanged: every text child must carry non-whitespace
|
|
348
|
+
* content, must not begin or end with whitespace, and must not sit next to
|
|
349
|
+
* another text child, since inserting a newline between two of them would
|
|
350
|
+
* merge into one text with a space in the middle. Otherwise the children are
|
|
351
|
+
* emitted verbatim on one line, whatever `printWidth` says — width may choose
|
|
352
|
+
* a layout, never a meaning.
|
|
353
|
+
*/
|
|
354
|
+
private jsxChildren(open: Doc, children: readonly Node[], close: Doc): Doc {
|
|
355
|
+
if (!this.jsxChildrenMayBreak(children))
|
|
356
|
+
return concat([open, concat(children.map((c) => this.emit(c))), close]);
|
|
357
|
+
return group(
|
|
358
|
+
concat([
|
|
359
|
+
open,
|
|
360
|
+
indent(concat(children.map((c) => concat([softline, this.emit(c)])))),
|
|
361
|
+
softline,
|
|
362
|
+
close,
|
|
363
|
+
]),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private jsxChildrenMayBreak(children: readonly Node[]): boolean {
|
|
368
|
+
return children.every(
|
|
369
|
+
(child, index) =>
|
|
370
|
+
child.kind !== "JsxText" ||
|
|
371
|
+
(isBreakSafeJsxText(child.text) &&
|
|
372
|
+
children[index + 1]?.kind !== "JsxText"),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
280
376
|
private optType(type: Node | undefined): Doc {
|
|
281
377
|
return type ? concat([": ", this.emit(type)]) : "";
|
|
282
378
|
}
|
|
@@ -285,8 +381,16 @@ export class TsPrinter {
|
|
|
285
381
|
return body ? concat([" ", this.emit(body)]) : ";";
|
|
286
382
|
}
|
|
287
383
|
|
|
288
|
-
|
|
289
|
-
|
|
384
|
+
/**
|
|
385
|
+
* @param assignmentTarget Whether `node` occupies destructuring
|
|
386
|
+
* assignment-target position, where an array or object literal is a pattern
|
|
387
|
+
* rather than a value. The flag is set by the assignment and `for…in` /
|
|
388
|
+
* `for…of` cases, forwarded by every node that is transparent to it (a
|
|
389
|
+
* spread, a property's initializer, a parenthesis, an `=` default), and
|
|
390
|
+
* dropped by every other node.
|
|
391
|
+
*/
|
|
392
|
+
private emit(node: Node, assignmentTarget: boolean = false): Doc {
|
|
393
|
+
const body: Doc = this.emitNode(node, assignmentTarget);
|
|
290
394
|
const leading: SynthesizedComment[] | undefined =
|
|
291
395
|
getSyntheticLeadingComments(node);
|
|
292
396
|
const trailing: SynthesizedComment[] | undefined =
|
|
@@ -338,7 +442,7 @@ export class TsPrinter {
|
|
|
338
442
|
]);
|
|
339
443
|
}
|
|
340
444
|
|
|
341
|
-
private emitNode(node: Node): Doc {
|
|
445
|
+
private emitNode(node: Node, assignmentTarget: boolean): Doc {
|
|
342
446
|
switch (node.kind) {
|
|
343
447
|
/* names & tokens */
|
|
344
448
|
case "Identifier":
|
|
@@ -350,7 +454,7 @@ export class TsPrinter {
|
|
|
350
454
|
case "Token":
|
|
351
455
|
return node.token;
|
|
352
456
|
case "Decorator":
|
|
353
|
-
return concat(["@", this.leftSideExpression(node.expression)]);
|
|
457
|
+
return concat(["@", this.leftSideExpression(node.expression, false)]);
|
|
354
458
|
|
|
355
459
|
/* literals */
|
|
356
460
|
case "StringLiteral":
|
|
@@ -364,18 +468,29 @@ export class TsPrinter {
|
|
|
364
468
|
case "ArrayLiteralExpression":
|
|
365
469
|
return this.delim(
|
|
366
470
|
"[",
|
|
367
|
-
node.elements.map((e) =>
|
|
471
|
+
node.elements.map((e) =>
|
|
472
|
+
this.expressionForDisallowedComma(e, assignmentTarget),
|
|
473
|
+
),
|
|
368
474
|
"]",
|
|
369
|
-
{
|
|
475
|
+
{
|
|
476
|
+
trailingComma: this.literalTrailingComma(
|
|
477
|
+
node.elements,
|
|
478
|
+
assignmentTarget,
|
|
479
|
+
),
|
|
480
|
+
forceBreak: node.multiLine === true,
|
|
481
|
+
},
|
|
370
482
|
);
|
|
371
483
|
case "ObjectLiteralExpression":
|
|
372
484
|
return this.delim(
|
|
373
485
|
"{",
|
|
374
|
-
node.properties.map((p) => this.emit(p)),
|
|
486
|
+
node.properties.map((p) => this.emit(p, assignmentTarget)),
|
|
375
487
|
"}",
|
|
376
488
|
{
|
|
377
489
|
space: true,
|
|
378
|
-
trailingComma:
|
|
490
|
+
trailingComma: this.literalTrailingComma(
|
|
491
|
+
node.properties,
|
|
492
|
+
assignmentTarget,
|
|
493
|
+
),
|
|
379
494
|
forceBreak: node.multiLine === true,
|
|
380
495
|
},
|
|
381
496
|
);
|
|
@@ -383,7 +498,7 @@ export class TsPrinter {
|
|
|
383
498
|
return concat([
|
|
384
499
|
this.emit(node.name),
|
|
385
500
|
": ",
|
|
386
|
-
this.expressionForDisallowedComma(node.initializer),
|
|
501
|
+
this.expressionForDisallowedComma(node.initializer, assignmentTarget),
|
|
387
502
|
]);
|
|
388
503
|
case "ShorthandPropertyAssignment":
|
|
389
504
|
return concat([
|
|
@@ -400,24 +515,24 @@ export class TsPrinter {
|
|
|
400
515
|
case "SpreadAssignment":
|
|
401
516
|
return concat([
|
|
402
517
|
"...",
|
|
403
|
-
this.expressionForDisallowedComma(node.expression),
|
|
518
|
+
this.expressionForDisallowedComma(node.expression, assignmentTarget),
|
|
404
519
|
]);
|
|
405
520
|
case "PropertyAccessExpression":
|
|
406
521
|
return concat([
|
|
407
|
-
this.leftSideExpression(node.expression),
|
|
522
|
+
this.leftSideExpression(node.expression, false),
|
|
408
523
|
".",
|
|
409
524
|
this.emit(node.name),
|
|
410
525
|
]);
|
|
411
526
|
case "ElementAccessExpression":
|
|
412
527
|
return concat([
|
|
413
|
-
this.leftSideExpression(node.expression),
|
|
528
|
+
this.leftSideExpression(node.expression, false),
|
|
414
529
|
"[",
|
|
415
530
|
this.expressionForDisallowedComma(node.argumentExpression),
|
|
416
531
|
"]",
|
|
417
532
|
]);
|
|
418
533
|
case "CallExpression":
|
|
419
534
|
return concat([
|
|
420
|
-
this.leftSideExpression(node.expression),
|
|
535
|
+
this.leftSideExpression(node.expression, false),
|
|
421
536
|
this.typeArguments(node.typeArguments),
|
|
422
537
|
this.args(node.arguments),
|
|
423
538
|
]);
|
|
@@ -429,12 +544,26 @@ export class TsPrinter {
|
|
|
429
544
|
this.args(node.arguments ?? []),
|
|
430
545
|
]);
|
|
431
546
|
case "ParenthesizedExpression":
|
|
432
|
-
return concat(["(", this.emit(node.expression), ")"]);
|
|
547
|
+
return concat(["(", this.emit(node.expression, assignmentTarget), ")"]);
|
|
433
548
|
case "BinaryExpression":
|
|
549
|
+
// the left side of `=` is a destructuring assignment target, both for a
|
|
550
|
+
// top-level assignment and for a `[a = init]` default inside one
|
|
434
551
|
return group(
|
|
435
552
|
concat([
|
|
436
|
-
this.binaryOperand(
|
|
437
|
-
|
|
553
|
+
this.binaryOperand(
|
|
554
|
+
node.operator,
|
|
555
|
+
node.left,
|
|
556
|
+
true,
|
|
557
|
+
undefined,
|
|
558
|
+
node.operator === SyntaxKind.EqualsToken,
|
|
559
|
+
),
|
|
560
|
+
// Every operator but the comma is written with a space on each
|
|
561
|
+
// side. The comma is punctuation that attaches to what precedes it:
|
|
562
|
+
// `CommaListExpression` joins with ", ", the legacy printer and the
|
|
563
|
+
// repository's pinned Prettier both emit `a, b`, and this factory's
|
|
564
|
+
// own JSDoc for `createComma` shows `(a, b)`. Only the printer
|
|
565
|
+
// disagreed, with `a , b`.
|
|
566
|
+
node.operator === SyntaxKind.CommaToken ? "" : " ",
|
|
438
567
|
node.operator,
|
|
439
568
|
indent(
|
|
440
569
|
concat([
|
|
@@ -501,11 +630,11 @@ export class TsPrinter {
|
|
|
501
630
|
this.emit(node.type),
|
|
502
631
|
]);
|
|
503
632
|
case "NonNullExpression":
|
|
504
|
-
return concat([this.leftSideExpression(node.expression), "!"]);
|
|
633
|
+
return concat([this.leftSideExpression(node.expression, false), "!"]);
|
|
505
634
|
case "SpreadElement":
|
|
506
635
|
return concat([
|
|
507
636
|
"...",
|
|
508
|
-
this.expressionForDisallowedComma(node.expression),
|
|
637
|
+
this.expressionForDisallowedComma(node.expression, assignmentTarget),
|
|
509
638
|
]);
|
|
510
639
|
case "AwaitExpression":
|
|
511
640
|
return concat(["await ", this.prefixUnaryOperand(node.expression)]);
|
|
@@ -543,7 +672,7 @@ export class TsPrinter {
|
|
|
543
672
|
node.elements.map((e) => this.emit(e)),
|
|
544
673
|
"]",
|
|
545
674
|
{
|
|
546
|
-
trailingComma:
|
|
675
|
+
trailingComma: "onBreak",
|
|
547
676
|
},
|
|
548
677
|
);
|
|
549
678
|
case "ParenthesizedTypeNode":
|
|
@@ -564,8 +693,11 @@ export class TsPrinter {
|
|
|
564
693
|
case "TypeQueryNode":
|
|
565
694
|
return concat(["typeof ", this.emit(node.exprName)]);
|
|
566
695
|
case "ExpressionWithTypeArguments":
|
|
696
|
+
// heritage clauses take a LeftHandSideExpression: `class A extends
|
|
697
|
+
// (X || Y) {}` does not parse without the parentheses, and a bare comma
|
|
698
|
+
// sequence silently becomes two base classes
|
|
567
699
|
return concat([
|
|
568
|
-
this.
|
|
700
|
+
this.leftSideExpression(node.expression, false),
|
|
569
701
|
this.typeArguments(node.typeArguments),
|
|
570
702
|
]);
|
|
571
703
|
case "PropertySignature":
|
|
@@ -833,14 +965,20 @@ export class TsPrinter {
|
|
|
833
965
|
const named: Doc[] = [];
|
|
834
966
|
if (node.name) named.push(this.emit(node.name));
|
|
835
967
|
if (node.namedBindings) named.push(this.emit(node.namedBindings));
|
|
836
|
-
|
|
968
|
+
// The phase modifier is the keyword itself, so it prints as written —
|
|
969
|
+
// `type` and `defer` both reach here, where a boolean could only ever
|
|
970
|
+
// have produced the first.
|
|
971
|
+
return concat([
|
|
972
|
+
node.phaseModifier ? `${node.phaseModifier} ` : "",
|
|
973
|
+
join(", ", named),
|
|
974
|
+
]);
|
|
837
975
|
}
|
|
838
976
|
case "NamedImports":
|
|
839
977
|
return this.delim(
|
|
840
978
|
"{",
|
|
841
979
|
node.elements.map((e) => this.emit(e)),
|
|
842
980
|
"}",
|
|
843
|
-
{ space: true, trailingComma:
|
|
981
|
+
{ space: true, trailingComma: "onBreak" },
|
|
844
982
|
);
|
|
845
983
|
case "ImportSpecifier":
|
|
846
984
|
return concat([
|
|
@@ -868,7 +1006,7 @@ export class TsPrinter {
|
|
|
868
1006
|
"{",
|
|
869
1007
|
node.elements.map((e) => this.emit(e)),
|
|
870
1008
|
"}",
|
|
871
|
-
{ space: true, trailingComma:
|
|
1009
|
+
{ space: true, trailingComma: "onBreak" },
|
|
872
1010
|
);
|
|
873
1011
|
case "ExportSpecifier":
|
|
874
1012
|
return concat([
|
|
@@ -914,7 +1052,7 @@ export class TsPrinter {
|
|
|
914
1052
|
case "ForInStatement":
|
|
915
1053
|
return concat([
|
|
916
1054
|
"for (",
|
|
917
|
-
this.emit(node.initializer),
|
|
1055
|
+
this.emit(node.initializer, true),
|
|
918
1056
|
" in ",
|
|
919
1057
|
this.emit(node.expression),
|
|
920
1058
|
") ",
|
|
@@ -925,7 +1063,7 @@ export class TsPrinter {
|
|
|
925
1063
|
"for ",
|
|
926
1064
|
node.awaitModifier ? "await " : "",
|
|
927
1065
|
"(",
|
|
928
|
-
this.emit(node.initializer),
|
|
1066
|
+
this.emit(node.initializer, true),
|
|
929
1067
|
" of ",
|
|
930
1068
|
this.emit(node.expression),
|
|
931
1069
|
") ",
|
|
@@ -1046,7 +1184,17 @@ export class TsPrinter {
|
|
|
1046
1184
|
case "ModuleDeclaration":
|
|
1047
1185
|
return concat([
|
|
1048
1186
|
this.modifiers(node.modifiers, true),
|
|
1049
|
-
|
|
1187
|
+
// A string-literal name is always `module "…"`; the flag says nothing
|
|
1188
|
+
// there. For an identifier the flag is what chooses, which is the
|
|
1189
|
+
// upstream rule and the one `createModuleDeclaration` documents:
|
|
1190
|
+
// `namespace A` with `NodeFlags.Namespace`, `module A` without it.
|
|
1191
|
+
// The printer used to read the name kind alone, so an identifier
|
|
1192
|
+
// always printed `namespace` and the flag it published was inert.
|
|
1193
|
+
node.name.kind === "StringLiteral"
|
|
1194
|
+
? "module "
|
|
1195
|
+
: node.flags === NodeFlags.Namespace
|
|
1196
|
+
? "namespace "
|
|
1197
|
+
: "module ",
|
|
1050
1198
|
this.emit(node.name),
|
|
1051
1199
|
node.body ? concat([" ", this.emit(node.body)]) : ";",
|
|
1052
1200
|
]);
|
|
@@ -1154,6 +1302,22 @@ export class TsPrinter {
|
|
|
1154
1302
|
node.isTypeOf ? "typeof " : "",
|
|
1155
1303
|
"import(",
|
|
1156
1304
|
this.emit(node.argument),
|
|
1305
|
+
// An import type spells its attributes as a second call argument —
|
|
1306
|
+
// `import("m", { with: { … } }).T` — not as the trailing `with { … }`
|
|
1307
|
+
// an import declaration uses, so the elements are wrapped here rather
|
|
1308
|
+
// than emitted through the attributes node's own form.
|
|
1309
|
+
node.attributes && node.attributes.elements.length > 0
|
|
1310
|
+
? concat([
|
|
1311
|
+
", { ",
|
|
1312
|
+
node.attributes.token,
|
|
1313
|
+
": { ",
|
|
1314
|
+
join(
|
|
1315
|
+
", ",
|
|
1316
|
+
node.attributes.elements.map((e) => this.emit(e)),
|
|
1317
|
+
),
|
|
1318
|
+
" } }",
|
|
1319
|
+
])
|
|
1320
|
+
: "",
|
|
1157
1321
|
")",
|
|
1158
1322
|
node.qualifier ? concat([".", this.emit(node.qualifier)]) : "",
|
|
1159
1323
|
this.typeArguments(node.typeArguments),
|
|
@@ -1195,7 +1359,7 @@ export class TsPrinter {
|
|
|
1195
1359
|
]);
|
|
1196
1360
|
case "TaggedTemplateExpression":
|
|
1197
1361
|
return concat([
|
|
1198
|
-
this.leftSideExpression(node.tag),
|
|
1362
|
+
this.leftSideExpression(node.tag, false),
|
|
1199
1363
|
this.typeArguments(node.typeArguments),
|
|
1200
1364
|
this.emit(node.template),
|
|
1201
1365
|
]);
|
|
@@ -1276,13 +1440,13 @@ export class TsPrinter {
|
|
|
1276
1440
|
]);
|
|
1277
1441
|
case "PropertyAccessChain":
|
|
1278
1442
|
return concat([
|
|
1279
|
-
this.leftSideExpression(node.expression),
|
|
1443
|
+
this.leftSideExpression(node.expression, true),
|
|
1280
1444
|
node.questionDotToken ? "?." : ".",
|
|
1281
1445
|
this.emit(node.name),
|
|
1282
1446
|
]);
|
|
1283
1447
|
case "ElementAccessChain":
|
|
1284
1448
|
return concat([
|
|
1285
|
-
this.leftSideExpression(node.expression),
|
|
1449
|
+
this.leftSideExpression(node.expression, true),
|
|
1286
1450
|
node.questionDotToken ? "?." : "",
|
|
1287
1451
|
"[",
|
|
1288
1452
|
this.expressionForDisallowedComma(node.argumentExpression),
|
|
@@ -1290,27 +1454,20 @@ export class TsPrinter {
|
|
|
1290
1454
|
]);
|
|
1291
1455
|
case "CallChain":
|
|
1292
1456
|
return concat([
|
|
1293
|
-
this.leftSideExpression(node.expression),
|
|
1457
|
+
this.leftSideExpression(node.expression, true),
|
|
1294
1458
|
node.questionDotToken ? "?." : "",
|
|
1295
1459
|
this.typeArguments(node.typeArguments),
|
|
1296
1460
|
this.args(node.arguments),
|
|
1297
1461
|
]);
|
|
1298
1462
|
case "NonNullChain":
|
|
1299
|
-
return concat([this.leftSideExpression(node.expression), "!"]);
|
|
1463
|
+
return concat([this.leftSideExpression(node.expression, true), "!"]);
|
|
1300
1464
|
|
|
1301
1465
|
/* jsx */
|
|
1302
1466
|
case "JsxElement":
|
|
1303
|
-
return
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
concat(
|
|
1308
|
-
node.children.map((c) => concat([softline, this.emit(c)])),
|
|
1309
|
-
),
|
|
1310
|
-
),
|
|
1311
|
-
softline,
|
|
1312
|
-
this.emit(node.closingElement),
|
|
1313
|
-
]),
|
|
1467
|
+
return this.jsxChildren(
|
|
1468
|
+
this.emit(node.openingElement),
|
|
1469
|
+
node.children,
|
|
1470
|
+
this.emit(node.closingElement),
|
|
1314
1471
|
);
|
|
1315
1472
|
case "JsxSelfClosingElement":
|
|
1316
1473
|
return concat([
|
|
@@ -1331,24 +1488,19 @@ export class TsPrinter {
|
|
|
1331
1488
|
case "JsxClosingElement":
|
|
1332
1489
|
return concat(["</", this.emit(node.tagName), ">"]);
|
|
1333
1490
|
case "JsxFragment":
|
|
1334
|
-
return
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
concat(
|
|
1339
|
-
node.children.map((c) => concat([softline, this.emit(c)])),
|
|
1340
|
-
),
|
|
1341
|
-
),
|
|
1342
|
-
softline,
|
|
1343
|
-
this.emit(node.closingFragment),
|
|
1344
|
-
]),
|
|
1491
|
+
return this.jsxChildren(
|
|
1492
|
+
this.emit(node.openingFragment),
|
|
1493
|
+
node.children,
|
|
1494
|
+
this.emit(node.closingFragment),
|
|
1345
1495
|
);
|
|
1346
1496
|
case "JsxOpeningFragment":
|
|
1347
1497
|
return "<>";
|
|
1348
1498
|
case "JsxClosingFragment":
|
|
1349
1499
|
return "</>";
|
|
1350
1500
|
case "JsxText":
|
|
1351
|
-
|
|
1501
|
+
// the one node emitted as unquoted source text: its trailing spaces are
|
|
1502
|
+
// rendered content, so they must survive the layout engine's line trim
|
|
1503
|
+
return raw(node.text);
|
|
1352
1504
|
case "JsxAttribute":
|
|
1353
1505
|
return node.initializer === undefined
|
|
1354
1506
|
? this.emit(node.name)
|
|
@@ -1585,6 +1737,10 @@ export class TsPrinter {
|
|
|
1585
1737
|
? concat([this.emit(node.importClause), " from "])
|
|
1586
1738
|
: "",
|
|
1587
1739
|
this.emit(node.moduleSpecifier),
|
|
1740
|
+
// `@import { a } from "m" with { type: "json" }` — the same trailing
|
|
1741
|
+
// form an import declaration uses, which is why the attributes node
|
|
1742
|
+
// emits itself here rather than being unwrapped.
|
|
1743
|
+
node.attributes ? concat([" ", this.emit(node.attributes)]) : "",
|
|
1588
1744
|
this.jsDocComment(node.comment),
|
|
1589
1745
|
]);
|
|
1590
1746
|
case "JSDocTemplateTag":
|
|
@@ -1621,21 +1777,80 @@ export class TsPrinter {
|
|
|
1621
1777
|
* re-associate — matching the legacy printer's parenthesizer rules.
|
|
1622
1778
|
*/
|
|
1623
1779
|
private parenthesizedExpression(expression: Expression): Doc {
|
|
1624
|
-
return expression.kind ===
|
|
1780
|
+
return this.skipPartiallyEmittedExpressions(expression).kind ===
|
|
1781
|
+
"ParenthesizedExpression"
|
|
1625
1782
|
? this.emit(expression)
|
|
1626
1783
|
: concat(["(", this.emit(expression), ")"]);
|
|
1627
1784
|
}
|
|
1628
1785
|
|
|
1629
|
-
|
|
1786
|
+
/**
|
|
1787
|
+
* The partial-emission wrapper carries transform provenance but emits no
|
|
1788
|
+
* syntax of its own, so every grammar predicate must inspect its inner node.
|
|
1789
|
+
*/
|
|
1790
|
+
private skipPartiallyEmittedExpressions(expression: Expression): Expression {
|
|
1791
|
+
while (expression.kind === "PartiallyEmittedExpression")
|
|
1792
|
+
expression = expression.expression;
|
|
1793
|
+
return expression;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private expressionForDisallowedComma(
|
|
1797
|
+
expression: Expression,
|
|
1798
|
+
assignmentTarget: boolean = false,
|
|
1799
|
+
): Doc {
|
|
1630
1800
|
return this.expressionPrecedence(expression) > ExpressionPrecedence.Comma
|
|
1631
|
-
? this.emit(expression)
|
|
1801
|
+
? this.emit(expression, assignmentTarget)
|
|
1632
1802
|
: this.parenthesizedExpression(expression);
|
|
1633
1803
|
}
|
|
1634
1804
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1805
|
+
/**
|
|
1806
|
+
* Emit an operand the grammar requires to be a `LeftHandSideExpression`,
|
|
1807
|
+
* mirroring the legacy parenthesizer's
|
|
1808
|
+
* `parenthesizeLeftSideOfAccess(expression, optionalChain)`.
|
|
1809
|
+
*
|
|
1810
|
+
* `optionalChain` is the **consuming** node's own chain-ness, not the
|
|
1811
|
+
* operand's. An optional chain may be emitted bare only when the node
|
|
1812
|
+
* consuming it continues the same chain: `a?.b?.()` is one chain, while
|
|
1813
|
+
* `(a?.b)()` is a plain call on the chain's value. Emitting the second as
|
|
1814
|
+
* `a?.b()` re-parses as the first, which stops throwing on a nullish head,
|
|
1815
|
+
* and in `new`, tagged-template and decorator position it does not compile at
|
|
1816
|
+
* all.
|
|
1817
|
+
*/
|
|
1818
|
+
private leftSideExpression(
|
|
1819
|
+
expression: Expression,
|
|
1820
|
+
optionalChain: boolean,
|
|
1821
|
+
): Doc {
|
|
1822
|
+
return this.leftSideNeedsParentheses(expression, optionalChain)
|
|
1823
|
+
? this.parenthesizedExpression(expression)
|
|
1824
|
+
: this.emit(expression);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Whether {@link leftSideExpression} wraps this operand.
|
|
1829
|
+
*
|
|
1830
|
+
* The legacy rule also parenthesizes an argument-less `new` here, because it
|
|
1831
|
+
* prints `new X` bare and `new X.y` would re-parse with `y` on the target.
|
|
1832
|
+
* This printer always emits the argument list, so `new X().y` already says
|
|
1833
|
+
* what the tree says and needs no wrapper.
|
|
1834
|
+
*/
|
|
1835
|
+
private leftSideNeedsParentheses(
|
|
1836
|
+
expression: Expression,
|
|
1837
|
+
optionalChain: boolean,
|
|
1838
|
+
): boolean {
|
|
1839
|
+
if (!this.isLeftHandSideExpression(expression)) return true;
|
|
1840
|
+
return !optionalChain && this.isOptionalChain(expression);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
private isOptionalChain(expression: Expression): boolean {
|
|
1844
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1845
|
+
switch (expression.kind) {
|
|
1846
|
+
case "CallChain":
|
|
1847
|
+
case "ElementAccessChain":
|
|
1848
|
+
case "NonNullChain":
|
|
1849
|
+
case "PropertyAccessChain":
|
|
1850
|
+
return true;
|
|
1851
|
+
default:
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1639
1854
|
}
|
|
1640
1855
|
|
|
1641
1856
|
private newExpressionTarget(expression: Expression): Doc {
|
|
@@ -1648,21 +1863,77 @@ export class TsPrinter {
|
|
|
1648
1863
|
* Whether a `new` target must be parenthesized to keep its call arguments
|
|
1649
1864
|
* from re-binding to the `new` — mirroring the legacy printer's
|
|
1650
1865
|
* `parenthesizeExpressionOfNew`. A `new` target is grammatically a
|
|
1651
|
-
* `MemberExpression`, so a call anywhere on the target's left spine
|
|
1652
|
-
* a direct one: `new (f().bar)()`, `new (a.b().c)()`) would
|
|
1653
|
-
* re-parse with the call's arguments consumed by the `new` — a
|
|
1654
|
-
* program. Argument-less `new` on the spine is kept parenthesized
|
|
1655
|
-
* continuity with the direct case, though this printer always prints an
|
|
1656
|
-
* argument list, which already disambiguates it.
|
|
1866
|
+
* `MemberExpression`, so a call anywhere on the target's printed left spine
|
|
1867
|
+
* (not just a direct one: `new (f().bar)()`, `new (a.b().c)()`) would
|
|
1868
|
+
* otherwise re-parse with the call's arguments consumed by the `new` — a
|
|
1869
|
+
* different program. Argument-less `new` on the spine is kept parenthesized
|
|
1870
|
+
* for continuity with the direct case, though this printer always prints an
|
|
1871
|
+
* argument list, which already disambiguates it. Anything else falls back to
|
|
1872
|
+
* the shared left-side rule, which is what parenthesizes an optional-chain
|
|
1873
|
+
* target (`new (a?.b)()`, TS1209 without it).
|
|
1657
1874
|
*/
|
|
1658
1875
|
private newExpressionTargetNeedsParentheses(expression: Expression): boolean {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
leftmost.kind === "CallExpression" ||
|
|
1663
|
-
|
|
1664
|
-
(leftmost.kind === "NewExpression"
|
|
1665
|
-
|
|
1876
|
+
const leftmost: Expression | undefined =
|
|
1877
|
+
this.leftmostPrintedExpression(expression);
|
|
1878
|
+
if (leftmost !== undefined) {
|
|
1879
|
+
if (leftmost.kind === "CallExpression" || leftmost.kind === "CallChain")
|
|
1880
|
+
return true;
|
|
1881
|
+
if (leftmost.kind === "NewExpression")
|
|
1882
|
+
return leftmost.arguments === undefined;
|
|
1883
|
+
}
|
|
1884
|
+
return this.leftSideNeedsParentheses(expression, false);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* The node whose own text opens `expression`'s printed form, or `undefined`
|
|
1889
|
+
* when that text opens with a printer-inserted `(`.
|
|
1890
|
+
*
|
|
1891
|
+
* The legacy factory parenthesizes each operand as it builds the node, so its
|
|
1892
|
+
* `getLeftmostExpression` walk halts on the resulting
|
|
1893
|
+
* `ParenthesizedExpression`. This printer decides the same parentheses at
|
|
1894
|
+
* emit time instead, so the walk has to ask {@link leftSideNeedsParentheses}
|
|
1895
|
+
* the same question directly; otherwise `new` re-wraps a target whose call is
|
|
1896
|
+
* already behind parentheses, and `new (f?.()).bar()` comes out as `new
|
|
1897
|
+
* ((f?.()).bar)()`. Calls halt the walk, matching the legacy
|
|
1898
|
+
* `stopAtCallExpressions` mode this predicate is the only user of.
|
|
1899
|
+
*/
|
|
1900
|
+
private leftmostPrintedExpression(
|
|
1901
|
+
expression: Expression,
|
|
1902
|
+
): Expression | undefined {
|
|
1903
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1904
|
+
switch (expression.kind) {
|
|
1905
|
+
case "CallExpression":
|
|
1906
|
+
case "CallChain":
|
|
1907
|
+
return expression;
|
|
1908
|
+
case "ElementAccessExpression":
|
|
1909
|
+
case "NonNullExpression":
|
|
1910
|
+
case "PropertyAccessExpression":
|
|
1911
|
+
return this.leftmostPrintedLeftSide(expression.expression, false);
|
|
1912
|
+
case "ElementAccessChain":
|
|
1913
|
+
case "NonNullChain":
|
|
1914
|
+
case "PropertyAccessChain":
|
|
1915
|
+
return this.leftmostPrintedLeftSide(expression.expression, true);
|
|
1916
|
+
case "TaggedTemplateExpression":
|
|
1917
|
+
return this.leftmostPrintedLeftSide(expression.tag, false);
|
|
1918
|
+
case "AsExpression":
|
|
1919
|
+
case "SatisfiesExpression":
|
|
1920
|
+
return this.leftmostPrintedExpression(expression.expression);
|
|
1921
|
+
case "BinaryExpression":
|
|
1922
|
+
return this.leftmostPrintedExpression(expression.left);
|
|
1923
|
+
case "ConditionalExpression":
|
|
1924
|
+
return this.leftmostPrintedExpression(expression.condition);
|
|
1925
|
+
default:
|
|
1926
|
+
return expression;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
private leftmostPrintedLeftSide(
|
|
1931
|
+
operand: Expression,
|
|
1932
|
+
optionalChain: boolean,
|
|
1933
|
+
): Expression | undefined {
|
|
1934
|
+
return this.leftSideNeedsParentheses(operand, optionalChain)
|
|
1935
|
+
? undefined
|
|
1936
|
+
: this.leftmostPrintedExpression(operand);
|
|
1666
1937
|
}
|
|
1667
1938
|
|
|
1668
1939
|
private prefixUnaryOperand(operand: Expression, operator?: SyntaxKind): Doc {
|
|
@@ -1728,6 +1999,7 @@ export class TsPrinter {
|
|
|
1728
1999
|
operand: Expression,
|
|
1729
2000
|
isLeftSide: boolean,
|
|
1730
2001
|
leftOperand?: Expression,
|
|
2002
|
+
assignmentTarget: boolean = false,
|
|
1731
2003
|
): Doc {
|
|
1732
2004
|
return this.binaryOperandNeedsParentheses(
|
|
1733
2005
|
operator,
|
|
@@ -1736,7 +2008,7 @@ export class TsPrinter {
|
|
|
1736
2008
|
leftOperand,
|
|
1737
2009
|
)
|
|
1738
2010
|
? this.parenthesizedExpression(operand)
|
|
1739
|
-
: this.emit(operand);
|
|
2011
|
+
: this.emit(operand, assignmentTarget);
|
|
1740
2012
|
}
|
|
1741
2013
|
|
|
1742
2014
|
private binaryOperandNeedsParentheses(
|
|
@@ -1745,43 +2017,52 @@ export class TsPrinter {
|
|
|
1745
2017
|
isLeftSide: boolean,
|
|
1746
2018
|
leftOperand?: Expression,
|
|
1747
2019
|
): boolean {
|
|
1748
|
-
|
|
2020
|
+
const emittedOperand: Expression =
|
|
2021
|
+
this.skipPartiallyEmittedExpressions(operand);
|
|
2022
|
+
if (emittedOperand.kind === "ParenthesizedExpression") return false;
|
|
1749
2023
|
if (
|
|
1750
2024
|
operator === SyntaxKind.AsteriskAsteriskToken &&
|
|
1751
2025
|
isLeftSide &&
|
|
1752
|
-
this.expressionPrecedence(
|
|
2026
|
+
this.expressionPrecedence(emittedOperand) === ExpressionPrecedence.Unary
|
|
1753
2027
|
)
|
|
1754
2028
|
return true;
|
|
1755
2029
|
if (
|
|
1756
|
-
|
|
1757
|
-
this.mixingBinaryOperatorsRequiresParentheses(
|
|
2030
|
+
emittedOperand.kind === "BinaryExpression" &&
|
|
2031
|
+
this.mixingBinaryOperatorsRequiresParentheses(
|
|
2032
|
+
operator,
|
|
2033
|
+
emittedOperand.operator,
|
|
2034
|
+
)
|
|
1758
2035
|
)
|
|
1759
2036
|
return true;
|
|
1760
2037
|
|
|
1761
2038
|
const operatorPrecedence: ExpressionPrecedence =
|
|
1762
2039
|
this.binaryOperatorPrecedence(operator);
|
|
1763
2040
|
const operandPrecedence: ExpressionPrecedence =
|
|
1764
|
-
this.expressionPrecedence(
|
|
2041
|
+
this.expressionPrecedence(emittedOperand);
|
|
1765
2042
|
if (operandPrecedence < operatorPrecedence) return true;
|
|
1766
2043
|
if (operandPrecedence > operatorPrecedence) return false;
|
|
1767
2044
|
|
|
1768
2045
|
if (isLeftSide)
|
|
1769
2046
|
return this.binaryOperatorAssociativity(operator) === Associativity.Right;
|
|
1770
|
-
if (
|
|
2047
|
+
if (
|
|
2048
|
+
emittedOperand.kind === "BinaryExpression" &&
|
|
2049
|
+
emittedOperand.operator === operator
|
|
2050
|
+
) {
|
|
1771
2051
|
if (this.operatorHasAssociativeProperty(operator)) return false;
|
|
1772
2052
|
if (
|
|
1773
2053
|
operator === SyntaxKind.PlusToken &&
|
|
1774
2054
|
leftOperand !== undefined &&
|
|
1775
2055
|
this.literalKindOfBinaryPlusOperand(leftOperand) !== undefined &&
|
|
1776
2056
|
this.literalKindOfBinaryPlusOperand(leftOperand) ===
|
|
1777
|
-
this.literalKindOfBinaryPlusOperand(
|
|
2057
|
+
this.literalKindOfBinaryPlusOperand(emittedOperand)
|
|
1778
2058
|
)
|
|
1779
2059
|
return false;
|
|
1780
2060
|
}
|
|
1781
|
-
return this.expressionAssociativity(
|
|
2061
|
+
return this.expressionAssociativity(emittedOperand) === Associativity.Left;
|
|
1782
2062
|
}
|
|
1783
2063
|
|
|
1784
2064
|
private expressionPrecedence(expression: Expression): ExpressionPrecedence {
|
|
2065
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1785
2066
|
switch (expression.kind) {
|
|
1786
2067
|
case "CommaListExpression":
|
|
1787
2068
|
return ExpressionPrecedence.Comma;
|
|
@@ -1824,6 +2105,7 @@ export class TsPrinter {
|
|
|
1824
2105
|
}
|
|
1825
2106
|
|
|
1826
2107
|
private expressionAssociativity(expression: Expression): Associativity {
|
|
2108
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1827
2109
|
switch (expression.kind) {
|
|
1828
2110
|
case "NewExpression":
|
|
1829
2111
|
return expression.arguments === undefined
|
|
@@ -1941,6 +2223,7 @@ export class TsPrinter {
|
|
|
1941
2223
|
private literalKindOfBinaryPlusOperand(
|
|
1942
2224
|
expression: Expression,
|
|
1943
2225
|
): string | undefined {
|
|
2226
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1944
2227
|
switch (expression.kind) {
|
|
1945
2228
|
case "StringLiteral":
|
|
1946
2229
|
case "NumericLiteral":
|
|
@@ -1966,6 +2249,7 @@ export class TsPrinter {
|
|
|
1966
2249
|
}
|
|
1967
2250
|
|
|
1968
2251
|
private isLeftHandSideExpression(expression: Expression): boolean {
|
|
2252
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
1969
2253
|
switch (expression.kind) {
|
|
1970
2254
|
case "ArrowFunction":
|
|
1971
2255
|
case "ClassExpression":
|
|
@@ -2014,20 +2298,19 @@ export class TsPrinter {
|
|
|
2014
2298
|
|
|
2015
2299
|
/**
|
|
2016
2300
|
* Walk to the expression's leftmost node — the one that starts its printed
|
|
2017
|
-
* text
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2301
|
+
* text — matching the legacy `getLeftmostExpression`.
|
|
2302
|
+
*
|
|
2303
|
+
* Used by the statement, concise-body and export-default predicates, which
|
|
2304
|
+
* ask only whether the text opens with a `function`, `class` or `{` token.
|
|
2305
|
+
* The `new`-target predicate needs the printed left edge instead and uses
|
|
2306
|
+
* {@link leftmostPrintedExpression}.
|
|
2020
2307
|
*/
|
|
2021
|
-
private leftmostExpression(
|
|
2022
|
-
expression
|
|
2023
|
-
stopAtCall: boolean = false,
|
|
2024
|
-
): Expression {
|
|
2308
|
+
private leftmostExpression(expression: Expression): Expression {
|
|
2309
|
+
expression = this.skipPartiallyEmittedExpressions(expression);
|
|
2025
2310
|
switch (expression.kind) {
|
|
2311
|
+
case "AsExpression":
|
|
2026
2312
|
case "CallExpression":
|
|
2027
2313
|
case "CallChain":
|
|
2028
|
-
if (stopAtCall) return expression;
|
|
2029
|
-
return this.leftmostExpression(expression.expression, stopAtCall);
|
|
2030
|
-
case "AsExpression":
|
|
2031
2314
|
case "ElementAccessExpression":
|
|
2032
2315
|
case "ElementAccessChain":
|
|
2033
2316
|
case "NonNullExpression":
|
|
@@ -2035,13 +2318,13 @@ export class TsPrinter {
|
|
|
2035
2318
|
case "PropertyAccessExpression":
|
|
2036
2319
|
case "PropertyAccessChain":
|
|
2037
2320
|
case "SatisfiesExpression":
|
|
2038
|
-
return this.leftmostExpression(expression.expression
|
|
2321
|
+
return this.leftmostExpression(expression.expression);
|
|
2039
2322
|
case "BinaryExpression":
|
|
2040
|
-
return this.leftmostExpression(expression.left
|
|
2323
|
+
return this.leftmostExpression(expression.left);
|
|
2041
2324
|
case "ConditionalExpression":
|
|
2042
|
-
return this.leftmostExpression(expression.condition
|
|
2325
|
+
return this.leftmostExpression(expression.condition);
|
|
2043
2326
|
case "TaggedTemplateExpression":
|
|
2044
|
-
return this.leftmostExpression(expression.tag
|
|
2327
|
+
return this.leftmostExpression(expression.tag);
|
|
2045
2328
|
default:
|
|
2046
2329
|
return expression;
|
|
2047
2330
|
}
|
|
@@ -2051,6 +2334,7 @@ export class TsPrinter {
|
|
|
2051
2334
|
operator: SyntaxKind | undefined,
|
|
2052
2335
|
operand: Expression,
|
|
2053
2336
|
): boolean {
|
|
2337
|
+
operand = this.skipPartiallyEmittedExpressions(operand);
|
|
2054
2338
|
if (operator === undefined || operand.kind !== "PrefixUnaryExpression")
|
|
2055
2339
|
return false;
|
|
2056
2340
|
return (
|
|
@@ -2224,15 +2508,90 @@ const escapeTemplateText = (text: string): string =>
|
|
|
2224
2508
|
.replace(/\r\n/g, "\\r\\n")
|
|
2225
2509
|
.replace(/\r/g, "\\r");
|
|
2226
2510
|
|
|
2511
|
+
/**
|
|
2512
|
+
* Whether a JSX text child means the same thing with a line break and
|
|
2513
|
+
* indentation around it.
|
|
2514
|
+
*
|
|
2515
|
+
* JSX drops a whitespace-only child that contains a newline and trims an edge
|
|
2516
|
+
* whose whitespace contains one, so only a child with non-whitespace content
|
|
2517
|
+
* and no edge whitespace survives being moved onto its own line. Newlines
|
|
2518
|
+
* _inside_ the text are unaffected, because JSX collapses each interior line
|
|
2519
|
+
* break to a single space in either layout.
|
|
2520
|
+
*/
|
|
2521
|
+
const isBreakSafeJsxText = (text: string): boolean =>
|
|
2522
|
+
text.length !== 0 && !/^\s/.test(text) && !/\s$/.test(text);
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Escape a string literal's text so the printed program holds the value the AST
|
|
2526
|
+
* carries.
|
|
2527
|
+
*
|
|
2528
|
+
* The old set was the backslash, LF, CR, TAB and the active quote. Everything
|
|
2529
|
+
* else was emitted raw, which is three separate hazards rather than a cosmetic
|
|
2530
|
+
* gap: a C0 control or DEL lands in the generated file as itself; U+2028 and
|
|
2531
|
+
* U+2029 terminate a string literal in any JavaScript engine predating ES2019,
|
|
2532
|
+
* so the emitted program does not parse; and a lone surrogate becomes U+FFFD
|
|
2533
|
+
* the moment the text is written as UTF-8, so the generated program holds a
|
|
2534
|
+
* different string than the caller built.
|
|
2535
|
+
*
|
|
2536
|
+
* Iterated by code point rather than matched by a pattern. That is what makes
|
|
2537
|
+
* the surrogate case fall out instead of needing a rule: a well-formed pair
|
|
2538
|
+
* arrives as one two-unit string and passes through, and a lone surrogate
|
|
2539
|
+
* arrives as a single unit whose code point is in the surrogate range.
|
|
2540
|
+
*/
|
|
2227
2541
|
const escapeString = (text: string, singleQuote?: boolean): string => {
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2542
|
+
const quote = singleQuote === true ? "'" : '"';
|
|
2543
|
+
let escaped = "";
|
|
2544
|
+
for (const ch of text) {
|
|
2545
|
+
if (ch === "\\") {
|
|
2546
|
+
escaped += "\\\\";
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
if (ch === quote) {
|
|
2550
|
+
escaped += "\\" + ch;
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
// The inactive quote is ordinary text and stays as written.
|
|
2554
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
2555
|
+
const lone = code >= 0xd800 && code <= 0xdfff;
|
|
2556
|
+
if (
|
|
2557
|
+
ch.length === 2 ||
|
|
2558
|
+
(code >= 0x20 &&
|
|
2559
|
+
code !== 0x7f &&
|
|
2560
|
+
code !== 0x2028 &&
|
|
2561
|
+
code !== 0x2029 &&
|
|
2562
|
+
!lone)
|
|
2563
|
+
) {
|
|
2564
|
+
escaped += ch;
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
switch (code) {
|
|
2568
|
+
case 0x08:
|
|
2569
|
+
escaped += "\\b";
|
|
2570
|
+
continue;
|
|
2571
|
+
case 0x09:
|
|
2572
|
+
escaped += "\\t";
|
|
2573
|
+
continue;
|
|
2574
|
+
case 0x0a:
|
|
2575
|
+
escaped += "\\n";
|
|
2576
|
+
continue;
|
|
2577
|
+
case 0x0b:
|
|
2578
|
+
escaped += "\\v";
|
|
2579
|
+
continue;
|
|
2580
|
+
case 0x0c:
|
|
2581
|
+
escaped += "\\f";
|
|
2582
|
+
continue;
|
|
2583
|
+
case 0x0d:
|
|
2584
|
+
escaped += "\\r";
|
|
2585
|
+
continue;
|
|
2586
|
+
default:
|
|
2587
|
+
break;
|
|
2588
|
+
}
|
|
2589
|
+
escaped +=
|
|
2590
|
+
code > 0xff
|
|
2591
|
+
? "\\u" + code.toString(16).padStart(4, "0")
|
|
2592
|
+
: "\\x" + code.toString(16).padStart(2, "0");
|
|
2593
|
+
}
|
|
2594
|
+
return `${quote}${escaped}${quote}`;
|
|
2236
2595
|
};
|
|
2237
2596
|
|
|
2238
2597
|
const ExpressionPrecedence = {
|
|
@@ -2262,6 +2621,15 @@ const ExpressionPrecedence = {
|
|
|
2262
2621
|
type ExpressionPrecedence =
|
|
2263
2622
|
(typeof ExpressionPrecedence)[keyof typeof ExpressionPrecedence];
|
|
2264
2623
|
|
|
2624
|
+
/**
|
|
2625
|
+
* Whether a delimited list may end with a comma, and in which layout.
|
|
2626
|
+
*
|
|
2627
|
+
* `"onBreak"` is the cosmetic default: the comma appears only when the group
|
|
2628
|
+
* breaks. `"always"` and `"never"` are for the lists where the comma is part of
|
|
2629
|
+
* the program rather than its layout.
|
|
2630
|
+
*/
|
|
2631
|
+
type TrailingComma = "never" | "onBreak" | "always";
|
|
2632
|
+
|
|
2265
2633
|
const Associativity = {
|
|
2266
2634
|
Left: "left",
|
|
2267
2635
|
Right: "right",
|