ts-codemod-lib 4.0.0 → 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 (49) hide show
  1. package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
  2. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  3. package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
  4. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  5. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +4 -4
  6. package/dist/functions/ast-transformers/convert-to-readonly.mjs +248 -252
  7. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  8. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
  9. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
  10. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
  11. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
  12. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
  13. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  14. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  15. package/dist/functions/ast-transformers/transform-source-code.mjs +69 -69
  16. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  17. package/dist/functions/constants/mutable-prefixes.d.mts +8 -8
  18. package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
  19. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  20. package/dist/functions/functions/is-readonly-node.d.mts +2 -2
  21. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
  22. package/dist/functions/functions/is-readonly-node.mjs +13 -13
  23. package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
  24. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
  25. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  26. package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
  27. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  28. package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
  29. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
  30. package/dist/functions/utils/replace-node-with-text.mjs +0 -1
  31. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
  32. package/package.json +6 -6
  33. package/src/functions/ast-transformers/append-as-const.mts +242 -231
  34. package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
  35. package/src/functions/ast-transformers/convert-to-readonly.mts +467 -471
  36. package/src/functions/ast-transformers/convert-to-readonly.test.mts +60 -33
  37. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
  38. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
  39. package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
  40. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
  41. package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
  42. package/src/functions/ast-transformers/transform-source-code.mts +110 -110
  43. package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
  44. package/src/functions/constants/mutable-prefixes.mts +8 -8
  45. package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
  46. package/src/functions/functions/is-readonly-node.mts +25 -25
  47. package/src/functions/functions/wrap-with-parentheses.mts +138 -52
  48. package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
  49. package/src/functions/utils/replace-node-with-text.mts +10 -11
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-with-parentheses.mjs","sourceRoot":"","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,eAAe,GAAG,CAAC,IAAwB,EAAU,EAAE;IAC3D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,GAAG;YACN,OAAO,CAAC,CAAC;QAEX,KAAK,GAAG;YACN,OAAO,CAAC,CAAC,CAAC;QAEZ,sEAAsE;QACtE,0DAA0D;QAC1D,KAAK,SAAS,CAAC;QACf;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,wBAAwB,GAAG,CAAC,GAAW,EAAW,EAAE;IACxD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,4DAA4D;IAC5D,6DAA6D;IAC7D,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACvD,SAAS,IAAI,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAEhD,kFAAkF;QAClF,IAAI,SAAS,KAAK,CAAC,IAAI,KAAK,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAe,EAAU,EAAE;IAC7D,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAE/B,OAAO,wBAAwB,CAAC,OAAO,CAAC;QACtC,CAAC,CAAC,OAAO;QACT,CAAC,CAAE,IAAI,OAAO,GAAa,CAAC;AAChC,CAAC,CAAC"}
1
+ {"version":3,"file":"wrap-with-parentheses.mjs","sourceRoot":"","sources":["../../../src/functions/functions/wrap-with-parentheses.mts"],"names":[],"mappings":"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"}
@@ -3,19 +3,18 @@ import type * as tsm from 'ts-morph';
3
3
  * `node.replaceWithText(newNodeText)`, sparing a one-line text the indentation
4
4
  * lookup.
5
5
  *
6
- * `replaceWithText` writes the text through a writer whose indentation it
7
- * looks up first, for the lines after the first one, and the lookup reparses
8
- * the whole file in the language service: on top of the reparse every
9
- * replacement makes anyway, once per replacement, so it is quadratic in the
10
- * size of the file. A text with no `\n` has no line after the first, the
11
- * indentation is never applied, and a plain writer from the same project
12
- * writes the same text — so the lookup is skipped. A multi-line text takes
13
- * the original path.
6
+ * `replaceWithText` writes the text through a writer whose indentation it looks
7
+ * up first, for the lines after the first one, and the lookup reparses the
8
+ * whole file in the language service: on top of the reparse every replacement
9
+ * makes anyway, once per replacement, so it is quadratic in the size of the
10
+ * file. A text with no `\n` has no line after the first, the indentation is
11
+ * never applied, and a plain writer from the same project writes the same text
12
+ * — so the lookup is skipped. A multi-line text takes the original path.
14
13
  *
15
14
  * The writer is passed as `replaceWithText`'s second argument, which its
