@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/lib/TsPrinter.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getSyntheticLeadingComments, getSyntheticTrailingComments } from './comments.mjs';
2
- import { join, hardline, printDocToString, line, softline, group, concat, indent, ifBreak } from './internal/doc.mjs';
2
+ import { join, hardline, printDocToString, line, softline, group, concat, indent, ifBreak, raw } from './internal/doc.mjs';
3
3
  import { SyntaxKind } from './syntax/SyntaxKind.mjs';
4
4
  import { NodeFlags } from './syntax/NodeFlags.mjs';
5
5
 
@@ -73,7 +73,11 @@ class TsPrinter {
73
73
  return group(concat([
74
74
  open,
75
75
  indent(concat([ln, join(concat([",", line]), items)])),
76
- opts.trailingComma ? ifBreak(",") : "",
76
+ opts.trailingComma === "always"
77
+ ? ","
78
+ : opts.trailingComma === "onBreak"
79
+ ? ifBreak(",")
80
+ : "",
77
81
  ln,
78
82
  close,
79
83
  ]), opts.forceBreak === true);
@@ -129,28 +133,75 @@ class TsPrinter {
129
133
  // type-argument / type-parameter lists disallow a trailing comma (TS1009)
130
134
  return args && args.length
131
135
  ? this.delim("<", args.map((a) => this.emit(a)), ">", {
132
- trailingComma: false,
136
+ trailingComma: "never",
133
137
  })
134
138
  : "";
135
139
  }
136
140
  /**
137
- * Whether a broken parameter list / binding pattern may append a synthetic
138
- * trailing comma after its last element.
141
+ * Trailing-comma policy for a parameter list or binding pattern.
139
142
  *
140
- * A trailing comma after a rest element (`...rest`) is a syntax error (TS1013
141
- * / V8 `SyntaxError`), and one after a trailing elision (`OmittedExpression`)
142
- * is not cosmetic: `[a, ,]` parses to one more hole than `[a, ]`, so the flat
143
- * and broken layouts of the same node would disagree. Call arguments and
144
- * array / object literals are unaffected a trailing comma after a spread is
145
- * legal there.
143
+ * A comma the printer adds only because a group broke must never change
144
+ * whether the text parses, nor what it parses to. After a rest element
145
+ * (`...rest`) it changes the first: a trailing comma there is a syntax error
146
+ * (TS1013 / V8 `SyntaxError`). After a trailing elision it changes the
147
+ * second: `[a, ,]` has one more hole than `[a, ]`, so the flat and broken
148
+ * layouts of the same node would disagree. A binding pattern is the one place
149
+ * where dropping that hole is lossless, since a trailing hole binds nothing;
150
+ * {@link literalTrailingComma} materializes it instead, because in an array
151
+ * literal the hole is a value.
146
152
  */
147
153
  listTrailingComma(nodes) {
148
154
  const last = nodes[nodes.length - 1];
149
155
  if (last === undefined)
150
- return true;
156
+ return "onBreak";
151
157
  if (last.kind === "OmittedExpression")
152
- return false;
153
- return !("dotDotDotToken" in last && last.dotDotDotToken !== undefined);
158
+ return "never";
159
+ return "dotDotDotToken" in last && last.dotDotDotToken !== undefined
160
+ ? "never"
161
+ : "onBreak";
162
+ }
163
+ /**
164
+ * Trailing-comma policy for a call or `new` argument list.
165
+ *
166
+ * A trailing `OmittedExpression` prints as nothing, so the list already ends
167
+ * in the separator comma of its last real argument: `f(a, )`, which is what
168
+ * the legacy printer emits too and parses as one argument. Adding the break
169
+ * comma on top produces `f(a, ,)`, which is a syntax error. A trailing spread
170
+ * is unaffected — a comma after it is legal in an argument list.
171
+ */
172
+ argsTrailingComma(args) {
173
+ const last = args[args.length - 1];
174
+ return last !== undefined && last.kind === "OmittedExpression"
175
+ ? "never"
176
+ : "onBreak";
177
+ }
178
+ /**
179
+ * Trailing-comma policy for an array or object literal.
180
+ *
181
+ * Two positions make the comma load-bearing rather than cosmetic.
182
+ *
183
+ * A trailing elision is a **value**: the comma is the token that materializes
184
+ * the hole, so `["a", ]` has one element and `["a", ,]` has two. The legacy
185
+ * printer emits it in every layout, so this printer emits it in every layout
186
+ * too; leaving it to the break would make the same node mean different things
187
+ * at different widths.
188
+ *
189
+ * A destructuring **assignment target** is the same node kind as an rvalue
190
+ * literal, but ECMAScript forbids a comma after its `AssignmentRestElement` /
191
+ * `AssignmentRestProperty`: `[a, ...rest,] = source` is a syntax error, while
192
+ * the identical rvalue `[a, ...rest,]` is legal. Only the target position
193
+ * suppresses it, so the rvalue twin keeps its break comma.
194
+ */
195
+ literalTrailingComma(elements, assignmentTarget) {
196
+ const last = elements[elements.length - 1];
197
+ if (last === undefined)
198
+ return "onBreak";
199
+ if (last.kind === "OmittedExpression")
200
+ return "always";
201
+ return assignmentTarget &&
202
+ (last.kind === "SpreadElement" || last.kind === "SpreadAssignment")
203
+ ? "never"
204
+ : "onBreak";
154
205
  }
155
206
  params(params) {
156
207
  return this.delim("(", params.map((p) => this.emit(p)), ")", {
@@ -159,7 +210,7 @@ class TsPrinter {
159
210
  }
160
211
  args(args) {
161
212
  return this.delim("(", args.map((a) => this.expressionForDisallowedComma(a)), ")", {
162
- trailingComma: true,
213
+ trailingComma: this.argsTrailingComma(args),
163
214
  });
164
215
  }
165
216
  modifiers(mods, decoratorsOnNewLine) {
@@ -183,14 +234,56 @@ class TsPrinter {
183
234
  ])
184
235
  : "";
185
236
  }
237
+ /**
238
+ * Lay out a JSX element's or fragment's children.
239
+ *
240
+ * A line break between JSX children is not cosmetic. JSX deletes a
241
+ * whitespace-only text child that contains a newline and trims
242
+ * whitespace-carrying-a-newline off both edges of every other text child, so
243
+ * a break introduced only because the group did not fit changes what the
244
+ * component renders: `<div>Hello there, {name}!</div>` becomes `Hello
245
+ * there,NAME!`, and the separator in `<div>{a} {b}</div>` disappears
246
+ * outright.
247
+ *
248
+ * Children are therefore laid out across lines only when the break survives
249
+ * that transformation unchanged: every text child must carry non-whitespace
250
+ * content, must not begin or end with whitespace, and must not sit next to
251
+ * another text child, since inserting a newline between two of them would
252
+ * merge into one text with a space in the middle. Otherwise the children are
253
+ * emitted verbatim on one line, whatever `printWidth` says — width may choose
254
+ * a layout, never a meaning.
255
+ */
256
+ jsxChildren(open, children, close) {
257
+ if (!this.jsxChildrenMayBreak(children))
258
+ return concat([open, concat(children.map((c) => this.emit(c))), close]);
259
+ return group(concat([
260
+ open,
261
+ indent(concat(children.map((c) => concat([softline, this.emit(c)])))),
262
+ softline,
263
+ close,
264
+ ]));
265
+ }
266
+ jsxChildrenMayBreak(children) {
267
+ return children.every((child, index) => child.kind !== "JsxText" ||
268
+ (isBreakSafeJsxText(child.text) &&
269
+ children[index + 1]?.kind !== "JsxText"));
270
+ }
186
271
  optType(type) {
187
272
  return type ? concat([": ", this.emit(type)]) : "";
188
273
  }
189
274
  optBody(body) {
190
275
  return body ? concat([" ", this.emit(body)]) : ";";
191
276
  }
192
- emit(node) {
193
- const body = this.emitNode(node);
277
+ /**
278
+ * @param assignmentTarget Whether `node` occupies destructuring
279
+ * assignment-target position, where an array or object literal is a pattern
280
+ * rather than a value. The flag is set by the assignment and `for…in` /
281
+ * `for…of` cases, forwarded by every node that is transparent to it (a
282
+ * spread, a property's initializer, a parenthesis, an `=` default), and
283
+ * dropped by every other node.
284
+ */
285
+ emit(node, assignmentTarget = false) {
286
+ const body = this.emitNode(node, assignmentTarget);
194
287
  const leading = getSyntheticLeadingComments(node);
195
288
  const trailing = getSyntheticTrailingComments(node);
196
289
  if ((leading === undefined || leading.length === 0) &&
@@ -234,7 +327,7 @@ class TsPrinter {
234
327
  "*/",
235
328
  ]);
236
329
  }
237
- emitNode(node) {
330
+ emitNode(node, assignmentTarget) {
238
331
  switch (node.kind) {
239
332
  /* names & tokens */
240
333
  case "Identifier":
@@ -246,7 +339,7 @@ class TsPrinter {
246
339
  case "Token":
247
340
  return node.token;
248
341
  case "Decorator":
249
- return concat(["@", this.leftSideExpression(node.expression)]);
342
+ return concat(["@", this.leftSideExpression(node.expression, false)]);
250
343
  /* literals */
251
344
  case "StringLiteral":
252
345
  return escapeString(node.text, node.singleQuote);
@@ -256,18 +349,21 @@ class TsPrinter {
256
349
  return node.text;
257
350
  /* expressions */
258
351
  case "ArrayLiteralExpression":
259
- return this.delim("[", node.elements.map((e) => this.expressionForDisallowedComma(e)), "]", { trailingComma: true, forceBreak: node.multiLine === true });
352
+ return this.delim("[", node.elements.map((e) => this.expressionForDisallowedComma(e, assignmentTarget)), "]", {
353
+ trailingComma: this.literalTrailingComma(node.elements, assignmentTarget),
354
+ forceBreak: node.multiLine === true,
355
+ });
260
356
  case "ObjectLiteralExpression":
261
- return this.delim("{", node.properties.map((p) => this.emit(p)), "}", {
357
+ return this.delim("{", node.properties.map((p) => this.emit(p, assignmentTarget)), "}", {
262
358
  space: true,
263
- trailingComma: true,
359
+ trailingComma: this.literalTrailingComma(node.properties, assignmentTarget),
264
360
  forceBreak: node.multiLine === true,
265
361
  });
266
362
  case "PropertyAssignment":
267
363
  return concat([
268
364
  this.emit(node.name),
269
365
  ": ",
270
- this.expressionForDisallowedComma(node.initializer),
366
+ this.expressionForDisallowedComma(node.initializer, assignmentTarget),
271
367
  ]);
272
368
  case "ShorthandPropertyAssignment":
273
369
  return concat([
@@ -282,24 +378,24 @@ class TsPrinter {
282
378
  case "SpreadAssignment":
283
379
  return concat([
284
380
  "...",
285
- this.expressionForDisallowedComma(node.expression),
381
+ this.expressionForDisallowedComma(node.expression, assignmentTarget),
286
382
  ]);
287
383
  case "PropertyAccessExpression":
288
384
  return concat([
289
- this.leftSideExpression(node.expression),
385
+ this.leftSideExpression(node.expression, false),
290
386
  ".",
291
387
  this.emit(node.name),
292
388
  ]);
293
389
  case "ElementAccessExpression":
294
390
  return concat([
295
- this.leftSideExpression(node.expression),
391
+ this.leftSideExpression(node.expression, false),
296
392
  "[",
297
393
  this.expressionForDisallowedComma(node.argumentExpression),
298
394
  "]",
299
395
  ]);
300
396
  case "CallExpression":
301
397
  return concat([
302
- this.leftSideExpression(node.expression),
398
+ this.leftSideExpression(node.expression, false),
303
399
  this.typeArguments(node.typeArguments),
304
400
  this.args(node.arguments),
305
401
  ]);
@@ -311,11 +407,19 @@ class TsPrinter {
311
407
  this.args(node.arguments ?? []),
312
408
  ]);
313
409
  case "ParenthesizedExpression":
314
- return concat(["(", this.emit(node.expression), ")"]);
410
+ return concat(["(", this.emit(node.expression, assignmentTarget), ")"]);
315
411
  case "BinaryExpression":
412
+ // the left side of `=` is a destructuring assignment target, both for a
413
+ // top-level assignment and for a `[a = init]` default inside one
316
414
  return group(concat([
317
- this.binaryOperand(node.operator, node.left, true),
318
- " ",
415
+ this.binaryOperand(node.operator, node.left, true, undefined, node.operator === SyntaxKind.EqualsToken),
416
+ // Every operator but the comma is written with a space on each
417
+ // side. The comma is punctuation that attaches to what precedes it:
418
+ // `CommaListExpression` joins with ", ", the legacy printer and the
419
+ // repository's pinned Prettier both emit `a, b`, and this factory's
420
+ // own JSDoc for `createComma` shows `(a, b)`. Only the printer
421
+ // disagreed, with `a , b`.
422
+ node.operator === SyntaxKind.CommaToken ? "" : " ",
319
423
  node.operator,
320
424
  indent(concat([
321
425
  line,
@@ -375,11 +479,11 @@ class TsPrinter {
375
479
  this.emit(node.type),
376
480
  ]);
377
481
  case "NonNullExpression":
378
- return concat([this.leftSideExpression(node.expression), "!"]);
482
+ return concat([this.leftSideExpression(node.expression, false), "!"]);
379
483
  case "SpreadElement":
380
484
  return concat([
381
485
  "...",
382
- this.expressionForDisallowedComma(node.expression),
486
+ this.expressionForDisallowedComma(node.expression, assignmentTarget),
383
487
  ]);
384
488
  case "AwaitExpression":
385
489
  return concat(["await ", this.prefixUnaryOperand(node.expression)]);
@@ -412,7 +516,7 @@ class TsPrinter {
412
516
  ]);
413
517
  case "TupleTypeNode":
414
518
  return this.delim("[", node.elements.map((e) => this.emit(e)), "]", {
415
- trailingComma: true,
519
+ trailingComma: "onBreak",
416
520
  });
417
521
  case "ParenthesizedTypeNode":
418
522
  return concat(["(", this.emit(node.type), ")"]);
@@ -432,8 +536,11 @@ class TsPrinter {
432
536
  case "TypeQueryNode":
433
537
  return concat(["typeof ", this.emit(node.exprName)]);
434
538
  case "ExpressionWithTypeArguments":
539
+ // heritage clauses take a LeftHandSideExpression: `class A extends
540
+ // (X || Y) {}` does not parse without the parentheses, and a bare comma
541
+ // sequence silently becomes two base classes
435
542
  return concat([
436
- this.emit(node.expression),
543
+ this.leftSideExpression(node.expression, false),
437
544
  this.typeArguments(node.typeArguments),
438
545
  ]);
439
546
  case "PropertySignature":
@@ -684,10 +791,16 @@ class TsPrinter {
684
791
  named.push(this.emit(node.name));
685
792
  if (node.namedBindings)
686
793
  named.push(this.emit(node.namedBindings));
687
- return concat([node.isTypeOnly ? "type " : "", join(", ", named)]);
794
+ // The phase modifier is the keyword itself, so it prints as written —
795
+ // `type` and `defer` both reach here, where a boolean could only ever
796
+ // have produced the first.
797
+ return concat([
798
+ node.phaseModifier ? `${node.phaseModifier} ` : "",
799
+ join(", ", named),
800
+ ]);
688
801
  }
689
802
  case "NamedImports":
690
- return this.delim("{", node.elements.map((e) => this.emit(e)), "}", { space: true, trailingComma: true });
803
+ return this.delim("{", node.elements.map((e) => this.emit(e)), "}", { space: true, trailingComma: "onBreak" });
691
804
  case "ImportSpecifier":
692
805
  return concat([
693
806
  node.isTypeOnly ? "type " : "",
@@ -710,7 +823,7 @@ class TsPrinter {
710
823
  ";",
711
824
  ]);
712
825
  case "NamedExports":
713
- return this.delim("{", node.elements.map((e) => this.emit(e)), "}", { space: true, trailingComma: true });
826
+ return this.delim("{", node.elements.map((e) => this.emit(e)), "}", { space: true, trailingComma: "onBreak" });
714
827
  case "ExportSpecifier":
715
828
  return concat([
716
829
  node.isTypeOnly ? "type " : "",
@@ -747,7 +860,7 @@ class TsPrinter {
747
860
  case "ForInStatement":
748
861
  return concat([
749
862
  "for (",
750
- this.emit(node.initializer),
863
+ this.emit(node.initializer, true),
751
864
  " in ",
752
865
  this.emit(node.expression),
753
866
  ") ",
@@ -758,7 +871,7 @@ class TsPrinter {
758
871
  "for ",
759
872
  node.awaitModifier ? "await " : "",
760
873
  "(",
761
- this.emit(node.initializer),
874
+ this.emit(node.initializer, true),
762
875
  " of ",
763
876
  this.emit(node.expression),
764
877
  ") ",
@@ -863,7 +976,17 @@ class TsPrinter {
863
976
  case "ModuleDeclaration":
864
977
  return concat([
865
978
  this.modifiers(node.modifiers, true),
866
- node.name.kind === "StringLiteral" ? "module " : "namespace ",
979
+ // A string-literal name is always `module "…"`; the flag says nothing
980
+ // there. For an identifier the flag is what chooses, which is the
981
+ // upstream rule and the one `createModuleDeclaration` documents:
982
+ // `namespace A` with `NodeFlags.Namespace`, `module A` without it.
983
+ // The printer used to read the name kind alone, so an identifier
984
+ // always printed `namespace` and the flag it published was inert.
985
+ node.name.kind === "StringLiteral"
986
+ ? "module "
987
+ : node.flags === NodeFlags.Namespace
988
+ ? "namespace "
989
+ : "module ",
867
990
  this.emit(node.name),
868
991
  node.body ? concat([" ", this.emit(node.body)]) : ";",
869
992
  ]);
@@ -970,6 +1093,19 @@ class TsPrinter {
970
1093
  node.isTypeOf ? "typeof " : "",
971
1094
  "import(",
972
1095
  this.emit(node.argument),
1096
+ // An import type spells its attributes as a second call argument —
1097
+ // `import("m", { with: { … } }).T` — not as the trailing `with { … }`
1098
+ // an import declaration uses, so the elements are wrapped here rather
1099
+ // than emitted through the attributes node's own form.
1100
+ node.attributes && node.attributes.elements.length > 0
1101
+ ? concat([
1102
+ ", { ",
1103
+ node.attributes.token,
1104
+ ": { ",
1105
+ join(", ", node.attributes.elements.map((e) => this.emit(e))),
1106
+ " } }",
1107
+ ])
1108
+ : "",
973
1109
  ")",
974
1110
  node.qualifier ? concat([".", this.emit(node.qualifier)]) : "",
975
1111
  this.typeArguments(node.typeArguments),
@@ -1009,7 +1145,7 @@ class TsPrinter {
1009
1145
  ]);
1010
1146
  case "TaggedTemplateExpression":
1011
1147
  return concat([
1012
- this.leftSideExpression(node.tag),
1148
+ this.leftSideExpression(node.tag, false),
1013
1149
  this.typeArguments(node.typeArguments),
1014
1150
  this.emit(node.template),
1015
1151
  ]);
@@ -1077,13 +1213,13 @@ class TsPrinter {
1077
1213
  ]);
1078
1214
  case "PropertyAccessChain":
1079
1215
  return concat([
1080
- this.leftSideExpression(node.expression),
1216
+ this.leftSideExpression(node.expression, true),
1081
1217
  node.questionDotToken ? "?." : ".",
1082
1218
  this.emit(node.name),
1083
1219
  ]);
1084
1220
  case "ElementAccessChain":
1085
1221
  return concat([
1086
- this.leftSideExpression(node.expression),
1222
+ this.leftSideExpression(node.expression, true),
1087
1223
  node.questionDotToken ? "?." : "",
1088
1224
  "[",
1089
1225
  this.expressionForDisallowedComma(node.argumentExpression),
@@ -1091,21 +1227,16 @@ class TsPrinter {
1091
1227
  ]);
1092
1228
  case "CallChain":
1093
1229
  return concat([
1094
- this.leftSideExpression(node.expression),
1230
+ this.leftSideExpression(node.expression, true),
1095
1231
  node.questionDotToken ? "?." : "",
1096
1232
  this.typeArguments(node.typeArguments),
1097
1233
  this.args(node.arguments),
1098
1234
  ]);
1099
1235
  case "NonNullChain":
1100
- return concat([this.leftSideExpression(node.expression), "!"]);
1236
+ return concat([this.leftSideExpression(node.expression, true), "!"]);
1101
1237
  /* jsx */
1102
1238
  case "JsxElement":
1103
- return group(concat([
1104
- this.emit(node.openingElement),
1105
- indent(concat(node.children.map((c) => concat([softline, this.emit(c)])))),
1106
- softline,
1107
- this.emit(node.closingElement),
1108
- ]));
1239
+ return this.jsxChildren(this.emit(node.openingElement), node.children, this.emit(node.closingElement));
1109
1240
  case "JsxSelfClosingElement":
1110
1241
  return concat([
1111
1242
  "<",
@@ -1125,18 +1256,15 @@ class TsPrinter {
1125
1256
  case "JsxClosingElement":
1126
1257
  return concat(["</", this.emit(node.tagName), ">"]);
1127
1258
  case "JsxFragment":
1128
- return group(concat([
1129
- this.emit(node.openingFragment),
1130
- indent(concat(node.children.map((c) => concat([softline, this.emit(c)])))),
1131
- softline,
1132
- this.emit(node.closingFragment),
1133
- ]));
1259
+ return this.jsxChildren(this.emit(node.openingFragment), node.children, this.emit(node.closingFragment));
1134
1260
  case "JsxOpeningFragment":
1135
1261
  return "<>";
1136
1262
  case "JsxClosingFragment":
1137
1263
  return "</>";
1138
1264
  case "JsxText":
1139
- return node.text;
1265
+ // the one node emitted as unquoted source text: its trailing spaces are
1266
+ // rendered content, so they must survive the layout engine's line trim
1267
+ return raw(node.text);
1140
1268
  case "JsxAttribute":
1141
1269
  return node.initializer === undefined
1142
1270
  ? this.emit(node.name)
@@ -1353,6 +1481,10 @@ class TsPrinter {
1353
1481
  ? concat([this.emit(node.importClause), " from "])
1354
1482
  : "",
1355
1483
  this.emit(node.moduleSpecifier),
1484
+ // `@import { a } from "m" with { type: "json" }` — the same trailing
1485
+ // form an import declaration uses, which is why the attributes node
1486
+ // emits itself here rather than being unwrapped.
1487
+ node.attributes ? concat([" ", this.emit(node.attributes)]) : "",
1356
1488
  this.jsDocComment(node.comment),
1357
1489
  ]);
1358
1490
  case "JSDocTemplateTag":
@@ -1384,19 +1516,67 @@ class TsPrinter {
1384
1516
  * re-associate — matching the legacy printer's parenthesizer rules.
1385
1517
  */
1386
1518
  parenthesizedExpression(expression) {
1387
- return expression.kind === "ParenthesizedExpression"
1519
+ return this.skipPartiallyEmittedExpressions(expression).kind ===
1520
+ "ParenthesizedExpression"
1388
1521
  ? this.emit(expression)
1389
1522
  : concat(["(", this.emit(expression), ")"]);
1390
1523
  }
1391
- expressionForDisallowedComma(expression) {
1524
+ /**
1525
+ * The partial-emission wrapper carries transform provenance but emits no
1526
+ * syntax of its own, so every grammar predicate must inspect its inner node.
1527
+ */
1528
+ skipPartiallyEmittedExpressions(expression) {
1529
+ while (expression.kind === "PartiallyEmittedExpression")
1530
+ expression = expression.expression;
1531
+ return expression;
1532
+ }
1533
+ expressionForDisallowedComma(expression, assignmentTarget = false) {
1392
1534
  return this.expressionPrecedence(expression) > ExpressionPrecedence.Comma
1393
- ? this.emit(expression)
1535
+ ? this.emit(expression, assignmentTarget)
1394
1536
  : this.parenthesizedExpression(expression);
1395
1537
  }
1396
- leftSideExpression(expression) {
1397
- return this.isLeftHandSideExpression(expression)
1398
- ? this.emit(expression)
1399
- : this.parenthesizedExpression(expression);
1538
+ /**
1539
+ * Emit an operand the grammar requires to be a `LeftHandSideExpression`,
1540
+ * mirroring the legacy parenthesizer's
1541
+ * `parenthesizeLeftSideOfAccess(expression, optionalChain)`.
1542
+ *
1543
+ * `optionalChain` is the **consuming** node's own chain-ness, not the
1544
+ * operand's. An optional chain may be emitted bare only when the node
1545
+ * consuming it continues the same chain: `a?.b?.()` is one chain, while
1546
+ * `(a?.b)()` is a plain call on the chain's value. Emitting the second as
1547
+ * `a?.b()` re-parses as the first, which stops throwing on a nullish head,
1548
+ * and in `new`, tagged-template and decorator position it does not compile at
1549
+ * all.
1550
+ */
1551
+ leftSideExpression(expression, optionalChain) {
1552
+ return this.leftSideNeedsParentheses(expression, optionalChain)
1553
+ ? this.parenthesizedExpression(expression)
1554
+ : this.emit(expression);
1555
+ }
1556
+ /**
1557
+ * Whether {@link leftSideExpression} wraps this operand.
1558
+ *
1559
+ * The legacy rule also parenthesizes an argument-less `new` here, because it
1560
+ * prints `new X` bare and `new X.y` would re-parse with `y` on the target.
1561
+ * This printer always emits the argument list, so `new X().y` already says
1562
+ * what the tree says and needs no wrapper.
1563
+ */
1564
+ leftSideNeedsParentheses(expression, optionalChain) {
1565
+ if (!this.isLeftHandSideExpression(expression))
1566
+ return true;
1567
+ return !optionalChain && this.isOptionalChain(expression);
1568
+ }
1569
+ isOptionalChain(expression) {
1570
+ expression = this.skipPartiallyEmittedExpressions(expression);
1571
+ switch (expression.kind) {
1572
+ case "CallChain":
1573
+ case "ElementAccessChain":
1574
+ case "NonNullChain":
1575
+ case "PropertyAccessChain":
1576
+ return true;
1577
+ default:
1578
+ return false;
1579
+ }
1400
1580
  }
1401
1581
  newExpressionTarget(expression) {
1402
1582
  return this.newExpressionTargetNeedsParentheses(expression)
@@ -1407,20 +1587,69 @@ class TsPrinter {
1407
1587
  * Whether a `new` target must be parenthesized to keep its call arguments
1408
1588
  * from re-binding to the `new` — mirroring the legacy printer's
1409
1589
  * `parenthesizeExpressionOfNew`. A `new` target is grammatically a
1410
- * `MemberExpression`, so a call anywhere on the target's left spine (not just
1411
- * a direct one: `new (f().bar)()`, `new (a.b().c)()`) would otherwise
1412
- * re-parse with the call's arguments consumed by the `new` — a different
1413
- * program. Argument-less `new` on the spine is kept parenthesized for
1414
- * continuity with the direct case, though this printer always prints an
1415
- * argument list, which already disambiguates it.
1590
+ * `MemberExpression`, so a call anywhere on the target's printed left spine
1591
+ * (not just a direct one: `new (f().bar)()`, `new (a.b().c)()`) would
1592
+ * otherwise re-parse with the call's arguments consumed by the `new` — a
1593
+ * different program. Argument-less `new` on the spine is kept parenthesized
1594
+ * for continuity with the direct case, though this printer always prints an
1595
+ * argument list, which already disambiguates it. Anything else falls back to
1596
+ * the shared left-side rule, which is what parenthesizes an optional-chain
1597
+ * target (`new (a?.b)()`, TS1209 without it).
1416
1598
  */
1417
1599
  newExpressionTargetNeedsParentheses(expression) {
1418
- if (!this.isLeftHandSideExpression(expression))
1419
- return true;
1420
- const leftmost = this.leftmostExpression(expression, true);
1421
- return (leftmost.kind === "CallExpression" ||
1422
- leftmost.kind === "CallChain" ||
1423
- (leftmost.kind === "NewExpression" && leftmost.arguments === undefined));
1600
+ const leftmost = this.leftmostPrintedExpression(expression);
1601
+ if (leftmost !== undefined) {
1602
+ if (leftmost.kind === "CallExpression" || leftmost.kind === "CallChain")
1603
+ return true;
1604
+ if (leftmost.kind === "NewExpression")
1605
+ return leftmost.arguments === undefined;
1606
+ }
1607
+ return this.leftSideNeedsParentheses(expression, false);
1608
+ }
1609
+ /**
1610
+ * The node whose own text opens `expression`'s printed form, or `undefined`
1611
+ * when that text opens with a printer-inserted `(`.
1612
+ *
1613
+ * The legacy factory parenthesizes each operand as it builds the node, so its
1614
+ * `getLeftmostExpression` walk halts on the resulting
1615
+ * `ParenthesizedExpression`. This printer decides the same parentheses at
1616
+ * emit time instead, so the walk has to ask {@link leftSideNeedsParentheses}
1617
+ * the same question directly; otherwise `new` re-wraps a target whose call is
1618
+ * already behind parentheses, and `new (f?.()).bar()` comes out as `new
1619
+ * ((f?.()).bar)()`. Calls halt the walk, matching the legacy
1620
+ * `stopAtCallExpressions` mode this predicate is the only user of.
1621
+ */
1622
+ leftmostPrintedExpression(expression) {
1623
+ expression = this.skipPartiallyEmittedExpressions(expression);
1624
+ switch (expression.kind) {
1625
+ case "CallExpression":
1626
+ case "CallChain":
1627
+ return expression;
1628
+ case "ElementAccessExpression":
1629
+ case "NonNullExpression":
1630
+ case "PropertyAccessExpression":
1631
+ return this.leftmostPrintedLeftSide(expression.expression, false);
1632
+ case "ElementAccessChain":
1633
+ case "NonNullChain":
1634
+ case "PropertyAccessChain":
1635
+ return this.leftmostPrintedLeftSide(expression.expression, true);
1636
+ case "TaggedTemplateExpression":
1637
+ return this.leftmostPrintedLeftSide(expression.tag, false);
1638
+ case "AsExpression":
1639
+ case "SatisfiesExpression":
1640
+ return this.leftmostPrintedExpression(expression.expression);
1641
+ case "BinaryExpression":
1642
+ return this.leftmostPrintedExpression(expression.left);
1643
+ case "ConditionalExpression":
1644
+ return this.leftmostPrintedExpression(expression.condition);
1645
+ default:
1646
+ return expression;
1647
+ }
1648
+ }
1649
+ leftmostPrintedLeftSide(operand, optionalChain) {
1650
+ return this.leftSideNeedsParentheses(operand, optionalChain)
1651
+ ? undefined
1652
+ : this.leftmostPrintedExpression(operand);
1424
1653
  }
1425
1654
  prefixUnaryOperand(operand, operator) {
1426
1655
  const body = this.isUnaryExpression(operand)
@@ -1469,42 +1698,45 @@ class TsPrinter {
1469
1698
  ? this.expressionForDisallowedComma(expression)
1470
1699
  : this.parenthesizedExpression(expression);
1471
1700
  }
1472
- binaryOperand(operator, operand, isLeftSide, leftOperand) {
1701
+ binaryOperand(operator, operand, isLeftSide, leftOperand, assignmentTarget = false) {
1473
1702
  return this.binaryOperandNeedsParentheses(operator, operand, isLeftSide, leftOperand)
1474
1703
  ? this.parenthesizedExpression(operand)
1475
- : this.emit(operand);
1704
+ : this.emit(operand, assignmentTarget);
1476
1705
  }
1477
1706
  binaryOperandNeedsParentheses(operator, operand, isLeftSide, leftOperand) {
1478
- if (operand.kind === "ParenthesizedExpression")
1707
+ const emittedOperand = this.skipPartiallyEmittedExpressions(operand);
1708
+ if (emittedOperand.kind === "ParenthesizedExpression")
1479
1709
  return false;
1480
1710
  if (operator === SyntaxKind.AsteriskAsteriskToken &&
1481
1711
  isLeftSide &&
1482
- this.expressionPrecedence(operand) === ExpressionPrecedence.Unary)
1712
+ this.expressionPrecedence(emittedOperand) === ExpressionPrecedence.Unary)
1483
1713
  return true;
1484
- if (operand.kind === "BinaryExpression" &&
1485
- this.mixingBinaryOperatorsRequiresParentheses(operator, operand.operator))
1714
+ if (emittedOperand.kind === "BinaryExpression" &&
1715
+ this.mixingBinaryOperatorsRequiresParentheses(operator, emittedOperand.operator))
1486
1716
  return true;
1487
1717
  const operatorPrecedence = this.binaryOperatorPrecedence(operator);
1488
- const operandPrecedence = this.expressionPrecedence(operand);
1718
+ const operandPrecedence = this.expressionPrecedence(emittedOperand);
1489
1719
  if (operandPrecedence < operatorPrecedence)
1490
1720
  return true;
1491
1721
  if (operandPrecedence > operatorPrecedence)
1492
1722
  return false;
1493
1723
  if (isLeftSide)
1494
1724
  return this.binaryOperatorAssociativity(operator) === Associativity.Right;
1495
- if (operand.kind === "BinaryExpression" && operand.operator === operator) {
1725
+ if (emittedOperand.kind === "BinaryExpression" &&
1726
+ emittedOperand.operator === operator) {
1496
1727
  if (this.operatorHasAssociativeProperty(operator))
1497
1728
  return false;
1498
1729
  if (operator === SyntaxKind.PlusToken &&
1499
1730
  leftOperand !== undefined &&
1500
1731
  this.literalKindOfBinaryPlusOperand(leftOperand) !== undefined &&
1501
1732
  this.literalKindOfBinaryPlusOperand(leftOperand) ===
1502
- this.literalKindOfBinaryPlusOperand(operand))
1733
+ this.literalKindOfBinaryPlusOperand(emittedOperand))
1503
1734
  return false;
1504
1735
  }
1505
- return this.expressionAssociativity(operand) === Associativity.Left;
1736
+ return this.expressionAssociativity(emittedOperand) === Associativity.Left;
1506
1737
  }
1507
1738
  expressionPrecedence(expression) {
1739
+ expression = this.skipPartiallyEmittedExpressions(expression);
1508
1740
  switch (expression.kind) {
1509
1741
  case "CommaListExpression":
1510
1742
  return ExpressionPrecedence.Comma;
@@ -1546,6 +1778,7 @@ class TsPrinter {
1546
1778
  }
1547
1779
  }
1548
1780
  expressionAssociativity(expression) {
1781
+ expression = this.skipPartiallyEmittedExpressions(expression);
1549
1782
  switch (expression.kind) {
1550
1783
  case "NewExpression":
1551
1784
  return expression.arguments === undefined
@@ -1649,6 +1882,7 @@ class TsPrinter {
1649
1882
  operator === SyntaxKind.CommaToken);
1650
1883
  }
1651
1884
  literalKindOfBinaryPlusOperand(expression) {
1885
+ expression = this.skipPartiallyEmittedExpressions(expression);
1652
1886
  switch (expression.kind) {
1653
1887
  case "StringLiteral":
1654
1888
  case "NumericLiteral":
@@ -1671,6 +1905,7 @@ class TsPrinter {
1671
1905
  return this.expressionPrecedence(expression) >= ExpressionPrecedence.Unary;
1672
1906
  }
1673
1907
  isLeftHandSideExpression(expression) {
1908
+ expression = this.skipPartiallyEmittedExpressions(expression);
1674
1909
  switch (expression.kind) {
1675
1910
  case "ArrowFunction":
1676
1911
  case "ClassExpression":
@@ -1701,18 +1936,19 @@ class TsPrinter {
1701
1936
  }
1702
1937
  /**
1703
1938
  * Walk to the expression's leftmost node — the one that starts its printed
1704
- * text. With `stopAtCall`, calls terminate the walk instead of being walked
1705
- * through, matching the legacy `getLeftmostExpression`'s
1706
- * `stopAtCallExpressions` mode used by the `new`-target parenthesizer.
1939
+ * text matching the legacy `getLeftmostExpression`.
1940
+ *
1941
+ * Used by the statement, concise-body and export-default predicates, which
1942
+ * ask only whether the text opens with a `function`, `class` or `{` token.
1943
+ * The `new`-target predicate needs the printed left edge instead and uses
1944
+ * {@link leftmostPrintedExpression}.
1707
1945
  */
1708
- leftmostExpression(expression, stopAtCall = false) {
1946
+ leftmostExpression(expression) {
1947
+ expression = this.skipPartiallyEmittedExpressions(expression);
1709
1948
  switch (expression.kind) {
1949
+ case "AsExpression":
1710
1950
  case "CallExpression":
1711
1951
  case "CallChain":
1712
- if (stopAtCall)
1713
- return expression;
1714
- return this.leftmostExpression(expression.expression, stopAtCall);
1715
- case "AsExpression":
1716
1952
  case "ElementAccessExpression":
1717
1953
  case "ElementAccessChain":
1718
1954
  case "NonNullExpression":
@@ -1720,18 +1956,19 @@ class TsPrinter {
1720
1956
  case "PropertyAccessExpression":
1721
1957
  case "PropertyAccessChain":
1722
1958
  case "SatisfiesExpression":
1723
- return this.leftmostExpression(expression.expression, stopAtCall);
1959
+ return this.leftmostExpression(expression.expression);
1724
1960
  case "BinaryExpression":
1725
- return this.leftmostExpression(expression.left, stopAtCall);
1961
+ return this.leftmostExpression(expression.left);
1726
1962
  case "ConditionalExpression":
1727
- return this.leftmostExpression(expression.condition, stopAtCall);
1963
+ return this.leftmostExpression(expression.condition);
1728
1964
  case "TaggedTemplateExpression":
1729
- return this.leftmostExpression(expression.tag, stopAtCall);
1965
+ return this.leftmostExpression(expression.tag);
1730
1966
  default:
1731
1967
  return expression;
1732
1968
  }
1733
1969
  }
1734
1970
  needsPrefixUnaryGap(operator, operand) {
1971
+ operand = this.skipPartiallyEmittedExpressions(operand);
1735
1972
  if (operator === undefined || operand.kind !== "PrefixUnaryExpression")
1736
1973
  return false;
1737
1974
  return ((operator === SyntaxKind.PlusToken &&
@@ -1851,15 +2088,84 @@ const escapeTemplateText = (text) => text
1851
2088
  .replace(/\$\{/g, "\\${")
1852
2089
  .replace(/\r\n/g, "\\r\\n")
1853
2090
  .replace(/\r/g, "\\r");
2091
+ /**
2092
+ * Whether a JSX text child means the same thing with a line break and
2093
+ * indentation around it.
2094
+ *
2095
+ * JSX drops a whitespace-only child that contains a newline and trims an edge
2096
+ * whose whitespace contains one, so only a child with non-whitespace content
2097
+ * and no edge whitespace survives being moved onto its own line. Newlines
2098
+ * _inside_ the text are unaffected, because JSX collapses each interior line
2099
+ * break to a single space in either layout.
2100
+ */
2101
+ const isBreakSafeJsxText = (text) => text.length !== 0 && !/^\s/.test(text) && !/\s$/.test(text);
2102
+ /**
2103
+ * Escape a string literal's text so the printed program holds the value the AST
2104
+ * carries.
2105
+ *
2106
+ * The old set was the backslash, LF, CR, TAB and the active quote. Everything
2107
+ * else was emitted raw, which is three separate hazards rather than a cosmetic
2108
+ * gap: a C0 control or DEL lands in the generated file as itself; U+2028 and
2109
+ * U+2029 terminate a string literal in any JavaScript engine predating ES2019,
2110
+ * so the emitted program does not parse; and a lone surrogate becomes U+FFFD
2111
+ * the moment the text is written as UTF-8, so the generated program holds a
2112
+ * different string than the caller built.
2113
+ *
2114
+ * Iterated by code point rather than matched by a pattern. That is what makes
2115
+ * the surrogate case fall out instead of needing a rule: a well-formed pair
2116
+ * arrives as one two-unit string and passes through, and a lone surrogate
2117
+ * arrives as a single unit whose code point is in the surrogate range.
2118
+ */
1854
2119
  const escapeString = (text, singleQuote) => {
1855
- const escaped = text
1856
- .replace(/\\/g, "\\\\")
1857
- .replace(/\n/g, "\\n")
1858
- .replace(/\r/g, "\\r")
1859
- .replace(/\t/g, "\\t");
1860
- return singleQuote === true
1861
- ? `'${escaped.replace(/'/g, "\\'")}'`
1862
- : `"${escaped.replace(/"/g, '\\"')}"`;
2120
+ const quote = singleQuote === true ? "'" : '"';
2121
+ let escaped = "";
2122
+ for (const ch of text) {
2123
+ if (ch === "\\") {
2124
+ escaped += "\\\\";
2125
+ continue;
2126
+ }
2127
+ if (ch === quote) {
2128
+ escaped += "\\" + ch;
2129
+ continue;
2130
+ }
2131
+ // The inactive quote is ordinary text and stays as written.
2132
+ const code = ch.codePointAt(0) ?? 0;
2133
+ const lone = code >= 0xd800 && code <= 0xdfff;
2134
+ if (ch.length === 2 ||
2135
+ (code >= 0x20 &&
2136
+ code !== 0x7f &&
2137
+ code !== 0x2028 &&
2138
+ code !== 0x2029 &&
2139
+ !lone)) {
2140
+ escaped += ch;
2141
+ continue;
2142
+ }
2143
+ switch (code) {
2144
+ case 0x08:
2145
+ escaped += "\\b";
2146
+ continue;
2147
+ case 0x09:
2148
+ escaped += "\\t";
2149
+ continue;
2150
+ case 0x0a:
2151
+ escaped += "\\n";
2152
+ continue;
2153
+ case 0x0b:
2154
+ escaped += "\\v";
2155
+ continue;
2156
+ case 0x0c:
2157
+ escaped += "\\f";
2158
+ continue;
2159
+ case 0x0d:
2160
+ escaped += "\\r";
2161
+ continue;
2162
+ }
2163
+ escaped +=
2164
+ code > 0xff
2165
+ ? "\\u" + code.toString(16).padStart(4, "0")
2166
+ : "\\x" + code.toString(16).padStart(2, "0");
2167
+ }
2168
+ return `${quote}${escaped}${quote}`;
1863
2169
  };
1864
2170
  const ExpressionPrecedence = {
1865
2171
  Comma: 0,