ts-codemod-lib 4.0.0 → 4.1.0
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 +2 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +28 -7
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +452 -253
- 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/mutable-prefixes.d.mts +8 -8
- 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 +9 -9
- package/src/functions/ast-transformers/append-as-const.mts +242 -231
- package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
- package/src/functions/ast-transformers/convert-to-readonly-record-import.test.mts +458 -0
- package/src/functions/ast-transformers/convert-to-readonly.mts +1003 -694
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +80 -33
- 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/mutable-prefixes.mts +8 -8
- 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
|
@@ -91,25 +91,24 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
91
91
|
/**
|
|
92
92
|
* Prefixes of the names of mutable types (see `convert-to-readonly`): a
|
|
93
93
|
* variable whose type annotation names such a type, an expression that
|
|
94
|
-
* `satisfies` one and an arrow function body returning one get no
|
|
95
|
-
*
|
|
94
|
+
* `satisfies` one and an arrow function body returning one get no `as const`,
|
|
95
|
+
* which would make them unassignable to it.
|
|
96
96
|
*
|
|
97
97
|
* @default ['Mutable', 'mut_']
|
|
98
98
|
*/
|
|
99
99
|
mutableTypePrefixes?: string[];
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Whether to remove redundant `as const` assertions from call arguments
|
|
103
|
-
*
|
|
104
|
-
* parameter.
|
|
102
|
+
* Whether to remove redundant `as const` assertions from call arguments whose
|
|
103
|
+
* corresponding parameter type is exactly a `const`-modified type parameter.
|
|
105
104
|
*
|
|
106
105
|
* (e.g. `f([1, 2] as const)` becomes `f([1, 2])` for
|
|
107
|
-
* `function f<const T>(x: T): T`, because the `const` type parameter
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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`.)
|
|
110
109
|
*
|
|
111
|
-
* Only applies when the callee resolves to a single call signature within
|
|
112
|
-
*
|
|
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.
|
|
113
112
|
*
|
|
114
113
|
* @default true
|
|
115
114
|
*/
|
|
@@ -144,6 +143,110 @@ type AsConstContext = Readonly<{
|
|
|
144
143
|
isDirectUnderConstInitializer: boolean;
|
|
145
144
|
}>;
|
|
146
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
|
+
|
|
147
250
|
const transformNode = (
|
|
148
251
|
node: tsm.Node,
|
|
149
252
|
context: AsConstContext,
|
|
@@ -372,9 +475,9 @@ const transformNode = (
|
|
|
372
475
|
return;
|
|
373
476
|
}
|
|
374
477
|
|
|
375
|
-
// `<const>x` is handled as the `x as const` it means. Appending `as const`
|
|
376
|
-
//
|
|
377
|
-
//
|
|
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.
|
|
378
481
|
if (tsm.Node.isTypeAssertion(node) && isConstAssertionNode(node)) {
|
|
379
482
|
const asConstText = `${node.getExpression().getText()} as const` as const;
|
|
380
483
|
|
|
@@ -465,78 +568,54 @@ const transformNode = (
|
|
|
465
568
|
// return;
|
|
466
569
|
};
|
|
467
570
|
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
* A `const` type parameter makes TypeScript treat every inference candidate
|
|
474
|
-
* as if it were annotated with `as const`, so when the parameter type is the
|
|
475
|
-
* bare type parameter itself, removing the assertion cannot change the
|
|
476
|
-
* inferred type. Conservative bail-outs (the `as const` is kept) when:
|
|
477
|
-
*
|
|
478
|
-
* - the callee does not resolve to exactly one call signature (unresolved
|
|
479
|
-
* imports resolve to none, since each file is transformed in an isolated
|
|
480
|
-
* single-file project; overloaded functions resolve to several),
|
|
481
|
-
* - the call has explicit type arguments (inference is not involved),
|
|
482
|
-
* - the argument is a spread argument or follows one (its parameter position
|
|
483
|
-
* is not statically known),
|
|
484
|
-
* - the parameter type is not the bare type parameter (e.g. `readonly T[]`).
|
|
485
|
-
*/
|
|
486
|
-
const removeAsConstInConstTypeParameterArgs = (
|
|
487
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
488
|
-
sourceAst: tsm.SourceFile,
|
|
489
|
-
options: AppendAsConstTransformerOptionsInternal,
|
|
490
|
-
): void => {
|
|
491
|
-
// Reversed so that calls nested inside another call's arguments are
|
|
492
|
-
// processed before the outer call replaces (and thereby forgets) them.
|
|
493
|
-
for (const call of sourceAst
|
|
494
|
-
.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)
|
|
495
|
-
.toReversed()) {
|
|
496
|
-
if (call.wasForgotten()) {
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
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;
|
|
501
576
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
}
|
|
510
585
|
|
|
511
|
-
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
586
|
+
const lastParameter = parameters.at(-1);
|
|
514
587
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
);
|
|
588
|
+
return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
|
|
589
|
+
};
|
|
518
590
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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();
|
|
525
603
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
604
|
+
if (typeNode === undefined) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
531
607
|
|
|
532
|
-
|
|
608
|
+
if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
533
611
|
|
|
534
|
-
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
612
|
+
const typeName = typeNode.getTypeName();
|
|
537
613
|
|
|
538
|
-
|
|
539
|
-
|
|
614
|
+
return (
|
|
615
|
+
typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
616
|
+
Arr.isEmpty(typeNode.getTypeArguments()) &&
|
|
617
|
+
constTypeParameterNames.has(typeName.getText())
|
|
618
|
+
);
|
|
540
619
|
};
|
|
541
620
|
|
|
542
621
|
const removeAsConstArgsOfCall = (
|
|
@@ -597,61 +676,37 @@ const removeAsConstArgsOfCall = (
|
|
|
597
676
|
};
|
|
598
677
|
|
|
599
678
|
/**
|
|
600
|
-
*
|
|
601
|
-
*
|
|
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.
|
|
602
683
|
*/
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (!tsm.Node.isTypeAssertion(node)) {
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const typeNode = node.getTypeNode();
|
|
615
|
-
|
|
616
|
-
return (
|
|
617
|
-
typeNode !== undefined &&
|
|
618
|
-
typeNode.isKind(tsm.SyntaxKind.TypeReference) &&
|
|
619
|
-
typeNode.getTypeName().getText() === 'const' &&
|
|
620
|
-
Arr.isEmpty(typeNode.getTypeArguments())
|
|
621
|
-
);
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
/** `node` with the parentheses written around it taken off. */
|
|
625
|
-
const skipParenthesizedExpressions = (node: tsm.Node): tsm.Node =>
|
|
626
|
-
tsm.Node.isParenthesizedExpression(node)
|
|
627
|
-
? skipParenthesizedExpressions(node.getExpression())
|
|
628
|
-
: node;
|
|
629
|
-
|
|
630
|
-
type ResolvedCallTarget = Readonly<{
|
|
631
|
-
parameters: readonly tsm.ParameterDeclaration[];
|
|
632
|
-
constTypeParameterNames: ISet<string>;
|
|
633
|
-
}>;
|
|
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');
|
|
634
689
|
|
|
635
690
|
/**
|
|
636
691
|
* Resolves the callee of a call expression to the declaration of its single
|
|
637
|
-
* call signature, and collects the names of that declaration's
|
|
638
|
-
*
|
|
692
|
+
* call signature, and collects the names of that declaration's `const`-modified
|
|
693
|
+
* type parameters.
|
|
639
694
|
*
|
|
640
695
|
* Returns `undefined` when the callee does not resolve to exactly one call
|
|
641
|
-
* signature, the resolved declaration
|
|
642
|
-
* 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.
|
|
643
699
|
*/
|
|
644
700
|
const resolveSingleSignatureCallTarget = (
|
|
645
701
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
646
702
|
call: tsm.CallExpression,
|
|
647
703
|
): ResolvedCallTarget | undefined => {
|
|
648
|
-
// The callee's type is read as the call sees it, after any `as`,
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
// 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.
|
|
652
707
|
//
|
|
653
|
-
// 0 signatures: the callee is unresolved (e.g. imported, since each file
|
|
654
|
-
//
|
|
708
|
+
// 0 signatures: the callee is unresolved (e.g. imported, since each file is
|
|
709
|
+
// transformed in an isolated single-file project).
|
|
655
710
|
// 2+ signatures: overloads, where removing `as const` could change which
|
|
656
711
|
// overload is selected.
|
|
657
712
|
if (
|
|
@@ -677,9 +732,9 @@ const resolveSingleSignatureCallTarget = (
|
|
|
677
732
|
return undefined;
|
|
678
733
|
}
|
|
679
734
|
|
|
680
|
-
// Only type parameters declared on the call's own signature are inferred
|
|
681
|
-
//
|
|
682
|
-
//
|
|
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.
|
|
683
738
|
const constTypeParameterNames = ISet.create(
|
|
684
739
|
declaration
|
|
685
740
|
.getTypeParameters()
|
|
@@ -692,130 +747,86 @@ const resolveSingleSignatureCallTarget = (
|
|
|
692
747
|
}
|
|
693
748
|
|
|
694
749
|
return {
|
|
695
|
-
parameters: declaration.getParameters(),
|
|
750
|
+
parameters: dropThisParameter(declaration.getParameters()),
|
|
696
751
|
constTypeParameterNames,
|
|
697
752
|
} as const;
|
|
698
753
|
};
|
|
699
754
|
|
|
700
|
-
const getMatchedParameter = (
|
|
701
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
702
|
-
parameters: readonly tsm.ParameterDeclaration[],
|
|
703
|
-
argumentIndex: number,
|
|
704
|
-
): tsm.ParameterDeclaration | undefined => {
|
|
705
|
-
if (argumentIndex < parameters.length) {
|
|
706
|
-
return parameters[argumentIndex];
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const lastParameter = parameters.at(-1);
|
|
710
|
-
|
|
711
|
-
return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
|
|
712
|
-
};
|
|
713
|
-
|
|
714
755
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
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[]`).
|
|
719
772
|
*/
|
|
720
|
-
const
|
|
773
|
+
const removeAsConstInConstTypeParameterArgs = (
|
|
721
774
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
):
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
const typeName = typeNode.getTypeName();
|
|
736
|
-
|
|
737
|
-
return (
|
|
738
|
-
typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
739
|
-
Arr.isEmpty(typeNode.getTypeArguments()) &&
|
|
740
|
-
constTypeParameterNames.has(typeName.getText())
|
|
741
|
-
);
|
|
742
|
-
};
|
|
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
|
+
}
|
|
743
786
|
|
|
744
|
-
|
|
745
|
-
const lastNameOf = (
|
|
746
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
747
|
-
entityName: tsm.EntityName,
|
|
748
|
-
): string =>
|
|
749
|
-
(tsm.Node.isQualifiedName(entityName)
|
|
750
|
-
? entityName.getRight()
|
|
751
|
-
: entityName
|
|
752
|
-
).getText();
|
|
787
|
+
const args = call.getArguments();
|
|
753
788
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
}
|
|
762
797
|
|
|
763
|
-
|
|
764
|
-
|
|
798
|
+
if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
765
801
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
*/
|
|
770
|
-
const isMutableValueName = (
|
|
771
|
-
name: string,
|
|
772
|
-
options: AppendAsConstTransformerOptionsInternal,
|
|
773
|
-
): boolean =>
|
|
774
|
-
startsWithAny(options.mutableVariablePrefixes, name) ||
|
|
775
|
-
startsWithAny(options.mutableTypePrefixes, name);
|
|
802
|
+
const variableStatement = call.getFirstAncestorByKind(
|
|
803
|
+
tsm.SyntaxKind.VariableStatement,
|
|
804
|
+
);
|
|
776
805
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
*/
|
|
784
|
-
const refersToMutableType = (
|
|
785
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
786
|
-
typeNode: tsm.TypeNode | undefined,
|
|
787
|
-
options: AppendAsConstTransformerOptionsInternal,
|
|
788
|
-
): boolean => {
|
|
789
|
-
if (typeNode === undefined) {
|
|
790
|
-
return false;
|
|
791
|
-
}
|
|
806
|
+
if (
|
|
807
|
+
variableStatement !== undefined &&
|
|
808
|
+
hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
|
|
809
|
+
) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
792
812
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
lastNameOf(n.getTypeName()),
|
|
798
|
-
);
|
|
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;
|
|
799
817
|
}
|
|
800
818
|
|
|
801
|
-
|
|
802
|
-
const qualifier = n.getQualifier();
|
|
819
|
+
const callTarget = resolveSingleSignatureCallTarget(call);
|
|
803
820
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
// typeof import('./m').mut_x names a value
|
|
807
|
-
(n.compilerNode.isTypeOf
|
|
808
|
-
? isMutableValueName(firstNameOf(qualifier), options)
|
|
809
|
-
: startsWithAny(options.mutableTypePrefixes, lastNameOf(qualifier)))
|
|
810
|
-
);
|
|
821
|
+
if (callTarget === undefined) {
|
|
822
|
+
continue;
|
|
811
823
|
}
|
|
812
824
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
n.isKind(tsm.SyntaxKind.TypeQuery) &&
|
|
816
|
-
isMutableValueName(firstNameOf(n.getExprName()), options)
|
|
817
|
-
);
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
return refers(typeNode) || typeNode.getFirstDescendant(refers) !== undefined;
|
|
825
|
+
removeAsConstArgsOfCall(args, callTarget, options);
|
|
826
|
+
}
|
|
821
827
|
};
|
|
828
|
+
|
|
829
|
+
type ResolvedCallTarget = Readonly<{
|
|
830
|
+
parameters: readonly tsm.ParameterDeclaration[];
|
|
831
|
+
constTypeParameterNames: ISet<string>;
|
|
832
|
+
}>;
|
|
@@ -8,6 +8,12 @@ import * as prettier from 'prettier/standalone';
|
|
|
8
8
|
import { appendAsConstTransformer } from './append-as-const.mjs';
|
|
9
9
|
import { transformSourceCode } from './transform-source-code.mjs';
|
|
10
10
|
|
|
11
|
+
const formatter = (code: string): Promise<string> =>
|
|
12
|
+
prettier.format(code, {
|
|
13
|
+
parser: 'typescript',
|
|
14
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
15
|
+
});
|
|
16
|
+
|
|
11
17
|
const testFn = async ({
|
|
12
18
|
source,
|
|
13
19
|
expected,
|
|
@@ -38,12 +44,6 @@ const testFn = async ({
|
|
|
38
44
|
expect(transformed).toBe(expectedFormatted);
|
|
39
45
|
};
|
|
40
46
|
|
|
41
|
-
const formatter = (code: string): Promise<string> =>
|
|
42
|
-
prettier.format(code, {
|
|
43
|
-
parser: 'typescript',
|
|
44
|
-
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
45
|
-
});
|
|
46
|
-
|
|
47
47
|
describe(appendAsConstTransformer, () => {
|
|
48
48
|
describe('Primitive types (should not add as const)', () => {
|
|
49
49
|
test.each([
|
|
@@ -794,6 +794,39 @@ describe(appendAsConstTransformer, () => {
|
|
|
794
794
|
const m = h([1], [2] as const);
|
|
795
795
|
`,
|
|
796
796
|
},
|
|
797
|
+
{
|
|
798
|
+
name: 'a declared this parameter should not shift arguments onto the parameter to their left',
|
|
799
|
+
source: dedent`
|
|
800
|
+
declare function f<const T, U>(this: void, a: T, b: U): U;
|
|
801
|
+
const r = f(1, [1, 2] as const);
|
|
802
|
+
`,
|
|
803
|
+
expected: dedent`
|
|
804
|
+
declare function f<const T, U>(this: void, a: T, b: U): U;
|
|
805
|
+
const r = f(1, [1, 2] as const);
|
|
806
|
+
`,
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: 'a declared this parameter should still let the matching argument lose as const',
|
|
810
|
+
source: dedent`
|
|
811
|
+
declare function f<const T, U>(this: void, a: T, b: U): T;
|
|
812
|
+
const r = f([1, 2] as const, [3] as const);
|
|
813
|
+
`,
|
|
814
|
+
expected: dedent`
|
|
815
|
+
declare function f<const T, U>(this: void, a: T, b: U): T;
|
|
816
|
+
const r = f([1, 2], [3] as const);
|
|
817
|
+
`,
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: 'a this parameter in a function type should be skipped too',
|
|
821
|
+
source: dedent`
|
|
822
|
+
declare const g: <const T>(this: void, x: T) => T;
|
|
823
|
+
const r = g([1] as const);
|
|
824
|
+
`,
|
|
825
|
+
expected: dedent`
|
|
826
|
+
declare const g: <const T>(this: void, x: T) => T;
|
|
827
|
+
const r = g([1]);
|
|
828
|
+
`,
|
|
829
|
+
},
|
|
797
830
|
{
|
|
798
831
|
name: 'non-const type parameter should keep as const',
|
|
799
832
|
source: dedent`
|