ts-codemod-lib 3.3.3 → 3.4.1
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/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 +74 -1
- 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/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 +16 -16
- 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 +102 -7
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +203 -0
- 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/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.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -43,39 +43,39 @@
|
|
|
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.3",
|
|
47
|
+
"@vitest/browser-playwright": "5.0.2",
|
|
48
|
+
"@vitest/coverage-v8": "5.0.2",
|
|
49
|
+
"@vitest/ui": "5.0.2",
|
|
50
|
+
"cspell": "10.3.5",
|
|
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.11.0",
|
|
54
|
+
"eslint-config-typed": "5.17.0",
|
|
55
|
+
"eslint-plugin-ts-data-forge": "0.8.3",
|
|
56
|
+
"eslint-plugin-ts-fortress": "1.3.0",
|
|
57
|
+
"eslint-plugin-ts-type-forge": "0.8.5",
|
|
58
58
|
"jiti": "2.7.0",
|
|
59
59
|
"npm-run-all2": "9.0.3",
|
|
60
60
|
"playwright": "1.63.0",
|
|
61
|
-
"prettier": "3.9.
|
|
61
|
+
"prettier": "3.9.9",
|
|
62
62
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
63
63
|
"prettier-plugin-packagejson": "3.0.2",
|
|
64
64
|
"ts-repo-utils": "10.7.1",
|
|
65
|
-
"tsx": "4.23.
|
|
65
|
+
"tsx": "4.23.15",
|
|
66
66
|
"typedoc": "0.28.20",
|
|
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.1",
|
|
71
|
+
"vitest": "5.0.2"
|
|
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.10.0"
|
|
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,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
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
|
|
|
@@ -1303,6 +1303,7 @@ const transformIntersectionTypeNode = (
|
|
|
1303
1303
|
/**
|
|
1304
1304
|
* - `tr(A | B) |-> tr(A) | tr(B)`
|
|
1305
1305
|
* - `tr(Readonly<A> | Readonly<B>) |-> Readonly<tr(A) | tr(B)>`
|
|
1306
|
+
* - `tr(DeepReadonly<A> | DeepReadonly<B>) |-> DeepReadonly<tr(A) | tr(B)>`
|
|
1306
1307
|
*/
|
|
1307
1308
|
const transformUnionTypeNode = (
|
|
1308
1309
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1416,7 +1417,14 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1416
1417
|
others,
|
|
1417
1418
|
(a) =>
|
|
1418
1419
|
[
|
|
1419
|
-
|
|
1420
|
+
// `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
|
|
1421
|
+
// above. See `mergeDeepReadonlyUnionMembers` for why.
|
|
1422
|
+
operator === '|'
|
|
1423
|
+
? mergeDeepReadonlyUnionMembers(
|
|
1424
|
+
a.nodes,
|
|
1425
|
+
options.DeepReadonly.typeName,
|
|
1426
|
+
)
|
|
1427
|
+
: a.nodes.map((n) => wrapWithParentheses(n.getFullText())),
|
|
1420
1428
|
a.firstPosition,
|
|
1421
1429
|
] as const,
|
|
1422
1430
|
),
|
|
@@ -1450,6 +1458,93 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1450
1458
|
);
|
|
1451
1459
|
};
|
|
1452
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* - `tr(DeepReadonly<A> | B | DeepReadonly<C>) |-> DeepReadonly<A | C> | B`
|
|
1463
|
+
*
|
|
1464
|
+
* The merged member takes the place of the first `DeepReadonly` member.
|
|
1465
|
+
*
|
|
1466
|
+
* Only a union is merged, although `Readonly<A> & Readonly<B>` is merged into
|
|
1467
|
+
* `Readonly<A & B>`. The two differ in kind:
|
|
1468
|
+
*
|
|
1469
|
+
* - `Readonly` is a homomorphic mapped type, and over type literals
|
|
1470
|
+
* `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.)
|
|
1473
|
+
* - `DeepReadonly` is a conditional type that picks a branch by what `T` is
|
|
1474
|
+
* (primitive, function, Map, Set, array, object). It distributes over a union
|
|
1475
|
+
* because each member is tested on its own, but an intersection is tested as
|
|
1476
|
+
* a whole and matches every branch any one member matches: with
|
|
1477
|
+
* `F = () => void`, `F & { x: number[] }` extends a function type because `F`
|
|
1478
|
+
* does, so `DeepReadonly<F & { x: number[] }>` hits that branch and returns
|
|
1479
|
+
* the intersection unchanged, leaving `x` mutable, while `DeepReadonly<F> &
|
|
1480
|
+
* DeepReadonly<{ x: number[] }>` is `F & { readonly x: readonly number[] }`.
|
|
1481
|
+
* The codemod cannot tell whether a type argument is such a member, so it
|
|
1482
|
+
* merges no intersection.
|
|
1483
|
+
*
|
|
1484
|
+
* The test "premises of merging DeepReadonly members" pins both facts against
|
|
1485
|
+
* ts-type-forge's `DeepReadonly`.
|
|
1486
|
+
*/
|
|
1487
|
+
const mergeDeepReadonlyUnionMembers = (
|
|
1488
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1489
|
+
nodes: readonly tsm.TypeNode[],
|
|
1490
|
+
deepReadonlyTypeName: string,
|
|
1491
|
+
): readonly string[] => {
|
|
1492
|
+
const typeArgsOfDeepReadonly = nodes.map((n) =>
|
|
1493
|
+
getDeepReadonlyTypeArg(n, deepReadonlyTypeName),
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
const typeArgs = typeArgsOfDeepReadonly.filter((t) => t !== undefined);
|
|
1497
|
+
|
|
1498
|
+
if (typeArgs.length < 2) {
|
|
1499
|
+
return nodes.map((n) => wrapWithParentheses(n.getFullText()));
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const firstIndex = typeArgsOfDeepReadonly.findIndex((t) => t !== undefined);
|
|
1503
|
+
|
|
1504
|
+
const merged = unionToString({
|
|
1505
|
+
types: typeArgs.map((t_) => {
|
|
1506
|
+
const t = removeParentheses(t_);
|
|
1507
|
+
|
|
1508
|
+
// NOTE: DeepReadonly<A | B> | DeepReadonly<C> -> DeepReadonly<A | B | C>
|
|
1509
|
+
// NOTE: DeepReadonly<() => void> | DeepReadonly<B> -> DeepReadonly<(() => void) | B>
|
|
1510
|
+
return t.isKind(tsm.SyntaxKind.UnionType)
|
|
1511
|
+
? t.getFullText()
|
|
1512
|
+
: wrapWithParentheses(t.getFullText());
|
|
1513
|
+
}),
|
|
1514
|
+
op: '|',
|
|
1515
|
+
wrapWithReadonly: deepReadonlyTypeName,
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
return nodes.flatMap((n, i) =>
|
|
1519
|
+
typeArgsOfDeepReadonly[i] === undefined
|
|
1520
|
+
? [wrapWithParentheses(n.getFullText())]
|
|
1521
|
+
: i === firstIndex
|
|
1522
|
+
? [merged]
|
|
1523
|
+
: [],
|
|
1524
|
+
);
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
/** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
|
|
1528
|
+
const getDeepReadonlyTypeArg = (
|
|
1529
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1530
|
+
node: tsm.TypeNode,
|
|
1531
|
+
deepReadonlyTypeName: string,
|
|
1532
|
+
): tsm.TypeNode | undefined => {
|
|
1533
|
+
if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
1534
|
+
return undefined;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const typeName = node.getTypeName();
|
|
1538
|
+
|
|
1539
|
+
const typeArguments = node.getTypeArguments();
|
|
1540
|
+
|
|
1541
|
+
return typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
1542
|
+
typeName.getText() === deepReadonlyTypeName &&
|
|
1543
|
+
Arr.isFixedLengthArray(1, typeArguments)
|
|
1544
|
+
? typeArguments[0]
|
|
1545
|
+
: undefined;
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1453
1548
|
const unionToString = ({
|
|
1454
1549
|
types: typesRaw,
|
|
1455
1550
|
op,
|
|
@@ -6,6 +6,10 @@ import dedent from 'dedent';
|
|
|
6
6
|
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
7
7
|
import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
|
|
8
8
|
import * as prettier from 'prettier/standalone';
|
|
9
|
+
import { expectType } from 'ts-data-forge';
|
|
10
|
+
// Aliased: an import named `DeepReadonly` makes `vitest/prefer-describe-function-title`
|
|
11
|
+
// rewrite `describe('DeepReadonly', ...)` into a reference to this type.
|
|
12
|
+
import type { DeepReadonly as TsTypeForgeDeepReadonly } from 'ts-type-forge';
|
|
9
13
|
import {
|
|
10
14
|
convertToReadonlyTransformer,
|
|
11
15
|
type ReadonlyTransformerOptions,
|
|
@@ -2831,6 +2835,205 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
2831
2835
|
])('$name', testFn);
|
|
2832
2836
|
});
|
|
2833
2837
|
|
|
2838
|
+
describe('Union of DeepReadonly members', () => {
|
|
2839
|
+
test.each([
|
|
2840
|
+
{
|
|
2841
|
+
name: 'DeepReadonly<A> | DeepReadonly<B> -> DeepReadonly<A | B>',
|
|
2842
|
+
source: dedent`
|
|
2843
|
+
type T = DeepReadonly<{ a: number[] }> | DeepReadonly<{ b: string[] }>;
|
|
2844
|
+
`,
|
|
2845
|
+
expected: dedent`
|
|
2846
|
+
type T = DeepReadonly<{ a: number[] } | { b: string[] }>;
|
|
2847
|
+
`,
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
name: 'Three DeepReadonly members',
|
|
2851
|
+
source: dedent`
|
|
2852
|
+
type T<A, B> = DeepReadonly<A> | DeepReadonly<B> | DeepReadonly<{ c: boolean[] }>;
|
|
2853
|
+
`,
|
|
2854
|
+
expected: dedent`
|
|
2855
|
+
type T<A, B> = DeepReadonly<A | B | { c: boolean[] }>;
|
|
2856
|
+
`,
|
|
2857
|
+
},
|
|
2858
|
+
{
|
|
2859
|
+
name: 'DeepReadonly members separated by other members',
|
|
2860
|
+
source: dedent`
|
|
2861
|
+
type T = DeepReadonly<{ a: number[] }> | string | Map<string, number> | DeepReadonly<{ b: string[] }>;
|
|
2862
|
+
`,
|
|
2863
|
+
expected: dedent`
|
|
2864
|
+
type T = DeepReadonly<{ a: number[] } | { b: string[] }> | ReadonlyMap<string, number> | string;
|
|
2865
|
+
`,
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
name: 'A DeepReadonly member whose argument is itself a union',
|
|
2869
|
+
source: dedent`
|
|
2870
|
+
type T = DeepReadonly<{ a: number[] } | { b: string[] }> | DeepReadonly<{ c: boolean[] }>;
|
|
2871
|
+
`,
|
|
2872
|
+
expected: dedent`
|
|
2873
|
+
type T = DeepReadonly<{ a: number[] } | { b: string[] } | { c: boolean[] }>;
|
|
2874
|
+
`,
|
|
2875
|
+
},
|
|
2876
|
+
{
|
|
2877
|
+
// `() => void | X` would parse as a function returning `void | X`.
|
|
2878
|
+
name: 'A function type argument keeps its parentheses',
|
|
2879
|
+
source: dedent`
|
|
2880
|
+
type T = DeepReadonly<() => void> | DeepReadonly<{ b: string[] }>;
|
|
2881
|
+
`,
|
|
2882
|
+
expected: dedent`
|
|
2883
|
+
type T = DeepReadonly<(() => void) | { b: string[] }>;
|
|
2884
|
+
`,
|
|
2885
|
+
},
|
|
2886
|
+
{
|
|
2887
|
+
name: 'Nested in a type literal',
|
|
2888
|
+
source: dedent`
|
|
2889
|
+
type T = { x: DeepReadonly<{ a: number[] }> | DeepReadonly<{ b: string[] }> };
|
|
2890
|
+
`,
|
|
2891
|
+
expected: dedent`
|
|
2892
|
+
type T = Readonly<{ x: DeepReadonly<{ a: number[] } | { b: string[] }> }>;
|
|
2893
|
+
`,
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
name: 'A single DeepReadonly member is left as written',
|
|
2897
|
+
source: dedent`
|
|
2898
|
+
type T = DeepReadonly<{ a: number[] }> | { b: string[] };
|
|
2899
|
+
`,
|
|
2900
|
+
expected: dedent`
|
|
2901
|
+
type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: readonly string[] }>;
|
|
2902
|
+
`,
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
name: 'DeepReadonly and Readonly members are not merged',
|
|
2906
|
+
source: dedent`
|
|
2907
|
+
type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: string[] }>;
|
|
2908
|
+
`,
|
|
2909
|
+
expected: dedent`
|
|
2910
|
+
type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: readonly string[] }>;
|
|
2911
|
+
`,
|
|
2912
|
+
},
|
|
2913
|
+
// Unlike `Readonly<A> & Readonly<B>`, an intersection of DeepReadonly
|
|
2914
|
+
// members is never merged; see `mergeDeepReadonlyUnionMembers` and the
|
|
2915
|
+
// premises test below.
|
|
2916
|
+
{
|
|
2917
|
+
name: 'An intersection of DeepReadonly members is left as written',
|
|
2918
|
+
source: dedent`
|
|
2919
|
+
type T = DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>;
|
|
2920
|
+
`,
|
|
2921
|
+
expected: dedent`
|
|
2922
|
+
type T = DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>;
|
|
2923
|
+
`,
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
// The case merging would get wrong: `DeepReadonly<F & X>` is `F & X`.
|
|
2927
|
+
name: 'An intersection with a function type argument is left as written',
|
|
2928
|
+
source: dedent`
|
|
2929
|
+
type T = DeepReadonly<() => void> & DeepReadonly<{ x: number[] }>;
|
|
2930
|
+
`,
|
|
2931
|
+
expected: dedent`
|
|
2932
|
+
type T = DeepReadonly<() => void> & DeepReadonly<{ x: number[] }>;
|
|
2933
|
+
`,
|
|
2934
|
+
},
|
|
2935
|
+
{
|
|
2936
|
+
name: 'An intersection of generic DeepReadonly members is left as written',
|
|
2937
|
+
source: dedent`
|
|
2938
|
+
type T<A, B> = DeepReadonly<A> & DeepReadonly<B>;
|
|
2939
|
+
`,
|
|
2940
|
+
expected: dedent`
|
|
2941
|
+
type T<A, B> = DeepReadonly<A> & DeepReadonly<B>;
|
|
2942
|
+
`,
|
|
2943
|
+
},
|
|
2944
|
+
{
|
|
2945
|
+
name: 'An intersection under a custom typeName is left as written',
|
|
2946
|
+
source: dedent`
|
|
2947
|
+
type T = DeepRO<{ a: number[] }> & DeepRO<{ b: string[] }>;
|
|
2948
|
+
`,
|
|
2949
|
+
expected: dedent`
|
|
2950
|
+
type T = DeepRO<{ a: number[] }> & DeepRO<{ b: string[] }>;
|
|
2951
|
+
`,
|
|
2952
|
+
options: { DeepReadonly: { typeName: 'DeepRO' } },
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
// The union around it has one DeepReadonly member, so nothing merges.
|
|
2956
|
+
name: 'An intersection of DeepReadonly members inside a union is left as written',
|
|
2957
|
+
source: dedent`
|
|
2958
|
+
type T = (DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>) | DeepReadonly<{ c: boolean[] }>;
|
|
2959
|
+
`,
|
|
2960
|
+
expected: dedent`
|
|
2961
|
+
type T = (DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>) | DeepReadonly<{ c: boolean[] }>;
|
|
2962
|
+
`,
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
name: 'Custom DeepReadonly typeName',
|
|
2966
|
+
source: dedent`
|
|
2967
|
+
type T = DeepRO<{ a: number[] }> | DeepRO<{ b: string[] }>;
|
|
2968
|
+
`,
|
|
2969
|
+
expected: dedent`
|
|
2970
|
+
type T = DeepRO<{ a: number[] } | { b: string[] }>;
|
|
2971
|
+
`,
|
|
2972
|
+
options: { DeepReadonly: { typeName: 'DeepRO' } },
|
|
2973
|
+
},
|
|
2974
|
+
{
|
|
2975
|
+
name: 'Other generic types are not merged',
|
|
2976
|
+
source: dedent`
|
|
2977
|
+
type T<A, B> = Partial<A> | Partial<B>;
|
|
2978
|
+
`,
|
|
2979
|
+
expected: dedent`
|
|
2980
|
+
type T<A, B> = Partial<A> | Partial<B>;
|
|
2981
|
+
`,
|
|
2982
|
+
},
|
|
2983
|
+
])('$name', testFn);
|
|
2984
|
+
|
|
2985
|
+
// What `mergeDeepReadonlyUnionMembers` relies on. If ts-type-forge's
|
|
2986
|
+
// `DeepReadonly` changes so that one of these no longer holds, revisit
|
|
2987
|
+
// which operators the codemod merges.
|
|
2988
|
+
test('premises of merging DeepReadonly members', () => {
|
|
2989
|
+
type F = () => void;
|
|
2990
|
+
|
|
2991
|
+
type X = Readonly<{ x: number[] }>;
|
|
2992
|
+
|
|
2993
|
+
type A = Readonly<{ a: number[] }>;
|
|
2994
|
+
|
|
2995
|
+
type B = Readonly<{ b: string[] }>;
|
|
2996
|
+
|
|
2997
|
+
// `|` commutes with DeepReadonly, so merging a union is safe.
|
|
2998
|
+
expectType<
|
|
2999
|
+
TsTypeForgeDeepReadonly<A | B>,
|
|
3000
|
+
TsTypeForgeDeepReadonly<A> | TsTypeForgeDeepReadonly<B>
|
|
3001
|
+
>('=');
|
|
3002
|
+
|
|
3003
|
+
expectType<
|
|
3004
|
+
TsTypeForgeDeepReadonly<F | X>,
|
|
3005
|
+
TsTypeForgeDeepReadonly<F> | TsTypeForgeDeepReadonly<X>
|
|
3006
|
+
>('=');
|
|
3007
|
+
|
|
3008
|
+
expectType<
|
|
3009
|
+
TsTypeForgeDeepReadonly<readonly string[] | X>,
|
|
3010
|
+
TsTypeForgeDeepReadonly<readonly string[]> | TsTypeForgeDeepReadonly<X>
|
|
3011
|
+
>('=');
|
|
3012
|
+
|
|
3013
|
+
// `&` does not: an intersection is tested as a whole and matches every
|
|
3014
|
+
// branch any one member matches. `F & X` hits the function branch and
|
|
3015
|
+
// comes back unchanged, leaving `x` mutable.
|
|
3016
|
+
expectType<TsTypeForgeDeepReadonly<F & X>, F & X>('=');
|
|
3017
|
+
|
|
3018
|
+
expectType<
|
|
3019
|
+
TsTypeForgeDeepReadonly<F & X>,
|
|
3020
|
+
TsTypeForgeDeepReadonly<F> & TsTypeForgeDeepReadonly<X>
|
|
3021
|
+
>('!=');
|
|
3022
|
+
|
|
3023
|
+
expectType<
|
|
3024
|
+
TsTypeForgeDeepReadonly<readonly string[] & X>,
|
|
3025
|
+
TsTypeForgeDeepReadonly<readonly string[]> & TsTypeForgeDeepReadonly<X>
|
|
3026
|
+
>('!=');
|
|
3027
|
+
|
|
3028
|
+
// `Readonly` over type literals does commute with `&`, which is what
|
|
3029
|
+
// merging `{ a: A } & Readonly<{ b: B }>` into one `Readonly` relies on.
|
|
3030
|
+
expectType<
|
|
3031
|
+
Readonly<{ a: number[] } & { b: string[] }>,
|
|
3032
|
+
Readonly<{ a: number[] }> & Readonly<{ b: string[] }>
|
|
3033
|
+
>('~=');
|
|
3034
|
+
});
|
|
3035
|
+
});
|
|
3036
|
+
|
|
2834
3037
|
describe('Parenthesized types', () => {
|
|
2835
3038
|
test.each([
|
|
2836
3039
|
{
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Arr } 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 { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import type { TsMorphTransformer } from './types.mjs';
|
|
6
6
|
|
|
7
7
|
const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
8
8
|
|
|
@@ -280,27 +280,49 @@ const isIgnoredByComment = (
|
|
|
280
280
|
* the target of an assignment the element type without `undefined`, so the
|
|
281
281
|
* type comparison in {@link collectAssertionPositions} already passes them
|
|
282
282
|
* over. The exceptions handled here are the ones it does widen.
|
|
283
|
+
*
|
|
284
|
+
* The position is that of the outermost parenthesis around the access:
|
|
285
|
+
* `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
|
|
286
|
+
* does, while the `!` still goes right after the access, inside them.
|
|
283
287
|
*/
|
|
284
288
|
const isAssertableReadPosition = (
|
|
285
289
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
286
290
|
node: IndexReadExpression,
|
|
287
291
|
): boolean => {
|
|
288
|
-
const
|
|
292
|
+
const outer = outermostParenthesized(node);
|
|
293
|
+
|
|
294
|
+
const parent = outer.getParent();
|
|
289
295
|
|
|
290
296
|
if (parent === undefined) {
|
|
291
297
|
return false;
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
return (
|
|
295
|
-
!isAlreadyAssertedOrGuarded(node, parent) &&
|
|
296
|
-
!isWriteTarget(
|
|
297
|
-
!isTestedForAbsence(
|
|
301
|
+
!isAlreadyAssertedOrGuarded(node, outer, parent) &&
|
|
302
|
+
!isWriteTarget(outer, parent) &&
|
|
303
|
+
!isTestedForAbsence(outer, parent)
|
|
298
304
|
);
|
|
299
305
|
};
|
|
300
306
|
|
|
301
|
-
/** `
|
|
307
|
+
/** `node`, or the outermost of the parentheses written around it. */
|
|
308
|
+
const outermostParenthesized = (node: tsm.Node): tsm.Node => {
|
|
309
|
+
const parent = node.getParent();
|
|
310
|
+
|
|
311
|
+
return parent?.isKind(tsm.SyntaxKind.ParenthesizedExpression) === true
|
|
312
|
+
? outermostParenthesized(parent)
|
|
313
|
+
: node;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`.
|
|
318
|
+
*
|
|
319
|
+
* `outer` is `node` with its parentheses (see
|
|
320
|
+
* {@link outermostParenthesized}); a chain continues from the access itself
|
|
321
|
+
* but is guarded from the outside of the parentheses (`(xs[0])?.foo`).
|
|
322
|
+
*/
|
|
302
323
|
const isAlreadyAssertedOrGuarded = (
|
|
303
324
|
node: tsm.Node,
|
|
325
|
+
outer: tsm.Node,
|
|
304
326
|
parent: tsm.Node,
|
|
305
327
|
): boolean => {
|
|
306
328
|
if (
|
|
@@ -322,7 +344,7 @@ const isAlreadyAssertedOrGuarded = (
|
|
|
322
344
|
parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
323
345
|
parent.isKind(tsm.SyntaxKind.CallExpression)) &&
|
|
324
346
|
parent.hasQuestionDotToken() &&
|
|
325
|
-
parent.getExpression() ===
|
|
347
|
+
parent.getExpression() === outer
|
|
326
348
|
);
|
|
327
349
|
};
|
|
328
350
|
|