ts-codemod-lib 3.5.1 → 4.0.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.
Files changed (24) hide show
  1. package/README.md +7 -3
  2. package/dist/functions/ast-transformers/append-as-const.d.mts +13 -4
  3. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  4. package/dist/functions/ast-transformers/append-as-const.mjs +80 -7
  5. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  6. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +23 -4
  7. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  8. package/dist/functions/ast-transformers/convert-to-readonly.mjs +40 -19
  9. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  10. package/dist/functions/constants/index.d.mts +1 -0
  11. package/dist/functions/constants/index.d.mts.map +1 -1
  12. package/dist/functions/constants/index.mjs +1 -0
  13. package/dist/functions/constants/index.mjs.map +1 -1
  14. package/dist/functions/constants/mutable-prefixes.d.mts +23 -0
  15. package/dist/functions/constants/mutable-prefixes.d.mts.map +1 -0
  16. package/dist/functions/constants/mutable-prefixes.mjs +28 -0
  17. package/dist/functions/constants/mutable-prefixes.mjs.map +1 -0
  18. package/package.json +2 -2
  19. package/src/functions/ast-transformers/append-as-const.mts +152 -18
  20. package/src/functions/ast-transformers/append-as-const.test.mts +79 -1
  21. package/src/functions/ast-transformers/convert-to-readonly.mts +124 -41
  22. package/src/functions/ast-transformers/convert-to-readonly.test.mts +321 -41
  23. package/src/functions/constants/index.mts +1 -0
  24. package/src/functions/constants/mutable-prefixes.mts +28 -0
