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