ts-codemod-lib 3.4.1 → 3.4.3

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.
@@ -33,6 +33,7 @@ import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
33
33
  import {
34
34
  groupUnionIntersectionTypes,
35
35
  invalidDeepReadonlyTypeName,
36
+ isMergeableReadonlyTypeReferenceNode,
36
37
  nextReadonlyContext,
37
38
  type ReadonlyContext,
38
39
  } from './readonly-transformer-helpers/index.mjs';
@@ -655,32 +656,61 @@ const transformTypeReferenceNode = (
655
656
  );
656
657
 
657
658
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
658
- const T = removeParentheses(node.getTypeArguments()[0]!);
659
+ const typeArgument = node.getTypeArguments()[0]!;
660
+
661
+ const T = removeParentheses(typeArgument);
662
+
663
+ // The comments in front of the type argument and inside its parentheses,
664
+ // which `T.getFullText()` would leave behind with the parentheses.
665
+ const commentsInFrontOfT = commentsBetween(
666
+ node.getSourceFile(),
667
+ typeArgument.getPos(),
668
+ T.getStart(),
669
+ );
670
+
671
+ const TText = `${commentsInFrontOfT}${T.getText()}` as const;
672
+
673
+ // `T` with the parentheses that `Readonly<...>` made unnecessary and its
674
+ // position may need (`Readonly<A | B>[]` -> `(A | B)[]`).
675
+ const unwrappedText =
676
+ T.isKind(tsm.SyntaxKind.UnionType) ||
677
+ T.isKind(tsm.SyntaxKind.IntersectionType)
678
+ ? wrapWithParentheses(TText)
679
+ : TText;
659
680
 
660
681
  // DeepReadonly<Readonly<T>> -> DeepReadonly<T>
661
682
  if (readonlyContext.type === 'DeepReadonly') {
662
- options.replaceNode(node, T.getFullText());
683
+ options.replaceNode(node, unwrappedText);
663
684
 
664
685
  return;
665
686
  }
666
687
 
667
688
  // Readonly<{ ... }>[I] -> { ... }[I]
668
689
  if (readonlyContext.indexedAccessDepth > 0) {
669
- options.replaceNode(node, T.getFullText());
690
+ options.replaceNode(node, unwrappedText);
670
691
 
671
692
  return;
672
693
  }
673
694
 
674
695
  // Readonly<Readonly<T>> -> Readonly<T>
675
696
  if (isReadonlyTypeReferenceNode(T)) {
676
- options.replaceNode(node, T.getFullText());
697
+ options.replaceNode(node, TText);
698
+
699
+ return;
700
+ }
701
+
702
+ // Readonly<DeepReadonly<T>> -> DeepReadonly<T>
703
+ if (
704
+ getDeepReadonlyTypeArg(T, options.DeepReadonly.typeName) !== undefined
705
+ ) {
706
+ options.replaceNode(node, TText);
677
707
 
678
708
  return;
679
709
  }
680
710
 
681
711
  // Readonly<number> -> number
682
712
  if (isAtomicTypeNode(T)) {
683
- options.replaceNode(node, T.getFullText());
713
+ options.replaceNode(node, TText);
684
714
 
685
715
  return;
686
716
  }
