ts-codemod-lib 3.5.0 → 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 (29) hide show
  1. package/README.md +32 -9
  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 +81 -5
  5. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  6. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +33 -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 +155 -30
  9. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  10. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +7 -1
  11. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
  12. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +25 -2
  13. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
  14. package/dist/functions/constants/index.d.mts +1 -0
  15. package/dist/functions/constants/index.d.mts.map +1 -1
  16. package/dist/functions/constants/index.mjs +1 -0
  17. package/dist/functions/constants/index.mjs.map +1 -1
  18. package/dist/functions/constants/mutable-prefixes.d.mts +23 -0
  19. package/dist/functions/constants/mutable-prefixes.d.mts.map +1 -0
  20. package/dist/functions/constants/mutable-prefixes.mjs +28 -0
  21. package/dist/functions/constants/mutable-prefixes.mjs.map +1 -0
  22. package/package.json +19 -19
  23. package/src/functions/ast-transformers/append-as-const.mts +152 -15
  24. package/src/functions/ast-transformers/append-as-const.test.mts +79 -1
  25. package/src/functions/ast-transformers/convert-to-readonly.mts +302 -57
  26. package/src/functions/ast-transformers/convert-to-readonly.test.mts +738 -49
  27. package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +36 -5
  28. package/src/functions/constants/index.mts +1 -0
  29. 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,9 +64,17 @@ export const convertToReadonlyTransformer = (
59
64
  const DeepReadonlyTypeName =
60
65
  options?.DeepReadonly?.typeName ?? 'DeepReadonly';
61
66
 
62
- const ignorePrefixes = ISet.create(
63
- options?.ignorePrefixes ?? ['mut_', '#mut_', '_mut_', 'draft'],
64
- );
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
+ }
65
78
 
