ts-codemod-lib 2.1.5 → 2.1.6
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 +3 -1
- package/dist/cmd/append-as-const.mjs +1 -1
- package/dist/cmd/convert-interface-to-type.mjs +1 -1
- package/dist/cmd/convert-to-readonly.mjs +1 -1
- package/dist/cmd/replace-any-with-unknown.mjs +1 -1
- package/dist/cmd/replace-record-with-unknown-record.mjs +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +5 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +134 -80
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cmd/append-as-const.mts +1 -1
- package/src/cmd/convert-interface-to-type.mts +1 -1
- package/src/cmd/convert-to-readonly.mts +1 -1
- package/src/cmd/replace-any-with-unknown.mts +1 -1
- package/src/cmd/replace-record-with-unknown-record.mts +1 -1
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +169 -94
- package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +312 -0
package/README.md
CHANGED
|
@@ -314,6 +314,8 @@ const transformedCode = transformSourceCode(originalCode, isTsx, [
|
|
|
314
314
|
]);
|
|
315
315
|
|
|
316
316
|
const expected = dedent`
|
|
317
|
+
import { type UnknownRecord } from "ts-type-forge";
|
|
318
|
+
|
|
317
319
|
export type A = Readonly< {
|
|
318
320
|
name?: string;
|
|
319
321
|
point: (readonly [x: number, y: number, z?: number]);
|
|
@@ -341,7 +343,7 @@ const expected = dedent`
|
|
|
341
343
|
|
|
342
344
|
if (import.meta.vitest !== undefined) {
|
|
343
345
|
test('transformSourceCode', () => {
|
|
344
|
-
assert.
|
|
346
|
+
assert.strictEqual(transformedCode, expected);
|
|
345
347
|
});
|
|
346
348
|
}
|
|
347
349
|
```
|
|
@@ -10,7 +10,7 @@ import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
|
10
10
|
const transformer = appendAsConstTransformer();
|
|
11
11
|
const cmdDef = cmd.command({
|
|
12
12
|
name: transformer.name,
|
|
13
|
-
version: '2.1.
|
|
13
|
+
version: '2.1.6',
|
|
14
14
|
args: {
|
|
15
15
|
baseDir: cmd.positional({
|
|
16
16
|
type: cmd.string,
|
|
@@ -10,7 +10,7 @@ import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
|
10
10
|
const transformer = convertInterfaceToTypeTransformer();
|
|
11
11
|
const cmdDef = cmd.command({
|
|
12
12
|
name: transformer.name,
|
|
13
|
-
version: '2.1.
|
|
13
|
+
version: '2.1.6',
|
|
14
14
|
args: {
|
|
15
15
|
baseDir: cmd.positional({
|
|
16
16
|
type: cmd.string,
|
|
@@ -10,7 +10,7 @@ import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
|
10
10
|
const transformer = convertToReadonlyTransformer();
|
|
11
11
|
const cmdDef = cmd.command({
|
|
12
12
|
name: transformer.name,
|
|
13
|
-
version: '2.1.
|
|
13
|
+
version: '2.1.6',
|
|
14
14
|
args: {
|
|
15
15
|
baseDir: cmd.positional({
|
|
16
16
|
type: cmd.string,
|
|
@@ -10,7 +10,7 @@ import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
|
10
10
|
const transformer = replaceAnyWithUnknownTransformer();
|
|
11
11
|
const cmdDef = cmd.command({
|
|
12
12
|
name: transformer.name,
|
|
13
|
-
version: '2.1.
|
|
13
|
+
version: '2.1.6',
|
|
14
14
|
args: {
|
|
15
15
|
baseDir: cmd.positional({
|
|
16
16
|
type: cmd.string,
|
|
@@ -10,7 +10,7 @@ import { runTransformerCLI } from './run-transformer-cli.mjs';
|
|
|
10
10
|
const transformer = replaceRecordWithUnknownRecordTransformer();
|
|
11
11
|
const cmdDef = cmd.command({
|
|
12
12
|
name: transformer.name,
|
|
13
|
-
version: '2.1.
|
|
13
|
+
version: '2.1.6',
|
|
14
14
|
args: {
|
|
15
15
|
baseDir: cmd.positional({
|
|
16
16
|
type: cmd.string,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type TsMorphTransformer } from './types.mjs';
|
|
2
2
|
/**
|
|
3
3
|
* Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`,
|
|
4
|
-
* and index signatures `[k: string]: unknown` with `UnknownRecord
|
|
4
|
+
* and index signatures `[k: string]: unknown` with `UnknownRecord`.
|
|
5
|
+
*
|
|
6
|
+
* When a replacement occurs, also inserts
|
|
7
|
+
* `import { type UnknownRecord } from 'ts-type-forge';` at the top of the
|
|
8
|
+
* source file (or merges into an existing import from `'ts-type-forge'`).
|
|
5
9
|
*/
|
|
6
10
|
export declare const replaceRecordWithUnknownRecordTransformer: () => TsMorphTransformer;
|
|
7
11
|
//# sourceMappingURL=replace-record-with-unknown-record.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-record-with-unknown-record.d.mts","sourceRoot":"","sources":["../../../src/functions/ast-transformers/replace-record-with-unknown-record.mts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"replace-record-with-unknown-record.d.mts","sourceRoot":"","sources":["../../../src/functions/ast-transformers/replace-record-with-unknown-record.mts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQtD;;;;;;;GAOG;AACH,eAAO,MAAM,yCAAyC,QAChD,kBAeS,CAAC"}
|
|
@@ -5,64 +5,81 @@ import '../functions/is-atomic-type-node.mjs';
|
|
|
5
5
|
|
|
6
6
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types -- ts-morph uses mutable types */
|
|
7
7
|
const TRANSFORMER_NAME = 'replace-record-with-unknown-record';
|
|
8
|
+
const UNKNOWN_RECORD_MODULE = 'ts-type-forge';
|
|
9
|
+
const UNKNOWN_RECORD_NAME = 'UnknownRecord';
|
|
8
10
|
/**
|
|
9
11
|
* Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`,
|
|
10
|
-
* and index signatures `[k: string]: unknown` with `UnknownRecord
|
|
12
|
+
* and index signatures `[k: string]: unknown` with `UnknownRecord`.
|
|
13
|
+
*
|
|
14
|
+
* When a replacement occurs, also inserts
|
|
15
|
+
* `import { type UnknownRecord } from 'ts-type-forge';` at the top of the
|
|
16
|
+
* source file (or merges into an existing import from `'ts-type-forge'`).
|
|
11
17
|
*/
|
|
12
18
|
const replaceRecordWithUnknownRecordTransformer = () => ({
|
|
13
19
|
name: TRANSFORMER_NAME,
|
|
14
20
|
transform: (sourceAst) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const containerModifications = [
|
|
22
|
+
processDeclarations(sourceAst),
|
|
23
|
+
...sourceAst
|
|
24
|
+
.getModules()
|
|
25
|
+
.map((namespace) => processDeclarations(namespace)),
|
|
26
|
+
];
|
|
27
|
+
if (containerModifications.includes(true)) {
|
|
28
|
+
ensureUnknownRecordImport(sourceAst);
|
|
21
29
|
}
|
|
22
30
|
},
|
|
23
31
|
});
|
|
24
32
|
const processDeclarations = (container) => {
|
|
25
|
-
const
|
|
26
|
-
for (const typeAlias of typeAliases) {
|
|
33
|
+
const typeAliasModifications = container.getTypeAliases().map((typeAlias) => {
|
|
27
34
|
if (hasDisableNextLineComment(typeAlias, TRANSFORMER_NAME)) {
|
|
28
|
-
|
|
35
|
+
return false;
|
|
29
36
|
}
|
|
30
37
|
const typeNode = typeAlias.getTypeNode();
|
|
31
38
|
if (typeNode === undefined)
|
|
32
|
-
|
|
33
|
-
visitTypeNode(typeNode);
|
|
39
|
+
return false;
|
|
40
|
+
return visitTypeNode(typeNode);
|
|
41
|
+
});
|
|
42
|
+
const interfaceModifications = container
|
|
43
|
+
.getInterfaces()
|
|
44
|
+
.map((interfaceDecl) => processInterfaceDeclaration(interfaceDecl));
|
|
45
|
+
return [...typeAliasModifications, ...interfaceModifications].includes(true);
|
|
46
|
+
};
|
|
47
|
+
const processInterfaceDeclaration = (interfaceDecl) => {
|
|
48
|
+
if (hasDisableNextLineComment(interfaceDecl, TRANSFORMER_NAME)) {
|
|
49
|
+
return false;
|
|
34
50
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Otherwise, check properties for Record types
|
|
59
|
-
for (const property of interfaceDecl.getProperties()) {
|
|
60
|
-
const typeNode = property.getTypeNode();
|
|
61
|
-
if (typeNode === undefined)
|
|
62
|
-
continue;
|
|
63
|
-
visitTypeNode(typeNode);
|
|
64
|
-
}
|
|
51
|
+
// Check if interface has index signature [k: string]: unknown
|
|
52
|
+
const indexSignatures = interfaceDecl.getIndexSignatures();
|
|
53
|
+
const hasStringUnknownSignature = indexSignatures.some((sig) => {
|
|
54
|
+
const keyType = sig.getKeyType();
|
|
55
|
+
const returnType = sig.getReturnType();
|
|
56
|
+
return keyType.getText() === 'string' && returnType.getText() === 'unknown';
|
|
57
|
+
});
|
|
58
|
+
// If it has the signature and no other members, replace entire interface with type alias.
|
|
59
|
+
// Skip when the interface carries semantics that a plain type alias would lose
|
|
60
|
+
// (type parameters, extends clauses, declare modifier, default export).
|
|
61
|
+
if (hasStringUnknownSignature &&
|
|
62
|
+
Arr.isArrayOfLength(interfaceDecl.getProperties(), 0) &&
|
|
63
|
+
Arr.isArrayOfLength(indexSignatures, 1) &&
|
|
64
|
+
Arr.isArrayOfLength(interfaceDecl.getTypeParameters(), 0) &&
|
|
65
|
+
Arr.isArrayOfLength(interfaceDecl.getExtends(), 0) &&
|
|
66
|
+
!interfaceDecl.hasDeclareKeyword() &&
|
|
67
|
+
!interfaceDecl.hasDefaultKeyword()) {
|
|
68
|
+
const exportPrefix = interfaceDecl.hasExportKeyword() ? 'export ' : '';
|
|
69
|
+
const interfaceName = interfaceDecl.getName();
|
|
70
|
+
interfaceDecl.replaceWithText(`${exportPrefix}type ${interfaceName} = UnknownRecord;`);
|
|
71
|
+
return true;
|
|
65
72
|
}
|
|
73
|
+
// Otherwise, check properties for Record types
|
|
74
|
+
return interfaceDecl
|
|
75
|
+
.getProperties()
|
|
76
|
+
.map((property) => {
|
|
77
|
+
const typeNode = property.getTypeNode();
|
|
78
|
+
if (typeNode === undefined)
|
|
79
|
+
return false;
|
|
80
|
+
return visitTypeNode(typeNode);
|
|
81
|
+
})
|
|
82
|
+
.includes(true);
|
|
66
83
|
};
|
|
67
84
|
const visitTypeNode = (node) => {
|
|
68
85
|
if (tsm.Node.isTypeReference(node)) {
|
|
@@ -79,15 +96,14 @@ const visitTypeNode = (node) => {
|
|
|
79
96
|
Arr.isArrayOfLength(indexSigs, 1) &&
|
|
80
97
|
isStringUnknownIndexSignature(indexSigs[0])) {
|
|
81
98
|
node.replaceWithText('UnknownRecord');
|
|
82
|
-
return;
|
|
99
|
+
return true;
|
|
83
100
|
}
|
|
84
101
|
// Otherwise, recurse into the type literal to visit its properties
|
|
85
|
-
visitTypeNode(typeArg);
|
|
86
|
-
return;
|
|
102
|
+
return visitTypeNode(typeArg);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
|
-
replaceIfRecordUnknown(node);
|
|
106
|
+
return replaceIfRecordUnknown(node);
|
|
91
107
|
}
|
|
92
108
|
// Check for type literal { [k: string]: unknown }
|
|
93
109
|
if (tsm.Node.isTypeLiteral(node)) {
|
|
@@ -98,47 +114,52 @@ const visitTypeNode = (node) => {
|
|
|
98
114
|
Arr.isArrayOfLength(indexSigs, 1) &&
|
|
99
115
|
isStringUnknownIndexSignature(indexSigs[0])) {
|
|
100
116
|
node.replaceWithText('UnknownRecord');
|
|
101
|
-
return;
|
|
117
|
+
return true;
|
|
102
118
|
}
|
|
119
|
+
return members
|
|
120
|
+
.map((member) => {
|
|
121
|
+
if (tsm.Node.isPropertySignature(member)) {
|
|
122
|
+
const typeNode = member.getTypeNode();
|
|
123
|
+
return typeNode === undefined ? false : visitTypeNode(typeNode);
|
|
124
|
+
}
|
|
125
|
+
if (tsm.Node.isIndexSignatureDeclaration(member)) {
|
|
126
|
+
const typeNode = member.getReturnTypeNode();
|
|
127
|
+
return typeNode === undefined ? false : visitTypeNode(typeNode);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
})
|
|
131
|
+
.includes(true);
|
|
103
132
|
}
|
|
104
133
|
// Recursively visit child type nodes
|
|
105
|
-
if (tsm.Node.isUnionTypeNode(node)) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
if (tsm.Node.isUnionTypeNode(node) || tsm.Node.isIntersectionTypeNode(node)) {
|
|
135
|
+
return node
|
|
136
|
+
.getTypeNodes()
|
|
137
|
+
.map((typeNode) => visitTypeNode(typeNode))
|
|
138
|
+
.includes(true);
|
|
109
139
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
visitTypeNode(typeNode);
|
|
113
|
-
}
|
|
140
|
+
if (tsm.Node.isArrayTypeNode(node)) {
|
|
141
|
+
return visitTypeNode(node.getElementTypeNode());
|
|
114
142
|
}
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
if (tsm.Node.isTupleTypeNode(node)) {
|
|
144
|
+
return node
|
|
145
|
+
.getElements()
|
|
146
|
+
.map((element) => visitTypeNode(element))
|
|
147
|
+
.includes(true);
|
|
117
148
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
visitTypeNode(element);
|
|
121
|
-
}
|
|
149
|
+
if (tsm.Node.isParenthesizedTypeNode(node)) {
|
|
150
|
+
return visitTypeNode(node.getTypeNode());
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
152
|
+
// Handles `readonly T[]`, `readonly [...]`, `keyof T`
|
|
153
|
+
if (tsm.Node.isTypeOperatorTypeNode(node)) {
|
|
154
|
+
return visitTypeNode(node.getTypeNode());
|
|
125
155
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (tsm.Node.isPropertySignature(member)) {
|
|
129
|
-
const typeNode = member.getTypeNode();
|
|
130
|
-
if (typeNode !== undefined) {
|
|
131
|
-
visitTypeNode(typeNode);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else if (tsm.Node.isIndexSignatureDeclaration(member)) {
|
|
135
|
-
const typeNode = member.getReturnTypeNode();
|
|
136
|
-
if (typeNode !== undefined) {
|
|
137
|
-
visitTypeNode(typeNode);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
156
|
+
if (tsm.Node.isRestTypeNode(node)) {
|
|
157
|
+
return visitTypeNode(node.getTypeNode());
|
|
141
158
|
}
|
|
159
|
+
if (tsm.Node.isNamedTupleMember(node)) {
|
|
160
|
+
return visitTypeNode(node.getTypeNode());
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
142
163
|
};
|
|
143
164
|
const isStringUnknownIndexSignature = (sig) => {
|
|
144
165
|
const keyType = sig.getKeyTypeNode();
|
|
@@ -155,8 +176,10 @@ const replaceIfRecordUnknown = (node) => {
|
|
|
155
176
|
typeArgs[0].getText() === 'string' &&
|
|
156
177
|
typeArgs[1].getText() === 'unknown') {
|
|
157
178
|
node.replaceWithText('UnknownRecord');
|
|
179
|
+
return true;
|
|
158
180
|
}
|
|
159
|
-
|
|
181
|
+
// Recurse into type arguments (e.g. Record<string, Record<string, unknown>>)
|
|
182
|
+
return typeArgs.map((typeArg) => visitTypeNode(typeArg)).includes(true);
|
|
160
183
|
}
|
|
161
184
|
case 'Readonly': {
|
|
162
185
|
// Check if it's Readonly<Record<string, unknown>>
|
|
@@ -171,14 +194,45 @@ const replaceIfRecordUnknown = (node) => {
|
|
|
171
194
|
innerTypeArgs[0].getText() === 'string' &&
|
|
172
195
|
innerTypeArgs[1].getText() === 'unknown') {
|
|
173
196
|
node.replaceWithText('UnknownRecord');
|
|
197
|
+
return true;
|
|
174
198
|
}
|
|
175
199
|
}
|
|
176
200
|
}
|
|
201
|
+
// Otherwise, recurse into the type argument
|
|
202
|
+
return visitTypeNode(innerType);
|
|
177
203
|
}
|
|
178
|
-
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
default: {
|
|
207
|
+
// Recurse into type arguments for any other generic type
|
|
208
|
+
return node
|
|
209
|
+
.getTypeArguments()
|
|
210
|
+
.map((typeArg) => visitTypeNode(typeArg))
|
|
211
|
+
.includes(true);
|
|
179
212
|
}
|
|
180
213
|
}
|
|
181
214
|
};
|
|
215
|
+
const ensureUnknownRecordImport = (sourceFile) => {
|
|
216
|
+
const existingImport = sourceFile
|
|
217
|
+
.getImportDeclarations()
|
|
218
|
+
.find((decl) => decl.getModuleSpecifierValue() === UNKNOWN_RECORD_MODULE);
|
|
219
|
+
if (existingImport === undefined) {
|
|
220
|
+
sourceFile.insertImportDeclaration(0, {
|
|
221
|
+
moduleSpecifier: UNKNOWN_RECORD_MODULE,
|
|
222
|
+
namedImports: [{ name: UNKNOWN_RECORD_NAME, isTypeOnly: true }],
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const alreadyImported = existingImport
|
|
227
|
+
.getNamedImports()
|
|
228
|
+
.some((ni) => ni.getName() === UNKNOWN_RECORD_NAME);
|
|
229
|
+
if (alreadyImported)
|
|
230
|
+
return;
|
|
231
|
+
existingImport.addNamedImport({
|
|
232
|
+
name: UNKNOWN_RECORD_NAME,
|
|
233
|
+
isTypeOnly: true,
|
|
234
|
+
});
|
|
235
|
+
};
|
|
182
236
|
|
|
183
237
|
export { replaceRecordWithUnknownRecordTransformer };
|
|
184
238
|
//# sourceMappingURL=replace-record-with-unknown-record.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replace-record-with-unknown-record.mjs","sources":["../../../src/functions/ast-transformers/replace-record-with-unknown-record.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAAA;AAMA,MAAM,gBAAgB,GAAG,oCAAoC;AAE7D
|
|
1
|
+
{"version":3,"file":"replace-record-with-unknown-record.mjs","sources":["../../../src/functions/ast-transformers/replace-record-with-unknown-record.mts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -6,95 +6,113 @@ import { type TsMorphTransformer } from './types.mjs';
|
|
|
6
6
|
|
|
7
7
|
const TRANSFORMER_NAME = 'replace-record-with-unknown-record';
|
|
8
8
|
|
|
9
|
+
const UNKNOWN_RECORD_MODULE = 'ts-type-forge';
|
|
10
|
+
|
|
11
|
+
const UNKNOWN_RECORD_NAME = 'UnknownRecord';
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Replaces `Readonly<Record<string, unknown>>`, `Record<string, unknown>`,
|
|
11
|
-
* and index signatures `[k: string]: unknown` with `UnknownRecord
|
|
15
|
+
* and index signatures `[k: string]: unknown` with `UnknownRecord`.
|
|
16
|
+
*
|
|
17
|
+
* When a replacement occurs, also inserts
|
|
18
|
+
* `import { type UnknownRecord } from 'ts-type-forge';` at the top of the
|
|
19
|
+
* source file (or merges into an existing import from `'ts-type-forge'`).
|
|
12
20
|
*/
|
|
13
21
|
export const replaceRecordWithUnknownRecordTransformer =
|
|
14
22
|
(): TsMorphTransformer =>
|
|
15
23
|
({
|
|
16
24
|
name: TRANSFORMER_NAME,
|
|
17
25
|
transform: (sourceAst) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const containerModifications = [
|
|
27
|
+
processDeclarations(sourceAst),
|
|
28
|
+
...sourceAst
|
|
29
|
+
.getModules()
|
|
30
|
+
.map((namespace) => processDeclarations(namespace)),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (containerModifications.includes(true)) {
|
|
34
|
+
ensureUnknownRecordImport(sourceAst);
|
|
26
35
|
}
|
|
27
36
|
},
|
|
28
37
|
}) as const;
|
|
29
38
|
|
|
30
39
|
const processDeclarations = (
|
|
31
40
|
container: tsm.SourceFile | tsm.ModuleDeclaration,
|
|
32
|
-
):
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
for (const typeAlias of typeAliases) {
|
|
41
|
+
): boolean => {
|
|
42
|
+
const typeAliasModifications = container.getTypeAliases().map((typeAlias) => {
|
|
36
43
|
if (hasDisableNextLineComment(typeAlias, TRANSFORMER_NAME)) {
|
|
37
|
-
|
|
44
|
+
return false;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const typeNode = typeAlias.getTypeNode();
|
|
41
48
|
|
|
42
|
-
if (typeNode === undefined)
|
|
49
|
+
if (typeNode === undefined) return false;
|
|
43
50
|
|
|
44
|
-
visitTypeNode(typeNode);
|
|
45
|
-
}
|
|
51
|
+
return visitTypeNode(typeNode);
|
|
52
|
+
});
|
|
46
53
|
|
|
47
|
-
const
|
|
54
|
+
const interfaceModifications = container
|
|
55
|
+
.getInterfaces()
|
|
56
|
+
.map((interfaceDecl) => processInterfaceDeclaration(interfaceDecl));
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
58
|
+
return [...typeAliasModifications, ...interfaceModifications].includes(true);
|
|
59
|
+
};
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
const processInterfaceDeclaration = (
|
|
62
|
+
interfaceDecl: tsm.InterfaceDeclaration,
|
|
63
|
+
): boolean => {
|
|
64
|
+
if (hasDisableNextLineComment(interfaceDecl, TRANSFORMER_NAME)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
// Check if interface has index signature [k: string]: unknown
|
|
69
|
+
const indexSignatures = interfaceDecl.getIndexSignatures();
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
const hasStringUnknownSignature = indexSignatures.some((sig) => {
|
|
72
|
+
const keyType = sig.getKeyType();
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
keyType.getText() === 'string' && returnType.getText() === 'unknown'
|
|
64
|
-
);
|
|
65
|
-
});
|
|
74
|
+
const returnType = sig.getReturnType();
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const properties = interfaceDecl.getProperties();
|
|
76
|
+
return keyType.getText() === 'string' && returnType.getText() === 'unknown';
|
|
77
|
+
});
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
// If it has the signature and no other members, replace entire interface with type alias.
|
|
80
|
+
// Skip when the interface carries semantics that a plain type alias would lose
|
|
81
|
+
// (type parameters, extends clauses, declare modifier, default export).
|
|
82
|
+
if (
|
|
83
|
+
hasStringUnknownSignature &&
|
|
84
|
+
Arr.isArrayOfLength(interfaceDecl.getProperties(), 0) &&
|
|
85
|
+
Arr.isArrayOfLength(indexSignatures, 1) &&
|
|
86
|
+
Arr.isArrayOfLength(interfaceDecl.getTypeParameters(), 0) &&
|
|
87
|
+
Arr.isArrayOfLength(interfaceDecl.getExtends(), 0) &&
|
|
88
|
+
!interfaceDecl.hasDeclareKeyword() &&
|
|
89
|
+
!interfaceDecl.hasDefaultKeyword()
|
|
90
|
+
) {
|
|
91
|
+
const exportPrefix = interfaceDecl.hasExportKeyword() ? 'export ' : '';
|
|
77
92
|
|
|
78
|
-
|
|
79
|
-
`export type ${interfaceName} = UnknownRecord;`,
|
|
80
|
-
);
|
|
93
|
+
const interfaceName = interfaceDecl.getName();
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
95
|
+
interfaceDecl.replaceWithText(
|
|
96
|
+
`${exportPrefix}type ${interfaceName} = UnknownRecord;`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
// Otherwise, check properties for Record types
|
|
103
|
+
return interfaceDecl
|
|
104
|
+
.getProperties()
|
|
105
|
+
.map((property) => {
|
|
88
106
|
const typeNode = property.getTypeNode();
|
|
89
107
|
|
|
90
|
-
if (typeNode === undefined)
|
|
108
|
+
if (typeNode === undefined) return false;
|
|
91
109
|
|
|
92
|
-
visitTypeNode(typeNode);
|
|
93
|
-
}
|
|
94
|
-
|
|
110
|
+
return visitTypeNode(typeNode);
|
|
111
|
+
})
|
|
112
|
+
.includes(true);
|
|
95
113
|
};
|
|
96
114
|
|
|
97
|
-
const visitTypeNode = (node: tsm.TypeNode):
|
|
115
|
+
const visitTypeNode = (node: tsm.TypeNode): boolean => {
|
|
98
116
|
if (tsm.Node.isTypeReference(node)) {
|
|
99
117
|
// Check if it's Readonly<{ [k: string]: unknown }>
|
|
100
118
|
if (node.getTypeName().getText() === 'Readonly') {
|
|
@@ -118,18 +136,16 @@ const visitTypeNode = (node: tsm.TypeNode): void => {
|
|
|
118
136
|
) {
|
|
119
137
|
node.replaceWithText('UnknownRecord');
|
|
120
138
|
|
|
121
|
-
return;
|
|
139
|
+
return true;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
// Otherwise, recurse into the type literal to visit its properties
|
|
125
|
-
visitTypeNode(typeArg);
|
|
126
|
-
|
|
127
|
-
return;
|
|
143
|
+
return visitTypeNode(typeArg);
|
|
128
144
|
}
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
replaceIfRecordUnknown(node);
|
|
148
|
+
return replaceIfRecordUnknown(node);
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
// Check for type literal { [k: string]: unknown }
|
|
@@ -148,44 +164,65 @@ const visitTypeNode = (node: tsm.TypeNode): void => {
|
|
|
148
164
|
) {
|
|
149
165
|
node.replaceWithText('UnknownRecord');
|
|
150
166
|
|
|
151
|
-
return;
|
|
167
|
+
return true;
|
|
152
168
|
}
|
|
153
|
-
}
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
for (const typeNode of node.getTypeNodes()) {
|
|
162
|
-
visitTypeNode(typeNode);
|
|
163
|
-
}
|
|
164
|
-
} else if (tsm.Node.isArrayTypeNode(node)) {
|
|
165
|
-
visitTypeNode(node.getElementTypeNode());
|
|
166
|
-
} else if (tsm.Node.isTupleTypeNode(node)) {
|
|
167
|
-
for (const element of node.getElements()) {
|
|
168
|
-
visitTypeNode(element);
|
|
169
|
-
}
|
|
170
|
-
} else if (tsm.Node.isParenthesizedTypeNode(node)) {
|
|
171
|
-
visitTypeNode(node.getTypeNode());
|
|
172
|
-
} else if (tsm.Node.isTypeLiteral(node)) {
|
|
173
|
-
for (const member of node.getMembers()) {
|
|
174
|
-
if (tsm.Node.isPropertySignature(member)) {
|
|
175
|
-
const typeNode = member.getTypeNode();
|
|
176
|
-
|
|
177
|
-
if (typeNode !== undefined) {
|
|
178
|
-
visitTypeNode(typeNode);
|
|
170
|
+
return members
|
|
171
|
+
.map((member) => {
|
|
172
|
+
if (tsm.Node.isPropertySignature(member)) {
|
|
173
|
+
const typeNode = member.getTypeNode();
|
|
174
|
+
|
|
175
|
+
return typeNode === undefined ? false : visitTypeNode(typeNode);
|
|
179
176
|
}
|
|
180
|
-
} else if (tsm.Node.isIndexSignatureDeclaration(member)) {
|
|
181
|
-
const typeNode = member.getReturnTypeNode();
|
|
182
177
|
|
|
183
|
-
if (
|
|
184
|
-
|
|
178
|
+
if (tsm.Node.isIndexSignatureDeclaration(member)) {
|
|
179
|
+
const typeNode = member.getReturnTypeNode();
|
|
180
|
+
|
|
181
|
+
return typeNode === undefined ? false : visitTypeNode(typeNode);
|
|
185
182
|
}
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
})
|
|
186
|
+
.includes(true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Recursively visit child type nodes
|
|
190
|
+
if (tsm.Node.isUnionTypeNode(node) || tsm.Node.isIntersectionTypeNode(node)) {
|
|
191
|
+
return node
|
|
192
|
+
.getTypeNodes()
|
|
193
|
+
.map((typeNode) => visitTypeNode(typeNode))
|
|
194
|
+
.includes(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (tsm.Node.isArrayTypeNode(node)) {
|
|
198
|
+
return visitTypeNode(node.getElementTypeNode());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (tsm.Node.isTupleTypeNode(node)) {
|
|
202
|
+
return node
|
|
203
|
+
.getElements()
|
|
204
|
+
.map((element) => visitTypeNode(element))
|
|
205
|
+
.includes(true);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (tsm.Node.isParenthesizedTypeNode(node)) {
|
|
209
|
+
return visitTypeNode(node.getTypeNode());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Handles `readonly T[]`, `readonly [...]`, `keyof T`
|
|
213
|
+
if (tsm.Node.isTypeOperatorTypeNode(node)) {
|
|
214
|
+
return visitTypeNode(node.getTypeNode());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (tsm.Node.isRestTypeNode(node)) {
|
|
218
|
+
return visitTypeNode(node.getTypeNode());
|
|
188
219
|
}
|
|
220
|
+
|
|
221
|
+
if (tsm.Node.isNamedTupleMember(node)) {
|
|
222
|
+
return visitTypeNode(node.getTypeNode());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return false;
|
|
189
226
|
};
|
|
190
227
|
|
|
191
228
|
const isStringUnknownIndexSignature = (
|
|
@@ -198,7 +235,7 @@ const isStringUnknownIndexSignature = (
|
|
|
198
235
|
return keyType.getText() === 'string' && returnType?.getText() === 'unknown';
|
|
199
236
|
};
|
|
200
237
|
|
|
201
|
-
const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode):
|
|
238
|
+
const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
|
|
202
239
|
const typeName = node.getTypeName().getText();
|
|
203
240
|
|
|
204
241
|
switch (typeName) {
|
|
@@ -212,9 +249,12 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): void => {
|
|
|
212
249
|
typeArgs[1].getText() === 'unknown'
|
|
213
250
|
) {
|
|
214
251
|
node.replaceWithText('UnknownRecord');
|
|
252
|
+
|
|
253
|
+
return true;
|
|
215
254
|
}
|
|
216
255
|
|
|
217
|
-
|
|
256
|
+
// Recurse into type arguments (e.g. Record<string, Record<string, unknown>>)
|
|
257
|
+
return typeArgs.map((typeArg) => visitTypeNode(typeArg)).includes(true);
|
|
218
258
|
}
|
|
219
259
|
case 'Readonly': {
|
|
220
260
|
// Check if it's Readonly<Record<string, unknown>>
|
|
@@ -235,16 +275,51 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): void => {
|
|
|
235
275
|
innerTypeArgs[1].getText() === 'unknown'
|
|
236
276
|
) {
|
|
237
277
|
node.replaceWithText('UnknownRecord');
|
|
278
|
+
|
|
279
|
+
return true;
|
|
238
280
|
}
|
|
239
281
|
}
|
|
240
282
|
}
|
|
283
|
+
|
|
284
|
+
// Otherwise, recurse into the type argument
|
|
285
|
+
return visitTypeNode(innerType);
|
|
241
286
|
}
|
|
242
287
|
|
|
243
|
-
|
|
288
|
+
return false;
|
|
244
289
|
}
|
|
245
290
|
|
|
246
291
|
default: {
|
|
247
|
-
|
|
292
|
+
// Recurse into type arguments for any other generic type
|
|
293
|
+
return node
|
|
294
|
+
.getTypeArguments()
|
|
295
|
+
.map((typeArg) => visitTypeNode(typeArg))
|
|
296
|
+
.includes(true);
|
|
248
297
|
}
|
|
249
298
|
}
|
|
250
299
|
};
|
|
300
|
+
|
|
301
|
+
const ensureUnknownRecordImport = (sourceFile: tsm.SourceFile): void => {
|
|
302
|
+
const existingImport = sourceFile
|
|
303
|
+
.getImportDeclarations()
|
|
304
|
+
.find((decl) => decl.getModuleSpecifierValue() === UNKNOWN_RECORD_MODULE);
|
|
305
|
+
|
|
306
|
+
if (existingImport === undefined) {
|
|
307
|
+
sourceFile.insertImportDeclaration(0, {
|
|
308
|
+
moduleSpecifier: UNKNOWN_RECORD_MODULE,
|
|
309
|
+
namedImports: [{ name: UNKNOWN_RECORD_NAME, isTypeOnly: true }],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const alreadyImported = existingImport
|
|
316
|
+
.getNamedImports()
|
|
317
|
+
.some((ni) => ni.getName() === UNKNOWN_RECORD_NAME);
|
|
318
|
+
|
|
319
|
+
if (alreadyImported) return;
|
|
320
|
+
|
|
321
|
+
existingImport.addNamedImport({
|
|
322
|
+
name: UNKNOWN_RECORD_NAME,
|
|
323
|
+
isTypeOnly: true,
|
|
324
|
+
});
|
|
325
|
+
};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/* eslint-disable tree-shakable/import-star */
|
|
2
|
+
/* eslint-disable vitest/expect-expect */
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
5
|
+
import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
|
|
6
|
+
import * as prettier from 'prettier/standalone';
|
|
7
|
+
import { replaceRecordWithUnknownRecordTransformer } from './replace-record-with-unknown-record.mjs';
|
|
8
|
+
import { transformSourceCode } from './transform-source-code.mjs';
|
|
9
|
+
|
|
10
|
+
const testFn = async ({
|
|
11
|
+
source,
|
|
12
|
+
expected,
|
|
13
|
+
}: Readonly<{
|
|
14
|
+
source: string;
|
|
15
|
+
expected: string;
|
|
16
|
+
}>): Promise<void> => {
|
|
17
|
+
const transformed = await formatter(
|
|
18
|
+
transformSourceCode(source, false, [
|
|
19
|
+
replaceRecordWithUnknownRecordTransformer(),
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const expectedFormatted = await formatter(expected);
|
|
24
|
+
|
|
25
|
+
expect(transformed).toBe(expectedFormatted);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const formatter = (code: string): Promise<string> =>
|
|
29
|
+
prettier.format(code, {
|
|
30
|
+
parser: 'typescript',
|
|
31
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe(replaceRecordWithUnknownRecordTransformer, () => {
|
|
35
|
+
test.each([
|
|
36
|
+
{
|
|
37
|
+
name: 'Simple Record<string, unknown> with import added',
|
|
38
|
+
source: 'type Foo = Record<string, unknown>;',
|
|
39
|
+
expected: dedent`
|
|
40
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
41
|
+
|
|
42
|
+
type Foo = UnknownRecord;
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Readonly<Record<string, unknown>>',
|
|
47
|
+
source: 'type Foo = Readonly<Record<string, unknown>>;',
|
|
48
|
+
expected: dedent`
|
|
49
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
50
|
+
|
|
51
|
+
type Foo = UnknownRecord;
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Type literal { [k: string]: unknown }',
|
|
56
|
+
source: 'type Foo = { [k: string]: unknown };',
|
|
57
|
+
expected: dedent`
|
|
58
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
59
|
+
|
|
60
|
+
type Foo = UnknownRecord;
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'Readonly type literal Readonly<{ [k: string]: unknown }>',
|
|
65
|
+
source: 'type Foo = Readonly<{ [k: string]: unknown }>;',
|
|
66
|
+
expected: dedent`
|
|
67
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
68
|
+
|
|
69
|
+
type Foo = UnknownRecord;
|
|
70
|
+
`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Exported interface with only [k: string]: unknown',
|
|
74
|
+
source: dedent`
|
|
75
|
+
export interface Foo {
|
|
76
|
+
[k: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
`,
|
|
79
|
+
expected: dedent`
|
|
80
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
81
|
+
|
|
82
|
+
export type Foo = UnknownRecord;
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Non-exported interface with only [k: string]: unknown preserves missing export',
|
|
87
|
+
source: dedent`
|
|
88
|
+
interface Foo {
|
|
89
|
+
[k: string]: unknown;
|
|
90
|
+
}
|
|
91
|
+
`,
|
|
92
|
+
expected: dedent`
|
|
93
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
94
|
+
|
|
95
|
+
type Foo = UnknownRecord;
|
|
96
|
+
`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Generic interface with [k: string]: unknown is left untouched',
|
|
100
|
+
source: dedent`
|
|
101
|
+
export interface Foo<T> {
|
|
102
|
+
[k: string]: unknown;
|
|
103
|
+
}
|
|
104
|
+
`,
|
|
105
|
+
expected: dedent`
|
|
106
|
+
export interface Foo<T> {
|
|
107
|
+
[k: string]: unknown;
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'Interface with extends + [k: string]: unknown is left untouched',
|
|
113
|
+
source: dedent`
|
|
114
|
+
interface Bar {
|
|
115
|
+
a: number;
|
|
116
|
+
}
|
|
117
|
+
export interface Foo extends Bar {
|
|
118
|
+
[k: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
`,
|
|
121
|
+
expected: dedent`
|
|
122
|
+
interface Bar {
|
|
123
|
+
a: number;
|
|
124
|
+
}
|
|
125
|
+
export interface Foo extends Bar {
|
|
126
|
+
[k: string]: unknown;
|
|
127
|
+
}
|
|
128
|
+
`,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'declare interface with [k: string]: unknown is left untouched',
|
|
132
|
+
source: dedent`
|
|
133
|
+
declare interface Foo {
|
|
134
|
+
[k: string]: unknown;
|
|
135
|
+
}
|
|
136
|
+
`,
|
|
137
|
+
expected: dedent`
|
|
138
|
+
declare interface Foo {
|
|
139
|
+
[k: string]: unknown;
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'Generic interface still has its property types transformed',
|
|
145
|
+
source: dedent`
|
|
146
|
+
export interface Foo<T> {
|
|
147
|
+
value: Record<string, unknown>;
|
|
148
|
+
tag: T;
|
|
149
|
+
}
|
|
150
|
+
`,
|
|
151
|
+
expected: dedent`
|
|
152
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
153
|
+
|
|
154
|
+
export interface Foo<T> {
|
|
155
|
+
value: UnknownRecord;
|
|
156
|
+
tag: T;
|
|
157
|
+
}
|
|
158
|
+
`,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'Record nested under readonly tuple (regression: TypeOperator)',
|
|
162
|
+
source: dedent`
|
|
163
|
+
type Foo =
|
|
164
|
+
| readonly [
|
|
165
|
+
'always',
|
|
166
|
+
Readonly<{
|
|
167
|
+
patternOptions?: Readonly<Record<string, unknown>>;
|
|
168
|
+
}>,
|
|
169
|
+
];
|
|
170
|
+
`,
|
|
171
|
+
expected: dedent`
|
|
172
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
173
|
+
|
|
174
|
+
type Foo =
|
|
175
|
+
| readonly [
|
|
176
|
+
'always',
|
|
177
|
+
Readonly<{
|
|
178
|
+
patternOptions?: UnknownRecord;
|
|
179
|
+
}>,
|
|
180
|
+
];
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'Record nested under readonly array (regression: TypeOperator)',
|
|
185
|
+
source: dedent`
|
|
186
|
+
type Foo = Readonly<{
|
|
187
|
+
forbid?: readonly (string | Record<string, unknown>)[];
|
|
188
|
+
}>;
|
|
189
|
+
`,
|
|
190
|
+
expected: dedent`
|
|
191
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
192
|
+
|
|
193
|
+
type Foo = Readonly<{
|
|
194
|
+
forbid?: readonly (string | UnknownRecord)[];
|
|
195
|
+
}>;
|
|
196
|
+
`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'readonly Readonly<Record<...>>[] at type alias root (regression)',
|
|
200
|
+
source: 'type Foo = readonly Readonly<Record<string, unknown>>[];',
|
|
201
|
+
expected: dedent`
|
|
202
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
203
|
+
|
|
204
|
+
type Foo = readonly UnknownRecord[];
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'Record inside namespace',
|
|
209
|
+
source: dedent`
|
|
210
|
+
namespace Ns {
|
|
211
|
+
export type Foo = Record<string, unknown>;
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
expected: dedent`
|
|
215
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
216
|
+
|
|
217
|
+
namespace Ns {
|
|
218
|
+
export type Foo = UnknownRecord;
|
|
219
|
+
}
|
|
220
|
+
`,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'Record nested in union inside namespace',
|
|
224
|
+
source: dedent`
|
|
225
|
+
namespace Ns {
|
|
226
|
+
export type Options = Readonly<{
|
|
227
|
+
targets:
|
|
228
|
+
| string
|
|
229
|
+
| readonly unknown[]
|
|
230
|
+
| Readonly<Record<string, unknown>>;
|
|
231
|
+
}>;
|
|
232
|
+
}
|
|
233
|
+
`,
|
|
234
|
+
expected: dedent`
|
|
235
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
236
|
+
|
|
237
|
+
namespace Ns {
|
|
238
|
+
export type Options = Readonly<{
|
|
239
|
+
targets: string | readonly unknown[] | UnknownRecord;
|
|
240
|
+
}>;
|
|
241
|
+
}
|
|
242
|
+
`,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'No transformation -> no import added',
|
|
246
|
+
source: 'type Foo = string;',
|
|
247
|
+
expected: 'type Foo = string;',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'Existing UnknownRecord import is not duplicated',
|
|
251
|
+
source: dedent`
|
|
252
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
253
|
+
|
|
254
|
+
type Foo = Record<string, unknown>;
|
|
255
|
+
`,
|
|
256
|
+
expected: dedent`
|
|
257
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
258
|
+
|
|
259
|
+
type Foo = UnknownRecord;
|
|
260
|
+
`,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'Merged into existing ts-type-forge import with other names',
|
|
264
|
+
source: dedent`
|
|
265
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
266
|
+
|
|
267
|
+
type Foo = Record<string, unknown>;
|
|
268
|
+
`,
|
|
269
|
+
expected: dedent`
|
|
270
|
+
import { type DeepReadonly, type UnknownRecord } from 'ts-type-forge';
|
|
271
|
+
|
|
272
|
+
type Foo = UnknownRecord;
|
|
273
|
+
`,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'Disable comment skips the type alias and does not add import',
|
|
277
|
+
source: dedent`
|
|
278
|
+
// transformer-ignore-next-line
|
|
279
|
+
type Foo = Record<string, unknown>;
|
|
280
|
+
`,
|
|
281
|
+
expected: dedent`
|
|
282
|
+
// transformer-ignore-next-line
|
|
283
|
+
type Foo = Record<string, unknown>;
|
|
284
|
+
`,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'Transformer-specific disable comment is respected',
|
|
288
|
+
source: dedent`
|
|
289
|
+
// transformer-ignore-next-line replace-record-with-unknown-record
|
|
290
|
+
type A = Record<string, unknown>;
|
|
291
|
+
type B = Record<string, unknown>;
|
|
292
|
+
`,
|
|
293
|
+
expected: dedent`
|
|
294
|
+
import { type UnknownRecord } from 'ts-type-forge';
|
|
295
|
+
|
|
296
|
+
// transformer-ignore-next-line replace-record-with-unknown-record
|
|
297
|
+
type A = Record<string, unknown>;
|
|
298
|
+
type B = UnknownRecord;
|
|
299
|
+
`,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'Record<string, string> (non-unknown value) is left alone',
|
|
303
|
+
source: 'type Foo = Record<string, string>;',
|
|
304
|
+
expected: 'type Foo = Record<string, string>;',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'Record<number, unknown> (non-string key) is left alone',
|
|
308
|
+
source: 'type Foo = Record<number, unknown>;',
|
|
309
|
+
expected: 'type Foo = Record<number, unknown>;',
|
|
310
|
+
},
|
|
311
|
+
])('$name', testFn);
|
|
312
|
+
});
|