ts-codemod-lib 3.3.1 → 3.3.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 +38 -16
- package/dist/functions/ast-transformers/append-as-const.mjs +11 -10
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +16 -12
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +32 -9
- 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 +23 -0
- 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.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +8 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +10 -19
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.mjs +101 -49
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/functions/has-call-or-construct-signature.d.mts +18 -0
- package/dist/functions/functions/has-call-or-construct-signature.d.mts.map +1 -0
- package/dist/functions/functions/has-call-or-construct-signature.mjs +87 -0
- package/dist/functions/functions/has-call-or-construct-signature.mjs.map +1 -0
- package/dist/functions/functions/index.d.mts +1 -0
- package/dist/functions/functions/index.d.mts.map +1 -1
- package/dist/functions/functions/index.mjs +1 -0
- package/dist/functions/functions/index.mjs.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +1 -4
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts +14 -5
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +25 -14
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/package.json +43 -41
- package/src/functions/ast-transformers/append-as-const.mts +22 -20
- package/src/functions/ast-transformers/convert-to-readonly.mts +19 -13
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +121 -10
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +32 -9
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +1 -1
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +8 -1
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +13 -22
- package/src/functions/ast-transformers/transform-source-code.mts +158 -57
- package/src/functions/ast-transformers/transform-source-code.test.mts +123 -0
- package/src/functions/functions/has-call-or-construct-signature.mts +120 -0
- package/src/functions/functions/index.mts +1 -0
- package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +1 -5
- package/src/functions/functions/wrap-with-parentheses.mts +30 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-codemod-lib",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -38,44 +38,44 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"ts-data-forge": "^14.
|
|
41
|
+
"ts-data-forge": "^14.7.1",
|
|
42
42
|
"ts-morph": "^28.0.0",
|
|
43
43
|
"ts-type-forge": "^9.2.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "26.
|
|
47
|
-
"@vitest/browser-playwright": "
|
|
48
|
-
"@vitest/coverage-v8": "
|
|
49
|
-
"@vitest/ui": "
|
|
50
|
-
"cspell": "10.
|
|
46
|
+
"@types/node": "26.6.1",
|
|
47
|
+
"@vitest/browser-playwright": "5.0.1",
|
|
48
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
49
|
+
"@vitest/ui": "5.0.1",
|
|
50
|
+
"cspell": "10.3.3",
|
|
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.
|
|
56
|
-
"eslint-plugin-ts-fortress": "1.2.
|
|
57
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
53
|
+
"eslint": "10.10.0",
|
|
54
|
+
"eslint-config-typed": "5.14.0",
|
|
55
|
+
"eslint-plugin-ts-data-forge": "0.8.1",
|
|
56
|
+
"eslint-plugin-ts-fortress": "1.2.3",
|
|
57
|
+
"eslint-plugin-ts-type-forge": "0.8.4",
|
|
58
58
|
"jiti": "2.7.0",
|
|
59
59
|
"npm-run-all2": "9.0.3",
|
|
60
|
-
"playwright": "1.
|
|
61
|
-
"prettier": "3.9.
|
|
60
|
+
"playwright": "1.63.0",
|
|
61
|
+
"prettier": "3.9.7",
|
|
62
62
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
63
63
|
"prettier-plugin-packagejson": "3.0.2",
|
|
64
|
-
"ts-repo-utils": "10.
|
|
65
|
-
"tsx": "4.23.
|
|
64
|
+
"ts-repo-utils": "10.7.1",
|
|
65
|
+
"tsx": "4.23.13",
|
|
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.
|
|
71
|
-
"vitest": "
|
|
70
|
+
"vite": "8.3.0",
|
|
71
|
+
"vitest": "5.0.1"
|
|
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.9.0"
|
|
79
79
|
},
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public",
|
|
@@ -83,37 +83,39 @@
|
|
|
83
83
|
},
|
|
84
84
|
"scripts": {
|
|
85
85
|
"build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
|
|
86
|
-
"
|
|
86
|
+
"check:cspell": "cspell \"**\" --gitignore --gitignore-root ../../ --no-progress",
|
|
87
|
+
"check:dist": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-dist.mts",
|
|
87
88
|
"check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
|
|
89
|
+
"check:lint": "eslint .",
|
|
90
|
+
"check:test": "pnpm run z:vitest:node run",
|
|
91
|
+
"check:test:browser": "pnpm run z:vitest --project='Browser' run",
|
|
92
|
+
"check:test:cov": "pnpm run z:vitest:node run --coverage",
|
|
93
|
+
"check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
88
94
|
"check-all": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-all.mts",
|
|
89
95
|
"clean": "pnpm run /clean:.*/",
|
|
90
96
|
"clean:logs": "npx rimraf ./*.log",
|
|
91
|
-
"codemod": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --uncommitted --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fmt",
|
|
92
|
-
"codemod:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --diff-from origin/main --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fmt",
|
|
93
|
-
"codemod:full": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fmt",
|
|
94
|
-
"cspell": "cspell \"**\" --gitignore --gitignore-root ../../ --no-progress",
|
|
95
97
|
"doc": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/gen-docs.mts",
|
|
96
|
-
"doc:embed": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
|
|
97
98
|
"doc:preview": "vite preview --config ./configs/vite.doc.config.mts",
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"fmt
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"lint": "eslint .",
|
|
105
|
-
"
|
|
106
|
-
"
|
|
99
|
+
"fix:codemod": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --uncommitted --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fix:fmt",
|
|
100
|
+
"fix:codemod:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --diff-from origin/main --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fix:fmt",
|
|
101
|
+
"fix:codemod:full": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../ts-codemod-cli/src/cmd/ts-codemod.mts --transformer append-as-const --transformer convert-interface-to-type --transformer convert-to-readonly --transformer replace-any-with-unknown --transformer replace-record-with-unknown-record '{src,scripts,samples,test}/**/*.{mts,tsx}' && pnpm run fix:fmt",
|
|
102
|
+
"fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
|
|
103
|
+
"fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
|
|
104
|
+
"fix:fmt:full": "prettier --write .",
|
|
105
|
+
"fix:lint": "eslint . --fix",
|
|
106
|
+
"fmt": "pnpm run fix:fmt",
|
|
107
|
+
"gen": "run-s gen:index gen:readme gen:jsdoc",
|
|
108
|
+
"gen:index": "run-s gen:index:src fix:fmt",
|
|
109
|
+
"gen:index:src": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/gen-index-ts.mts ./src --index-ext .mts --export-ext .mjs --target-ext .mts --target-ext .tsx --exclude entry-point.mts",
|
|
110
|
+
"gen:jsdoc": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples-in-jsdoc.mts",
|
|
111
|
+
"gen:readme": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
|
|
107
112
|
"run:test": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/test-code/transform-test-code.mts",
|
|
108
|
-
"test": "pnpm run
|
|
109
|
-
"test:browser": "pnpm run z:vitest --project='Browser' run",
|
|
110
|
-
"test:cov": "pnpm run z:vitest:node run --coverage",
|
|
113
|
+
"test": "pnpm run check:test",
|
|
111
114
|
"test:cov:ui": "vite preview --outDir ./coverage",
|
|
112
115
|
"test:ui": "pnpm run z:vitest:node --ui",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"type-check": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
116
|
+
"watch:doc": "typedoc --options ./configs/typedoc.config.mjs --watch",
|
|
117
|
+
"watch:test": "pnpm run z:vitest:node watch",
|
|
118
|
+
"watch:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit --watch -p ./tsconfig.json",
|
|
117
119
|
"z:vitest": "vitest --config ./configs/vitest.config.mts",
|
|
118
120
|
"z:vitest:node": "pnpm run z:vitest --project='Node.js'"
|
|
119
121
|
}
|
|
@@ -362,28 +362,30 @@ const transformNode = (
|
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
if (tsm.Node.isConditionalExpression(node)) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
node.getWhenTrue(),
|
|
369
|
-
{
|
|
370
|
-
isDirectUnderConstInitializer: context.isDirectUnderConstInitializer,
|
|
371
|
-
isUnderConstContext: false,
|
|
372
|
-
},
|
|
373
|
-
options,
|
|
374
|
-
);
|
|
365
|
+
if (!tsm.Node.isConditionalExpression(node)) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
375
368
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
369
|
+
// For conditional expressions, traverse both branches in a non-const context
|
|
370
|
+
transformNode(
|
|
371
|
+
node.getWhenTrue(),
|
|
372
|
+
{
|
|
373
|
+
isDirectUnderConstInitializer: context.isDirectUnderConstInitializer,
|
|
374
|
+
isUnderConstContext: false,
|
|
375
|
+
},
|
|
376
|
+
options,
|
|
377
|
+
);
|
|
384
378
|
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
transformNode(
|
|
380
|
+
node.getWhenFalse(),
|
|
381
|
+
{
|
|
382
|
+
isDirectUnderConstInitializer: context.isDirectUnderConstInitializer,
|
|
383
|
+
isUnderConstContext: false,
|
|
384
|
+
},
|
|
385
|
+
options,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// return;
|
|
387
389
|
};
|
|
388
390
|
|
|
389
391
|
/**
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type StrictExtract,
|
|
16
16
|
} from 'ts-type-forge';
|
|
17
17
|
import {
|
|
18
|
+
hasCallOrConstructSignature,
|
|
18
19
|
hasDisableNextLineComment,
|
|
19
20
|
isAtomicTypeNode,
|
|
20
21
|
isConditionalTypeDistributionGuard,
|
|
@@ -1032,10 +1033,24 @@ const transformTypeLiteralNode = (
|
|
|
1032
1033
|
return;
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
1036
|
+
// Whether this position is one where the literal is wrapped at all — a
|
|
1037
|
+
// context that already carries `Readonly` or `DeepReadonly` does it, and
|
|
1038
|
+
// inside an indexed access the modifier makes no difference to the result.
|
|
1039
|
+
const wrappingApplies =
|
|
1040
|
+
readonlyContext.type === 'none' && readonlyContext.indexedAccessDepth === 0;
|
|
1041
|
+
|
|
1042
|
+
// A call or construct signature does not survive `Readonly<T>`, so a literal
|
|
1043
|
+
// carrying one is left unwrapped and marked readonly member by member
|
|
1044
|
+
// instead. See `hasCallOrConstructSignature`.
|
|
1045
|
+
const skipWrapping = wrappingApplies && hasCallOrConstructSignature(node);
|
|
1046
|
+
|
|
1035
1047
|
// Recursive processing
|
|
1036
1048
|
transformMembers(
|
|
1037
1049
|
node.getMembers(),
|
|
1038
|
-
|
|
1050
|
+
// A wrapper — the one added below, or the one the context already carries —
|
|
1051
|
+
// is what makes the members readonly, so an inner modifier would be
|
|
1052
|
+
// redundant. Without one, each member has to say so itself.
|
|
1053
|
+
skipWrapping ? 'add' : 'remove',
|
|
1039
1054
|
nextReadonlyContext({
|
|
1040
1055
|
currentReadonlyContext: readonlyContext,
|
|
1041
1056
|
nextReadonlyContextType: 'none',
|
|
@@ -1044,18 +1059,9 @@ const transformTypeLiteralNode = (
|
|
|
1044
1059
|
options,
|
|
1045
1060
|
);
|
|
1046
1061
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
// Don't wrap with Readonly if already readonly
|
|
1051
|
-
return;
|
|
1052
|
-
|
|
1053
|
-
case 'none': {
|
|
1054
|
-
if (readonlyContext.indexedAccessDepth === 0) {
|
|
1055
|
-
// `{ readonly x: X, readonly y: Y } |-> Readonly<{ x: X, y: Y }>`
|
|
1056
|
-
options.replaceNode(node, `Readonly<${node.getFullText()}>`);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1062
|
+
if (wrappingApplies && !skipWrapping) {
|
|
1063
|
+
// `{ readonly x: X, readonly y: Y } |-> Readonly<{ x: X, y: Y }>`
|
|
1064
|
+
options.replaceNode(node, `Readonly<${node.getFullText()}>`);
|
|
1059
1065
|
}
|
|
1060
1066
|
};
|
|
1061
1067
|
|
|
@@ -6,7 +6,6 @@ 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 { isString } from 'ts-data-forge';
|
|
10
9
|
import {
|
|
11
10
|
convertToReadonlyTransformer,
|
|
12
11
|
type ReadonlyTransformerOptions,
|
|
@@ -64,15 +63,8 @@ const normalizeWhitespaceForComparison = (code: string): string => {
|
|
|
64
63
|
// 1. Protect newlines immediately following line comments
|
|
65
64
|
const protectedCode = code.replaceAll(
|
|
66
65
|
/(\/\/.*?)\r?\n/gu,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// is not something the signature can promise. This group is not optional,
|
|
70
|
-
// so the narrowing never actually fails — it just has to be written.
|
|
71
|
-
(match: string, ...args: readonly unknown[]): string => {
|
|
72
|
-
const comment = args[0];
|
|
73
|
-
|
|
74
|
-
return isString(comment) ? `${comment}${placeholder}` : match;
|
|
75
|
-
},
|
|
66
|
+
(match: string, comment: string | undefined): string =>
|
|
67
|
+
comment === undefined ? match : `${comment}${placeholder}`,
|
|
76
68
|
);
|
|
77
69
|
|
|
78
70
|
// 2. Collapse multiple whitespace characters (including unprotected newlines) into a single space
|
|
@@ -3255,6 +3247,125 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
3255
3247
|
])('$name', testFn);
|
|
3256
3248
|
});
|
|
3257
3249
|
|
|
3250
|
+
describe('Call and construct signatures', () => {
|
|
3251
|
+
// `Readonly<T>` is a mapped type over `keyof T`, and a call or construct
|
|
3252
|
+
// signature is not a property, so wrapping a type literal that carries one
|
|
3253
|
+
// silently drops it — the value stops being callable. See #1881.
|
|
3254
|
+
test.each([
|
|
3255
|
+
{
|
|
3256
|
+
name: 'Overloaded call signatures are not wrapped',
|
|
3257
|
+
source: dedent`
|
|
3258
|
+
const panic: {
|
|
3259
|
+
(message: string, options?: { cause?: unknown }): never;
|
|
3260
|
+
(error: Error): never;
|
|
3261
|
+
} = impl;
|
|
3262
|
+
`,
|
|
3263
|
+
expected: dedent`
|
|
3264
|
+
const panic: {
|
|
3265
|
+
(message: string, options?: Readonly<{ cause?: unknown }>): never;
|
|
3266
|
+
(error: Error): never;
|
|
3267
|
+
} = impl;
|
|
3268
|
+
`,
|
|
3269
|
+
},
|
|
3270
|
+
{
|
|
3271
|
+
name: 'A single call signature is not wrapped',
|
|
3272
|
+
source: dedent`
|
|
3273
|
+
type F = { (arg: number[]): string[] };
|
|
3274
|
+
`,
|
|
3275
|
+
expected: dedent`
|
|
3276
|
+
type F = { (arg: readonly number[]): readonly string[] };
|
|
3277
|
+
`,
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
name: 'A construct signature is not wrapped',
|
|
3281
|
+
source: dedent`
|
|
3282
|
+
type C = { new (arg: string[]): { prop: number[] } };
|
|
3283
|
+
`,
|
|
3284
|
+
expected: dedent`
|
|
3285
|
+
type C = { new (arg: readonly string[]): Readonly<{ prop: readonly number[] }> };
|
|
3286
|
+
`,
|
|
3287
|
+
},
|
|
3288
|
+
{
|
|
3289
|
+
name: 'Members alongside a call signature are marked readonly in place',
|
|
3290
|
+
source: dedent`
|
|
3291
|
+
type F = {
|
|
3292
|
+
(arg: number[]): void;
|
|
3293
|
+
prop: string[];
|
|
3294
|
+
[key: string]: unknown;
|
|
3295
|
+
};
|
|
3296
|
+
`,
|
|
3297
|
+
expected: dedent`
|
|
3298
|
+
type F = {
|
|
3299
|
+
(arg: readonly number[]): void;
|
|
3300
|
+
readonly prop: readonly string[];
|
|
3301
|
+
readonly [key: string]: unknown;
|
|
3302
|
+
};
|
|
3303
|
+
`,
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
name: 'Members alongside a construct signature are marked readonly in place',
|
|
3307
|
+
source: dedent`
|
|
3308
|
+
type C = {
|
|
3309
|
+
new (arg: string[]): unknown;
|
|
3310
|
+
readonly prop: number[];
|
|
3311
|
+
};
|
|
3312
|
+
`,
|
|
3313
|
+
expected: dedent`
|
|
3314
|
+
type C = {
|
|
3315
|
+
new (arg: readonly string[]): unknown;
|
|
3316
|
+
readonly prop: readonly number[];
|
|
3317
|
+
};
|
|
3318
|
+
`,
|
|
3319
|
+
},
|
|
3320
|
+
{
|
|
3321
|
+
name: 'A callable type literal in a union is left alone',
|
|
3322
|
+
source: dedent`
|
|
3323
|
+
type T = { (): void } | { a: number[] };
|
|
3324
|
+
`,
|
|
3325
|
+
expected: dedent`
|
|
3326
|
+
type T = { (): void } | Readonly<{ a: readonly number[] }>;
|
|
3327
|
+
`,
|
|
3328
|
+
},
|
|
3329
|
+
{
|
|
3330
|
+
name: 'A callable type literal in an intersection is left alone',
|
|
3331
|
+
source: dedent`
|
|
3332
|
+
type T = { (): void } & { a: number[] };
|
|
3333
|
+
`,
|
|
3334
|
+
expected: dedent`
|
|
3335
|
+
type T = { (): void } & Readonly<{ a: readonly number[] }>;
|
|
3336
|
+
`,
|
|
3337
|
+
},
|
|
3338
|
+
{
|
|
3339
|
+
name: 'A nested callable type literal is left alone',
|
|
3340
|
+
source: dedent`
|
|
3341
|
+
type T = { fn: { (): void }; a: number[] };
|
|
3342
|
+
`,
|
|
3343
|
+
expected: dedent`
|
|
3344
|
+
type T = Readonly<{ fn: { (): void }; a: readonly number[] }>;
|
|
3345
|
+
`,
|
|
3346
|
+
},
|
|
3347
|
+
])('$name', testFn);
|
|
3348
|
+
|
|
3349
|
+
test('a callable type literal reaches a fixed point', () => {
|
|
3350
|
+
const code = dedent`
|
|
3351
|
+
const panic: {
|
|
3352
|
+
(message: string, options?: { cause?: unknown }): never;
|
|
3353
|
+
(error: Error): never;
|
|
3354
|
+
} = impl;
|
|
3355
|
+
`;
|
|
3356
|
+
|
|
3357
|
+
const once = transformSourceCode(code, false, [
|
|
3358
|
+
convertToReadonlyTransformer(),
|
|
3359
|
+
]);
|
|
3360
|
+
|
|
3361
|
+
const twice = transformSourceCode(once, false, [
|
|
3362
|
+
convertToReadonlyTransformer(),
|
|
3363
|
+
]);
|
|
3364
|
+
|
|
3365
|
+
expect(twice).toBe(once);
|
|
3366
|
+
});
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3258
3369
|
describe('Generic Types', () => {
|
|
3259
3370
|
test.each([
|
|
3260
3371
|
{
|
|
@@ -37,17 +37,40 @@ const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
|
37
37
|
* as the `if` of `const v = xs[0]!; if (v === undefined) …` now is.
|
|
38
38
|
*
|
|
39
39
|
* @example
|
|
40
|
+
*
|
|
40
41
|
* ```ts
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* const before = dedent`
|
|
43
|
+
* declare const xs: readonly number[];
|
|
44
|
+
* declare const pair: readonly [number, number];
|
|
45
|
+
* declare const rec: Record<string, number>;
|
|
46
|
+
* declare const known: { a: number };
|
|
47
|
+
*
|
|
48
|
+
* xs[0].toFixed();
|
|
49
|
+
* rec['a'].toFixed();
|
|
50
|
+
* rec.a.toFixed();
|
|
51
|
+
* pair[1].toFixed();
|
|
52
|
+
* known.a.toFixed();
|
|
53
|
+
* `;
|
|
54
|
+
*
|
|
55
|
+
* const after = transformSourceCode(before, false, [
|
|
56
|
+
* enableNoUncheckedIndexedAccessTransformer(),
|
|
57
|
+
* ]);
|
|
58
|
+
*
|
|
59
|
+
* assert.deepStrictEqual(
|
|
60
|
+
* after.trim(),
|
|
61
|
+
* dedent`
|
|
62
|
+
* declare const xs: readonly number[];
|
|
63
|
+
* declare const pair: readonly [number, number];
|
|
64
|
+
* declare const rec: Record<string, number>;
|
|
65
|
+
* declare const known: { a: number };
|
|
45
66
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
67
|
+
* xs[0]!.toFixed();
|
|
68
|
+
* rec['a']!.toFixed();
|
|
69
|
+
* rec.a!.toFixed();
|
|
70
|
+
* pair[1].toFixed();
|
|
71
|
+
* known.a.toFixed();
|
|
72
|
+
* `,
|
|
73
|
+
* );
|
|
51
74
|
* ```
|
|
52
75
|
*/
|
|
53
76
|
export const enableNoUncheckedIndexedAccessTransformer = (
|
|
@@ -84,7 +84,7 @@ const transformInProject = (
|
|
|
84
84
|
* off. A case that went through it therefore paid for the whole default
|
|
85
85
|
* library, `lib.esnext.full.d.ts` with the DOM inside it, being parsed and
|
|
86
86
|
* checked from scratch: ~300 ms a case, of which the file under test was a
|
|
87
|
-
* fraction of a millisecond. Sixty of those took `test:cov` past Vitest's
|
|
87
|
+
* fraction of a millisecond. Sixty of those took `check:test:cov` past Vitest's
|
|
88
88
|
* timeout on CI.
|
|
89
89
|
*
|
|
90
90
|
* Reusing the project is half of the answer and the smaller half — measured,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as tsm from 'ts-morph';
|
|
2
2
|
import {
|
|
3
|
+
hasCallOrConstructSignature,
|
|
3
4
|
isAtomicTypeNode,
|
|
4
5
|
isReadonlyTupleOrArrayTypeNode,
|
|
5
6
|
isReadonlyTypeReferenceNode,
|
|
@@ -121,8 +122,14 @@ export const groupUnionIntersectionTypes = (
|
|
|
121
122
|
continue;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// A type literal that declares a call or construct signature is deliberately
|
|
126
|
+
// not grouped here: this group is joined and wrapped in a single
|
|
127
|
+
// `Readonly<...>`, which such a literal does not survive. It falls through
|
|
128
|
+
// to `others`, which leaves it as written. See
|
|
129
|
+
// `hasCallOrConstructSignature`.
|
|
124
130
|
if (
|
|
125
|
-
t.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
131
|
+
(t.isKind(tsm.SyntaxKind.TypeLiteral) &&
|
|
132
|
+
!hasCallOrConstructSignature(t)) ||
|
|
126
133
|
isReadonlyTypeReferenceNode(t)
|
|
127
134
|
) {
|
|
128
135
|
if (mut_grouped.typeLiterals === undefined) {
|
|
@@ -44,9 +44,7 @@ const processDeclarations = (
|
|
|
44
44
|
|
|
45
45
|
const typeNode = typeAlias.getTypeNode();
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return visitTypeNode(typeNode);
|
|
47
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
50
48
|
});
|
|
51
49
|
|
|
52
50
|
const interfaceModifications = container
|
|
@@ -103,9 +101,7 @@ const processInterfaceDeclaration = (
|
|
|
103
101
|
.map((property) => {
|
|
104
102
|
const typeNode = property.getTypeNode();
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return visitTypeNode(typeNode);
|
|
104
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
109
105
|
})
|
|
110
106
|
.includes(true);
|
|
111
107
|
};
|
|
@@ -170,13 +166,13 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
|
170
166
|
if (tsm.Node.isPropertySignature(member)) {
|
|
171
167
|
const typeNode = member.getTypeNode();
|
|
172
168
|
|
|
173
|
-
return typeNode
|
|
169
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
174
170
|
}
|
|
175
171
|
|
|
176
172
|
if (tsm.Node.isIndexSignatureDeclaration(member)) {
|
|
177
173
|
const typeNode = member.getReturnTypeNode();
|
|
178
174
|
|
|
179
|
-
return typeNode
|
|
175
|
+
return typeNode !== undefined && visitTypeNode(typeNode);
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
return false;
|
|
@@ -202,19 +198,12 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
|
202
198
|
}
|
|
203
199
|
|
|
204
200
|
// Handles `readonly T[]`, `readonly [...]`, `keyof T`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (tsm.Node.isNamedTupleMember(node)) {
|
|
214
|
-
return visitTypeNode(node.getTypeNode());
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return false;
|
|
201
|
+
return (
|
|
202
|
+
(tsm.Node.isTypeOperatorTypeNode(node) ||
|
|
203
|
+
tsm.Node.isRestTypeNode(node) ||
|
|
204
|
+
tsm.Node.isNamedTupleMember(node)) &&
|
|
205
|
+
visitTypeNode(node.getTypeNode())
|
|
206
|
+
);
|
|
218
207
|
};
|
|
219
208
|
|
|
220
209
|
const isStringUnknownIndexSignature = (
|
|
@@ -305,7 +294,9 @@ const ensureUnknownRecordImport = (sourceFile: tsm.SourceFile): void => {
|
|
|
305
294
|
.getNamedImports()
|
|
306
295
|
.some((ni) => ni.getName() === UNKNOWN_RECORD_NAME);
|
|
307
296
|
|
|
308
|
-
if (alreadyImported)
|
|
297
|
+
if (alreadyImported) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
309
300
|
|
|
310
301
|
existingImport.addNamedImport({
|
|
311
302
|
name: UNKNOWN_RECORD_NAME,
|