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.
- package/README.md +4 -4
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +183 -34
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +32 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +88 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/package.json +16 -16
- package/src/functions/ast-transformers/convert-to-readonly.mts +279 -45
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +534 -7
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +124 -3
|
@@ -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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 ${
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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)
|
|
973
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
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) =>
|
|
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.
|
|
1472
|
-
*
|
|
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
|
|
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 (
|
|
1499
|
-
return nodes.map(
|
|
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
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
? [
|
|
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
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
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 });
|