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.
Files changed (59) hide show
  1. package/README.md +7 -3
  2. package/dist/functions/ast-transformers/append-as-const.d.mts +20 -12
  3. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  4. package/dist/functions/ast-transformers/append-as-const.mjs +170 -88
  5. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  6. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +25 -6
  7. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  8. package/dist/functions/ast-transformers/convert-to-readonly.mjs +272 -255
  9. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  10. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
  11. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
  12. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
  13. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
  14. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
  15. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  17. package/dist/functions/ast-transformers/transform-source-code.mjs +69 -69
  18. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  19. package/dist/functions/constants/index.d.mts +1 -0
  20. package/dist/functions/constants/index.d.mts.map +1 -1
  21. package/dist/functions/constants/index.mjs +1 -0
  22. package/dist/functions/constants/index.mjs.map +1 -1
  23. package/dist/functions/constants/mutable-prefixes.d.mts +23 -0
  24. package/dist/functions/constants/mutable-prefixes.d.mts.map +1 -0
  25. package/dist/functions/constants/mutable-prefixes.mjs +28 -0
  26. package/dist/functions/constants/mutable-prefixes.mjs.map +1 -0
  27. package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
  28. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  29. package/dist/functions/functions/is-readonly-node.d.mts +2 -2
  30. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
  31. package/dist/functions/functions/is-readonly-node.mjs +13 -13
  32. package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
  33. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
  34. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  35. package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
  36. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  37. package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
  38. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
  39. package/dist/functions/utils/replace-node-with-text.mjs +0 -1
  40. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
  41. package/package.json +6 -6
  42. package/src/functions/ast-transformers/append-as-const.mts +319 -174
  43. package/src/functions/ast-transformers/append-as-const.test.mts +118 -7
  44. package/src/functions/ast-transformers/convert-to-readonly.mts +568 -489
  45. package/src/functions/ast-transformers/convert-to-readonly.test.mts +381 -74
  46. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
  47. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
  48. package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
  49. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
  50. package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
  51. package/src/functions/ast-transformers/transform-source-code.mts +110 -110
  52. package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
  53. package/src/functions/constants/index.mts +1 -0
  54. package/src/functions/constants/mutable-prefixes.mts +28 -0
  55. package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
  56. package/src/functions/functions/is-readonly-node.mts +25 -25
  57. package/src/functions/functions/wrap-with-parentheses.mts +138 -52
  58. package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
  59. 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
- // The ESLint counterpart is `ignoredMutablePattern` in
20
- // `libs/eslint-config-typed/src/rules/eslint-functional-rules.mts`; change
21
- // the two together.
22
- const ignorePrefixes = ISet.create(
23
- options?.ignorePrefixes ?? ['mut_', '#mut_', '_mut_', 'draft'],
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
- ignoredPrefixes: ignorePrefixes,
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
- * A mute keywords to ignore the readonly conversion.
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
- * (e.g. `"mut_"`)
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 ['mut_', '#mut_', '_mut_', 'draft']
97
+ * @default ['Mutable', 'mut_']
74
98
  */
75
- ignorePrefixes?: string[];
99
+ mutableTypePrefixes?: string[];
76
100
 
77
101
  /**
78
- * Whether to remove redundant `as const` assertions from call arguments
79
- * whose corresponding parameter type is exactly a `const`-modified type
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
- * already makes TypeScript infer the argument as if it were annotated
85
- * with `as const`.)
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
- * the transformed file itself; imported callees are left as they are.
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
- ignoredPrefixes: ISet<string>;
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 ignorePrefix
150
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
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('skipped variable declaration by ignorePrefixes');
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 ignoredPrefixes in ArrayBindingPattern
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 ignoredPrefixes in ObjectBindingPattern
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
- // to `x` in place would read as `(<const>x) as const`, and every position
322
- // that reaches here already takes an `as` expression.
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
- * Removes redundant `as const` assertions from call arguments whose
415
- * corresponding parameter type is exactly a `const`-modified type parameter
416
- * (e.g. `f([1, 2] as const)` for `function f<const T>(x: T): T`).
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
- // Cheap syntactic pre-check before touching the type checker
448
- if (
449
- args.every(
450
- (arg) => !isConstAssertionNode(skipParenthesizedExpressions(arg)),
451
- )
452
- ) {
453
- continue;
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
- if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
457
- continue;
458
- }
586
+ const lastParameter = parameters.at(-1);
459
587
 
460
- const variableStatement = call.getFirstAncestorByKind(
461
- tsm.SyntaxKind.VariableStatement,
462
- );
588
+ return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
589
+ };
463
590
 
464
- if (
465
- variableStatement !== undefined &&
466
- hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
467
- ) {
468
- continue;
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
- // With explicit type arguments no inference happens, so the `as const`
472
- // may be load-bearing for assignability.
473
- if (!Arr.isEmpty(call.getTypeArguments())) {
474
- continue;
475
- }
604
+ if (typeNode === undefined) {
605
+ return false;
606
+ }
476
607
 
477
- const callTarget = resolveSingleSignatureCallTarget(call);
608
+ if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
609
+ return false;
610
+ }
478
611
 
479
- if (callTarget === undefined) {
480
- continue;
481
- }
612
+ const typeName = typeNode.getTypeName();
482
613
 
483
- removeAsConstArgsOfCall(args, callTarget, options);
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
- * `x as const`, or `<const>x`, the angle-bracket spelling TypeScript treats
546
- * identically (outside `.tsx`, where it does not parse).
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 isConstAssertionNode = (
549
- node: tsm.Node,
550
- ): node is tsm.AsExpression | tsm.TypeAssertion => {
551
- if (isAsConstNode(node)) {
552
- return true;
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
- * `const`-modified type parameters.
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 has no parameters or type parameters,
587
- * or none of the type parameters carry the `const` modifier.
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
- // `satisfies` or `!` around it: that is the signature the argument is
595
- // inferred against, and an `as const` has no run-time effect for an inner
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
- // is transformed in an isolated single-file project).
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
- // at the call site; a `const` type parameter of an enclosing declaration
627
- // (e.g. a generic class containing this method) is already fixed there.
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
- const getMatchedParameter = (
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
- parameters: readonly tsm.ParameterDeclaration[],
648
- argumentIndex: number,
649
- ): tsm.ParameterDeclaration | undefined => {
650
- if (argumentIndex < parameters.length) {
651
- return parameters[argumentIndex];
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
- const lastParameter = parameters.at(-1);
787
+ const args = call.getArguments();
655
788
 
656
- return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
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
- * Whether the parameter's declared type is exactly a bare reference to one of
661
- * the given `const`-modified type parameters (i.e. `x: T`, not `x: readonly
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
- if (typeNode === undefined) {
673
- return false;
674
- }
802
+ const variableStatement = call.getFirstAncestorByKind(
803
+ tsm.SyntaxKind.VariableStatement,
804
+ );
675
805
 
676
- if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
677
- return false;
678
- }
806
+ if (
807
+ variableStatement !== undefined &&
808
+ hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
809
+ ) {
810
+ continue;
811
+ }
679
812
 
680
- const typeName = typeNode.getTypeName();
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
- return (
683
- typeName.isKind(tsm.SyntaxKind.Identifier) &&
684
- Arr.isEmpty(typeNode.getTypeArguments()) &&
685
- constTypeParameterNames.has(typeName.getText())
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
+ }>;