ts-codemod-lib 2.2.0 → 2.2.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/dist/cmd/append-as-const.mjs +51 -48
- package/dist/cmd/append-as-const.mjs.map +1 -1
- package/dist/cmd/convert-interface-to-type.mjs +51 -48
- package/dist/cmd/convert-interface-to-type.mjs.map +1 -1
- package/dist/cmd/convert-to-readonly.mjs +51 -48
- package/dist/cmd/convert-to-readonly.mjs.map +1 -1
- package/dist/cmd/replace-any-with-unknown.mjs +51 -48
- package/dist/cmd/replace-any-with-unknown.mjs.map +1 -1
- package/dist/cmd/replace-record-with-unknown-record.mjs +51 -48
- package/dist/cmd/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/cmd/run-transformer-cli.d.mts.map +1 -1
- package/dist/cmd/run-transformer-cli.mjs +138 -127
- package/dist/cmd/run-transformer-cli.mjs.map +1 -1
- package/dist/entry-point.mjs +1 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +205 -191
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +62 -82
- package/dist/functions/ast-transformers/convert-interface-to-type.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 +743 -828
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +2 -14
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts +2 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +8 -8
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.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 +51 -62
- 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 +12 -12
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +16 -14
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +25 -27
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +149 -208
- 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 +70 -72
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/constants/ignore-comment-text.d.mts +2 -2
- package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -1
- package/dist/functions/constants/ignore-comment-text.mjs +8 -8
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.mjs +33 -65
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- 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/is-as-const-node.d.mts +1 -1
- package/dist/functions/functions/is-as-const-node.d.mts.map +1 -1
- package/dist/functions/functions/is-as-const-node.mjs +15 -23
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.d.mts.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.mjs +22 -36
- package/dist/functions/functions/is-atomic-type-node.mjs.map +1 -1
- package/dist/functions/functions/is-conditional-type-distribution-guard.d.mts +23 -0
- package/dist/functions/functions/is-conditional-type-distribution-guard.d.mts.map +1 -0
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs +16 -0
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs.map +1 -0
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +11 -21
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/is-spread-parameter-node.d.mts.map +1 -1
- package/dist/functions/functions/is-spread-parameter-node.mjs +2 -4
- package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -1
- package/dist/functions/functions/remove-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/remove-parentheses.mjs +1 -4
- package/dist/functions/functions/remove-parentheses.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 +7 -24
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
- package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -1
- package/dist/functions/functions/unwrap-readonly.mjs +1 -3
- package/dist/functions/functions/unwrap-readonly.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +26 -51
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/index.mjs +1 -0
- package/dist/functions/index.mjs.map +1 -1
- package/dist/functions/utils/replace-with-debug.d.mts.map +1 -1
- package/dist/functions/utils/replace-with-debug.mjs +2 -2
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +32 -31
- package/src/cmd/append-as-const.mts +3 -3
- package/src/cmd/convert-interface-to-type.mts +3 -3
- package/src/cmd/convert-to-readonly.mts +3 -3
- package/src/cmd/replace-any-with-unknown.mts +3 -3
- package/src/cmd/replace-record-with-unknown-record.mts +3 -3
- package/src/cmd/run-transformer-cli.mts +21 -17
- package/src/functions/ast-transformers/convert-interface-to-type.mts +1 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +77 -37
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +117 -1
- package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +6 -6
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +4 -4
- package/src/functions/functions/has-disable-next-line-comment.mts +5 -5
- package/src/functions/functions/index.mts +1 -0
- package/src/functions/functions/is-conditional-type-distribution-guard.mts +45 -0
- package/src/functions/functions/wrap-with-parentheses.mts +17 -23
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as cmd from 'cmd-ts';
|
|
4
|
-
import { Result } from 'ts-data-forge';
|
|
4
|
+
import { Arr, Result } from 'ts-data-forge';
|
|
5
5
|
import { convertToReadonlyTransformer } from '../functions/index.mjs';
|
|
6
6
|
import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ const transformer = convertToReadonlyTransformer();
|
|
|
9
9
|
|
|
10
10
|
const cmdDef = cmd.command({
|
|
11
11
|
name: transformer.name,
|
|
12
|
-
version: '2.2.
|
|
12
|
+
version: '2.2.2',
|
|
13
13
|
args: {
|
|
14
14
|
baseDir: cmd.positional({
|
|
15
15
|
type: cmd.string,
|
|
@@ -64,4 +64,4 @@ const cmdDef = cmd.command({
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
await cmd.run(cmdDef, process.argv
|
|
67
|
+
await cmd.run(cmdDef, Arr.skip(process.argv, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as cmd from 'cmd-ts';
|
|
4
|
-
import { Result } from 'ts-data-forge';
|
|
4
|
+
import { Arr, Result } from 'ts-data-forge';
|
|
5
5
|
import { replaceAnyWithUnknownTransformer } from '../functions/index.mjs';
|
|
6
6
|
import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ const transformer = replaceAnyWithUnknownTransformer();
|
|
|
9
9
|
|
|
10
10
|
const cmdDef = cmd.command({
|
|
11
11
|
name: transformer.name,
|
|
12
|
-
version: '2.2.
|
|
12
|
+
version: '2.2.2',
|
|
13
13
|
args: {
|
|
14
14
|
baseDir: cmd.positional({
|
|
15
15
|
type: cmd.string,
|
|
@@ -64,4 +64,4 @@ const cmdDef = cmd.command({
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
await cmd.run(cmdDef, process.argv
|
|
67
|
+
await cmd.run(cmdDef, Arr.skip(process.argv, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as cmd from 'cmd-ts';
|
|
4
|
-
import { Result } from 'ts-data-forge';
|
|
4
|
+
import { Arr, Result } from 'ts-data-forge';
|
|
5
5
|
import { replaceRecordWithUnknownRecordTransformer } from '../functions/index.mjs';
|
|
6
6
|
import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ const transformer = replaceRecordWithUnknownRecordTransformer();
|
|
|
9
9
|
|
|
10
10
|
const cmdDef = cmd.command({
|
|
11
11
|
name: transformer.name,
|
|
12
|
-
version: '2.2.
|
|
12
|
+
version: '2.2.2',
|
|
13
13
|
args: {
|
|
14
14
|
baseDir: cmd.positional({
|
|
15
15
|
type: cmd.string,
|
|
@@ -64,4 +64,4 @@ const cmdDef = cmd.command({
|
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
await cmd.run(cmdDef, process.argv
|
|
67
|
+
await cmd.run(cmdDef, Arr.skip(process.argv, 2));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
-
import { Arr, Result, unknownToString } from 'ts-data-forge';
|
|
6
|
+
import { Arr, expectType, Result, unknownToString } from 'ts-data-forge';
|
|
7
7
|
import {
|
|
8
8
|
getDiffFrom,
|
|
9
9
|
getModifiedFiles,
|
|
@@ -199,8 +199,16 @@ const getFilesFromGlob = async (
|
|
|
199
199
|
>
|
|
200
200
|
> => {
|
|
201
201
|
const globResult = await glob(baseDir, {
|
|
202
|
-
|
|
202
|
+
// Never transform files inside dependencies. With pnpm,
|
|
203
|
+
// `packages/*/node_modules/<dep>` are symlinks whose `src/` directory would
|
|
204
|
+
// otherwise be matched by patterns such as
|
|
205
|
+
// `packages/**/{src,scripts,samples,test}/**`, so the codemod would rewrite
|
|
206
|
+
// dependency source files (and could hit pathological types there).
|
|
207
|
+
ignore: Arr.toPushed(exclude, '**/node_modules/**'),
|
|
203
208
|
absolute: true,
|
|
209
|
+
// Do not escape the project tree by following symlinks (e.g. the pnpm
|
|
210
|
+
// `node_modules` symlinks above, or workspace-package links).
|
|
211
|
+
followSymbolicLinks: false,
|
|
204
212
|
});
|
|
205
213
|
|
|
206
214
|
if (Result.isErr(globResult)) {
|
|
@@ -235,16 +243,12 @@ const transformFiles = async (
|
|
|
235
243
|
const result = await transformOneFile(filePath, transformers, silent);
|
|
236
244
|
|
|
237
245
|
if (Result.isOk(result)) {
|
|
238
|
-
|
|
239
|
-
case 'transformed':
|
|
240
|
-
mut_transformedCount += 1;
|
|
246
|
+
expectType<typeof result.value, 'unchanged' | 'transformed'>('=');
|
|
241
247
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
break;
|
|
248
|
+
if (result.value === 'transformed') {
|
|
249
|
+
mut_transformedCount += 1;
|
|
250
|
+
} else {
|
|
251
|
+
mut_unchangedCount += 1;
|
|
248
252
|
}
|
|
249
253
|
} else {
|
|
250
254
|
mut_errorFiles.push(path.basename(filePath));
|
|
@@ -285,14 +289,14 @@ const transformOneFile = async (
|
|
|
285
289
|
echoIfNotSilent(`⏭️ ${fileName} - no changes needed`);
|
|
286
290
|
|
|
287
291
|
return Result.ok('unchanged');
|
|
288
|
-
}
|
|
289
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
290
|
-
await fs.writeFile(filePath, transformedCode, 'utf8');
|
|
292
|
+
}
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
295
|
+
await fs.writeFile(filePath, transformedCode, 'utf8');
|
|
293
296
|
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
echoIfNotSilent(`✅ ${fileName} - transformed`);
|
|
298
|
+
|
|
299
|
+
return Result.ok('transformed');
|
|
296
300
|
} catch (error) {
|
|
297
301
|
const errStr = unknownToString(error);
|
|
298
302
|
|
|
@@ -70,7 +70,7 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
|
|
|
70
70
|
|
|
71
71
|
const ownType = buildTypeLiteral(members);
|
|
72
72
|
|
|
73
|
-
return
|
|
73
|
+
return Arr.toPushed(extendedTypesWithMembers, ownType).join(' & ');
|
|
74
74
|
})();
|
|
75
75
|
|
|
76
76
|
// Get export keyword
|
|
@@ -12,6 +12,7 @@ import { type DeepReadonly, type SafeUintWithSmallInt } from 'ts-type-forge';
|
|
|
12
12
|
import {
|
|
13
13
|
hasDisableNextLineComment,
|
|
14
14
|
isAtomicTypeNode,
|
|
15
|
+
isConditionalTypeDistributionGuard,
|
|
15
16
|
isReadonlyArrayTypeNode,
|
|
16
17
|
isReadonlyTupleOrArrayTypeNode,
|
|
17
18
|
isReadonlyTupleTypeNode,
|
|
@@ -705,7 +706,12 @@ const transformArrayTypeNode = (
|
|
|
705
706
|
// Unnecessary `Readonly` wrapper will be remove in transformTypeReferenceNode
|
|
706
707
|
case 'Readonly':
|
|
707
708
|
case 'none':
|
|
708
|
-
|
|
709
|
+
// Skip the `A[] extends B[] ? ...` union-distribution-guard idiom: adding
|
|
710
|
+
// `readonly` there does not improve immutability and breaks the idiom.
|
|
711
|
+
if (
|
|
712
|
+
readonlyContext.indexedAccessDepth === 0 &&
|
|
713
|
+
!isConditionalTypeDistributionGuard(node)
|
|
714
|
+
) {
|
|
709
715
|
const readonlyText = `readonly ${node.getFullText()}` as const;
|
|
710
716
|
|
|
711
717
|
options.replaceNode(
|
|
@@ -752,7 +758,12 @@ const transformTupleTypeNode = (
|
|
|
752
758
|
// Unnecessary `Readonly` wrapper will be remove in transformTypeReferenceNode
|
|
753
759
|
case 'Readonly':
|
|
754
760
|
case 'none':
|
|
755
|
-
|
|
761
|
+
// Skip the `[A] extends [B] ? ...` union-distribution-guard idiom: adding
|
|
762
|
+
// `readonly` there does not improve immutability and breaks the idiom.
|
|
763
|
+
if (
|
|
764
|
+
readonlyContext.indexedAccessDepth === 0 &&
|
|
765
|
+
!isConditionalTypeDistributionGuard(node)
|
|
766
|
+
) {
|
|
756
767
|
const readonlyText = `readonly ${node.getFullText()}` as const;
|
|
757
768
|
|
|
758
769
|
options.replaceNode(
|
|
@@ -1043,50 +1054,54 @@ const transformClassDeclarationNode = (
|
|
|
1043
1054
|
|
|
1044
1055
|
for (const ctor of node.getConstructors()) {
|
|
1045
1056
|
for (const param of ctor.getParameters()) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
'skipped class constructor parameter by disable-next-line comment',
|
|
1049
|
-
);
|
|
1057
|
+
transformConstructorParameter(param, options);
|
|
1058
|
+
}
|
|
1050
1059
|
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1060
|
+
const body = ctor.getBody();
|
|
1053
1061
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1062
|
+
if (body !== undefined) {
|
|
1063
|
+
transformNode(body, initialReadonlyContext, options);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1056
1067
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1068
|
+
const transformConstructorParameter = (
|
|
1069
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1070
|
+
param: tsm.ParameterDeclaration,
|
|
1071
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
1072
|
+
): void => {
|
|
1073
|
+
if (hasDisableNextLineComment(param)) {
|
|
1074
|
+
options.debugPrint(
|
|
1075
|
+
'skipped class constructor parameter by disable-next-line comment',
|
|
1076
|
+
);
|
|
1061
1077
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1064
1080
|
|
|
1065
|
-
|
|
1066
|
-
// protected -> protected readonly
|
|
1067
|
-
// private -> private readonly
|
|
1068
|
-
if (
|
|
1069
|
-
scope === tsm.Scope.Public ||
|
|
1070
|
-
scope === tsm.Scope.Protected ||
|
|
1071
|
-
scope === tsm.Scope.Private
|
|
1072
|
-
) {
|
|
1073
|
-
param.setIsReadonly(true);
|
|
1074
|
-
}
|
|
1081
|
+
const name = param.getName();
|
|
1075
1082
|
|
|
1076
|
-
|
|
1083
|
+
if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1077
1086
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1087
|
+
// Check if parameter is a property declaration (public/protected/private)
|
|
1088
|
+
const scope = param.getScope();
|
|
1082
1089
|
|
|
1083
|
-
|
|
1090
|
+
// public -> public readonly
|
|
1091
|
+
// protected -> protected readonly
|
|
1092
|
+
// private -> private readonly
|
|
1093
|
+
if (
|
|
1094
|
+
scope === tsm.Scope.Public ||
|
|
1095
|
+
scope === tsm.Scope.Protected ||
|
|
1096
|
+
scope === tsm.Scope.Private
|
|
1097
|
+
) {
|
|
1098
|
+
param.setIsReadonly(true);
|
|
1099
|
+
}
|
|
1084
1100
|
|
|
1085
|
-
|
|
1086
|
-
transformNode(body, initialReadonlyContext, options);
|
|
1087
|
-
}
|
|
1101
|
+
const type = param.getTypeNode();
|
|
1088
1102
|
|
|
1089
|
-
|
|
1103
|
+
if (type !== undefined) {
|
|
1104
|
+
transformNode(type, initialReadonlyContext, options);
|
|
1090
1105
|
}
|
|
1091
1106
|
};
|
|
1092
1107
|
|
|
@@ -1238,6 +1253,20 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1238
1253
|
|
|
1239
1254
|
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
1240
1255
|
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
1256
|
+
// Readonly<number & { x: X } & { y: Y } & readonly E[]>
|
|
1257
|
+
// -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
|
|
1258
|
+
|
|
1259
|
+
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
1260
|
+
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
1261
|
+
//
|
|
1262
|
+
// NOTE: The result is wrapped in parentheses (via `unionToString`). This is
|
|
1263
|
+
// intentional and required for precedence safety: without it, a member such
|
|
1264
|
+
// as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
|
|
1265
|
+
// operator would produce invalid syntax when this node is spliced back in.
|
|
1266
|
+
// Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
|
|
1267
|
+
// collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
|
|
1268
|
+
// — see the note there. That prevents the parentheses from growing without
|
|
1269
|
+
// bound when a union/intersection sits inside a `ParenthesizedType`.
|
|
1241
1270
|
options.replaceNode(
|
|
1242
1271
|
node,
|
|
1243
1272
|
unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
|
|
@@ -1296,7 +1325,18 @@ const transformParenthesizedTypeNode = (
|
|
|
1296
1325
|
isAtomicTypeNode(T) ||
|
|
1297
1326
|
// remove () if T is TypeLiteralNode
|
|
1298
1327
|
// e.g. `({ member: V }) |-> { member: V }`
|
|
1299
|
-
T.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
1328
|
+
T.isKind(tsm.SyntaxKind.TypeLiteral) ||
|
|
1329
|
+
// collapse redundant nested parentheses if T is itself a ParenthesizedType
|
|
1330
|
+
// e.g. `((X)) |-> (X)`
|
|
1331
|
+
//
|
|
1332
|
+
// This is what makes the transform terminate on nested parentheses. The
|
|
1333
|
+
// union/intersection handler defensively wraps its result in parentheses
|
|
1334
|
+
// for precedence safety, which turns `(A | B)` into `((A | B))` when the
|
|
1335
|
+
// union already sits inside a `ParenthesizedType`. Collapsing the redundant
|
|
1336
|
+
// outer layer here means each pass removes exactly the layer the wrapping
|
|
1337
|
+
// added, so the parentheses no longer grow without bound (which previously
|
|
1338
|
+
// overflowed the parser stack on inputs like `type T = (('a' | 'b'))`).
|
|
1339
|
+
T.isKind(tsm.SyntaxKind.ParenthesizedType)
|
|
1300
1340
|
) {
|
|
1301
1341
|
options.replaceNode(node, T.getFullText());
|
|
1302
1342
|
}
|
|
@@ -59,7 +59,10 @@ const normalizeWhitespaceForComparison = (code: string): string => {
|
|
|
59
59
|
const placeholder = '___LINE_COMMENT_NEWLINE_PLACEHOLDER___';
|
|
60
60
|
|
|
61
61
|
// 1. Protect newlines immediately following line comments
|
|
62
|
-
const protectedCode = code.replaceAll(
|
|
62
|
+
const protectedCode = code.replaceAll(
|
|
63
|
+
/(\/\/.*?)\r?\n/gu,
|
|
64
|
+
(_match, comment: string) => `${comment}${placeholder}`,
|
|
65
|
+
);
|
|
63
66
|
|
|
64
67
|
// 2. Collapse multiple whitespace characters (including unprotected newlines) into a single space
|
|
65
68
|
const collapsedCode = protectedCode.replaceAll(/\s+/gu, ' ');
|
|
@@ -738,6 +741,56 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
738
741
|
},
|
|
739
742
|
])('$name', testFn);
|
|
740
743
|
|
|
744
|
+
describe('Conditional type union-distribution-guard idiom', () => {
|
|
745
|
+
test.each([
|
|
746
|
+
{
|
|
747
|
+
name: 'Single-element tuples in check and extends positions are left as-is',
|
|
748
|
+
source: dedent`
|
|
749
|
+
type TypeExtends<A, B> = [A] extends [B] ? true : false;
|
|
750
|
+
`,
|
|
751
|
+
expected: dedent`
|
|
752
|
+
type TypeExtends<A, B> = [A] extends [B] ? true : false;
|
|
753
|
+
`,
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'Multi-element guard tuples are left as-is',
|
|
757
|
+
source: dedent`
|
|
758
|
+
type Eq<A, B> = [A, B] extends [B, A] ? true : false;
|
|
759
|
+
`,
|
|
760
|
+
expected: dedent`
|
|
761
|
+
type Eq<A, B> = [A, B] extends [B, A] ? true : false;
|
|
762
|
+
`,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'Tuples in true/false branches are still converted',
|
|
766
|
+
source: dedent`
|
|
767
|
+
type T<A> = [A] extends [string] ? [A, A] : [number];
|
|
768
|
+
`,
|
|
769
|
+
expected: dedent`
|
|
770
|
+
type T<A> = [A] extends [string] ? readonly [A, A] : readonly [number];
|
|
771
|
+
`,
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'Arrays in check and extends positions are left as-is',
|
|
775
|
+
source: dedent`
|
|
776
|
+
type ArrayExtends<A, B> = A[] extends B[] ? true : false;
|
|
777
|
+
`,
|
|
778
|
+
expected: dedent`
|
|
779
|
+
type ArrayExtends<A, B> = A[] extends B[] ? true : false;
|
|
780
|
+
`,
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: 'Arrays in true/false branches are still converted',
|
|
784
|
+
source: dedent`
|
|
785
|
+
type T<A> = A[] extends string[] ? A[] : number[];
|
|
786
|
+
`,
|
|
787
|
+
expected: dedent`
|
|
788
|
+
type T<A> = A[] extends string[] ? readonly A[] : readonly number[];
|
|
789
|
+
`,
|
|
790
|
+
},
|
|
791
|
+
])('$name', testFn);
|
|
792
|
+
});
|
|
793
|
+
|
|
741
794
|
describe('Rest types', () => {
|
|
742
795
|
test.each([
|
|
743
796
|
{
|
|
@@ -2574,6 +2627,69 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
2574
2627
|
type ParenReadonly = Readonly<{ a: number }>
|
|
2575
2628
|
`,
|
|
2576
2629
|
},
|
|
2630
|
+
{
|
|
2631
|
+
// Regression: redundant nested parentheses around a union used to make
|
|
2632
|
+
// the transformer re-wrap the union on every pass, growing parentheses
|
|
2633
|
+
// without bound until ts-morph's parser overflowed its stack (hang).
|
|
2634
|
+
name: 'Doubled parentheses around a union (no infinite paren growth)',
|
|
2635
|
+
source: dedent`
|
|
2636
|
+
type DoubleParenUnion = (('a' | 'b'));
|
|
2637
|
+
`,
|
|
2638
|
+
expected: dedent`
|
|
2639
|
+
type DoubleParenUnion = 'a' | 'b';
|
|
2640
|
+
`,
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
name: 'Doubled parentheses around an object union (no infinite paren growth)',
|
|
2644
|
+
source: dedent`
|
|
2645
|
+
type DoubleParenObjUnion = (({ a: number[] } | { b: string[] }));
|
|
2646
|
+
`,
|
|
2647
|
+
expected: dedent`
|
|
2648
|
+
type DoubleParenObjUnion = Readonly<
|
|
2649
|
+
{ a: readonly number[] } | { b: readonly string[] }
|
|
2650
|
+
>;
|
|
2651
|
+
`,
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
name: 'Doubled parentheses around a union in a conditional type (no infinite paren growth)',
|
|
2655
|
+
source: dedent`
|
|
2656
|
+
type DoubleParenCond = true extends true ? (('a' | 'b')) : 'c';
|
|
2657
|
+
`,
|
|
2658
|
+
expected: dedent`
|
|
2659
|
+
type DoubleParenCond = true extends true ? 'a' | 'b' : 'c';
|
|
2660
|
+
`,
|
|
2661
|
+
},
|
|
2662
|
+
{
|
|
2663
|
+
// Precedence guard: the defensive parentheses around a union/intersection
|
|
2664
|
+
// must NOT be dropped. A function type used as a union member is invalid
|
|
2665
|
+
// TypeScript unless parenthesized (`string | () => number` is a syntax
|
|
2666
|
+
// error), so the wrapping has to survive the transform.
|
|
2667
|
+
name: 'Function type in a union stays parenthesized (precedence)',
|
|
2668
|
+
source: dedent`
|
|
2669
|
+
type FnUnion = string | (() => number);
|
|
2670
|
+
`,
|
|
2671
|
+
expected: dedent`
|
|
2672
|
+
type FnUnion = string | (() => number);
|
|
2673
|
+
`,
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
name: 'Bare function type in a union is parenthesized (precedence)',
|
|
2677
|
+
source: dedent`
|
|
2678
|
+
type FnUnionBare = string | () => number;
|
|
2679
|
+
`,
|
|
2680
|
+
expected: dedent`
|
|
2681
|
+
type FnUnionBare = string | (() => number);
|
|
2682
|
+
`,
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: 'Mixed & and | keeps precedence parentheses',
|
|
2686
|
+
source: dedent`
|
|
2687
|
+
type Mixed = number & { a: string } | { b: number };
|
|
2688
|
+
`,
|
|
2689
|
+
expected: dedent`
|
|
2690
|
+
type Mixed = (number & Readonly<{ a: string }>) | Readonly<{ b: number }>;
|
|
2691
|
+
`,
|
|
2692
|
+
},
|
|
2577
2693
|
])('$name', testFn);
|
|
2578
2694
|
});
|
|
2579
2695
|
|
|
@@ -15,12 +15,12 @@ import { type SafeUintWithSmallInt } from 'ts-type-forge';
|
|
|
15
15
|
* - `"none"`: All other cases
|
|
16
16
|
*/
|
|
17
17
|
export type ReadonlyContext = Readonly<
|
|
18
|
-
|
|
18
|
+
(
|
|
19
19
|
| { type: 'DeepReadonly' }
|
|
20
20
|
| { type: 'Readonly' }
|
|
21
21
|
| { type: 'readonly' }
|
|
22
22
|
| { type: 'none' }
|
|
23
|
-
)
|
|
23
|
+
) & { indexedAccessDepth: SafeUintWithSmallInt }
|
|
24
24
|
>;
|
|
25
25
|
|
|
26
26
|
export const nextReadonlyContext = <
|
|
@@ -31,9 +31,7 @@ export const nextReadonlyContext = <
|
|
|
31
31
|
nextReadonlyContextType: next,
|
|
32
32
|
indexedAccessDepthChange = 'infinity',
|
|
33
33
|
}: Readonly<
|
|
34
|
-
|
|
35
|
-
currentReadonlyContext: Curr;
|
|
36
|
-
} & (
|
|
34
|
+
(
|
|
37
35
|
| {
|
|
38
36
|
nextReadonlyContextType: Next;
|
|
39
37
|
indexedAccessDepthChange: 'decr' | 'incr' | 'keep' | 'infinity';
|
|
@@ -45,7 +43,9 @@ export const nextReadonlyContext = <
|
|
|
45
43
|
>;
|
|
46
44
|
indexedAccessDepthChange?: 'infinity';
|
|
47
45
|
}
|
|
48
|
-
)
|
|
46
|
+
) & {
|
|
47
|
+
currentReadonlyContext: Curr;
|
|
48
|
+
}
|
|
49
49
|
>): Readonly<{
|
|
50
50
|
type: Extract<ReadonlyContext['type'], 'DeepReadonly'> | Next;
|
|
51
51
|
indexedAccessDepth: SafeUintWithSmallInt;
|
|
@@ -23,12 +23,12 @@ export const replaceRecordWithUnknownRecordTransformer =
|
|
|
23
23
|
({
|
|
24
24
|
name: TRANSFORMER_NAME,
|
|
25
25
|
transform: (sourceAst) => {
|
|
26
|
-
const containerModifications =
|
|
27
|
-
|
|
28
|
-
...sourceAst
|
|
26
|
+
const containerModifications = Arr.toUnshifted(
|
|
27
|
+
sourceAst
|
|
29
28
|
.getModules()
|
|
30
29
|
.map((namespace) => processDeclarations(namespace)),
|
|
31
|
-
|
|
30
|
+
processDeclarations(sourceAst),
|
|
31
|
+
);
|
|
32
32
|
|
|
33
33
|
if (containerModifications.includes(true)) {
|
|
34
34
|
ensureUnknownRecordImport(sourceAst);
|
|
@@ -52,17 +52,17 @@ export const hasDisableNextLineComment = (
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
if (matchedPrefix !== undefined) {
|
|
55
|
+
// If no transformer name specified, check if comment applies to all transformers
|
|
56
|
+
if (transformerName === undefined) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
// Extract the part after the prefix
|
|
56
61
|
const afterPrefix = commentText
|
|
57
62
|
.slice(commentText.indexOf(matchedPrefix))
|
|
58
63
|
.replace(matchedPrefix, '')
|
|
59
64
|
.trim();
|
|
60
65
|
|
|
61
|
-
// If no transformer name specified, check if comment applies to all transformers
|
|
62
|
-
if (transformerName === undefined) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
66
|
// If no specific transformers listed in comment, it applies to all
|
|
67
67
|
if (afterPrefix === '') {
|
|
68
68
|
return true;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './has-disable-next-line-comment.mjs';
|
|
2
2
|
export * from './is-as-const-node.mjs';
|
|
3
3
|
export * from './is-atomic-type-node.mjs';
|
|
4
|
+
export * from './is-conditional-type-distribution-guard.mjs';
|
|
4
5
|
export * from './is-readonly-node.mjs';
|
|
5
6
|
export * from './is-spread-parameter-node.mjs';
|
|
6
7
|
export * from './remove-parentheses.mjs';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as tsm from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines whether a type node is an operand (check or extends type) of a
|
|
5
|
+
* conditional type whose check type is wrapped to suppress union distribution,
|
|
6
|
+
* i.e. the `X` and `Y` in `X extends Y ? ... : ...` where `X` is itself a tuple
|
|
7
|
+
* or array.
|
|
8
|
+
*
|
|
9
|
+
* Wrapping a naked type parameter so that it is no longer naked is a well-known
|
|
10
|
+
* type-implementation idiom used to suppress union distribution. Both
|
|
11
|
+
* `[A] extends [B] ? ...` (tuple wrapping) and `A[] extends B[] ? ...` (array
|
|
12
|
+
* wrapping) rely on this. Adding a `readonly` modifier to those operands neither
|
|
13
|
+
* makes the type more immutable in any meaningful way nor preserves the idiom's
|
|
14
|
+
* intent, so such operands must be left untouched.
|
|
15
|
+
*
|
|
16
|
+
* A naked check type such as `T extends string[] ? ...` still distributes, so it
|
|
17
|
+
* is not a guard; arrays/tuples in its extends position are converted as usual.
|
|
18
|
+
*
|
|
19
|
+
* @param node - The type node to check
|
|
20
|
+
* @returns true if the node is the check or extends type of a distribution-guarded
|
|
21
|
+
* conditional type
|
|
22
|
+
*/
|
|
23
|
+
export const isConditionalTypeDistributionGuard = (node: tsm.Node): boolean => {
|
|
24
|
+
const parent = node.getParent();
|
|
25
|
+
|
|
26
|
+
if (parent?.isKind(tsm.SyntaxKind.ConditionalType) !== true) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const checkType = parent.getCheckType();
|
|
31
|
+
|
|
32
|
+
// The guard is only in effect when the check type itself is wrapped in a tuple
|
|
33
|
+
// or array; otherwise (e.g. a naked `T`) the conditional still distributes.
|
|
34
|
+
if (
|
|
35
|
+
!checkType.isKind(tsm.SyntaxKind.TupleType) &&
|
|
36
|
+
!checkType.isKind(tsm.SyntaxKind.ArrayType)
|
|
37
|
+
) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
checkType.compilerNode === node.compilerNode ||
|
|
43
|
+
parent.getExtendsType().compilerNode === node.compilerNode
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { asUint32, range } from 'ts-data-forge';
|
|
12
12
|
|
|
13
|
+
const parenDepthDelta = (char: string): number => {
|
|
14
|
+
switch (char) {
|
|
15
|
+
case '(':
|
|
16
|
+
return 1;
|
|
17
|
+
|
|
18
|
+
case ')':
|
|
19
|
+
return -1;
|
|
20
|
+
|
|
21
|
+
default:
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
13
26
|
const isWrappedWithParentheses = (str: string): boolean => {
|
|
14
27
|
const trimmed = str.trim();
|
|
15
28
|
|
|
@@ -22,30 +35,11 @@ const isWrappedWithParentheses = (str: string): boolean => {
|
|
|
22
35
|
let mut_depth = 0;
|
|
23
36
|
|
|
24
37
|
for (const mut_i of range(0, asUint32(trimmed.length))) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
switch (char) {
|
|
28
|
-
case '(': {
|
|
29
|
-
mut_depth += 1;
|
|
30
|
-
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
case ')': {
|
|
35
|
-
mut_depth -= 1;
|
|
36
|
-
|
|
37
|
-
// If we reach depth 0 before the end, the outer parentheses don't wrap everything
|
|
38
|
-
if (mut_depth === 0 && mut_i < trimmed.length - 1) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
38
|
+
mut_depth += parenDepthDelta(trimmed.charAt(mut_i));
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
40
|
+
// If we reach depth 0 before the end, the outer parentheses don't wrap everything
|
|
41
|
+
if (mut_depth === 0 && mut_i < trimmed.length - 1) {
|
|
42
|
+
return false;
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
45
|
|