@@ -2,6 +2,7 @@ import {
2
2
  Arr,
3
3
  expectType,
4
4
  ISet,
5
+ isRecord,
5
6
  isString,
6
7
  mapNullable,
7
8
  match,
@@ -14,6 +15,10 @@ import type {
14
15
  StrictExclude,
15
16
  StrictExtract,
16
17
  } from 'ts-type-forge';
18
+ import {
19
+ DEFAULT_MUTABLE_PREFIXES,
20
+ DEFAULT_MUTABLE_TYPE_PREFIXES,
21
+ } from '../constants/index.mjs';
17
22
  import {
18
23
  hasCallOrConstructSignature,
19
24
  hasDisableNextLineComment,
@@ -59,13 +64,17 @@ export const convertToReadonlyTransformer = (
59
64
  const DeepReadonlyTypeName =
60
65
  options?.DeepReadonly?.typeName ?? 'DeepReadonly';
61
66
 
62
- // The ESLint counterpart is `ignoredMutablePattern` in
63
- // `libs/eslint-config-typed/src/rules/eslint-functional-rules.mts`; change
64
- // the two together. (ESLint matches a property access by its own
65
- // `ignoreAccessorPattern`, `**.mut_**`, instead.)
66
- const ignorePrefixes = ISet.create(
67
- options?.ignorePrefixes ?? ['mut_', '#mut_', '_mut_', 'draft'],
68
- );
67
+ // Removed in 4.0.0; a caller without types would otherwise lose it silently.
68
+ const untypedOptions: unknown = options;
69
+
70
+ if (
71
+ isRecord(untypedOptions) &&
72
+ untypedOptions['ignorePrefixes'] !== undefined
73
+ ) {
74
+ throw new Error(
75
+ `The "ignorePrefixes" option of ${TRANSFORMER_NAME} was replaced by "mutableVariablePrefixes", "mutableTypePrefixes" and "mutablePropertyPrefixes".`,
76
+ );
77
+ }
69
78
 
70
79
  const optionsInternal: ReadonlyTransformerOptionsInternal = {
71
80
  DeepReadonly: {
@@ -73,7 +82,15 @@ export const convertToReadonlyTransformer = (
73
82
  applyLevel: 'keep',
74
83
  },
75
84
  ignoreEmptyObjectTypes: options?.ignoreEmptyObjectTypes ?? true,
76
- ignoredPrefixes: ignorePrefixes,
85
+ mutableVariablePrefixes: ISet.create(
86
+ options?.mutableVariablePrefixes ?? DEFAULT_MUTABLE_PREFIXES,
87
+ ),
88
+ mutableTypePrefixes: ISet.create(
89
+ options?.mutableTypePrefixes ?? DEFAULT_MUTABLE_TYPE_PREFIXES,
90
+ ),
91
+ mutablePropertyPrefixes: ISet.create(
92
+ options?.mutablePropertyPrefixes ?? DEFAULT_MUTABLE_PREFIXES,
93
+ ),
77
94
  recordStyle: options?.recordStyle ?? 'ReadonlyRecord',
78
95
  debugPrint: options?.debug === true ? console.debug : () => {},
79
96
  replaceNode:
@@ -139,9 +156,30 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
139
156
  ignoreEmptyObjectTypes?: boolean;
140
157
 
141
158
  /**
142
- * A mute keywords to ignore the readonly conversion.
159
+ * Prefixes of the names of variables, parameters (but for a constructor
160
+ * parameter property, which `mutablePropertyPrefixes` decides) and
161
+ * functions that are left mutable: their types are not made readonly.
143
162
  *
144
- * (e.g. `"mut_"`)
163
+ * @default ['mut_']
164
+ */
165
+ mutableVariablePrefixes?: string[];
166
+
167
+ /**
168
+ * Prefixes of the names of type aliases, interfaces, class declarations and
169
+ * named class expressions that are left mutable: their declarations are not
170
+ * made readonly.
171
+ *
172
+ * @default ['Mutable', 'mut_']
173
+ */
174
+ mutableTypePrefixes?: string[];
175
+
176
+ /**
177
+ * Prefixes of the names of properties, methods and accessors (of type
178
+ * literals, interfaces and classes, constructor parameter properties
179
+ * included) that are left mutable: they get no `readonly`, and their types
180
+ * are not made readonly. A private name is matched with or after its `#`, so
181
+ * both `mut_` and `#mut_` cover `#mut_x`. Of a named tuple member with such a prefix, only the type
182
+ * is kept; the tuple is still made `readonly`.
145
183
  *
146
184
  * A type literal with a property, a method or an accessor of such a prefix
147
185
  * is not wrapped with `Readonly<...>`, which would make that member readonly
@@ -153,9 +191,9 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
153
191
  * it is a member of a `Readonly<...>` intersection beside a type that may be
154
192
  * an array.
155
193
  *
156
- * @default ['mut_', '#mut_', '_mut_', 'draft']
194
+ * @default ['mut_']
157
195
  */
158
- ignorePrefixes?: string[];
196
+ mutablePropertyPrefixes?: string[];
159
197
 
160
198
  /**
161
199
  * The output style used when making `Record<K, V>` readonly.
@@ -190,7 +228,11 @@ type ReadonlyTransformerOptionsInternal = Readonly<{
190
228
 
191
229
  ignoreEmptyObjectTypes: boolean;
192
230
 
193
- ignoredPrefixes: ISet<string>;
231
+ mutableVariablePrefixes: ISet<string>;
232
+
233
+ mutableTypePrefixes: ISet<string>;
234
+
235
+ mutablePropertyPrefixes: ISet<string>;
194
236
 
195
237
  recordStyle: 'Readonly<Record>' | 'ReadonlyRecord';
196
238
 
@@ -230,24 +272,26 @@ const transformNode = (
230
272
  return;
231
273
  }
232
274
 
233
- // check for ignorePrefix
275
+ // check for mutableVariablePrefixes
234
276
  if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
235
277
  const nodeName = node.getName();
236
278
 
237
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
279
+ if (options.mutableVariablePrefixes.some((p) => nodeName.startsWith(p))) {
238
280
  // Skip conversion for variable declarations with ignored prefixes
239
281
  // Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
240
- options.debugPrint('skipped variable declaration by ignorePrefixes');
282
+ options.debugPrint(
283
+ 'skipped variable declaration by mutableVariablePrefixes',
284
+ );
241
285
 
242
286
  return;
243
287
  }
244
288
 
245
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
289
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
246
290
  // if (ts.isArrayBindingPattern(nodeName)) {
247
291
  // // for (const [i, el] of nodeName.elements.entries())
248
292
  // }
249
293
 
250
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
294
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
251
295
  // if (ts.isObjectBindingPattern(nodeName)) {
252
296
  // // for (const [i, el] of nodeName.elements.entries())
253
297
  // }
@@ -258,7 +302,9 @@ const transformNode = (
258
302
  if (
259
303
  (node.isKind(tsm.SyntaxKind.FunctionDeclaration) ||
260
304
  node.isKind(tsm.SyntaxKind.FunctionExpression)) &&
261
- options.ignoredPrefixes.some((p) => node.getName()?.startsWith(p) === true)
305
+ options.mutableVariablePrefixes.some(
306
+ (p) => node.getName()?.startsWith(p) === true,
307
+ )
262
308
  ) {
263
309
  return;
264
310
  }
@@ -266,26 +312,30 @@ const transformNode = (
266
312
  if (node.isKind(tsm.SyntaxKind.Parameter)) {
267
313
  const nodeName = node.getName();
268
314
 
269
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
270
- // Skip readonly conversion for variable declarations with ignored prefixes
271
- // Example: const mut_foo: string[] = []; -> remains as is, without readonly conversion
315
+ if (
316
+ mutablePrefixesOfParameter(node, options).some((p) =>
317
+ nodeName.startsWith(p),
318
+ )
319
+ ) {
320
+ // Skip readonly conversion for parameters with mutable prefixes
321
+ // Example: (mut_foo: string[]) => {} -> remains as is, without readonly conversion
272
322
  return;
273
323
  }
274
324
 
275
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
325
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
276
326
  // if (ts.isArrayBindingPattern(nodeName)) {
277
327
  // }
278
328
 
279
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
329
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
280
330
  // if (ts.isObjectBindingPattern(nodeName)) {
281
331
  // }
282
332
  }
283
333
 
284
334
  // Skip readonly conversion for type alias declarations with ignored prefixes
285
- // Example: type mut_Foo = number[]; -> remains as number[] without readonly
335
+ // Example: type MutableFoo = number[]; -> remains as number[] without readonly
286
336
  if (
287
337
  node.isKind(tsm.SyntaxKind.TypeAliasDeclaration) &&
288
- options.ignoredPrefixes.some((p) => node.getName().startsWith(p))
338
+ hasMutableTypeName(node.getName(), options)
289
339
  ) {
290
340
  return;
291
341
  }
@@ -364,8 +414,8 @@ const transformNode = (
364
414
 
365
415
  if (node.isKind(tsm.SyntaxKind.InterfaceDeclaration)) {
366
416
  // Skip readonly conversion for interface declarations with ignored prefixes
367
- // Example: interface mut_Interface {...} -> properties remain without readonly
368
- if (options.ignoredPrefixes.some((p) => node.getName().startsWith(p))) {
417
+ // Example: interface MutableInterface {...} -> properties remain without readonly
418
+ if (hasMutableTypeName(node.getName(), options)) {
369
419
  return;
370
420
  }
371
421
 
@@ -374,14 +424,18 @@ const transformNode = (
374
424
  return;
375
425
  }
376
426
 
427
+ // Example: const C = class MutableClass {...} -> properties remain without readonly
428
+ if (
429
+ node.isKind(tsm.SyntaxKind.ClassExpression) &&
430
+ hasMutableTypeName(node.getName(), options)
431
+ ) {
432
+ return;
433
+ }
434
+
377
435
  if (node.isKind(tsm.SyntaxKind.ClassDeclaration)) {
378
436
  // Skip readonly conversion for class declarations with ignored prefixes
379
- // Example: class mut_Class {...} -> properties remain without readonly
380
- if (
381
- options.ignoredPrefixes.some(
382
- (p) => node.getName()?.startsWith(p) === true,
383
- )
384
- ) {
437
+ // Example: class MutableClass {...} -> properties remain without readonly
438
+ if (hasMutableTypeName(node.getName(), options)) {
385
439
  return;
386
440
  }
387
441
 
@@ -1002,7 +1056,11 @@ const transformTupleTypeNode = (
1002
1056
 
1003
1057
  for (const el of node.getElements()) {
1004
1058
  if (el.isKind(tsm.SyntaxKind.NamedTupleMember)) {
1005
- if (options.ignoredPrefixes.every((p) => !el.getName().startsWith(p))) {
1059
+ if (
1060
+ options.mutablePropertyPrefixes.every(
1061
+ (p) => !el.getName().startsWith(p),
1062
+ )
1063
+ ) {
1006
1064
  transformNode(el.getTypeNode(), nextReadonlyContextValue, options);
1007
1065
  }
1008
1066
  } else {
@@ -1362,7 +1420,9 @@ const transformConstructorParameter = (
1362
1420
 
1363
1421
  const name = param.getName();
1364
1422
 
1365
- if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
1423
+ if (
1424
+ mutablePrefixesOfParameter(param, options).some((p) => name.startsWith(p))
1425
+ ) {
1366
1426
  return;
1367
1427
  }
1368
1428
 
@@ -2097,17 +2157,19 @@ const checkIfPropertyNameShouldBeIgnored = (
2097
2157
  // mut_x: number[]
2098
2158
  (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
2099
2159
  pipe(nameNode.getText()).map((nm) =>
2100
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2160
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2101
2161
  ).value) ||
2102
2162
  // "mut_x": number[]
2103
2163
  (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
2104
2164
  pipe(nameNode.getLiteralValue()).map((nm) =>
2105
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2165
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2106
2166
  ).value) ||
2107
- // #memberName: number[] (class only)
2167
+ // #memberName: number[] (class only), matched with or after its `#`
2108
2168
  (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
2109
2169
  pipe(nameNode.getText()).map((nm) =>
2110
- options.ignoredPrefixes.some((p) => nm.startsWith(`#${p}`)),
2170
+ options.mutablePropertyPrefixes.some(
2171
+ (p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
2172
+ ),
2111
2173
  ).value) ||
2112
2174
  // ['mut_x']: number[], [`mut_x`]: number[]
2113
2175
  (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
@@ -2118,7 +2180,7 @@ const checkIfPropertyNameShouldBeIgnored = (
2118
2180
  ) {
2119
2181
  const nm = exp.getLiteralValue();
2120
2182
 
2121
- return options.ignoredPrefixes.some((p) => nm.startsWith(p));
2183
+ return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
2122
2184
  }
2123
2185
 
2124
2186
  return false;
@@ -2209,3 +2271,24 @@ const takesOffReadonly = (
2209
2271
  isLeftUnwrapped(node, options) &&
2210
2272
  !hasCallOrConstructSignature(node) &&
2211
2273
  !hasDisableNextLineCommentAround(node);
2274
+
2275
+ /** Whether a type alias, interface or class name starts with a `mutableTypePrefixes` entry. */
2276
+ const hasMutableTypeName = (
2277
+ name: string | undefined,
2278
+ options: ReadonlyTransformerOptionsInternal,
2279
+ ): boolean =>
2280
+ name !== undefined &&
2281
+ options.mutableTypePrefixes.some((p) => name.startsWith(p));
2282
+
2283
+ /**
2284
+ * A parameter property is a property of the class, as a `PropertyDeclaration`
2285
+ * is; any other parameter is a variable.
2286
+ */
2287
+ const mutablePrefixesOfParameter = (
2288
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2289
+ param: tsm.ParameterDeclaration,
2290
+ options: ReadonlyTransformerOptionsInternal,
2291
+ ): ISet<string> =>
2292
+ param.isParameterProperty()
2293
+ ? options.mutablePropertyPrefixes
2294
+ : options.mutableVariablePrefixes;