@ttsc/factory 0.19.2 → 0.20.0

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.
Files changed (56) hide show
  1. package/README.md +2 -0
  2. package/lib/TsPrinter.d.ts +124 -17
  3. package/lib/TsPrinter.js +414 -106
  4. package/lib/TsPrinter.js.map +1 -1
  5. package/lib/TsPrinter.mjs +413 -107
  6. package/lib/TsPrinter.mjs.map +1 -1
  7. package/lib/ast/expressions/Expression.d.ts +5 -1
  8. package/lib/ast/imports/ImportClause.d.ts +10 -2
  9. package/lib/ast/jsdoc/JSDocImportTag.d.ts +3 -0
  10. package/lib/ast/types/ImportTypeNode.d.ts +3 -0
  11. package/lib/factory/expressions/createComma.d.ts +3 -3
  12. package/lib/factory/expressions/createComma.js +3 -3
  13. package/lib/factory/expressions/createComma.mjs +3 -3
  14. package/lib/factory/imports/createImportClause.d.ts +9 -3
  15. package/lib/factory/imports/createImportClause.js +8 -3
  16. package/lib/factory/imports/createImportClause.js.map +1 -1
  17. package/lib/factory/imports/createImportClause.mjs +8 -3
  18. package/lib/factory/imports/createImportClause.mjs.map +1 -1
  19. package/lib/factory/jsdoc/createJSDocImportTag.d.ts +3 -2
  20. package/lib/factory/jsdoc/createJSDocImportTag.js +3 -1
  21. package/lib/factory/jsdoc/createJSDocImportTag.js.map +1 -1
  22. package/lib/factory/jsdoc/createJSDocImportTag.mjs +3 -1
  23. package/lib/factory/jsdoc/createJSDocImportTag.mjs.map +1 -1
  24. package/lib/factory/types/createImportTypeNode.d.ts +8 -2
  25. package/lib/factory/types/createImportTypeNode.js +13 -1
  26. package/lib/factory/types/createImportTypeNode.js.map +1 -1
  27. package/lib/factory/types/createImportTypeNode.mjs +13 -1
  28. package/lib/factory/types/createImportTypeNode.mjs.map +1 -1
  29. package/lib/internal/doc.d.ts +13 -0
  30. package/lib/internal/doc.js +31 -4
  31. package/lib/internal/doc.js.map +1 -1
  32. package/lib/internal/doc.mjs +30 -4
  33. package/lib/internal/doc.mjs.map +1 -1
  34. package/lib/syntax/NodeFlags.d.ts +6 -4
  35. package/lib/syntax/NodeFlags.js +6 -4
  36. package/lib/syntax/NodeFlags.js.map +1 -1
  37. package/lib/syntax/NodeFlags.mjs +6 -4
  38. package/lib/syntax/NodeFlags.mjs.map +1 -1
  39. package/lib/syntax/SyntaxKind.d.ts +2 -0
  40. package/lib/syntax/SyntaxKind.js +6 -0
  41. package/lib/syntax/SyntaxKind.js.map +1 -1
  42. package/lib/syntax/SyntaxKind.mjs +6 -0
  43. package/lib/syntax/SyntaxKind.mjs.map +1 -1
  44. package/package.json +1 -1
  45. package/src/TsPrinter.ts +489 -121
  46. package/src/ast/expressions/Expression.ts +8 -0
  47. package/src/ast/imports/ImportClause.ts +10 -2
  48. package/src/ast/jsdoc/JSDocImportTag.ts +4 -0
  49. package/src/ast/types/ImportTypeNode.ts +4 -0
  50. package/src/factory/expressions/createComma.ts +3 -3
  51. package/src/factory/imports/createImportClause.ts +12 -6
  52. package/src/factory/jsdoc/createJSDocImportTag.ts +5 -1
  53. package/src/factory/types/createImportTypeNode.ts +21 -3
  54. package/src/internal/doc.ts +30 -3
  55. package/src/syntax/NodeFlags.ts +6 -4
  56. 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?: boolean;
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 ? ifBreak(",") : "",
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: false,
199
+ trailingComma: "never",
195
200
  },
196
201
  )
197
202
  : "";
198
203
  }
199
204
 
