ts-codemod-lib 3.5.1 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/functions/ast-transformers/append-as-const.d.mts +20 -12
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +170 -88
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +25 -6
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +272 -255
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
- 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 +69 -69
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/constants/index.d.mts +1 -0
- package/dist/functions/constants/index.d.mts.map +1 -1
- package/dist/functions/constants/index.mjs +1 -0
- package/dist/functions/constants/index.mjs.map +1 -1
- package/dist/functions/constants/mutable-prefixes.d.mts +23 -0
- package/dist/functions/constants/mutable-prefixes.d.mts.map +1 -0
- package/dist/functions/constants/mutable-prefixes.mjs +28 -0
- package/dist/functions/constants/mutable-prefixes.mjs.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts +2 -2
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +13 -13
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
- package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
- package/dist/functions/utils/replace-node-with-text.mjs +0 -1
- package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
- package/package.json +6 -6
- package/src/functions/ast-transformers/append-as-const.mts +319 -174
- package/src/functions/ast-transformers/append-as-const.test.mts +118 -7
- package/src/functions/ast-transformers/convert-to-readonly.mts +568 -489
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +381 -74
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
- package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
- package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
- package/src/functions/ast-transformers/transform-source-code.mts +110 -110
- package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
- package/src/functions/constants/index.mts +1 -0
- package/src/functions/constants/mutable-prefixes.mts +28 -0
- package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
- package/src/functions/functions/is-readonly-node.mts +25 -25
- package/src/functions/functions/wrap-with-parentheses.mts +138 -52
- package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
- package/src/functions/utils/replace-node-with-text.mts +10 -11
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { Arr, ISet } from 'ts-data-forge';
|
|
1
|
+
import { Arr, ISet, isRecord } from 'ts-data-forge';
|
|
2
2
|
import * as tsm from 'ts-morph';
|
|
3
3
|
import type { DeepReadonly } from 'ts-type-forge';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_MUTABLE_PREFIXES,
|
|
6
|
+
DEFAULT_MUTABLE_TYPE_PREFIXES,
|
|
7
|
+
} from '../constants/index.mjs';
|
|
4
8
|
import {
|
|
5
9
|
hasDisableNextLineComment,
|
|
6
10
|
isAsConstNode,
|
|
@@ -16,16 +20,26 @@ const TRANSFORMER_NAME = 'append-as-const';
|
|
|
16
20
|
export const appendAsConstTransformer = (
|
|
17
21
|
options?: AppendAsConstTransformerOptions,
|
|
18
22
|
): TsMorphTransformer => {
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Removed in 4.0.0; a caller without types would otherwise lose it silently.
|
|
24
|
+
const untypedOptions: unknown = options;
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
isRecord(untypedOptions) &&
|
|
28
|
+
untypedOptions['ignorePrefixes'] !== undefined
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`The "ignorePrefixes" option of ${TRANSFORMER_NAME} was renamed to "mutableVariablePrefixes".`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
25
34
|
|
|
26
35
|
const optionsInternal: AppendAsConstTransformerOptionsInternal = {
|
|
27
36
|
applyLevel: options?.applyLevel ?? 'avoidInFunctionArgs',
|
|
28
|
-
|
|
37
|
+
mutableVariablePrefixes: ISet.create(
|
|
38
|
+
options?.mutableVariablePrefixes ?? DEFAULT_MUTABLE_PREFIXES,
|
|
39
|
+
),
|
|
40
|
+
mutableTypePrefixes: ISet.create(
|
|
41
|
+
options?.mutableTypePrefixes ?? DEFAULT_MUTABLE_TYPE_PREFIXES,
|
|
42
|
+
),
|
|
29
43
|
removeAsConstForConstTypeParameters:
|
|
30
44
|
options?.removeAsConstForConstTypeParameters ?? true,
|
|
31
45
|
|
|
@@ -66,26 +80,35 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
66
80
|
applyLevel?: 'all' | 'avoidInFunctionArgs';
|
|
67
81
|
|
|
68
82
|
/**
|
|
69
|
-
*
|
|
83
|
+
* Prefixes of the names of variables that are left mutable: their
|
|
84
|
+
* initializers get no `as const`, nor does a value annotated with their type
|
|
85
|
+
* (`typeof mut_x`).
|
|
70
86
|
*
|
|
71
|
-
*
|
|
87
|
+
* @default ['mut_']
|
|
88
|
+
*/
|
|
89
|
+
mutableVariablePrefixes?: string[];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prefixes of the names of mutable types (see `convert-to-readonly`): a
|
|
93
|
+
* variable whose type annotation names such a type, an expression that
|
|
94
|
+
* `satisfies` one and an arrow function body returning one get no `as const`,
|
|
95
|
+
* which would make them unassignable to it.
|
|
72
96
|
*
|
|
73
|
-
* @default ['
|
|
97
|
+
* @default ['Mutable', 'mut_']
|
|
74
98
|
*/
|
|
75
|
-
|
|
99
|
+
mutableTypePrefixes?: string[];
|
|
76
100
|
|
|
77
101
|
/**
|
|
78
|
-
* Whether to remove redundant `as const` assertions from call arguments
|
|
79
|
-
*
|
|
80
|
-
* parameter.
|
|
102
|
+
* Whether to remove redundant `as const` assertions from call arguments whose
|
|
103
|
+
* corresponding parameter type is exactly a `const`-modified type parameter.
|
|
81
104
|
*
|
|
82
105
|
* (e.g. `f([1, 2] as const)` becomes `f([1, 2])` for
|
|
83
|
-
* `function f<const T>(x: T): T`, because the `const` type parameter
|
|
84
|
-
*
|
|
85
|
-
*
|
|
106
|
+
* `function f<const T>(x: T): T`, because the `const` type parameter already
|
|
107
|
+
* makes TypeScript infer the argument as if it were annotated with
|
|
108
|
+
* `as const`.)
|
|
86
109
|
*
|
|
87
|
-
* Only applies when the callee resolves to a single call signature within
|
|
88
|
-
*
|
|
110
|
+
* Only applies when the callee resolves to a single call signature within the
|
|
111
|
+
* transformed file itself; imported callees are left as they are.
|
|
89
112
|
*
|
|
90
113
|
* @default true
|
|
91
114
|
*/
|
|
@@ -96,7 +119,8 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
96
119
|
|
|
97
120
|
type AppendAsConstTransformerOptionsInternal = DeepReadonly<{
|
|
98
121
|
applyLevel: 'all' | 'avoidInFunctionArgs';
|
|
99
|
-
|
|
122
|
+
mutableVariablePrefixes: ISet<string>;
|
|
123
|
+
mutableTypePrefixes: ISet<string>;
|
|
100
124
|
removeAsConstForConstTypeParameters: boolean;
|
|
101
125
|
|
|
102
126
|
debugPrint: (...args: readonly unknown[]) => void;
|
|
@@ -119,6 +143,110 @@ type AsConstContext = Readonly<{
|
|
|
119
143
|
isDirectUnderConstInitializer: boolean;
|
|
120
144
|
}>;
|
|
121
145
|
|
|
146
|
+
/**
|
|
147
|
+
* `x as const`, or `<const>x`, the angle-bracket spelling TypeScript treats
|
|
148
|
+
* identically (outside `.tsx`, where it does not parse).
|
|
149
|
+
*/
|
|
150
|
+
const isConstAssertionNode = (
|
|
151
|
+
node: tsm.Node,
|
|
152
|
+
): node is tsm.AsExpression | tsm.TypeAssertion => {
|
|
153
|
+
if (isAsConstNode(node)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!tsm.Node.isTypeAssertion(node)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const typeNode = node.getTypeNode();
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
typeNode !== undefined &&
|
|
165
|
+
typeNode.isKind(tsm.SyntaxKind.TypeReference) &&
|
|
166
|
+
typeNode.getTypeName().getText() === 'const' &&
|
|
167
|
+
Arr.isEmpty(typeNode.getTypeArguments())
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** `ns.MutableFoo` -> `MutableFoo` */
|
|
172
|
+
const lastNameOf = (
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
174
|
+
entityName: tsm.EntityName,
|
|
175
|
+
): string =>
|
|
176
|
+
(tsm.Node.isQualifiedName(entityName)
|
|
177
|
+
? entityName.getRight()
|
|
178
|
+
: entityName
|
|
179
|
+
).getText();
|
|
180
|
+
|
|
181
|
+
/** `mut_x.items` -> `mut_x` */
|
|
182
|
+
const firstNameOf = (
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
184
|
+
entityName: tsm.EntityName,
|
|
185
|
+
): string =>
|
|
186
|
+
tsm.Node.isQualifiedName(entityName)
|
|
187
|
+
? firstNameOf(entityName.getLeft())
|
|
188
|
+
: entityName.getText();
|
|
189
|
+
|
|
190
|
+
const startsWithAny = (prefixes: ISet<string>, name: string): boolean =>
|
|
191
|
+
prefixes.some((p) => name.startsWith(p));
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The name a `typeof` starts from: a variable (`mut_x`) or a class, which
|
|
195
|
+
* `convert-to-readonly` leaves mutable by its type prefix (`MutableStore`).
|
|
196
|
+
*/
|
|
197
|
+
const isMutableValueName = (
|
|
198
|
+
name: string,
|
|
199
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
200
|
+
): boolean =>
|
|
201
|
+
startsWithAny(options.mutableVariablePrefixes, name) ||
|
|
202
|
+
startsWithAny(options.mutableTypePrefixes, name);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether `typeNode` names a type of a `mutableTypePrefixes` prefix anywhere in
|
|
206
|
+
* it (`MutableFoo`, `ns.MutableFoo[]`, `import('./m').MutableFoo | X`), or the
|
|
207
|
+
* type of a variable of a `mutableVariablePrefixes` prefix or a class of a
|
|
208
|
+
* `mutableTypePrefixes` one (`typeof mut_x`, `typeof MutableStore`). It reads
|
|
209
|
+
* names only, so an alias of another name for such a type is missed.
|
|
210
|
+
*/
|
|
211
|
+
const refersToMutableType = (
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
213
|
+
typeNode: tsm.TypeNode | undefined,
|
|
214
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
215
|
+
): boolean => {
|
|
216
|
+
if (typeNode === undefined) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const refers = (n: tsm.Node): boolean => {
|
|
221
|
+
if (n.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
222
|
+
return startsWithAny(
|
|
223
|
+
options.mutableTypePrefixes,
|
|
224
|
+
lastNameOf(n.getTypeName()),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (n.isKind(tsm.SyntaxKind.ImportType)) {
|
|
229
|
+
const qualifier = n.getQualifier();
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
qualifier !== undefined &&
|
|
233
|
+
// typeof import('./m').mut_x names a value
|
|
234
|
+
(n.compilerNode.isTypeOf
|
|
235
|
+
? isMutableValueName(firstNameOf(qualifier), options)
|
|
236
|
+
: startsWithAny(options.mutableTypePrefixes, lastNameOf(qualifier)))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// typeof mut_x, typeof mut_x.items, typeof MutableClass
|
|
241
|
+
return (
|
|
242
|
+
n.isKind(tsm.SyntaxKind.TypeQuery) &&
|
|
243
|
+
isMutableValueName(firstNameOf(n.getExprName()), options)
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return refers(typeNode) || typeNode.getFirstDescendant(refers) !== undefined;
|
|
248
|
+
};
|
|
249
|
+
|
|
122
250
|
const transformNode = (
|
|
123
251
|
node: tsm.Node,
|
|
124
252
|
context: AsConstContext,
|
|
@@ -146,11 +274,13 @@ const transformNode = (
|
|
|
146
274
|
if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
|
|
147
275
|
const nodeName = node.getName();
|
|
148
276
|
|
|
149
|
-
// check for
|
|
150
|
-
if (options.
|
|
277
|
+
// check for mutableVariablePrefixes
|
|
278
|
+
if (options.mutableVariablePrefixes.some((p) => nodeName.startsWith(p))) {
|
|
151
279
|
// Skip conversion for variable declarations with ignored prefixes
|
|
152
280
|
// Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
|
|
153
|
-
options.debugPrint(
|
|
281
|
+
options.debugPrint(
|
|
282
|
+
'skipped variable declaration by mutableVariablePrefixes',
|
|
283
|
+
);
|
|
154
284
|
|
|
155
285
|
return;
|
|
156
286
|
}
|
|
@@ -170,6 +300,18 @@ const transformNode = (
|
|
|
170
300
|
return;
|
|
171
301
|
}
|
|
172
302
|
|
|
303
|
+
// Example: const foo: MutableFoo = { a: [] }; -> remains as is, because
|
|
304
|
+
// `as const` would make the initializer unassignable to `MutableFoo`
|
|
305
|
+
const typeNode = node.getTypeNode();
|
|
306
|
+
|
|
307
|
+
if (refersToMutableType(typeNode, options)) {
|
|
308
|
+
options.debugPrint(
|
|
309
|
+
'skipped variable declaration annotated with a mutable type',
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
173
315
|
const declarationKindKeywords = node
|
|
174
316
|
.getVariableStatement()
|
|
175
317
|
?.getDeclarationKindKeywords()
|
|
@@ -192,13 +334,13 @@ const transformNode = (
|
|
|
192
334
|
}
|
|
193
335
|
|
|
194
336
|
// const [a, b] = ...;
|
|
195
|
-
// TODO: Support
|
|
337
|
+
// TODO: Support mutableVariablePrefixes in ArrayBindingPattern
|
|
196
338
|
// if (ts.isArrayBindingPattern(nodeName)) {
|
|
197
339
|
// // for (const [i, el] of nodeName.elements.entries())
|
|
198
340
|
// }
|
|
199
341
|
|
|
200
342
|
// const { x, y } = ...;
|
|
201
|
-
// TODO: Support
|
|
343
|
+
// TODO: Support mutableVariablePrefixes in ObjectBindingPattern
|
|
202
344
|
// if (ts.isObjectBindingPattern(nodeName)) {
|
|
203
345
|
// // for (const [i, el] of nodeName.elements.entries())
|
|
204
346
|
// }
|
|
@@ -221,6 +363,14 @@ const transformNode = (
|
|
|
221
363
|
|
|
222
364
|
// pass by ([X satisfies ...] -> X)
|
|
223
365
|
if (tsm.Node.isSatisfiesExpression(node)) {
|
|
366
|
+
// Example: { a: [] } satisfies MutableFoo -> remains as is, because
|
|
367
|
+
// `as const` would make it fail to satisfy `MutableFoo`
|
|
368
|
+
if (refersToMutableType(node.getTypeNode(), options)) {
|
|
369
|
+
options.debugPrint('skipped satisfies expression with a mutable type');
|
|
370
|
+
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
224
374
|
transformNode(node.getExpression(), context, options);
|
|
225
375
|
|
|
226
376
|
return;
|
|
@@ -239,6 +389,14 @@ const transformNode = (
|
|
|
239
389
|
|
|
240
390
|
// pass by arrow function body ([() => X] -> X)
|
|
241
391
|
if (tsm.Node.isArrowFunction(node)) {
|
|
392
|
+
// Example: (): MutableFoo => ({ a: [] }) -> remains as is, because
|
|
393
|
+
// `as const` would make the body unassignable to `MutableFoo`
|
|
394
|
+
if (refersToMutableType(node.getReturnTypeNode(), options)) {
|
|
395
|
+
options.debugPrint('skipped arrow function with a mutable return type');
|
|
396
|
+
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
242
400
|
const body = node.getBody();
|
|
243
401
|
|
|
244
402
|
transformNode(body, context, options);
|
|
@@ -317,9 +475,9 @@ const transformNode = (
|
|
|
317
475
|
return;
|
|
318
476
|
}
|
|
319
477
|
|
|
320
|
-
// `<const>x` is handled as the `x as const` it means. Appending `as const`
|
|
321
|
-
//
|
|
322
|
-
//
|
|
478
|
+
// `<const>x` is handled as the `x as const` it means. Appending `as const` to
|
|
479
|
+
// `x` in place would read as `(<const>x) as const`, and every position that
|
|
480
|
+
// reaches here already takes an `as` expression.
|
|
323
481
|
if (tsm.Node.isTypeAssertion(node) && isConstAssertionNode(node)) {
|
|
324
482
|
const asConstText = `${node.getExpression().getText()} as const` as const;
|
|
325
483
|
|
|
@@ -410,78 +568,54 @@ const transformNode = (
|
|
|
410
568
|
// return;
|
|
411
569
|
};
|
|
412
570
|
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
* A `const` type parameter makes TypeScript treat every inference candidate
|
|
419
|
-
* as if it were annotated with `as const`, so when the parameter type is the
|
|
420
|
-
* bare type parameter itself, removing the assertion cannot change the
|
|
421
|
-
* inferred type. Conservative bail-outs (the `as const` is kept) when:
|
|
422
|
-
*
|
|
423
|
-
* - the callee does not resolve to exactly one call signature (unresolved
|
|
424
|
-
* imports resolve to none, since each file is transformed in an isolated
|
|
425
|
-
* single-file project; overloaded functions resolve to several),
|
|
426
|
-
* - the call has explicit type arguments (inference is not involved),
|
|
427
|
-
* - the argument is a spread argument or follows one (its parameter position
|
|
428
|
-
* is not statically known),
|
|
429
|
-
* - the parameter type is not the bare type parameter (e.g. `readonly T[]`).
|
|
430
|
-
*/
|
|
431
|
-
const removeAsConstInConstTypeParameterArgs = (
|
|
432
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
433
|
-
sourceAst: tsm.SourceFile,
|
|
434
|
-
options: AppendAsConstTransformerOptionsInternal,
|
|
435
|
-
): void => {
|
|
436
|
-
// Reversed so that calls nested inside another call's arguments are
|
|
437
|
-
// processed before the outer call replaces (and thereby forgets) them.
|
|
438
|
-
for (const call of sourceAst
|
|
439
|
-
.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)
|
|
440
|
-
.toReversed()) {
|
|
441
|
-
if (call.wasForgotten()) {
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const args = call.getArguments();
|
|
571
|
+
/** `node` with the parentheses written around it taken off. */
|
|
572
|
+
const skipParenthesizedExpressions = (node: tsm.Node): tsm.Node =>
|
|
573
|
+
tsm.Node.isParenthesizedExpression(node)
|
|
574
|
+
? skipParenthesizedExpressions(node.getExpression())
|
|
575
|
+
: node;
|
|
446
576
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
577
|
+
const getMatchedParameter = (
|
|
578
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
579
|
+
parameters: readonly tsm.ParameterDeclaration[],
|
|
580
|
+
argumentIndex: number,
|
|
581
|
+
): tsm.ParameterDeclaration | undefined => {
|
|
582
|
+
if (argumentIndex < parameters.length) {
|
|
583
|
+
return parameters[argumentIndex];
|
|
584
|
+
}
|
|
455
585
|
|
|
456
|
-
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
586
|
+
const lastParameter = parameters.at(-1);
|
|
459
587
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
);
|
|
588
|
+
return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
|
|
589
|
+
};
|
|
463
590
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Whether the parameter's declared type is exactly a bare reference to one of
|
|
593
|
+
* the given `const`-modified type parameters (i.e. `x: T`, not `x: readonly
|
|
594
|
+
* T[]` or `x: Wrapper<T>` — there the argument expression's own type is still
|
|
595
|
+
* observable, so the `as const` is kept).
|
|
596
|
+
*/
|
|
597
|
+
const isBareConstTypeParameterReference = (
|
|
598
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
599
|
+
parameter: tsm.ParameterDeclaration,
|
|
600
|
+
constTypeParameterNames: ISet<string>,
|
|
601
|
+
): boolean => {
|
|
602
|
+
const typeNode = parameter.getTypeNode();
|
|
470
603
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
604
|
+
if (typeNode === undefined) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
476
607
|
|
|
477
|
-
|
|
608
|
+
if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
478
611
|
|
|
479
|
-
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
612
|
+
const typeName = typeNode.getTypeName();
|
|
482
613
|
|
|
483
|
-
|
|
484
|
-
|
|
614
|
+
return (
|
|
615
|
+
typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
616
|
+
Arr.isEmpty(typeNode.getTypeArguments()) &&
|
|
617
|
+
constTypeParameterNames.has(typeName.getText())
|
|
618
|
+
);
|
|
485
619
|
};
|
|
486
620
|
|
|
487
621
|
const removeAsConstArgsOfCall = (
|
|
@@ -542,61 +676,37 @@ const removeAsConstArgsOfCall = (
|
|
|
542
676
|
};
|
|
543
677
|
|
|
544
678
|
/**
|
|
545
|
-
*
|
|
546
|
-
*
|
|
679
|
+
* The parameters an argument list is matched against: a declared `this`
|
|
680
|
+
* parameter (`function f(this: void, a: T)`) stays in the AST's parameter list
|
|
681
|
+
* but takes no argument, so leaving it there would match every argument to the
|
|
682
|
+
* parameter on its left. TypeScript only allows it first.
|
|
547
683
|
*/
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (!tsm.Node.isTypeAssertion(node)) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const typeNode = node.getTypeNode();
|
|
560
|
-
|
|
561
|
-
return (
|
|
562
|
-
typeNode !== undefined &&
|
|
563
|
-
typeNode.isKind(tsm.SyntaxKind.TypeReference) &&
|
|
564
|
-
typeNode.getTypeName().getText() === 'const' &&
|
|
565
|
-
Arr.isEmpty(typeNode.getTypeArguments())
|
|
566
|
-
);
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
/** `node` with the parentheses written around it taken off. */
|
|
570
|
-
const skipParenthesizedExpressions = (node: tsm.Node): tsm.Node =>
|
|
571
|
-
tsm.Node.isParenthesizedExpression(node)
|
|
572
|
-
? skipParenthesizedExpressions(node.getExpression())
|
|
573
|
-
: node;
|
|
574
|
-
|
|
575
|
-
type ResolvedCallTarget = Readonly<{
|
|
576
|
-
parameters: readonly tsm.ParameterDeclaration[];
|
|
577
|
-
constTypeParameterNames: ISet<string>;
|
|
578
|
-
}>;
|
|
684
|
+
const dropThisParameter = (
|
|
685
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ts-morph nodes are mutable class instances
|
|
686
|
+
parameters: readonly tsm.ParameterDeclaration[],
|
|
687
|
+
): readonly tsm.ParameterDeclaration[] =>
|
|
688
|
+
parameters.filter((parameter) => parameter.getName() !== 'this');
|
|
579
689
|
|
|
580
690
|
/**
|
|
581
691
|
* Resolves the callee of a call expression to the declaration of its single
|
|
582
|
-
* call signature, and collects the names of that declaration's
|
|
583
|
-
*
|
|
692
|
+
* call signature, and collects the names of that declaration's `const`-modified
|
|
693
|
+
* type parameters.
|
|
584
694
|
*
|
|
585
695
|
* Returns `undefined` when the callee does not resolve to exactly one call
|
|
586
|
-
* signature, the resolved declaration
|
|
587
|
-
* or none of
|
|
696
|
+
* signature, the resolved declaration is of a kind that cannot carry both
|
|
697
|
+
* parameters and type parameters, or none of its type parameters carries the
|
|
698
|
+
* `const` modifier.
|
|
588
699
|
*/
|
|
589
700
|
const resolveSingleSignatureCallTarget = (
|
|
590
701
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
591
702
|
call: tsm.CallExpression,
|
|
592
703
|
): ResolvedCallTarget | undefined => {
|
|
593
|
-
// The callee's type is read as the call sees it, after any `as`,
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// layer to contradict.
|
|
704
|
+
// The callee's type is read as the call sees it, after any `as`, `satisfies`
|
|
705
|
+
// or `!` around it: that is the signature the argument is inferred against,
|
|
706
|
+
// and an `as const` has no run-time effect for an inner layer to contradict.
|
|
597
707
|
//
|
|
598
|
-
// 0 signatures: the callee is unresolved (e.g. imported, since each file
|
|
599
|
-
//
|
|
708
|
+
// 0 signatures: the callee is unresolved (e.g. imported, since each file is
|
|
709
|
+
// transformed in an isolated single-file project).
|
|
600
710
|
// 2+ signatures: overloads, where removing `as const` could change which
|
|
601
711
|
// overload is selected.
|
|
602
712
|
if (
|
|
@@ -622,9 +732,9 @@ const resolveSingleSignatureCallTarget = (
|
|
|
622
732
|
return undefined;
|
|
623
733
|
}
|
|
624
734
|
|
|
625
|
-
// Only type parameters declared on the call's own signature are inferred
|
|
626
|
-
//
|
|
627
|
-
//
|
|
735
|
+
// Only type parameters declared on the call's own signature are inferred at
|
|
736
|
+
// the call site; a `const` type parameter of an enclosing declaration (e.g. a
|
|
737
|
+
// generic class containing this method) is already fixed there.
|
|
628
738
|
const constTypeParameterNames = ISet.create(
|
|
629
739
|
declaration
|
|
630
740
|
.getTypeParameters()
|
|
@@ -637,51 +747,86 @@ const resolveSingleSignatureCallTarget = (
|
|
|
637
747
|
}
|
|
638
748
|
|
|
639
749
|
return {
|
|
640
|
-
parameters: declaration.getParameters(),
|
|
750
|
+
parameters: dropThisParameter(declaration.getParameters()),
|
|
641
751
|
constTypeParameterNames,
|
|
642
752
|
} as const;
|
|
643
753
|
};
|
|
644
754
|
|
|
645
|
-
|
|
755
|
+
/**
|
|
756
|
+
* Removes redundant `as const` assertions from call arguments whose
|
|
757
|
+
* corresponding parameter type is exactly a `const`-modified type parameter
|
|
758
|
+
* (e.g. `f([1, 2] as const)` for `function f<const T>(x: T): T`).
|
|
759
|
+
*
|
|
760
|
+
* A `const` type parameter makes TypeScript treat every inference candidate as
|
|
761
|
+
* if it were annotated with `as const`, so when the parameter type is the bare
|
|
762
|
+
* type parameter itself, removing the assertion cannot change the inferred
|
|
763
|
+
* type. Conservative bail-outs (the `as const` is kept) when:
|
|
764
|
+
*
|
|
765
|
+
* - the callee does not resolve to exactly one call signature (unresolved
|
|
766
|
+
* imports resolve to none, since each file is transformed in an isolated
|
|
767
|
+
* single-file project; overloaded functions resolve to several),
|
|
768
|
+
* - the call has explicit type arguments (inference is not involved),
|
|
769
|
+
* - the argument is a spread argument or follows one (its parameter position
|
|
770
|
+
* is not statically known),
|
|
771
|
+
* - the parameter type is not the bare type parameter (e.g. `readonly T[]`).
|
|
772
|
+
*/
|
|
773
|
+
const removeAsConstInConstTypeParameterArgs = (
|
|
646
774
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
):
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
775
|
+
sourceAst: tsm.SourceFile,
|
|
776
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
777
|
+
): void => {
|
|
778
|
+
// Reversed so that calls nested inside another call's arguments are processed
|
|
779
|
+
// before the outer call replaces (and thereby forgets) them.
|
|
780
|
+
for (const call of sourceAst
|
|
781
|
+
.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)
|
|
782
|
+
.toReversed()) {
|
|
783
|
+
if (call.wasForgotten()) {
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
653
786
|
|
|
654
|
-
|
|
787
|
+
const args = call.getArguments();
|
|
655
788
|
|
|
656
|
-
|
|
657
|
-
|
|
789
|
+
// Cheap syntactic pre-check before touching the type checker
|
|
790
|
+
if (
|
|
791
|
+
args.every(
|
|
792
|
+
(arg) => !isConstAssertionNode(skipParenthesizedExpressions(arg)),
|
|
793
|
+
)
|
|
794
|
+
) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
658
797
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
* T[]` or `x: Wrapper<T>` — there the argument expression's own type is still
|
|
663
|
-
* observable, so the `as const` is kept).
|
|
664
|
-
*/
|
|
665
|
-
const isBareConstTypeParameterReference = (
|
|
666
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
667
|
-
parameter: tsm.ParameterDeclaration,
|
|
668
|
-
constTypeParameterNames: ISet<string>,
|
|
669
|
-
): boolean => {
|
|
670
|
-
const typeNode = parameter.getTypeNode();
|
|
798
|
+
if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
671
801
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
802
|
+
const variableStatement = call.getFirstAncestorByKind(
|
|
803
|
+
tsm.SyntaxKind.VariableStatement,
|
|
804
|
+
);
|
|
675
805
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
806
|
+
if (
|
|
807
|
+
variableStatement !== undefined &&
|
|
808
|
+
hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
|
|
809
|
+
) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
679
812
|
|
|
680
|
-
|
|
813
|
+
// With explicit type arguments no inference happens, so the `as const` may
|
|
814
|
+
// be load-bearing for assignability.
|
|
815
|
+
if (!Arr.isEmpty(call.getTypeArguments())) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
681
818
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
819
|
+
const callTarget = resolveSingleSignatureCallTarget(call);
|
|
820
|
+
|
|
821
|
+
if (callTarget === undefined) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
removeAsConstArgsOfCall(args, callTarget, options);
|
|
826
|
+
}
|
|
687
827
|
};
|
|
828
|
+
|
|
829
|
+
type ResolvedCallTarget = Readonly<{
|
|
830
|
+
parameters: readonly tsm.ParameterDeclaration[];
|
|
831
|
+
constTypeParameterNames: ISet<string>;
|
|
832
|
+
}>;
|