66
79
  const optionsInternal: ReadonlyTransformerOptionsInternal = {
67
80
  DeepReadonly: {
@@ -69,7 +82,15 @@ export const convertToReadonlyTransformer = (
69
82
  applyLevel: 'keep',
70
83
  },
71
84
  ignoreEmptyObjectTypes: options?.ignoreEmptyObjectTypes ?? true,
72
- 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
+ ),
73
94
  recordStyle: options?.recordStyle ?? 'ReadonlyRecord',
74
95
  debugPrint: options?.debug === true ? console.debug : () => {},
75
96
  replaceNode:
@@ -135,13 +156,44 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
135
156
  ignoreEmptyObjectTypes?: boolean;
136
157
 
137
158
  /**
138
- * 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.
162
+ *
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.
139
171
  *
140
- * (e.g. `"mut_"`)
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`.
183
+ *
184
+ * A type literal with a property, a method or an accessor of such a prefix
185
+ * is not wrapped with `Readonly<...>`, which would make that member readonly
186
+ * too; its other members are marked `readonly` one by one instead, and a
187
+ * `Readonly<...>` written around it is taken off. It is wrapped as before
188
+ * when it also has a member that cannot be marked `readonly` by itself (a
189
+ * method signature or an accessor without such a prefix, or a property or
190
+ * index signature not yet `readonly` that an ignore comment skips), or when
191
+ * it is a member of a `Readonly<...>` intersection beside a type that may be
192
+ * an array.
141
193
  *
142
- * @default ['mut_', '#mut_', '_mut_', 'draft']
194
+ * @default ['mut_']
143
195
  */
144
- ignorePrefixes?: string[];
196
+ mutablePropertyPrefixes?: string[];
145
197
 
146
198
  /**
147
199
  * The output style used when making `Record<K, V>` readonly.
@@ -176,7 +228,11 @@ type ReadonlyTransformerOptionsInternal = Readonly<{
176
228
 
177
229
  ignoreEmptyObjectTypes: boolean;
178
230
 
179
- ignoredPrefixes: ISet<string>;
231
+ mutableVariablePrefixes: ISet<string>;
232
+
233
+ mutableTypePrefixes: ISet<string>;
234
+
235
+ mutablePropertyPrefixes: ISet<string>;
180
236
 
181
237
  recordStyle: 'Readonly<Record>' | 'ReadonlyRecord';
182
238
 
@@ -216,24 +272,26 @@ const transformNode = (
216
272
  return;
217
273
  }
218
274
 
219
- // check for ignorePrefix
275
+ // check for mutableVariablePrefixes
220
276
  if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
221
277
  const nodeName = node.getName();
222
278
 
223
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
279
+ if (options.mutableVariablePrefixes.some((p) => nodeName.startsWith(p))) {
224
280
  // Skip conversion for variable declarations with ignored prefixes
225
281
  // Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
226
- options.debugPrint('skipped variable declaration by ignorePrefixes');
282
+ options.debugPrint(
283
+ 'skipped variable declaration by mutableVariablePrefixes',
284
+ );
227
285
 
228
286
  return;
229
287
  }
230
288
 
231
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
289
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
232
290
  // if (ts.isArrayBindingPattern(nodeName)) {
233
291
  // // for (const [i, el] of nodeName.elements.entries())
234
292
  // }
235
293
 
236
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
294
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
237
295
  // if (ts.isObjectBindingPattern(nodeName)) {
238
296
  // // for (const [i, el] of nodeName.elements.entries())
239
297
  // }
@@ -244,7 +302,9 @@ const transformNode = (
244
302
  if (
245
303
  (node.isKind(tsm.SyntaxKind.FunctionDeclaration) ||
246
304
  node.isKind(tsm.SyntaxKind.FunctionExpression)) &&
247
- options.ignoredPrefixes.some((p) => node.getName()?.startsWith(p) === true)
305
+ options.mutableVariablePrefixes.some(
306
+ (p) => node.getName()?.startsWith(p) === true,
307
+ )
248
308
  ) {
249
309
  return;
250
310
  }
@@ -252,26 +312,30 @@ const transformNode = (
252
312
  if (node.isKind(tsm.SyntaxKind.Parameter)) {
253
313
  const nodeName = node.getName();
254
314
 
255
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
256
- // Skip readonly conversion for variable declarations with ignored prefixes
257
- // 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
258
322
  return;
259
323
  }
260
324
 
261
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
325
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
262
326
  // if (ts.isArrayBindingPattern(nodeName)) {
263
327
  // }
264
328
 
265
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
329
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
266
330
  // if (ts.isObjectBindingPattern(nodeName)) {
267
331
  // }
268
332
  }
269
333
 
270
334
  // Skip readonly conversion for type alias declarations with ignored prefixes
271
- // Example: type mut_Foo = number[]; -> remains as number[] without readonly
335
+ // Example: type MutableFoo = number[]; -> remains as number[] without readonly
272
336
  if (
273
337
  node.isKind(tsm.SyntaxKind.TypeAliasDeclaration) &&
274
- options.ignoredPrefixes.some((p) => node.getName().startsWith(p))
338
+ hasMutableTypeName(node.getName(), options)
275
339
  ) {
276
340
  return;
277
341
  }
@@ -350,8 +414,8 @@ const transformNode = (
350
414
 
351
415
  if (node.isKind(tsm.SyntaxKind.InterfaceDeclaration)) {
352
416
  // Skip readonly conversion for interface declarations with ignored prefixes
353
- // Example: interface mut_Interface {...} -> properties remain without readonly
354
- if (options.ignoredPrefixes.some((p) => node.getName().startsWith(p))) {
417
+ // Example: interface MutableInterface {...} -> properties remain without readonly
418
+ if (hasMutableTypeName(node.getName(), options)) {
355
419
  return;
356
420
  }
357
421
 
@@ -360,14 +424,18 @@ const transformNode = (
360
424
  return;
361
425
  }
362
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
+
363
435
  if (node.isKind(tsm.SyntaxKind.ClassDeclaration)) {
364
436
  // Skip readonly conversion for class declarations with ignored prefixes
365
- // Example: class mut_Class {...} -> properties remain without readonly
366
- if (
367
- options.ignoredPrefixes.some(
368
- (p) => node.getName()?.startsWith(p) === true,
369
- )
370
- ) {
437
+ // Example: class MutableClass {...} -> properties remain without readonly
438
+ if (hasMutableTypeName(node.getName(), options)) {
371
439
  return;
372
440
  }
373
441
 
@@ -421,6 +489,8 @@ const transformNode = (
421
489
  * - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
422
490
  * - `tr(DeepReadonly<Readonly<E>>) |-> DeepReadonly<tr(E)>`
423
491
  * - `tr(Readonly<number>) |-> number`
492
+ * - `tr(Readonly<{ mut_x: X; y: Y }>) |-> { mut_x: X; readonly y: tr(Y) }` (see
493
+ * `isLeftUnwrapped`)
424
494
  * - `tr(Readonly<E[]>) |-> readonly tr(E)[]`
425
495
  * - `tr(Readonly<[E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
426
496
  * - `tr(Readonly<readonly E[]>) |-> readonly tr(E)[]`
@@ -647,14 +717,29 @@ const transformTypeReferenceNode = (
647
717
  );
648
718
  }
649
719
 
720
+ // Read before the recursion, which may replace the node.
721
+ const typeArgumentBefore = removeParentheses(typeArguments[0]);
722
+
723
+ // Readonly<{ mut_x: X; y: Y }> -> { mut_x: X; readonly y: Y }
724
+ // `Readonly` would make the property of an ignored prefix readonly too, so
725
+ // it is taken off, and the literal is transformed where nothing wraps it,
726
+ // which marks its other members readonly in place. See `takesOffReadonly`.
727
+ const takesOffWrapper =
728
+ readonlyContext.type !== 'DeepReadonly' &&
729
+ readonlyContext.indexedAccessDepth === 0 &&
730
+ typeArgumentBefore.isKind(tsm.SyntaxKind.TypeLiteral) &&
731
+ takesOffReadonly(typeArgumentBefore, options);
732
+
650
733
  // Recursive processing
651
734
  transformNode(
652
735
  typeArguments[0],
653
- nextReadonlyContext({
654
- currentReadonlyContext: readonlyContext,
655
- nextReadonlyContextType: 'Readonly',
656
- indexedAccessDepthChange: 'keep',
657
- }),
736
+ takesOffWrapper
737
+ ? initialReadonlyContext
738
+ : nextReadonlyContext({
739
+ currentReadonlyContext: readonlyContext,
740
+ nextReadonlyContextType: 'Readonly',
741
+ indexedAccessDepthChange: 'keep',
742
+ }),
658
743
  options,
659
744
  );
660
745
 
@@ -695,6 +780,29 @@ const transformTypeReferenceNode = (
695
780
  return;
696
781
  }
697
782
 
783
+ // Readonly<Readonly<{ mut_x: X }>> -> { mut_x: X } as well: the inner
784
+ // `Readonly` has already been taken off the literal.
785
+ if (
786
+ takesOffWrapper ||
787
+ (T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
788
+ ) {
789
+ // The comments after the literal, which nothing else carries over, go
790
+ // in front of it: a line comment after it would end the type with a
791
+ // line break, which a following `[]` cannot be parsed across.
792
+ const commentsAfterT = commentsBetween(
793
+ node.getSourceFile(),
794
+ T.getEnd(),
795
+ node.getEnd(),
796
+ );
797
+
798
+ options.replaceNode(
799
+ node,
800
+ `${commentsInFrontOfT}${commentsAfterT}${T.getText()}`,
801
+ );
802
+
803
+ return;
804
+ }
805
+
698
806
  // Readonly<Readonly<T>> -> Readonly<T>
699
807
  if (isReadonlyTypeReferenceNode(T)) {
700
808
  options.replaceNode(node, TText);
@@ -948,7 +1056,11 @@ const transformTupleTypeNode = (
948
1056
 
949
1057
  for (const el of node.getElements()) {
950
1058
  if (el.isKind(tsm.SyntaxKind.NamedTupleMember)) {
951
- if (options.ignoredPrefixes.every((p) => !el.getName().startsWith(p))) {
1059
+ if (
1060
+ options.mutablePropertyPrefixes.every(
1061
+ (p) => !el.getName().startsWith(p),
1062
+ )
1063
+ ) {
952
1064
  transformNode(el.getTypeNode(), nextReadonlyContextValue, options);
953
1065
  }
954
1066
  } else {
@@ -1085,10 +1197,13 @@ const transformTypeLiteralNode = (
1085
1197
  const wrappingApplies =
1086
1198
  readonlyContext.type === 'none' && readonlyContext.indexedAccessDepth === 0;
1087
1199
 
1088
- // A call or construct signature does not survive `Readonly<T>`, so a literal
1089
- // carrying one is left unwrapped and marked readonly member by member
1090
- // instead. See `hasCallOrConstructSignature`.
1091
- const skipWrapping = wrappingApplies && hasCallOrConstructSignature(node);
1200
+ // A call or construct signature does not survive `Readonly<T>`, and a
1201
+ // property of an ignored prefix (`mut_`) must stay mutable, so a literal
1202
+ // carrying either is left unwrapped and marked readonly member by member
1203
+ // instead. See `hasCallOrConstructSignature` and `isLeftUnwrapped`.
1204
+ const skipWrapping =
1205
+ wrappingApplies &&
1206
+ (hasCallOrConstructSignature(node) || isLeftUnwrapped(node, options));
1092
1207
 
1093
1208
  // Recursive processing
1094
1209
  transformMembers(
@@ -1305,7 +1420,9 @@ const transformConstructorParameter = (
1305
1420
 
1306
1421
  const name = param.getName();
1307
1422
 
1308
- if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
1423
+ if (
1424
+ mutablePrefixesOfParameter(param, options).some((p) => name.startsWith(p))
1425
+ ) {
1309
1426
  return;
1310
1427
  }
1311
1428
 
@@ -1393,16 +1510,25 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1393
1510
  memberText: string,
1394
1511
  ): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
1395
1512
 
1513
+ // Whether a type literal member is wrapped at all; under `DeepReadonly` or an
1514
+ // indexed access none is.
1515
+ const wrapsTypeLiterals =
1516
+ nextReadonlyContextValue.type !== 'DeepReadonly' &&
1517
+ nextReadonlyContextValue.indexedAccessDepth === 0;
1518
+
1519
+ // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1520
+ // handler of `Readonly` then takes off as redundant.
1521
+ const underReadonly =
1522
+ readonlyContext.type === 'Readonly' &&
1523
+ readonlyContext.indexedAccessDepth === 0 &&
1524
+ isTypeArgumentOfReadonly(node);
1525
+
1396
1526
  const { primitives, arraysAndTuples, typeLiterals, others } =
1397
1527
  groupUnionIntersectionTypes(newTypes, {
1398
1528
  operator,
1399
- // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1400
- // handler of `Readonly` then takes off as redundant.
1401
- underReadonly:
1402
- readonlyContext.type === 'Readonly' &&
1403
- readonlyContext.indexedAccessDepth === 0 &&
1404
- isTypeArgumentOfReadonly(node),
1529
+ underReadonly,
1405
1530
  deepReadonlyTypeName: options.DeepReadonly.typeName,
1531
+ keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
1406
1532
  });
1407
1533
 
1408
1534
  options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
@@ -1445,9 +1571,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1445
1571
  ),
1446
1572
  ),
1447
1573
  op: operator,
1448
- wrapWithReadonly:
1449
- nextReadonlyContextValue.type !== 'DeepReadonly' &&
1450
- nextReadonlyContextValue.indexedAccessDepth === 0,
1574
+ wrapWithReadonly: wrapsTypeLiterals,
1451
1575
  }),
1452
1576
  ] as const);
1453
1577
 
@@ -1909,6 +2033,17 @@ const transformMembers = (
1909
2033
  continue;
1910
2034
  }
1911
2035
 
2036
+ // A method or an accessor of an ignored prefix is left as written, as in a
2037
+ // class.
2038
+ if (
2039
+ (mb.isKind(tsm.SyntaxKind.MethodSignature) ||
2040
+ mb.isKind(tsm.SyntaxKind.GetAccessor) ||
2041
+ mb.isKind(tsm.SyntaxKind.SetAccessor)) &&
2042
+ checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options)
2043
+ ) {
2044
+ continue;
2045
+ }
2046
+
1912
2047
  if (mb.isKind(tsm.SyntaxKind.IndexSignature)) {
1913
2048
  transformIndexSignatureDeclaration(
1914
2049
  mb,
@@ -2022,28 +2157,138 @@ const checkIfPropertyNameShouldBeIgnored = (
2022
2157
  // mut_x: number[]
2023
2158
  (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
2024
2159
  pipe(nameNode.getText()).map((nm) =>
2025
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2160
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2026
2161
  ).value) ||
2027
2162
  // "mut_x": number[]
2028
2163
  (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
2029
2164
  pipe(nameNode.getLiteralValue()).map((nm) =>
2030
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2165
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2031
2166
  ).value) ||
2032
- // #memberName: number[] (class only)
2167
+ // #memberName: number[] (class only), matched with or after its `#`
2033
2168
  (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
2034
2169
  pipe(nameNode.getText()).map((nm) =>
2035
- options.ignoredPrefixes.some((p) => nm.startsWith(`#${p}`)),
2170
+ options.mutablePropertyPrefixes.some(
2171
+ (p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
2172
+ ),
2036
2173
  ).value) ||
2037
- // [`mut_x`]: number[]
2174
+ // ['mut_x']: number[], [`mut_x`]: number[]
2038
2175
  (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
2039
2176
  pipe(nameNode.getExpression()).map((exp) => {
2040
- if (exp.isKind(tsm.SyntaxKind.StringLiteral)) {
2177
+ if (
2178
+ exp.isKind(tsm.SyntaxKind.StringLiteral) ||
2179
+ exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
2180
+ ) {
2041
2181
  const nm = exp.getLiteralValue();
2042
2182
 
2043
- return options.ignoredPrefixes.some((p) => nm.startsWith(p));
2183
+ return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
2044
2184
  }
2045
2185
 
2046
2186
  return false;
2047
2187
  }).value)
2048
2188
  );
2049
2189
  };
2190
+
2191
+ /**
2192
+ * Whether a type literal is left without `Readonly<...>` and marked readonly
2193
+ * member by member, because it has a property, a method or an accessor of an
2194
+ * ignored prefix (`{ mut_x: X[] }`), which `transformMembers` leaves mutable
2195
+ * and `Readonly<T>` would not.
2196
+ *
2197
+ * It keeps its `Readonly<...>` when that would leave another member mutable,
2198
+ * since only a property or index signature can be marked `readonly` itself:
2199
+ *
2200
+ * - a method signature or an accessor, which `Readonly<T>` makes readonly too,
2201
+ * unless its name has an ignored prefix as well;
2202
+ * - a property or index signature the ignore comment skips (as
2203
+ * `transformMembers` does), which gets no `readonly` of its own, unless it
2204
+ * is `readonly` already.
2205
+ */
2206
+ const isLeftUnwrapped = (
2207
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2208
+ node: tsm.TypeLiteralNode,
2209
+ options: ReadonlyTransformerOptionsInternal,
2210
+ ): boolean => {
2211
+ const members = node.getMembers();
2212
+
2213
+ const isMethodOrAccessor = (
2214
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2215
+ mb: tsm.TypeElementTypes,
2216
+ ): mb is
2217
+ | tsm.MethodSignature
2218
+ | tsm.GetAccessorDeclaration
2219
+ | tsm.SetAccessorDeclaration =>
2220
+ mb.isKind(tsm.SyntaxKind.MethodSignature) ||
2221
+ mb.isKind(tsm.SyntaxKind.GetAccessor) ||
2222
+ mb.isKind(tsm.SyntaxKind.SetAccessor);
2223
+
2224
+ const isIgnoredMember = (
2225
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2226
+ mb: tsm.TypeElementTypes,
2227
+ ): boolean =>
2228
+ (mb.isKind(tsm.SyntaxKind.PropertySignature) || isMethodOrAccessor(mb)) &&
2229
+ checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options);
2230
+
2231
+ const needsReadonlyWrapper = (
2232
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2233
+ mb: tsm.TypeElementTypes,
2234
+ ): boolean =>
2235
+ !isIgnoredMember(mb) &&
2236
+ (isMethodOrAccessor(mb) ||
2237
+ ((mb.isKind(tsm.SyntaxKind.PropertySignature) ||
2238
+ mb.isKind(tsm.SyntaxKind.IndexSignature)) &&
2239
+ !mb.hasModifier(tsm.SyntaxKind.ReadonlyKeyword) &&
2240
+ hasDisableNextLineComment(mb)));
2241
+
2242
+ return members.some(isIgnoredMember) && !members.some(needsReadonlyWrapper);
2243
+ };
2244
+
2245
+ /** Whether `node`, or a parenthesized type around it, carries the ignore comment. */
2246
+ const hasDisableNextLineCommentAround = (node: tsm.Node): boolean => {
2247
+ if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
2248
+ return true;
2249
+ }
2250
+
2251
+ const parent = node.getParent();
2252
+
2253
+ return (
2254
+ parent?.isKind(tsm.SyntaxKind.ParenthesizedType) === true &&
2255
+ hasDisableNextLineCommentAround(parent)
2256
+ );
2257
+ };
2258
+
2259
+ /**
2260
+ * Whether a type literal that is already wrapped, or would be merged into the
2261
+ * `Readonly<...>` of a union or intersection, is taken out of it. See
2262
+ * `isLeftUnwrapped`; in addition, the ignore comment leaves the literal where
2263
+ * it is, and taking `Readonly` off one with a call or construct signature
2264
+ * would bring back the signature it dropped.
2265
+ */
2266
+ const takesOffReadonly = (
2267
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2268
+ node: tsm.TypeLiteralNode,
2269
+ options: ReadonlyTransformerOptionsInternal,
2270
+ ): boolean =>
2271
+ isLeftUnwrapped(node, options) &&
2272
+ !hasCallOrConstructSignature(node) &&
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;