200
205
  /**
201
- * Whether a broken parameter list / binding pattern may append a synthetic
202
- * trailing comma after its last element.
206
+ * Trailing-comma policy for a parameter list or binding pattern.
203
207
  *
204
- * A trailing comma after a rest element (`...rest`) is a syntax error (TS1013
205
- * / V8 `SyntaxError`), and one after a trailing elision (`OmittedExpression`)
206
- * is not cosmetic: `[a, ,]` parses to one more hole than `[a, ]`, so the flat
207
- * and broken layouts of the same node would disagree. Call arguments and
208
- * array / object literals are unaffected a trailing comma after a spread is
209
- * legal there.
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[]): boolean {
218
+ private listTrailingComma(nodes: readonly Node[]): TrailingComma {
212
219
  const last: Node | undefined = nodes[nodes.length - 1];
213
- if (last === undefined) return true;
214
- if (last.kind === "OmittedExpression") return false;
215
- return !("dotDotDotToken" in last && last.dotDotDotToken !== undefined);
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: true,
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
- private emit(node: Node): Doc {
289
- const body: Doc = this.emitNode(node);
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) => this.expressionForDisallowedComma(e)),
471
+ node.elements.map((e) =>
472
+ this.expressionForDisallowedComma(e, assignmentTarget),
473
+ ),
368
474
  "]",
369
- { trailingComma: true, forceBreak: node.multiLine === true },
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: true,
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(node.operator, node.left, true),
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: true,
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.emit(node.expression),
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
- return concat([node.isTypeOnly ? "type " : "", join(", ", named)]);
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: true },
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: true },
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
- node.name.kind === "StringLiteral" ? "module " : "namespace ",
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 group(
1304
- concat([
1305
- this.emit(node.openingElement),
1306
- indent(
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 group(
1335
- concat([
1336
- this.emit(node.openingFragment),
1337
- indent(
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
- return node.text;
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 === "ParenthesizedExpression"
1780
+ return this.skipPartiallyEmittedExpressions(expression).kind ===
1781
+ "ParenthesizedExpression"
1625
1782
  ? this.emit(expression)
1626
1783
  : concat(["(", this.emit(expression), ")"]);
1627
1784
  }
1628
1785
 
1629
- private expressionForDisallowedComma(expression: Expression): Doc {
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
- private leftSideExpression(expression: Expression): Doc {
1636
- return this.isLeftHandSideExpression(expression)
1637
- ? this.emit(expression)
1638
- : this.parenthesizedExpression(expression);
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 (not just
1652
- * a direct one: `new (f().bar)()`, `new (a.b().c)()`) would otherwise
1653
- * re-parse with the call's arguments consumed by the `new` — a different
1654
- * program. Argument-less `new` on the spine is kept parenthesized for
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
- if (!this.isLeftHandSideExpression(expression)) return true;
1660
- const leftmost: Expression = this.leftmostExpression(expression, true);
1661
- return (
1662
- leftmost.kind === "CallExpression" ||
1663
- leftmost.kind === "CallChain" ||
1664
- (leftmost.kind === "NewExpression" && leftmost.arguments === undefined)
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
- if (operand.kind === "ParenthesizedExpression") return false;
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(operand) === ExpressionPrecedence.Unary
2026
+ this.expressionPrecedence(emittedOperand) === ExpressionPrecedence.Unary
1753
2027
  )
1754
2028
  return true;
1755
2029
  if (
1756
- operand.kind === "BinaryExpression" &&
1757
- this.mixingBinaryOperatorsRequiresParentheses(operator, operand.operator)
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(operand);
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 (operand.kind === "BinaryExpression" && operand.operator === operator) {
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(operand)
2057
+ this.literalKindOfBinaryPlusOperand(emittedOperand)
1778
2058
  )
1779
2059
  return false;
1780
2060
  }
1781
- return this.expressionAssociativity(operand) === Associativity.Left;
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. With `stopAtCall`, calls terminate the walk instead of being walked
2018
- * through, matching the legacy `getLeftmostExpression`'s
2019
- * `stopAtCallExpressions` mode used by the `new`-target parenthesizer.
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: 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, stopAtCall);
2321
+ return this.leftmostExpression(expression.expression);
2039
2322
  case "BinaryExpression":
2040
- return this.leftmostExpression(expression.left, stopAtCall);
2323
+ return this.leftmostExpression(expression.left);
2041
2324
  case "ConditionalExpression":
2042
- return this.leftmostExpression(expression.condition, stopAtCall);
2325
+ return this.leftmostExpression(expression.condition);
2043
2326
  case "TaggedTemplateExpression":
2044
- return this.leftmostExpression(expression.tag, stopAtCall);
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 escaped: string = text
2229
- .replace(/\\/g, "\\\\")
2230
- .replace(/\n/g, "\\n")
2231
- .replace(/\r/g, "\\r")
2232
- .replace(/\t/g, "\\t");
2233
- return singleQuote === true
2234
- ? `'${escaped.replace(/'/g, "\\'")}'`
2235
- : `"${escaped.replace(/"/g, '\\"')}"`;
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",