@@ -696,11 +726,11 @@ const transformTypeReferenceNode = (
696
726
  if (Arr.isFixedLengthArray(2, recordTypeArguments)) {
697
727
  options.replaceNode(
698
728
  node,
699
- readonlyRecordText(
729
+ `${commentsInFrontOfT}${readonlyRecordText(
700
730
  recordTypeArguments[0].getFullText(),
701
731
  recordTypeArguments[1].getFullText(),
702
732
  options,
703
- ),
733
+ )}`,
704
734
  );
705
735
 
706
736
  return;
@@ -716,7 +746,7 @@ const transformTypeReferenceNode = (
716
746
  T.isKind(tsm.SyntaxKind.ArrayType) ||
717
747
  T.isKind(tsm.SyntaxKind.TupleType)
718
748
  ) {
719
- const readonlyText = `readonly ${T.getFullText()}` as const;
749
+ const readonlyText = `readonly ${TText}` as const;
720
750
 
721
751
  options.replaceNode(
722
752
  node,
@@ -732,18 +762,21 @@ const transformTypeReferenceNode = (
732
762
  // Readonly<readonly E[]> -> readonly E[]
733
763
  // Readonly<readonly [E1, E2, E3]> -> readonly [E1, E2, E3]
734
764
  if (isReadonlyTupleOrArrayTypeNode(T)) {
735
- options.replaceNode(node, T.getFullText());
765
+ options.replaceNode(node, TText);
736
766
 
737
767
  return;
738
768
  }
739
769
 
740
770
  // T = A | B | C
741
771
  // T = A & B & C
772
+ //
773
+ // The union or intersection has already put back under `Readonly` every
774
+ // member that still needs it, so this one is redundant.
742
775
  if (
743
776
  T.isKind(tsm.SyntaxKind.UnionType) ||
744
777
  T.isKind(tsm.SyntaxKind.IntersectionType)
745
778
  ) {
746
- options.replaceNode(node, T.getFullText());
779
+ options.replaceNode(node, unwrappedText);
747
780
 
748
781
  return;
749
782
  }
@@ -883,7 +916,7 @@ const transformArrayTypeNode = (
883
916
  readonlyContext.indexedAccessDepth === 0 &&
884
917
  !isConditionalTypeDistributionGuard(node)
885
918
  ) {
886
- const readonlyText = `readonly ${node.getFullText()}` as const;
919
+ const readonlyText = `readonly ${node.getText()}` as const;
887
920
 
888
921
  options.replaceNode(
889
922
  node,
@@ -935,7 +968,7 @@ const transformTupleTypeNode = (
935
968
  readonlyContext.indexedAccessDepth === 0 &&
936
969
  !isConditionalTypeDistributionGuard(node)
937
970
  ) {
938
- const readonlyText = `readonly ${node.getFullText()}` as const;
971
+ const readonlyText = `readonly ${node.getText()}` as const;
939
972
 
940
973
  options.replaceNode(
941
974
  node,
@@ -969,12 +1002,22 @@ const transformRestTypeNode = (
969
1002
 
970
1003
  // `tr("...readonly E[]") |-> ...tr(E)[]`
971
1004
  // `tr("...readonly [E1, E2]") |-> ...[tr(E1), tr(E2)]`
972
- if (isReadonlyArrayTypeNode(R) || isReadonlyTupleTypeNode(R)) {
973
- options.replaceNode(
974
- node,
975
- `...${R.getTypeNode().getFullText()}` /* = tr(E)[] or [tr(E1), tr(E2)] */,
976
- );
1005
+ if (!isReadonlyArrayTypeNode(R) && !isReadonlyTupleTypeNode(R)) {
1006
+ return;
977
1007
  }
1008
+
1009
+ // The comments in front of `R` and inside the parentheses around it, which
1010
+ // `R.getTypeNode().getFullText()` does not reach.
1011
+ const commentsInFrontOfR = commentsBetween(
1012
+ node.getSourceFile(),
1013
+ node.getTypeNode().getPos(),
1014
+ R.getStart(),
1015
+ );
1016
+
1017
+ options.replaceNode(
1018
+ node,
1019
+ `...${commentsInFrontOfR}${R.getTypeNode().getFullText()}` /* = tr(E)[] or [tr(E1), tr(E2)] */,
1020
+ );
978
1021
  };
979
1022
 
980
1023
  /**
@@ -1061,7 +1104,7 @@ const transformTypeLiteralNode = (
1061
1104
 
1062
1105
  if (wrappingApplies && !skipWrapping) {
1063
1106
  // `{ readonly x: X, readonly y: Y } |-> Readonly<{ x: X, y: Y }>`
1064
- options.replaceNode(node, `Readonly<${node.getFullText()}>`);
1107
+ options.replaceNode(node, `Readonly<${node.getText()}>`);
1065
1108
  }
1066
1109
  };
1067
1110
 
@@ -1286,7 +1329,8 @@ const transformConstructorParameter = (
1286
1329
 
1287
1330
  /**
1288
1331
  * - `tr(A & B) -> tr(A) & tr(B)`
1289
- * - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`
1332
+ * - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`, when `A` and `B`
1333
+ * are type literals (see `isMergeableReadonlyTypeReferenceNode`)
1290
1334
  */
1291
1335
  const transformIntersectionTypeNode = (
1292
1336
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1340,15 +1384,30 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1340
1384
 
1341
1385
  const newTypes = node.getTypeNodes();
1342
1386
 
1387
+ const withLeadingComments = (
1388
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1389
+ member: tsm.TypeNode,
1390
+ memberText: string,
1391
+ ): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
1392
+
1343
1393
  const { primitives, arraysAndTuples, typeLiterals, others } =
1344
- groupUnionIntersectionTypes(newTypes);
1394
+ groupUnionIntersectionTypes(newTypes, {
1395
+ operator,
1396
+ // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1397
+ // handler of `Readonly` then takes off as redundant.
1398
+ underReadonly:
1399
+ readonlyContext.type === 'Readonly' &&
1400
+ readonlyContext.indexedAccessDepth === 0 &&
1401
+ isTypeArgumentOfReadonly(node),
1402
+ deepReadonlyTypeName: options.DeepReadonly.typeName,
1403
+ });
1345
1404
 
1346
1405
  options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
1347
1406
 
1348
1407
  // Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
1349
1408
  const isEmptyTypeLiteral = (
1350
1409
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1351
- n: tsm.TypeLiteralNode | ReadonlyTypeReferenceNode,
1410
+ n: tsm.TypeNode,
1352
1411
  ): boolean =>
1353
1412
  n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
1354
1413
 
@@ -1370,10 +1429,17 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1370
1429
  : ([
1371
1430
  unionToString({
1372
1431
  types: nonEmptyTypeLiterals.map((n) =>
1373
- isReadonlyTypeReferenceNode(n)
1374
- ? // NOTE: Readonly<A & B> -> (A & B)
1375
- unwrapReadonlyTypeArgText(n)
1376
- : (n satisfies tsm.TypeLiteralNode).getFullText(),
1432
+ withLeadingComments(
1433
+ n,
1434
+ isMergeableReadonlyTypeReferenceNode(n, operator)
1435
+ ? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
1436
+ // NOTE: Readonly<A | B> -> A | B (spliced into `|`)
1437
+ readonlyTypeArgTextSplicedInto(n, operator)
1438
+ : n.isKind(tsm.SyntaxKind.TypeLiteral)
1439
+ ? n.getText()
1440
+ : // A member kept under `Readonly`; see `underReadonly`
1441
+ wrapWithParentheses(n.getText()),
1442
+ ),
1377
1443
  ),
1378
1444
  op: operator,
1379
1445
  wrapWithReadonly:
@@ -1386,11 +1452,19 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1386
1452
  const sorted: readonly string[] = [
1387
1453
  mapNullable(
1388
1454
  primitives,
1389
- (a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
1455
+ (a) =>
1456
+ [
1457
+ a.nodes.map((n) => withLeadingComments(n, n.getText())),
1458
+ a.firstPosition,
1459
+ ] as const,
1390
1460
  ),
1391
1461
  mapNullable(
1392
1462
  arraysAndTuples,
1393
- (a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
1463
+ (a) =>
1464
+ [
1465
+ a.nodes.map((n) => withLeadingComments(n, n.getText())),
1466
+ a.firstPosition,
1467
+ ] as const,
1394
1468
  ),
1395
1469
  nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
1396
1470
  ? mapNullable(
@@ -1403,7 +1477,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1403
1477
  typeLiterals,
1404
1478
  (a) =>
1405
1479
  [
1406
- emptyTypeLiterals.map((n) => n.getFullText()),
1480
+ emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
1407
1481
  // Use a position after non-empty type literals if they exist
1408
1482
  a.firstPosition +
1409
1483
  (nonEmptyTypeLiterals !== undefined &&
@@ -1423,8 +1497,11 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1423
1497
  ? mergeDeepReadonlyUnionMembers(
1424
1498
  a.nodes,
1425
1499
  options.DeepReadonly.typeName,
1500
+ withLeadingComments,
1426
1501
  )
1427
- : a.nodes.map((n) => wrapWithParentheses(n.getFullText())),
1502
+ : a.nodes.map((n) =>
1503
+ withLeadingComments(n, wrapWithParentheses(n.getText())),
1504
+ ),
1428
1505
  a.firstPosition,
1429
1506
  ] as const,
1430
1507
  ),
@@ -1468,8 +1545,9 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1468
1545
  *
1469
1546
  * - `Readonly` is a homomorphic mapped type, and over type literals
1470
1547
  * `Readonly<A & B>` and `Readonly<A> & Readonly<B>` have the same properties
1471
- * with the same modifiers. (That merge also takes `Readonly<T>` with any
1472
- * `T`, where this does not hold for an array `T`; it is not relied on here.)
1548
+ * with the same modifiers. That does not hold for an array `A`, so an
1549
+ * intersection merges only `Readonly<T>` whose `T` is known to be a type
1550
+ * literal; see `isMergeableReadonlyTypeReferenceNode`.
1473
1551
  * - `DeepReadonly` is a conditional type that picks a branch by what `T` is
1474
1552
  * (primitive, function, Map, Set, array, object). It distributes over a union
1475
1553
  * because each member is tested on its own, but an intersection is tested as
@@ -1488,28 +1566,52 @@ const mergeDeepReadonlyUnionMembers = (
1488
1566
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1489
1567
  nodes: readonly tsm.TypeNode[],
1490
1568
  deepReadonlyTypeName: string,
1569
+ withLeadingComments: (
1570
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1571
+ member: tsm.TypeNode,
1572
+ memberText: string,
1573
+ ) => string,
1491
1574
  ): readonly string[] => {
1492
1575
  const typeArgsOfDeepReadonly = nodes.map((n) =>
1493
1576
  getDeepReadonlyTypeArg(n, deepReadonlyTypeName),
1494
1577
  );
1495
1578
 
1496
- const typeArgs = typeArgsOfDeepReadonly.filter((t) => t !== undefined);
1579
+ const asIs = (
1580
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1581
+ n: tsm.TypeNode,
1582
+ ): string => withLeadingComments(n, wrapWithParentheses(n.getText()));
1497
1583
 
1498
- if (typeArgs.length < 2) {
1499
- return nodes.map((n) => wrapWithParentheses(n.getFullText()));
1584
+ if (typeArgsOfDeepReadonly.filter((t) => t !== undefined).length < 2) {
1585
+ return nodes.map(asIs);
1500
1586
  }
1501
1587
 
1502
1588
  const firstIndex = typeArgsOfDeepReadonly.findIndex((t) => t !== undefined);
1503
1589
 
1504
1590
  const merged = unionToString({
1505
- types: typeArgs.map((t_) => {
1591
+ // A member's comments move inside, onto its type argument:
1592
+ // `/** a */ DeepReadonly<A> | /** b */ DeepReadonly<B>`
1593
+ // -> `DeepReadonly</** a */ A | /** b */ B>`
1594
+ types: nodes.flatMap((n, i) => {
1595
+ const t_ = typeArgsOfDeepReadonly[i];
1596
+
1597
+ if (t_ === undefined) {
1598
+ return [];
1599
+ }
1600
+
1506
1601
  const t = removeParentheses(t_);
1507
1602
 
1508
1603
  // NOTE: DeepReadonly<A | B> | DeepReadonly<C> -> DeepReadonly<A | B | C>
1509
1604
  // NOTE: DeepReadonly<() => void> | DeepReadonly<B> -> DeepReadonly<(() => void) | B>
1510
- return t.isKind(tsm.SyntaxKind.UnionType)
1511
- ? t.getFullText()
1512
- : wrapWithParentheses(t.getFullText());
1605
+ const tText = textWithoutParentheses(t_, t);
1606
+
1607
+ return [
1608
+ withLeadingComments(
1609
+ n,
1610
+ t.isKind(tsm.SyntaxKind.UnionType)
1611
+ ? tText
1612
+ : wrapWithParentheses(tText),
1613
+ ),
1614
+ ];
1513
1615
  }),
1514
1616
  op: '|',
1515
1617
  wrapWithReadonly: deepReadonlyTypeName,
@@ -1517,13 +1619,146 @@ const mergeDeepReadonlyUnionMembers = (
1517
1619
 
1518
1620
  return nodes.flatMap((n, i) =>
1519
1621
  typeArgsOfDeepReadonly[i] === undefined
1520
- ? [wrapWithParentheses(n.getFullText())]
1622
+ ? [asIs(n)]
1521
1623
  : i === firstIndex
1522
1624
  ? [merged]
1523
1625
  : [],
1524
1626
  );
1525
1627
  };
1526
1628
 
1629
+ /** Whether `node` is the type argument of `Readonly<...>`, parentheses aside. */
1630
+ const isTypeArgumentOfReadonly = (
1631
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1632
+ node: tsm.TypeNode,
1633
+ ): boolean => {
1634
+ const parent = node.getParent();
1635
+
1636
+ return (
1637
+ parent !== undefined &&
1638
+ (parent.isKind(tsm.SyntaxKind.ParenthesizedType)
1639
+ ? isTypeArgumentOfReadonly(parent)
1640
+ : isReadonlyTypeReferenceNode(parent))
1641
+ );
1642
+ };
1643
+
1644
+ /**
1645
+ * The comments in front of a union or intersection member, for the text that
1646
+ * replaces the whole type.
1647
+ *
1648
+ * `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
1649
+ * in front of that stays in the file by itself. With no leading `|` / `&`, the
1650
+ * first member starts there too, and its comments are the type's: repeating
1651
+ * them in the replacement would copy them once per run. Every other member
1652
+ * has a separator in front of it, and a comment before the separator belongs
1653
+ * to no member's full text, so it is gone once the separator is rewritten. It
1654
+ * goes with the member after it, like the comments after the separator.
1655
+ *
1656
+ * `member` may be the type inside a member's parentheses; the comments inside
1657
+ * them are collected too, and the parentheses are not.
1658
+ */
1659
+ const leadingCommentsOfMember = (
1660
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1661
+ node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1662
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1663
+ member: tsm.TypeNode,
1664
+ ): string =>
1665
+ commentsBetween(
1666
+ node.getSourceFile(),
1667
+ node
1668
+ .getTypeNodes()
1669
+ .findLast((m) => m.getEnd() <= member.getStart())
1670
+ ?.getEnd() ?? node.getStart(),
1671
+ member.getStart(),
1672
+ );
1673
+
1674
+ /**
1675
+ * `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
1676
+ * intersection (`&`) of the members it is merged with.
1677
+ *
1678
+ * `T` is wrapped in parentheses for precedence safety (a function type
1679
+ * argument needs them), unless it is a union spliced into a union or an
1680
+ * intersection spliced into an intersection, which needs none.
1681
+ */
1682
+ const readonlyTypeArgTextSplicedInto = (
1683
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1684
+ node: ReadonlyTypeReferenceNode,
1685
+ operator: '&' | '|',
1686
+ ): string => {
1687
+ const typeArgument = node.getTypeArguments()[0];
1688
+
1689
+ const T = removeParentheses(typeArgument);
1690
+
1691
+ return T.isKind(
1692
+ operator === '|'
1693
+ ? tsm.SyntaxKind.UnionType
1694
+ : tsm.SyntaxKind.IntersectionType,
1695
+ )
1696
+ ? textWithoutParentheses(typeArgument, T)
1697
+ : unwrapReadonlyTypeArgText(node);
1698
+ };
1699
+
1700
+ /**
1701
+ * The text of `inner`, a type `removeParentheses` took out of `outer`, with
1702
+ * the comments in `outer`'s full text in front of it — before the parentheses
1703
+ * and inside them — and without the parentheses.
1704
+ *
1705
+ * `inner.getFullText()` would drop those before the parentheses, which are
1706
+ * as much a part of `outer` as the rest of it.
1707
+ */
1708
+ const textWithoutParentheses = (
1709
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1710
+ outer: tsm.TypeNode,
1711
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1712
+ inner: tsm.TypeNode,
1713
+ ): string =>
1714
+ `${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
1715
+
1716
+ /**
1717
+ * The comments in `[from, to)` of a source file, each followed by what keeps
1718
+ * the next token off it, and nothing else in that range — no whitespace, and
1719
+ * no token such as a separator or a parenthesis.
1720
+ */
1721
+ const commentsBetween = (
1722
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1723
+ sourceFile: tsm.SourceFile,
1724
+ from: number,
1725
+ to: number,
1726
+ ): string => {
1727
+ if (to <= from) {
1728
+ return '';
1729
+ }
1730
+
1731
+ const scanner = tsm.ts.createScanner(
1732
+ tsm.ts.ScriptTarget.Latest,
1733
+ false,
1734
+ tsm.ts.LanguageVariant.Standard,
1735
+ sourceFile.getFullText(),
1736
+ undefined,
1737
+ from,
1738
+ to - from,
1739
+ );
1740
+
1741
+ const mut_comments: string[] = [];
1742
+
1743
+ while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
1744
+ const token = scanner.getToken();
1745
+
1746
+ // A line comment runs to the end of the line, so it takes one along.
1747
+ const separator =
1748
+ token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
1749
+ ? '\n'
1750
+ : token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
1751
+ ? ' '
1752
+ : undefined;
1753
+
1754
+ if (separator !== undefined) {
1755
+ mut_comments.push(`${scanner.getTokenText()}${separator}`);
1756
+ }
1757
+ }
1758
+
1759
+ return mut_comments.join('');
1760
+ };
1761
+
1527
1762
  /** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
1528
1763
  const getDeepReadonlyTypeArg = (
1529
1764
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1554,12 +1789,11 @@ const unionToString = ({
1554
1789
  op: '&' | '|';
1555
1790
  wrapWithReadonly: boolean | string;
1556
1791
  }>): string => {
1557
- // The members arrive as `getFullText()`, which carries the whitespace in
1558
- // front of each one, and they are joined with a separator that supplies its
1559
- // own space. Left alone, that whitespace accumulates: every run of the
1560
- // transformer adds one more space before every member but the first, so a
1561
- // file never reaches a fixed point. Comments live in the same trivia and are
1562
- // not whitespace, so trimming keeps them.
1792
+ // The members are joined with a separator that supplies its own space, so
1793
+ // whitespace left at a member's edge would accumulate: every run of the
1794
+ // transformer would add one more space before every member but the first,
1795
+ // and a file would never reach a fixed point. Trimming keeps the comments in
1796
+ // front of a member, which are not whitespace.
1563
1797
  const types = typesRaw.map((s) => s.trim());
1564
1798
 
1565
1799
  return unionToStringImpl({ types, op, wrapWithReadonly });