ts-codemod-lib 3.4.0 → 3.4.2
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/append-as-const.d.mts +2 -2
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +13 -3
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +2 -2
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +157 -33
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +2 -2
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +24 -6
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.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/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.d.mts +1 -1
- package/dist/functions/functions/is-atomic-type-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/unwrap-readonly.d.mts +1 -1
- package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -1
- package/package.json +14 -14
- package/src/functions/ast-transformers/append-as-const.mts +21 -5
- package/src/functions/ast-transformers/append-as-const.test.mts +24 -0
- package/src/functions/ast-transformers/convert-interface-to-type.mts +1 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +249 -52
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +421 -7
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +30 -8
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +111 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +1 -1
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +124 -3
- package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +1 -1
- package/src/functions/ast-transformers/replace-any-with-unknown.mts +1 -1
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +1 -1
- package/src/functions/ast-transformers/transform-source-code.mts +1 -1
- package/src/functions/functions/is-atomic-type-node.mts +1 -1
- package/src/functions/functions/is-readonly-node.mts +1 -1
- package/src/functions/functions/unwrap-readonly.mts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-codemod-lib",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -43,18 +43,18 @@
|
|
|
43
43
|
"ts-type-forge": "^9.2.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "26.6.
|
|
47
|
-
"@vitest/browser-playwright": "5.0.
|
|
48
|
-
"@vitest/coverage-v8": "5.0.
|
|
49
|
-
"@vitest/ui": "5.0.
|
|
50
|
-
"cspell": "10.3.
|
|
46
|
+
"@types/node": "26.6.4",
|
|
47
|
+
"@vitest/browser-playwright": "5.0.3",
|
|
48
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
49
|
+
"@vitest/ui": "5.0.3",
|
|
50
|
+
"cspell": "10.3.6",
|
|
51
51
|
"dedent": "1.7.2",
|
|
52
52
|
"esbuild": "0.28.2",
|
|
53
|
-
"eslint": "10.
|
|
54
|
-
"eslint-config-typed": "5.
|
|
55
|
-
"eslint-plugin-ts-data-forge": "0.8.
|
|
56
|
-
"eslint-plugin-ts-fortress": "1.
|
|
57
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
53
|
+
"eslint": "10.12.0",
|
|
54
|
+
"eslint-config-typed": "5.17.1",
|
|
55
|
+
"eslint-plugin-ts-data-forge": "0.8.4",
|
|
56
|
+
"eslint-plugin-ts-fortress": "1.3.1",
|
|
57
|
+
"eslint-plugin-ts-type-forge": "0.8.6",
|
|
58
58
|
"jiti": "2.7.0",
|
|
59
59
|
"npm-run-all2": "9.0.3",
|
|
60
60
|
"playwright": "1.63.0",
|
|
@@ -67,15 +67,15 @@
|
|
|
67
67
|
"typedoc-github-theme": "0.4.0",
|
|
68
68
|
"typescript": "6.0.3",
|
|
69
69
|
"typescript-native": "npm:typescript@7.0.2",
|
|
70
|
-
"vite": "8.3.
|
|
71
|
-
"vitest": "5.0.
|
|
70
|
+
"vite": "8.3.2",
|
|
71
|
+
"vitest": "5.0.3"
|
|
72
72
|
},
|
|
73
73
|
"engines": {
|
|
74
74
|
"node": ">=22.22.2",
|
|
75
75
|
"pnpm": ">=9.0.0"
|
|
76
76
|
},
|
|
77
77
|
"volta": {
|
|
78
|
-
"node": "26.
|
|
78
|
+
"node": "26.11.1"
|
|
79
79
|
},
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Arr, ISet } from 'ts-data-forge';
|
|
2
2
|
import * as tsm from 'ts-morph';
|
|
3
|
-
import {
|
|
3
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
4
4
|
import {
|
|
5
5
|
hasDisableNextLineComment,
|
|
6
6
|
isAsConstNode,
|
|
7
7
|
} from '../functions/index.mjs';
|
|
8
8
|
import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import type { TsMorphTransformer } from './types.mjs';
|
|
10
10
|
|
|
11
11
|
const TRANSFORMER_NAME = 'append-as-const';
|
|
12
12
|
|
|
@@ -423,7 +423,9 @@ const removeAsConstInConstTypeParameterArgs = (
|
|
|
423
423
|
const args = call.getArguments();
|
|
424
424
|
|
|
425
425
|
// Cheap syntactic pre-check before touching the type checker
|
|
426
|
-
if (
|
|
426
|
+
if (
|
|
427
|
+
args.every((arg) => !isAsConstNode(skipParenthesizedExpressions(arg)))
|
|
428
|
+
) {
|
|
427
429
|
continue;
|
|
428
430
|
}
|
|
429
431
|
|
|
@@ -478,7 +480,14 @@ const removeAsConstArgsOfCall = (
|
|
|
478
480
|
return;
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
if (argument.wasForgotten()
|
|
483
|
+
if (argument.wasForgotten()) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// `f(([1] as const))` is the same argument as `f([1] as const)`.
|
|
488
|
+
const asConst = skipParenthesizedExpressions(argument);
|
|
489
|
+
|
|
490
|
+
if (!isAsConstNode(asConst)) {
|
|
482
491
|
continue;
|
|
483
492
|
}
|
|
484
493
|
|
|
@@ -503,10 +512,17 @@ const removeAsConstArgsOfCall = (
|
|
|
503
512
|
argument.getText(),
|
|
504
513
|
);
|
|
505
514
|
|
|
506
|
-
|
|
515
|
+
// The parentheses go with it: an argument needs none of its own.
|
|
516
|
+
options.replaceNode(argument, asConst.getExpression().getText());
|
|
507
517
|
}
|
|
508
518
|
};
|
|
509
519
|
|
|
520
|
+
/** `node` with the parentheses written around it taken off. */
|
|
521
|
+
const skipParenthesizedExpressions = (node: tsm.Node): tsm.Node =>
|
|
522
|
+
tsm.Node.isParenthesizedExpression(node)
|
|
523
|
+
? skipParenthesizedExpressions(node.getExpression())
|
|
524
|
+
: node;
|
|
525
|
+
|
|
510
526
|
type ResolvedCallTarget = Readonly<{
|
|
511
527
|
parameters: readonly tsm.ParameterDeclaration[];
|
|
512
528
|
constTypeParameterNames: ISet<string>;
|
|
@@ -759,6 +759,30 @@ describe(appendAsConstTransformer, () => {
|
|
|
759
759
|
const r = rest(1, [2, 3]);
|
|
760
760
|
`,
|
|
761
761
|
},
|
|
762
|
+
{
|
|
763
|
+
name: 'const type parameter should remove a parenthesized as const',
|
|
764
|
+
source: dedent`
|
|
765
|
+
function f<const T>(x: T): T { return x; }
|
|
766
|
+
const a = f(([1, 2] as const));
|
|
767
|
+
const b = f(((({ x: 1 }) as const)));
|
|
768
|
+
`,
|
|
769
|
+
expected: dedent`
|
|
770
|
+
function f<const T>(x: T): T { return x; }
|
|
771
|
+
const a = f([1, 2]);
|
|
772
|
+
const b = f({ x: 1 });
|
|
773
|
+
`,
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: 'non-const type parameter should keep a parenthesized as const',
|
|
777
|
+
source: dedent`
|
|
778
|
+
function g<T>(x: T): T { return x; }
|
|
779
|
+
const b = g(([1, 2] as const));
|
|
780
|
+
`,
|
|
781
|
+
expected: dedent`
|
|
782
|
+
function g<T>(x: T): T { return x; }
|
|
783
|
+
const b = g([1, 2] as const);
|
|
784
|
+
`,
|
|
785
|
+
},
|
|
762
786
|
{
|
|
763
787
|
name: 'mixed const and non-const type parameters should only remove for const ones',
|
|
764
788
|
source: dedent`
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Arr } from 'ts-data-forge';
|
|
4
4
|
import type * as tsm from 'ts-morph';
|
|
5
5
|
import { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import type { TsMorphTransformer } from './types.mjs';
|
|
7
7
|
|
|
8
8
|
const TRANSFORMER_NAME = 'convert-interface-to-type';
|
|
9
9
|
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
pipe,
|
|
9
9
|
} from 'ts-data-forge';
|
|
10
10
|
import * as tsm from 'ts-morph';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import type {
|
|
12
|
+
DeepReadonly,
|
|
13
|
+
SafeUintWithSmallInt,
|
|
14
|
+
StrictExclude,
|
|
15
|
+
StrictExtract,
|
|
16
16
|
} from 'ts-type-forge';
|
|
17
17
|
import {
|
|
18
18
|
hasCallOrConstructSignature,
|
|
@@ -27,16 +27,16 @@ import {
|
|
|
27
27
|
shouldAvoidParenthesesForReadonly,
|
|
28
28
|
unwrapReadonlyTypeArgText,
|
|
29
29
|
wrapWithParentheses,
|
|
30
|
-
type ReadonlyTypeReferenceNode,
|
|
31
30
|
} from '../functions/index.mjs';
|
|
32
31
|
import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
|
|
33
32
|
import {
|
|
34
33
|
groupUnionIntersectionTypes,
|
|
35
34
|
invalidDeepReadonlyTypeName,
|
|
35
|
+
isMergeableReadonlyTypeReferenceNode,
|
|
36
36
|
nextReadonlyContext,
|
|
37
37
|
type ReadonlyContext,
|
|
38
38
|
} from './readonly-transformer-helpers/index.mjs';
|
|
39
|
-
import {
|
|
39
|
+
import type { TsMorphTransformer } from './types.mjs';
|
|
40
40
|
|
|
41
41
|
const TRANSFORMER_NAME = 'convert-to-readonly';
|
|
42
42
|
|
|
@@ -655,32 +655,52 @@ const transformTypeReferenceNode = (
|
|
|
655
655
|
);
|
|
656
656
|
|
|
657
657
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
658
|
-
const
|
|
658
|
+
const typeArgument = node.getTypeArguments()[0]!;
|
|
659
|
+
|
|
660
|
+
const T = removeParentheses(typeArgument);
|
|
661
|
+
|
|
662
|
+
// The comments in front of the type argument and inside its parentheses,
|
|
663
|
+
// which `T.getFullText()` would leave behind with the parentheses.
|
|
664
|
+
const commentsInFrontOfT = commentsBetween(
|
|
665
|
+
node.getSourceFile(),
|
|
666
|
+
typeArgument.getPos(),
|
|
667
|
+
T.getStart(),
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const TText = `${commentsInFrontOfT}${T.getText()}` as const;
|
|
671
|
+
|
|
672
|
+
// `T` with the parentheses that `Readonly<...>` made unnecessary and its
|
|
673
|
+
// position may need (`Readonly<A | B>[]` -> `(A | B)[]`).
|
|
674
|
+
const unwrappedText =
|
|
675
|
+
T.isKind(tsm.SyntaxKind.UnionType) ||
|
|
676
|
+
T.isKind(tsm.SyntaxKind.IntersectionType)
|
|
677
|
+
? wrapWithParentheses(TText)
|
|
678
|
+
: TText;
|
|
659
679
|
|
|
660
680
|
// DeepReadonly<Readonly<T>> -> DeepReadonly<T>
|
|
661
681
|
if (readonlyContext.type === 'DeepReadonly') {
|
|
662
|
-
options.replaceNode(node,
|
|
682
|
+
options.replaceNode(node, unwrappedText);
|
|
663
683
|
|
|
664
684
|
return;
|
|
665
685
|
}
|
|
666
686
|
|
|
667
687
|
// Readonly<{ ... }>[I] -> { ... }[I]
|
|
668
688
|
if (readonlyContext.indexedAccessDepth > 0) {
|
|
669
|
-
options.replaceNode(node,
|
|
689
|
+
options.replaceNode(node, unwrappedText);
|
|
670
690
|
|
|
671
691
|
return;
|
|
672
692
|
}
|
|
673
693
|
|
|
674
694
|
// Readonly<Readonly<T>> -> Readonly<T>
|
|
675
695
|
if (isReadonlyTypeReferenceNode(T)) {
|
|
676
|
-
options.replaceNode(node,
|
|
696
|
+
options.replaceNode(node, TText);
|
|
677
697
|
|
|
678
698
|
return;
|
|
679
699
|
}
|
|
680
700
|
|
|
681
701
|
// Readonly<number> -> number
|
|
682
702
|
if (isAtomicTypeNode(T)) {
|
|
683
|
-
options.replaceNode(node,
|
|
703
|
+
options.replaceNode(node, TText);
|
|
684
704
|
|
|
685
705
|
return;
|
|
686
706
|
}
|
|
@@ -696,11 +716,11 @@ const transformTypeReferenceNode = (
|
|
|
696
716
|
if (Arr.isFixedLengthArray(2, recordTypeArguments)) {
|
|
697
717
|
options.replaceNode(
|
|
698
718
|
node,
|
|
699
|
-
readonlyRecordText(
|
|
719
|
+
`${commentsInFrontOfT}${readonlyRecordText(
|
|
700
720
|
recordTypeArguments[0].getFullText(),
|
|
701
721
|
recordTypeArguments[1].getFullText(),
|
|
702
722
|
options,
|
|
703
|
-
)
|
|
723
|
+
)}`,
|
|
704
724
|
);
|
|
705
725
|
|
|
706
726
|
return;
|
|
@@ -716,7 +736,7 @@ const transformTypeReferenceNode = (
|
|
|
716
736
|
T.isKind(tsm.SyntaxKind.ArrayType) ||
|
|
717
737
|
T.isKind(tsm.SyntaxKind.TupleType)
|
|
718
738
|
) {
|
|
719
|
-
const readonlyText = `readonly ${
|
|
739
|
+
const readonlyText = `readonly ${TText}` as const;
|
|
720
740
|
|
|
721
741
|
options.replaceNode(
|
|
722
742
|
node,
|
|
@@ -732,18 +752,21 @@ const transformTypeReferenceNode = (
|
|
|
732
752
|
// Readonly<readonly E[]> -> readonly E[]
|
|
733
753
|
// Readonly<readonly [E1, E2, E3]> -> readonly [E1, E2, E3]
|
|
734
754
|
if (isReadonlyTupleOrArrayTypeNode(T)) {
|
|
735
|
-
options.replaceNode(node,
|
|
755
|
+
options.replaceNode(node, TText);
|
|
736
756
|
|
|
737
757
|
return;
|
|
738
758
|
}
|
|
739
759
|
|
|
740
760
|
// T = A | B | C
|
|
741
761
|
// T = A & B & C
|
|
762
|
+
//
|
|
763
|
+
// The union or intersection has already put back under `Readonly` every
|
|
764
|
+
// member that still needs it, so this one is redundant.
|
|
742
765
|
if (
|
|
743
766
|
T.isKind(tsm.SyntaxKind.UnionType) ||
|
|
744
767
|
T.isKind(tsm.SyntaxKind.IntersectionType)
|
|
745
768
|
) {
|
|
746
|
-
options.replaceNode(node,
|
|
769
|
+
options.replaceNode(node, unwrappedText);
|
|
747
770
|
|
|
748
771
|
return;
|
|
749
772
|
}
|
|
@@ -883,7 +906,7 @@ const transformArrayTypeNode = (
|
|
|
883
906
|
readonlyContext.indexedAccessDepth === 0 &&
|
|
884
907
|
!isConditionalTypeDistributionGuard(node)
|
|
885
908
|
) {
|
|
886
|
-
const readonlyText = `readonly ${node.
|
|
909
|
+
const readonlyText = `readonly ${node.getText()}` as const;
|
|
887
910
|
|
|
888
911
|
options.replaceNode(
|
|
889
912
|
node,
|
|
@@ -935,7 +958,7 @@ const transformTupleTypeNode = (
|
|
|
935
958
|
readonlyContext.indexedAccessDepth === 0 &&
|
|
936
959
|
!isConditionalTypeDistributionGuard(node)
|
|
937
960
|
) {
|
|
938
|
-
const readonlyText = `readonly ${node.
|
|
961
|
+
const readonlyText = `readonly ${node.getText()}` as const;
|
|
939
962
|
|
|
940
963
|
options.replaceNode(
|
|
941
964
|
node,
|
|
@@ -969,12 +992,22 @@ const transformRestTypeNode = (
|
|
|
969
992
|
|
|
970
993
|
// `tr("...readonly E[]") |-> ...tr(E)[]`
|
|
971
994
|
// `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
|
-
);
|
|
995
|
+
if (!isReadonlyArrayTypeNode(R) && !isReadonlyTupleTypeNode(R)) {
|
|
996
|
+
return;
|
|
977
997
|
}
|
|
998
|
+
|
|
999
|
+
// The comments in front of `R` and inside the parentheses around it, which
|
|
1000
|
+
// `R.getTypeNode().getFullText()` does not reach.
|
|
1001
|
+
const commentsInFrontOfR = commentsBetween(
|
|
1002
|
+
node.getSourceFile(),
|
|
1003
|
+
node.getTypeNode().getPos(),
|
|
1004
|
+
R.getStart(),
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
options.replaceNode(
|
|
1008
|
+
node,
|
|
1009
|
+
`...${commentsInFrontOfR}${R.getTypeNode().getFullText()}` /* = tr(E)[] or [tr(E1), tr(E2)] */,
|
|
1010
|
+
);
|
|
978
1011
|
};
|
|
979
1012
|
|
|
980
1013
|
/**
|
|
@@ -1061,7 +1094,7 @@ const transformTypeLiteralNode = (
|
|
|
1061
1094
|
|
|
1062
1095
|
if (wrappingApplies && !skipWrapping) {
|
|
1063
1096
|
// `{ readonly x: X, readonly y: Y } |-> Readonly<{ x: X, y: Y }>`
|
|
1064
|
-
options.replaceNode(node, `Readonly<${node.
|
|
1097
|
+
options.replaceNode(node, `Readonly<${node.getText()}>`);
|
|
1065
1098
|
}
|
|
1066
1099
|
};
|
|
1067
1100
|
|
|
@@ -1286,7 +1319,8 @@ const transformConstructorParameter = (
|
|
|
1286
1319
|
|
|
1287
1320
|
/**
|
|
1288
1321
|
* - `tr(A & B) -> tr(A) & tr(B)`
|
|
1289
|
-
* - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)
|
|
1322
|
+
* - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`, when `A` and `B`
|
|
1323
|
+
* are type literals (see `isMergeableReadonlyTypeReferenceNode`)
|
|
1290
1324
|
*/
|
|
1291
1325
|
const transformIntersectionTypeNode = (
|
|
1292
1326
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1340,15 +1374,30 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1340
1374
|
|
|
1341
1375
|
const newTypes = node.getTypeNodes();
|
|
1342
1376
|
|
|
1377
|
+
const withLeadingComments = (
|
|
1378
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1379
|
+
member: tsm.TypeNode,
|
|
1380
|
+
memberText: string,
|
|
1381
|
+
): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
|
|
1382
|
+
|
|
1343
1383
|
const { primitives, arraysAndTuples, typeLiterals, others } =
|
|
1344
|
-
groupUnionIntersectionTypes(newTypes
|
|
1384
|
+
groupUnionIntersectionTypes(newTypes, {
|
|
1385
|
+
operator,
|
|
1386
|
+
// `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
|
|
1387
|
+
// handler of `Readonly` then takes off as redundant.
|
|
1388
|
+
underReadonly:
|
|
1389
|
+
readonlyContext.type === 'Readonly' &&
|
|
1390
|
+
readonlyContext.indexedAccessDepth === 0 &&
|
|
1391
|
+
isTypeArgumentOfReadonly(node),
|
|
1392
|
+
deepReadonlyTypeName: options.DeepReadonly.typeName,
|
|
1393
|
+
});
|
|
1345
1394
|
|
|
1346
1395
|
options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
|
|
1347
1396
|
|
|
1348
1397
|
// Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
|
|
1349
1398
|
const isEmptyTypeLiteral = (
|
|
1350
1399
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1351
|
-
n: tsm.
|
|
1400
|
+
n: tsm.TypeNode,
|
|
1352
1401
|
): boolean =>
|
|
1353
1402
|
n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
|
|
1354
1403
|
|
|
@@ -1370,10 +1419,16 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1370
1419
|
: ([
|
|
1371
1420
|
unionToString({
|
|
1372
1421
|
types: nonEmptyTypeLiterals.map((n) =>
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1422
|
+
withLeadingComments(
|
|
1423
|
+
n,
|
|
1424
|
+
isMergeableReadonlyTypeReferenceNode(n, operator)
|
|
1425
|
+
? // NOTE: Readonly<A & B> -> (A & B)
|
|
1426
|
+
unwrapReadonlyTypeArgText(n)
|
|
1427
|
+
: n.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
1428
|
+
? n.getText()
|
|
1429
|
+
: // A member kept under `Readonly`; see `underReadonly`
|
|
1430
|
+
wrapWithParentheses(n.getText()),
|
|
1431
|
+
),
|
|
1377
1432
|
),
|
|
1378
1433
|
op: operator,
|
|
1379
1434
|
wrapWithReadonly:
|
|
@@ -1386,11 +1441,19 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1386
1441
|
const sorted: readonly string[] = [
|
|
1387
1442
|
mapNullable(
|
|
1388
1443
|
primitives,
|
|
1389
|
-
(a) =>
|
|
1444
|
+
(a) =>
|
|
1445
|
+
[
|
|
1446
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
1447
|
+
a.firstPosition,
|
|
1448
|
+
] as const,
|
|
1390
1449
|
),
|
|
1391
1450
|
mapNullable(
|
|
1392
1451
|
arraysAndTuples,
|
|
1393
|
-
(a) =>
|
|
1452
|
+
(a) =>
|
|
1453
|
+
[
|
|
1454
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
1455
|
+
a.firstPosition,
|
|
1456
|
+
] as const,
|
|
1394
1457
|
),
|
|
1395
1458
|
nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
1396
1459
|
? mapNullable(
|
|
@@ -1403,7 +1466,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1403
1466
|
typeLiterals,
|
|
1404
1467
|
(a) =>
|
|
1405
1468
|
[
|
|
1406
|
-
emptyTypeLiterals.map((n) => n.
|
|
1469
|
+
emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
|
|
1407
1470
|
// Use a position after non-empty type literals if they exist
|
|
1408
1471
|
a.firstPosition +
|
|
1409
1472
|
(nonEmptyTypeLiterals !== undefined &&
|
|
@@ -1423,8 +1486,11 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1423
1486
|
? mergeDeepReadonlyUnionMembers(
|
|
1424
1487
|
a.nodes,
|
|
1425
1488
|
options.DeepReadonly.typeName,
|
|
1489
|
+
withLeadingComments,
|
|
1426
1490
|
)
|
|
1427
|
-
: a.nodes.map((n) =>
|
|
1491
|
+
: a.nodes.map((n) =>
|
|
1492
|
+
withLeadingComments(n, wrapWithParentheses(n.getText())),
|
|
1493
|
+
),
|
|
1428
1494
|
a.firstPosition,
|
|
1429
1495
|
] as const,
|
|
1430
1496
|
),
|
|
@@ -1468,8 +1534,9 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1468
1534
|
*
|
|
1469
1535
|
* - `Readonly` is a homomorphic mapped type, and over type literals
|
|
1470
1536
|
* `Readonly<A & B>` and `Readonly<A> & Readonly<B>` have the same properties
|
|
1471
|
-
* with the same modifiers.
|
|
1472
|
-
*
|
|
1537
|
+
* with the same modifiers. That does not hold for an array `A`, so an
|
|
1538
|
+
* intersection merges only `Readonly<T>` whose `T` is known to be a type
|
|
1539
|
+
* literal; see `isMergeableReadonlyTypeReferenceNode`.
|
|
1473
1540
|
* - `DeepReadonly` is a conditional type that picks a branch by what `T` is
|
|
1474
1541
|
* (primitive, function, Map, Set, array, object). It distributes over a union
|
|
1475
1542
|
* because each member is tested on its own, but an intersection is tested as
|
|
@@ -1488,28 +1555,52 @@ const mergeDeepReadonlyUnionMembers = (
|
|
|
1488
1555
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1489
1556
|
nodes: readonly tsm.TypeNode[],
|
|
1490
1557
|
deepReadonlyTypeName: string,
|
|
1558
|
+
withLeadingComments: (
|
|
1559
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1560
|
+
member: tsm.TypeNode,
|
|
1561
|
+
memberText: string,
|
|
1562
|
+
) => string,
|
|
1491
1563
|
): readonly string[] => {
|
|
1492
1564
|
const typeArgsOfDeepReadonly = nodes.map((n) =>
|
|
1493
1565
|
getDeepReadonlyTypeArg(n, deepReadonlyTypeName),
|
|
1494
1566
|
);
|
|
1495
1567
|
|
|
1496
|
-
const
|
|
1568
|
+
const asIs = (
|
|
1569
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1570
|
+
n: tsm.TypeNode,
|
|
1571
|
+
): string => withLeadingComments(n, wrapWithParentheses(n.getText()));
|
|
1497
1572
|
|
|
1498
|
-
if (
|
|
1499
|
-
return nodes.map(
|
|
1573
|
+
if (typeArgsOfDeepReadonly.filter((t) => t !== undefined).length < 2) {
|
|
1574
|
+
return nodes.map(asIs);
|
|
1500
1575
|
}
|
|
1501
1576
|
|
|
1502
1577
|
const firstIndex = typeArgsOfDeepReadonly.findIndex((t) => t !== undefined);
|
|
1503
1578
|
|
|
1504
1579
|
const merged = unionToString({
|
|
1505
|
-
|
|
1580
|
+
// A member's comments move inside, onto its type argument:
|
|
1581
|
+
// `/** a */ DeepReadonly<A> | /** b */ DeepReadonly<B>`
|
|
1582
|
+
// -> `DeepReadonly</** a */ A | /** b */ B>`
|
|
1583
|
+
types: nodes.flatMap((n, i) => {
|
|
1584
|
+
const t_ = typeArgsOfDeepReadonly[i];
|
|
1585
|
+
|
|
1586
|
+
if (t_ === undefined) {
|
|
1587
|
+
return [];
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1506
1590
|
const t = removeParentheses(t_);
|
|
1507
1591
|
|
|
1508
1592
|
// NOTE: DeepReadonly<A | B> | DeepReadonly<C> -> DeepReadonly<A | B | C>
|
|
1509
1593
|
// NOTE: DeepReadonly<() => void> | DeepReadonly<B> -> DeepReadonly<(() => void) | B>
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1594
|
+
const tText = textWithoutParentheses(t_, t);
|
|
1595
|
+
|
|
1596
|
+
return [
|
|
1597
|
+
withLeadingComments(
|
|
1598
|
+
n,
|
|
1599
|
+
t.isKind(tsm.SyntaxKind.UnionType)
|
|
1600
|
+
? tText
|
|
1601
|
+
: wrapWithParentheses(tText),
|
|
1602
|
+
),
|
|
1603
|
+
];
|
|
1513
1604
|
}),
|
|
1514
1605
|
op: '|',
|
|
1515
1606
|
wrapWithReadonly: deepReadonlyTypeName,
|
|
@@ -1517,13 +1608,120 @@ const mergeDeepReadonlyUnionMembers = (
|
|
|
1517
1608
|
|
|
1518
1609
|
return nodes.flatMap((n, i) =>
|
|
1519
1610
|
typeArgsOfDeepReadonly[i] === undefined
|
|
1520
|
-
? [
|
|
1611
|
+
? [asIs(n)]
|
|
1521
1612
|
: i === firstIndex
|
|
1522
1613
|
? [merged]
|
|
1523
1614
|
: [],
|
|
1524
1615
|
);
|
|
1525
1616
|
};
|
|
1526
1617
|
|
|
1618
|
+
/** Whether `node` is the type argument of `Readonly<...>`, parentheses aside. */
|
|
1619
|
+
const isTypeArgumentOfReadonly = (
|
|
1620
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1621
|
+
node: tsm.TypeNode,
|
|
1622
|
+
): boolean => {
|
|
1623
|
+
const parent = node.getParent();
|
|
1624
|
+
|
|
1625
|
+
return (
|
|
1626
|
+
parent !== undefined &&
|
|
1627
|
+
(parent.isKind(tsm.SyntaxKind.ParenthesizedType)
|
|
1628
|
+
? isTypeArgumentOfReadonly(parent)
|
|
1629
|
+
: isReadonlyTypeReferenceNode(parent))
|
|
1630
|
+
);
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* The comments in front of a union or intersection member, for the text that
|
|
1635
|
+
* replaces the whole type.
|
|
1636
|
+
*
|
|
1637
|
+
* `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
|
|
1638
|
+
* in front of that stays in the file by itself. With no leading `|` / `&`, the
|
|
1639
|
+
* first member starts there too, and its comments are the type's: repeating
|
|
1640
|
+
* them in the replacement would copy them once per run. Every other member
|
|
1641
|
+
* has a separator in front of it, and a comment before the separator belongs
|
|
1642
|
+
* to no member's full text, so it is gone once the separator is rewritten. It
|
|
1643
|
+
* goes with the member after it, like the comments after the separator.
|
|
1644
|
+
*
|
|
1645
|
+
* `member` may be the type inside a member's parentheses; the comments inside
|
|
1646
|
+
* them are collected too, and the parentheses are not.
|
|
1647
|
+
*/
|
|
1648
|
+
const leadingCommentsOfMember = (
|
|
1649
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1650
|
+
node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
|
|
1651
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1652
|
+
member: tsm.TypeNode,
|
|
1653
|
+
): string =>
|
|
1654
|
+
commentsBetween(
|
|
1655
|
+
node.getSourceFile(),
|
|
1656
|
+
node
|
|
1657
|
+
.getTypeNodes()
|
|
1658
|
+
.findLast((m) => m.getEnd() <= member.getStart())
|
|
1659
|
+
?.getEnd() ?? node.getStart(),
|
|
1660
|
+
member.getStart(),
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* The text of `inner`, a type `removeParentheses` took out of `outer`, with
|
|
1665
|
+
* the comments in `outer`'s full text in front of it — before the parentheses
|
|
1666
|
+
* and inside them — and without the parentheses.
|
|
1667
|
+
*
|
|
1668
|
+
* `inner.getFullText()` would drop those before the parentheses, which are
|
|
1669
|
+
* as much a part of `outer` as the rest of it.
|
|
1670
|
+
*/
|
|
1671
|
+
const textWithoutParentheses = (
|
|
1672
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1673
|
+
outer: tsm.TypeNode,
|
|
1674
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1675
|
+
inner: tsm.TypeNode,
|
|
1676
|
+
): string =>
|
|
1677
|
+
`${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* The comments in `[from, to)` of a source file, each followed by what keeps
|
|
1681
|
+
* the next token off it, and nothing else in that range — no whitespace, and
|
|
1682
|
+
* no token such as a separator or a parenthesis.
|
|
1683
|
+
*/
|
|
1684
|
+
const commentsBetween = (
|
|
1685
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1686
|
+
sourceFile: tsm.SourceFile,
|
|
1687
|
+
from: number,
|
|
1688
|
+
to: number,
|
|
1689
|
+
): string => {
|
|
1690
|
+
if (to <= from) {
|
|
1691
|
+
return '';
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const scanner = tsm.ts.createScanner(
|
|
1695
|
+
tsm.ts.ScriptTarget.Latest,
|
|
1696
|
+
false,
|
|
1697
|
+
tsm.ts.LanguageVariant.Standard,
|
|
1698
|
+
sourceFile.getFullText(),
|
|
1699
|
+
undefined,
|
|
1700
|
+
from,
|
|
1701
|
+
to - from,
|
|
1702
|
+
);
|
|
1703
|
+
|
|
1704
|
+
const mut_comments: string[] = [];
|
|
1705
|
+
|
|
1706
|
+
while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
|
|
1707
|
+
const token = scanner.getToken();
|
|
1708
|
+
|
|
1709
|
+
// A line comment runs to the end of the line, so it takes one along.
|
|
1710
|
+
const separator =
|
|
1711
|
+
token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
|
|
1712
|
+
? '\n'
|
|
1713
|
+
: token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
|
|
1714
|
+
? ' '
|
|
1715
|
+
: undefined;
|
|
1716
|
+
|
|
1717
|
+
if (separator !== undefined) {
|
|
1718
|
+
mut_comments.push(`${scanner.getTokenText()}${separator}`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return mut_comments.join('');
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1527
1725
|
/** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
|
|
1528
1726
|
const getDeepReadonlyTypeArg = (
|
|
1529
1727
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1554,12 +1752,11 @@ const unionToString = ({
|
|
|
1554
1752
|
op: '&' | '|';
|
|
1555
1753
|
wrapWithReadonly: boolean | string;
|
|
1556
1754
|
}>): string => {
|
|
1557
|
-
// The members
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
// not whitespace, so trimming keeps them.
|
|
1755
|
+
// The members are joined with a separator that supplies its own space, so
|
|
1756
|
+
// whitespace left at a member's edge would accumulate: every run of the
|
|
1757
|
+
// transformer would add one more space before every member but the first,
|
|
1758
|
+
// and a file would never reach a fixed point. Trimming keeps the comments in
|
|
1759
|
+
// front of a member, which are not whitespace.
|
|
1563
1760
|
const types = typesRaw.map((s) => s.trim());
|
|
1564
1761
|
|
|
1565
1762
|
return unionToStringImpl({ types, op, wrapWithReadonly });
|