16
- * declaration leaves out and ts-morph passes itself when it sets an
17
- * expression. Were it ever dropped, the method would look the indentation up
18
- * as before: slower, never different.
15
+ * declaration leaves out and ts-morph passes itself when it sets an expression.
16
+ * Were it ever dropped, the method would look the indentation up as before:
17
+ * slower, never different.
19
18
  */
20
19
  export declare const replaceNodeWithText: (node: tsm.Node, newNodeText: string) => tsm.Node;
21
20
  //# sourceMappingURL=replace-node-with-text.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"replace-node-with-text.d.mts","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,SACxB,GAAG,CAAC,IAAI,eACD,MAAM,KAClB,GAAG,CAAC,IAOA,CAAC"}
1
+ {"version":3,"file":"replace-node-with-text.d.mts","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AAErC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,SACxB,GAAG,CAAC,IAAI,eACD,MAAM,KAClB,GAAG,CAAC,IAOA,CAAC"}
@@ -15,7 +15,6 @@
15
15
 
16
16
 
17
17
 
18
-
19
18
  export const replaceNodeWithText = (node, newNodeText) => newNodeText.includes('\n')
20
19
  ? node.replaceWithText(newNodeText)
21
20
  : node.replaceWithText.call(node, newNodeText, node.getProject().createWriter());
