ts-codemod-lib 2.2.1 → 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/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 +742 -834
- 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/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.map +1 -1
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs +9 -33
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs.map +1 -1
- 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/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/package.json +30 -29
- 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 +64 -35
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +67 -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/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 { 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
|
|
@@ -1054,50 +1054,54 @@ const transformClassDeclarationNode = (
|
|
|
1054
1054
|
|
|
1055
1055
|
for (const ctor of node.getConstructors()) {
|
|
1056
1056
|
for (const param of ctor.getParameters()) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
'skipped class constructor parameter by disable-next-line comment',
|
|
1060
|
-
);
|
|
1057
|
+
transformConstructorParameter(param, options);
|
|
1058
|
+
}
|
|
1061
1059
|
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1060
|
+
const body = ctor.getBody();
|
|
1064
1061
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1062
|
+
if (body !== undefined) {
|
|
1063
|
+
transformNode(body, initialReadonlyContext, options);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
1067
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
+
);
|
|
1072
1077
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1075
1080
|
|
|
1076
|
-
|
|
1077
|
-
// protected -> protected readonly
|
|
1078
|
-
// private -> private readonly
|
|
1079
|
-
if (
|
|
1080
|
-
scope === tsm.Scope.Public ||
|
|
1081
|
-
scope === tsm.Scope.Protected ||
|
|
1082
|
-
scope === tsm.Scope.Private
|
|
1083
|
-
) {
|
|
1084
|
-
param.setIsReadonly(true);
|
|
1085
|
-
}
|
|
1081
|
+
const name = param.getName();
|
|
1086
1082
|
|
|
1087
|
-
|
|
1083
|
+
if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1088
1086
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1087
|
+
// Check if parameter is a property declaration (public/protected/private)
|
|
1088
|
+
const scope = param.getScope();
|
|
1093
1089
|
|
|
1094
|
-
|
|
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
|
+
}
|
|
1095
1100
|
|
|
1096
|
-
|
|
1097
|
-
transformNode(body, initialReadonlyContext, options);
|
|
1098
|
-
}
|
|
1101
|
+
const type = param.getTypeNode();
|
|
1099
1102
|
|
|
1100
|
-
|
|
1103
|
+
if (type !== undefined) {
|
|
1104
|
+
transformNode(type, initialReadonlyContext, options);
|
|
1101
1105
|
}
|
|
1102
1106
|
};
|
|
1103
1107
|
|
|
@@ -1249,6 +1253,20 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1249
1253
|
|
|
1250
1254
|
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
1251
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`.
|
|
1252
1270
|
options.replaceNode(
|
|
1253
1271
|
node,
|
|
1254
1272
|
unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
|
|
@@ -1307,7 +1325,18 @@ const transformParenthesizedTypeNode = (
|
|
|
1307
1325
|
isAtomicTypeNode(T) ||
|
|
1308
1326
|
// remove () if T is TypeLiteralNode
|
|
1309
1327
|
// e.g. `({ member: V }) |-> { member: V }`
|
|
1310
|
-
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)
|
|
1311
1340
|
) {
|
|
1312
1341
|
options.replaceNode(node, T.getFullText());
|
|
1313
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, ' ');
|
|
@@ -2624,6 +2627,69 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
2624
2627
|
type ParenReadonly = Readonly<{ a: number }>
|
|
2625
2628
|
`,
|
|
2626
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
|
+
},
|
|
2627
2693
|
])('$name', testFn);
|
|
2628
2694
|
});
|
|
2629
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;
|
|
@@ -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
|
|