@@ -1 +1 @@
1
- {"version":3,"file":"replace-node-with-text.mjs","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,IAAc,EACd,WAAmB,EACT,EAAE,CACZ,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC;IACxB,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;IACnC,CAAC,CAAE,IAAI,CAAC,eAA6C,CAAC,IAAI,CACtD,IAAI,EACJ,WAAW,EACX,IAAI,CAAC,UAAU,EAAE,CAAC,YAAY,EAAE,CACjC,CAAC"}
1
+ {"version":3,"file":"replace-node-with-text.mjs","sourceRoot":"","sources":["../../../src/functions/utils/replace-node-with-text.mts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,IAAc,EACd,WAAmB,EACT,EAAE,CACZ,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC;IACxB,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;IACnC,CAAC,CAAE,IAAI,CAAC,eAA6C,CAAC,IAAI,CACtD,IAAI,EACJ,WAAW,EACX,IAAI,CAAC,UAAU,EAAE,CAAC,YAAY,EAAE,CACjC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-codemod-lib",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript",
@@ -38,7 +38,7 @@
38
38
  "LICENSE"
39
39
  ],
40
40
  "dependencies": {
41
- "ts-data-forge": "^14.10.0",
41
+ "ts-data-forge": "^14.10.2",
42
42
  "ts-morph": "^28.0.0",
43
43
  "ts-type-forge": "^9.2.2"
44
44
  },
@@ -51,10 +51,10 @@
51
51
  "dedent": "1.7.2",
52
52
  "esbuild": "0.28.2",
53
53
  "eslint": "10.13.0",
54
- "eslint-config-typed": "6.0.0",
55
- "eslint-plugin-ts-data-forge": "0.10.4",
56
- "eslint-plugin-ts-fortress": "1.3.9",
57
- "eslint-plugin-ts-type-forge": "0.8.14",
54
+ "eslint-config-typed": "7.3.1",
55
+ "eslint-plugin-ts-data-forge": "0.10.8",
56
+ "eslint-plugin-ts-fortress": "1.3.12",
57
+ "eslint-plugin-ts-type-forge": "0.8.17",
58
58
  "jiti": "2.7.0",
59
59
  "npm-run-all2": "9.0.4",
60
60
  "playwright": "1.64.1",
@@ -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
- * `as const`, which would make them unassignable to it.
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
- * whose corresponding parameter type is exactly a `const`-modified type
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
- * already makes TypeScript infer the argument as if it were annotated
109
- * 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`.)
110
109
  *
111
- * Only applies when the callee resolves to a single call signature within
112
- * 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.
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
- // to `x` in place would read as `(<const>x) as const`, and every position
377
- // 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.
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
- * Removes redundant `as const` assertions from call arguments whose
470
- * corresponding parameter type is exactly a `const`-modified type parameter
471
- * (e.g. `f([1, 2] as const)` for `function f<const T>(x: T): T`).
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
- // Cheap syntactic pre-check before touching the type checker
503
- if (
504
- args.every(
505
- (arg) => !isConstAssertionNode(skipParenthesizedExpressions(arg)),
506
- )
507
- ) {
508
- continue;
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
- if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
512
- continue;
513
- }
586
+ const lastParameter = parameters.at(-1);
514
587
 
515
- const variableStatement = call.getFirstAncestorByKind(
516
- tsm.SyntaxKind.VariableStatement,
517
- );
588
+ return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
589
+ };
518
590
 
519
- if (
520
- variableStatement !== undefined &&
521
- hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
522
- ) {
523
- continue;
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
- // With explicit type arguments no inference happens, so the `as const`
527
- // may be load-bearing for assignability.
528
- if (!Arr.isEmpty(call.getTypeArguments())) {
529
- continue;
530
- }
604
+ if (typeNode === undefined) {
605
+ return false;
606
+ }
531
607
 
532
- const callTarget = resolveSingleSignatureCallTarget(call);
608
+ if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
609
+ return false;
610
+ }
533
611
 
534
- if (callTarget === undefined) {
535
- continue;
536
- }
612
+ const typeName = typeNode.getTypeName();
537
613
 
538
- removeAsConstArgsOfCall(args, callTarget, options);
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
- * `x as const`, or `<const>x`, the angle-bracket spelling TypeScript treats
601
- * 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.
602
683
  */
603
- const isConstAssertionNode = (
604
- node: tsm.Node,
605
- ): node is tsm.AsExpression | tsm.TypeAssertion => {
606
- if (isAsConstNode(node)) {
607
- return true;
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
- * `const`-modified type parameters.
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 has no parameters or type parameters,
642
- * 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.
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
- // `satisfies` or `!` around it: that is the signature the argument is
650
- // inferred against, and an `as const` has no run-time effect for an inner
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
- // 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).
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
- // at the call site; a `const` type parameter of an enclosing declaration
682
- // (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.
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
- * Whether the parameter's declared type is exactly a bare reference to one of
716
- * the given `const`-modified type parameters (i.e. `x: T`, not `x: readonly
717
- * T[]` or `x: Wrapper<T>` — there the argument expression's own type is still
718
- * observable, so the `as const` is kept).
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 isBareConstTypeParameterReference = (
773
+ const removeAsConstInConstTypeParameterArgs = (
721
774
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
722
- parameter: tsm.ParameterDeclaration,
723
- constTypeParameterNames: ISet<string>,
724
- ): boolean => {
725
- const typeNode = parameter.getTypeNode();
726
-
727
- if (typeNode === undefined) {
728
- return false;
729
- }
730
-
731
- if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
732
- return false;
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
- /** `ns.MutableFoo` -> `MutableFoo` */
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
- /** `mut_x.items` -> `mut_x` */
755
- const firstNameOf = (
756
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
757
- entityName: tsm.EntityName,
758
- ): string =>
759
- tsm.Node.isQualifiedName(entityName)
760
- ? firstNameOf(entityName.getLeft())
761
- : entityName.getText();
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
- const startsWithAny = (prefixes: ISet<string>, name: string): boolean =>
764
- prefixes.some((p) => name.startsWith(p));
798
+ if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
799
+ continue;
800
+ }
765
801
 
766
- /**
767
- * The name a `typeof` starts from: a variable (`mut_x`) or a class, which
768
- * `convert-to-readonly` leaves mutable by its type prefix (`MutableStore`).
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
- * Whether `typeNode` names a type of a `mutableTypePrefixes` prefix anywhere
779
- * in it (`MutableFoo`, `ns.MutableFoo[]`, `import('./m').MutableFoo | X`), or
780
- * the type of a variable of a `mutableVariablePrefixes` prefix or a class of
781
- * a `mutableTypePrefixes` one (`typeof mut_x`, `typeof MutableStore`). It reads names only, so an alias of another name for such
782
- * a type is missed.
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
- const refers = (n: tsm.Node): boolean => {
794
- if (n.isKind(tsm.SyntaxKind.TypeReference)) {
795
- return startsWithAny(
796
- options.mutableTypePrefixes,
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
- if (n.isKind(tsm.SyntaxKind.ImportType)) {
802
- const qualifier = n.getQualifier();
819
+ const callTarget = resolveSingleSignatureCallTarget(call);
803
820
 
804
- return (
805
- qualifier !== undefined &&
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
- // typeof mut_x, typeof mut_x.items, typeof MutableClass
814
- return (
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
+ }>;