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
@@ -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,12 +156,33 @@ 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 functions
161
+ * 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.
143
171
  *
144
- * (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`.
145
183
  *
146
- * A type literal with a property, a method or an accessor of such a prefix
147
- * is not wrapped with `Readonly<...>`, which would make that member readonly
184
+ * A type literal with a property, a method or an accessor of such a prefix is
185
+ * not wrapped with `Readonly<...>`, which would make that member readonly
148
186
  * too; its other members are marked `readonly` one by one instead, and a
149
187
  * `Readonly<...>` written around it is taken off. It is wrapped as before
150
188
  * when it also has a member that cannot be marked `readonly` by itself (a
@@ -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
 
@@ -203,6 +245,27 @@ const initialReadonlyContext: ReadonlyContext = {
203
245
  indexedAccessDepth: 0,
204
246
  } as const;
205
247
 
248
+ /** Whether a type alias, interface or class name starts with a `mutableTypePrefixes` entry. */
249
+ const hasMutableTypeName = (
250
+ name: string | undefined,
251
+ options: ReadonlyTransformerOptionsInternal,
252
+ ): boolean =>
253
+ name !== undefined &&
254
+ options.mutableTypePrefixes.some((p) => name.startsWith(p));
255
+
256
+ /**
257
+ * A parameter property is a property of the class, as a `PropertyDeclaration`
258
+ * is; any other parameter is a variable.
259
+ */
260
+ const mutablePrefixesOfParameter = (
261
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
262
+ param: tsm.ParameterDeclaration,
263
+ options: ReadonlyTransformerOptionsInternal,
264
+ ): ISet<string> =>
265
+ param.isParameterProperty()
266
+ ? options.mutablePropertyPrefixes
267
+ : options.mutableVariablePrefixes;
268
+
206
269
  const transformNode = (
207
270
  node: tsm.Node,
208
271
  readonlyContext: ReadonlyContext,
@@ -230,24 +293,26 @@ const transformNode = (
230
293
  return;
231
294
  }
232
295
 
233
- // check for ignorePrefix
296
+ // check for mutableVariablePrefixes
234
297
  if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
235
298
  const nodeName = node.getName();
236
299
 
237
- if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
300
+ if (options.mutableVariablePrefixes.some((p) => nodeName.startsWith(p))) {
238
301
  // Skip conversion for variable declarations with ignored prefixes
239
302
  // Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
240
- options.debugPrint('skipped variable declaration by ignorePrefixes');
303
+ options.debugPrint(
304
+ 'skipped variable declaration by mutableVariablePrefixes',
305
+ );
241
306
 
242
307
  return;
243
308
  }
244
309
 
245
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
310
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
246
311
  // if (ts.isArrayBindingPattern(nodeName)) {
247
312
  // // for (const [i, el] of nodeName.elements.entries())
248
313
  // }
249
314
 
250
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
315
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
251
316
  // if (ts.isObjectBindingPattern(nodeName)) {
252
317
  // // for (const [i, el] of nodeName.elements.entries())
253
318
  // }
@@ -258,7 +323,9 @@ const transformNode = (
258
323
  if (
259
324
  (node.isKind(tsm.SyntaxKind.FunctionDeclaration) ||
260
325
  node.isKind(tsm.SyntaxKind.FunctionExpression)) &&
261
- options.ignoredPrefixes.some((p) => node.getName()?.startsWith(p) === true)
326
+ options.mutableVariablePrefixes.some(
327
+ (p) => node.getName()?.startsWith(p) === true,
328
+ )
262
329
  ) {
263
330
  return;
264
331
  }
@@ -266,26 +333,30 @@ const transformNode = (
266
333
  if (node.isKind(tsm.SyntaxKind.Parameter)) {
267
334
  const nodeName = node.getName();
268
335
 
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
336
+ if (
337
+ mutablePrefixesOfParameter(node, options).some((p) =>
338
+ nodeName.startsWith(p),
339
+ )
340
+ ) {
341
+ // Skip readonly conversion for parameters with mutable prefixes
342
+ // Example: (mut_foo: string[]) => {} -> remains as is, without readonly conversion
272
343
  return;
273
344
  }
274
345
 
275
- // TODO: Support ignoredPrefixes in ArrayBindingPattern
346
+ // TODO: Support mutableVariablePrefixes in ArrayBindingPattern
276
347
  // if (ts.isArrayBindingPattern(nodeName)) {
277
348
  // }
278
349
 
279
- // TODO: Support ignoredPrefixes in ObjectBindingPattern
350
+ // TODO: Support mutableVariablePrefixes in ObjectBindingPattern
280
351
  // if (ts.isObjectBindingPattern(nodeName)) {
281
352
  // }
282
353
  }
283
354
 
284
355
  // Skip readonly conversion for type alias declarations with ignored prefixes
285
- // Example: type mut_Foo = number[]; -> remains as number[] without readonly
356
+ // Example: type MutableFoo = number[]; -> remains as number[] without readonly
286
357
  if (
287
358
  node.isKind(tsm.SyntaxKind.TypeAliasDeclaration) &&
288
- options.ignoredPrefixes.some((p) => node.getName().startsWith(p))
359
+ hasMutableTypeName(node.getName(), options)
289
360
  ) {
290
361
  return;
291
362
  }
@@ -364,8 +435,8 @@ const transformNode = (
364
435
 
365
436
  if (node.isKind(tsm.SyntaxKind.InterfaceDeclaration)) {
366
437
  // 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))) {
438
+ // Example: interface MutableInterface {...} -> properties remain without readonly
439
+ if (hasMutableTypeName(node.getName(), options)) {
369
440
  return;
370
441
  }
371
442
 
@@ -374,14 +445,18 @@ const transformNode = (
374
445
  return;
375
446
  }
376
447
 
448
+ // Example: const C = class MutableClass {...} -> properties remain without readonly
449
+ if (
450
+ node.isKind(tsm.SyntaxKind.ClassExpression) &&
451
+ hasMutableTypeName(node.getName(), options)
452
+ ) {
453
+ return;
454
+ }
455
+
377
456
  if (node.isKind(tsm.SyntaxKind.ClassDeclaration)) {
378
457
  // 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
- ) {
458
+ // Example: class MutableClass {...} -> properties remain without readonly
459
+ if (hasMutableTypeName(node.getName(), options)) {
385
460
  return;
386
461
  }
387
462
 
@@ -430,6 +505,220 @@ const transformNode = (
430
505
  // Transformer implementation for each node type
431
506
  //
432
507
 
508
+ /** Renders a readonly `Record<K, V>` in the configured record style. */
509
+ const readonlyRecordText = (
510
+ keyText: string,
511
+ valueText: string,
512
+ options: ReadonlyTransformerOptionsInternal,
513
+ ): string =>
514
+ options.recordStyle === 'ReadonlyRecord'
515
+ ? (`ReadonlyRecord<${keyText}, ${valueText}>` as const)
516
+ : (`Readonly<Record<${keyText}, ${valueText}>>` as const);
517
+
518
+ /**
519
+ * The comments in `[from, to)` of a source file, each followed by what keeps
520
+ * the next token off it, and nothing else in that range — no whitespace, and no
521
+ * token such as a separator or a parenthesis.
522
+ */
523
+ const commentsBetween = (
524
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
525
+ sourceFile: tsm.SourceFile,
526
+ from: number,
527
+ to: number,
528
+ ): string => {
529
+ if (to <= from) {
530
+ return '';
531
+ }
532
+
533
+ const scanner = tsm.ts.createScanner(
534
+ tsm.ts.ScriptTarget.Latest,
535
+ false,
536
+ tsm.ts.LanguageVariant.Standard,
537
+ sourceFile.getFullText(),
538
+ undefined,
539
+ from,
540
+ to - from,
541
+ );
542
+
543
+ const mut_comments: string[] = [];
544
+
545
+ while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
546
+ const token = scanner.getToken();
547
+
548
+ // A line comment runs to the end of the line, so it takes one along.
549
+ const separator =
550
+ token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
551
+ ? '\n'
552
+ : token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
553
+ ? ' '
554
+ : undefined;
555
+
556
+ if (separator !== undefined) {
557
+ mut_comments.push(`${scanner.getTokenText()}${separator}`);
558
+ }
559
+ }
560
+
561
+ return mut_comments.join('');
562
+ };
563
+
564
+ /** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
565
+ const getDeepReadonlyTypeArg = (
566
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
567
+ node: tsm.TypeNode,
568
+ deepReadonlyTypeName: string,
569
+ ): tsm.TypeNode | undefined => {
570
+ if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
571
+ return undefined;
572
+ }
573
+
574
+ const typeName = node.getTypeName();
575
+
576
+ const typeArguments = node.getTypeArguments();
577
+
578
+ return typeName.isKind(tsm.SyntaxKind.Identifier) &&
579
+ typeName.getText() === deepReadonlyTypeName &&
580
+ Arr.isFixedLengthArray(1, typeArguments)
581
+ ? typeArguments[0]
582
+ : undefined;
583
+ };
584
+
585
+ const checkIfPropertyNameShouldBeIgnored = (
586
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
587
+ nameNode: tsm.PropertyName,
588
+ options: ReadonlyTransformerOptionsInternal,
589
+ ): boolean => {
590
+ expectType<typeof nameNode, tsm.PropertyName>('=');
591
+
592
+ expectType<
593
+ tsm.PropertyName,
594
+ | tsm.NumericLiteral
595
+ | tsm.BigIntLiteral
596
+ | tsm.NoSubstitutionTemplateLiteral
597
+ | tsm.Identifier
598
+ | tsm.StringLiteral
599
+ | tsm.PrivateIdentifier
600
+ | tsm.ComputedPropertyName // [`mut_x`]: number[]
601
+ >('=');
602
+
603
+ return (
604
+ // mut_x: number[]
605
+ (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
606
+ pipe(nameNode.getText()).map((nm) =>
607
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
608
+ ).value) ||
609
+ // "mut_x": number[]
610
+ (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
611
+ pipe(nameNode.getLiteralValue()).map((nm) =>
612
+ options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
613
+ ).value) ||
614
+ // #memberName: number[] (class only), matched with or after its `#`
615
+ (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
616
+ pipe(nameNode.getText()).map((nm) =>
617
+ options.mutablePropertyPrefixes.some(
618
+ (p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
619
+ ),
620
+ ).value) ||
621
+ // ['mut_x']: number[], [`mut_x`]: number[]
622
+ (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
623
+ pipe(nameNode.getExpression()).map((exp) => {
624
+ if (
625
+ exp.isKind(tsm.SyntaxKind.StringLiteral) ||
626
+ exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
627
+ ) {
628
+ const nm = exp.getLiteralValue();
629
+
630
+ return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
631
+ }
632
+
633
+ return false;
634
+ }).value)
635
+ );
636
+ };
637
+
638
+ /**
639
+ * Whether a type literal is left without `Readonly<...>` and marked readonly
640
+ * member by member, because it has a property, a method or an accessor of an
641
+ * ignored prefix (`{ mut_x: X[] }`), which `transformMembers` leaves mutable
642
+ * and `Readonly<T>` would not.
643
+ *
644
+ * It keeps its `Readonly<...>` when that would leave another member mutable,
645
+ * since only a property or index signature can be marked `readonly` itself:
646
+ *
647
+ * - a method signature or an accessor, which `Readonly<T>` makes readonly too,
648
+ * unless its name has an ignored prefix as well;
649
+ * - a property or index signature the ignore comment skips (as
650
+ * `transformMembers` does), which gets no `readonly` of its own, unless it
651
+ * is `readonly` already.
652
+ */
653
+ const isLeftUnwrapped = (
654
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
655
+ node: tsm.TypeLiteralNode,
656
+ options: ReadonlyTransformerOptionsInternal,
657
+ ): boolean => {
658
+ const members = node.getMembers();
659
+
660
+ const isMethodOrAccessor = (
661
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
662
+ mb: tsm.TypeElementTypes,
663
+ ): mb is
664
+ | tsm.MethodSignature
665
+ | tsm.GetAccessorDeclaration
666
+ | tsm.SetAccessorDeclaration =>
667
+ mb.isKind(tsm.SyntaxKind.MethodSignature) ||
668
+ mb.isKind(tsm.SyntaxKind.GetAccessor) ||
669
+ mb.isKind(tsm.SyntaxKind.SetAccessor);
670
+
671
+ const isIgnoredMember = (
672
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
673
+ mb: tsm.TypeElementTypes,
674
+ ): boolean =>
675
+ (mb.isKind(tsm.SyntaxKind.PropertySignature) || isMethodOrAccessor(mb)) &&
676
+ checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options);
677
+
678
+ const needsReadonlyWrapper = (
679
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
680
+ mb: tsm.TypeElementTypes,
681
+ ): boolean =>
682
+ !isIgnoredMember(mb) &&
683
+ (isMethodOrAccessor(mb) ||
684
+ ((mb.isKind(tsm.SyntaxKind.PropertySignature) ||
685
+ mb.isKind(tsm.SyntaxKind.IndexSignature)) &&
686
+ !mb.hasModifier(tsm.SyntaxKind.ReadonlyKeyword) &&
687
+ hasDisableNextLineComment(mb)));
688
+
689
+ return members.some(isIgnoredMember) && !members.some(needsReadonlyWrapper);
690
+ };
691
+
692
+ /** Whether `node`, or a parenthesized type around it, carries the ignore comment. */
693
+ const hasDisableNextLineCommentAround = (node: tsm.Node): boolean => {
694
+ if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
695
+ return true;
696
+ }
697
+
698
+ const parent = node.getParent();
699
+
700
+ return (
701
+ parent?.isKind(tsm.SyntaxKind.ParenthesizedType) === true &&
702
+ hasDisableNextLineCommentAround(parent)
703
+ );
704
+ };
705
+
706
+ /**
707
+ * Whether a type literal that is already wrapped, or would be merged into the
708
+ * `Readonly<...>` of a union or intersection, is taken out of it. See
709
+ * `isLeftUnwrapped`; in addition, the ignore comment leaves the literal where
710
+ * it is, and taking `Readonly` off one with a call or construct signature would
711
+ * bring back the signature it dropped.
712
+ */
713
+ const takesOffReadonly = (
714
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
715
+ node: tsm.TypeLiteralNode,
716
+ options: ReadonlyTransformerOptionsInternal,
717
+ ): boolean =>
718
+ isLeftUnwrapped(node, options) &&
719
+ !hasCallOrConstructSignature(node) &&
720
+ !hasDisableNextLineCommentAround(node);
721
+
433
722
  /**
434
723
  * - `tr(ReadonlyArray<E>) |-> readonly tr(E)[]`
435
724
  * - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
@@ -732,9 +1021,9 @@ const transformTypeReferenceNode = (
732
1021
  takesOffWrapper ||
733
1022
  (T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
734
1023
  ) {
735
- // The comments after the literal, which nothing else carries over, go
736
- // in front of it: a line comment after it would end the type with a
737
- // line break, which a following `[]` cannot be parsed across.
1024
+ // The comments after the literal, which nothing else carries over, go in
1025
+ // front of it: a line comment after it would end the type with a line
1026
+ // break, which a following `[]` cannot be parsed across.
738
1027
  const commentsAfterT = commentsBetween(
739
1028
  node.getSourceFile(),
740
1029
  T.getEnd(),
@@ -931,16 +1220,6 @@ const transformTypeReferenceNode = (
931
1220
  }
932
1221
  };
933
1222
 
934
- /** Renders a readonly `Record<K, V>` in the configured record style. */
935
- const readonlyRecordText = (
936
- keyText: string,
937
- valueText: string,
938
- options: ReadonlyTransformerOptionsInternal,
939
- ): string =>
940
- options.recordStyle === 'ReadonlyRecord'
941
- ? (`ReadonlyRecord<${keyText}, ${valueText}>` as const)
942
- : (`Readonly<Record<${keyText}, ${valueText}>>` as const);
943
-
944
1223
  /** `tr(E[]) |-> tr(E)[]` */
945
1224
  const transformArrayTypeNode = (
946
1225
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1002,8 +1281,12 @@ const transformTupleTypeNode = (
1002
1281
 
1003
1282
  for (const el of node.getElements()) {
1004
1283
  if (el.isKind(tsm.SyntaxKind.NamedTupleMember)) {
1005
- if (options.ignoredPrefixes.every((p) => !el.getName().startsWith(p))) {
1006
- transformNode(el.getTypeNode(), nextReadonlyContextValue, options);
1284
+ if (
1285
+ options.mutablePropertyPrefixes.every(
1286
+ (p) => !el.getName().startsWith(p),
1287
+ )
1288
+ ) {
1289
+ transformNode(el.getTypeNode(), nextReadonlyContextValue, options);
1007
1290
  }
1008
1291
  } else {
1009
1292
  transformNode(el, nextReadonlyContextValue, options);
@@ -1362,7 +1645,9 @@ const transformConstructorParameter = (
1362
1645
 
1363
1646
  const name = param.getName();
1364
1647
 
1365
- if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
1648
+ if (
1649
+ mutablePrefixesOfParameter(param, options).some((p) => name.startsWith(p))
1650
+ ) {
1366
1651
  return;
1367
1652
  }
1368
1653
 
@@ -1421,6 +1706,222 @@ const transformUnionTypeNode = (
1421
1706
  transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
1422
1707
  };
1423
1708
 
1709
+ /**
1710
+ * The text of `inner`, a type `removeParentheses` took out of `outer`, with the
1711
+ * comments in `outer`'s full text in front of it — before the parentheses and
1712
+ * inside them — and without the parentheses.
1713
+ *
1714
+ * `inner.getFullText()` would drop those before the parentheses, which are as
1715
+ * much a part of `outer` as the rest of it.
1716
+ */
1717
+ const textWithoutParentheses = (
1718
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1719
+ outer: tsm.TypeNode,
1720
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1721
+ inner: tsm.TypeNode,
1722
+ ): string =>
1723
+ `${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
1724
+
1725
+ const unionToStringImpl = ({
1726
+ types,
1727
+ op,
1728
+ wrapWithReadonly,
1729
+ }: Readonly<{
1730
+ types: readonly string[];
1731
+ op: '&' | '|';
1732
+ wrapWithReadonly: boolean | string;
1733
+ }>): string =>
1734
+ Arr.isEmpty(types)
1735
+ ? 'never'
1736
+ : Arr.isFixedLengthArray(1, types)
1737
+ ? wrapWithReadonly === false
1738
+ ? wrapWithParentheses(types[0])
1739
+ : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>` as const)
1740
+ : wrapWithReadonly === false
1741
+ ? wrapWithParentheses(types.join(` ${op} `))
1742
+ : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>` as const);
1743
+
1744
+ const unionToString = ({
1745
+ types: typesRaw,
1746
+ op,
1747
+ wrapWithReadonly,
1748
+ }: Readonly<{
1749
+ types: readonly string[];
1750
+ op: '&' | '|';
1751
+ wrapWithReadonly: boolean | string;
1752
+ }>): string => {
1753
+ // The members are joined with a separator that supplies its own space, so
1754
+ // whitespace left at a member's edge would accumulate: every run of the
1755
+ // transformer would add one more space before every member but the first, and
1756
+ // a file would never reach a fixed point. Trimming keeps the comments in
1757
+ // front of a member, which are not whitespace.
1758
+ const types = typesRaw.map((s) => s.trim());
1759
+
1760
+ return unionToStringImpl({ types, op, wrapWithReadonly });
1761
+ };
1762
+
1763
+ /**
1764
+ * - `tr(DeepReadonly<A> | B | DeepReadonly<C>) |-> DeepReadonly<A | C> | B`
1765
+ *
1766
+ * The merged member takes the place of the first `DeepReadonly` member.
1767
+ *
1768
+ * Only a union is merged, although `Readonly<A> & Readonly<B>` is merged into
1769
+ * `Readonly<A & B>`. The two differ in kind:
1770
+ *
1771
+ * - `Readonly` is a homomorphic mapped type, and over type literals
1772
+ * `Readonly<A & B>` and `Readonly<A> & Readonly<B>` have the same properties
1773
+ * with the same modifiers. That does not hold for an array `A`, so an
1774
+ * intersection merges only `Readonly<T>` whose `T` is known to be a type
1775
+ * literal; see `isMergeableReadonlyTypeReferenceNode`.
1776
+ * - `DeepReadonly` is a conditional type that picks a branch by what `T` is
1777
+ * (primitive, function, Map, Set, array, object). It distributes over a union
1778
+ * because each member is tested on its own, but an intersection is tested as
1779
+ * a whole and matches every branch any one member matches: with
1780
+ * `F = () => void`, `F & { x: number[] }` extends a function type because `F`
1781
+ * does, so `DeepReadonly<F & { x: number[] }>` hits that branch and returns
1782
+ * the intersection unchanged, leaving `x` mutable, while `DeepReadonly<F> &
1783
+ * DeepReadonly<{ x: number[] }>` is `F & { readonly x: readonly number[] }`.
1784
+ * The codemod cannot tell whether a type argument is such a member, so it
1785
+ * merges no intersection.
1786
+ *
1787
+ * The test "premises of merging DeepReadonly members" pins both facts against
1788
+ * ts-type-forge's `DeepReadonly`.
1789
+ */
1790
+ const mergeDeepReadonlyUnionMembers = (
1791
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1792
+ nodes: readonly tsm.TypeNode[],
1793
+ deepReadonlyTypeName: string,
1794
+ withLeadingComments: (
1795
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1796
+ member: tsm.TypeNode,
1797
+ memberText: string,
1798
+ ) => string,
1799
+ ): readonly string[] => {
1800
+ const typeArgsOfDeepReadonly = nodes.map((n) =>
1801
+ getDeepReadonlyTypeArg(n, deepReadonlyTypeName),
1802
+ );
1803
+
1804
+ const asIs = (
1805
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1806
+ n: tsm.TypeNode,
1807
+ ): string => withLeadingComments(n, wrapWithParentheses(n.getText()));
1808
+
1809
+ if (typeArgsOfDeepReadonly.filter((t) => t !== undefined).length < 2) {
1810
+ return nodes.map(asIs);
1811
+ }
1812
+
1813
+ const firstIndex = typeArgsOfDeepReadonly.findIndex((t) => t !== undefined);
1814
+
1815
+ const merged = unionToString({
1816
+ // A member's comments move inside, onto its type argument:
1817
+ // `/** a */ DeepReadonly<A> | /** b */ DeepReadonly<B>`
1818
+ // -> `DeepReadonly</** a */ A | /** b */ B>`
1819
+ types: nodes.flatMap((n, i) => {
1820
+ const t_ = typeArgsOfDeepReadonly[i];
1821
+
1822
+ if (t_ === undefined) {
1823
+ return [];
1824
+ }
1825
+
1826
+ const t = removeParentheses(t_);
1827
+
1828
+ // NOTE: DeepReadonly<A | B> | DeepReadonly<C> -> DeepReadonly<A | B | C>
1829
+ // NOTE: DeepReadonly<() => void> | DeepReadonly<B> -> DeepReadonly<(() => void) | B>
1830
+ const tText = textWithoutParentheses(t_, t);
1831
+
1832
+ return [
1833
+ withLeadingComments(
1834
+ n,
1835
+ t.isKind(tsm.SyntaxKind.UnionType)
1836
+ ? tText
1837
+ : wrapWithParentheses(tText),
1838
+ ),
1839
+ ];
1840
+ }),
1841
+ op: '|',
1842
+ wrapWithReadonly: deepReadonlyTypeName,
1843
+ });
1844
+
1845
+ return nodes.flatMap((n, i) =>
1846
+ typeArgsOfDeepReadonly[i] === undefined
1847
+ ? [asIs(n)]
1848
+ : i === firstIndex
1849
+ ? [merged]
1850
+ : [],
1851
+ );
1852
+ };
1853
+
1854
+ /** Whether `node` is the type argument of `Readonly<...>`, parentheses aside. */
1855
+ const isTypeArgumentOfReadonly = (
1856
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1857
+ node: tsm.TypeNode,
1858
+ ): boolean => {
1859
+ const parent = node.getParent();
1860
+
1861
+ return (
1862
+ parent !== undefined &&
1863
+ (parent.isKind(tsm.SyntaxKind.ParenthesizedType)
1864
+ ? isTypeArgumentOfReadonly(parent)
1865
+ : isReadonlyTypeReferenceNode(parent))
1866
+ );
1867
+ };
1868
+
1869
+ /**
1870
+ * The comments in front of a union or intersection member, for the text that
1871
+ * replaces the whole type.
1872
+ *
1873
+ * `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
1874
+ * in front of that stays in the file by itself. With no leading `|` / `&`, the
1875
+ * first member starts there too, and its comments are the type's: repeating
1876
+ * them in the replacement would copy them once per run. Every other member has
1877
+ * a separator in front of it, and a comment before the separator belongs to no
1878
+ * member's full text, so it is gone once the separator is rewritten. It goes
1879
+ * with the member after it, like the comments after the separator.
1880
+ *
1881
+ * `member` may be the type inside a member's parentheses; the comments inside
1882
+ * them are collected too, and the parentheses are not.
1883
+ */
1884
+ const leadingCommentsOfMember = (
1885
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1886
+ node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1887
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1888
+ member: tsm.TypeNode,
1889
+ ): string =>
1890
+ commentsBetween(
1891
+ node.getSourceFile(),
1892
+ node
1893
+ .getTypeNodes()
1894
+ .findLast((m) => m.getEnd() <= member.getStart())
1895
+ ?.getEnd() ?? node.getStart(),
1896
+ member.getStart(),
1897
+ );
1898
+
1899
+ /**
1900
+ * `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
1901
+ * intersection (`&`) of the members it is merged with.
1902
+ *
1903
+ * `T` is wrapped in parentheses for precedence safety (a function type argument
1904
+ * needs them), unless it is a union spliced into a union or an intersection
1905
+ * spliced into an intersection, which needs none.
1906
+ */
1907
+ const readonlyTypeArgTextSplicedInto = (
1908
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1909
+ node: ReadonlyTypeReferenceNode,
1910
+ operator: '&' | '|',
1911
+ ): string => {
1912
+ const typeArgument = node.getTypeArguments()[0];
1913
+
1914
+ const T = removeParentheses(typeArgument);
1915
+
1916
+ return T.isKind(
1917
+ operator === '|'
1918
+ ? tsm.SyntaxKind.UnionType
1919
+ : tsm.SyntaxKind.IntersectionType,
1920
+ )
1921
+ ? textWithoutParentheses(typeArgument, T)
1922
+ : unwrapReadonlyTypeArgText(node);
1923
+ };
1924
+
1424
1925
  const transformUnionOrIntersectionTypeNodeImpl = (
1425
1926
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1426
1927
  node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
@@ -1456,8 +1957,8 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1456
1957
  nextReadonlyContextValue.type !== 'DeepReadonly' &&
1457
1958
  nextReadonlyContextValue.indexedAccessDepth === 0;
1458
1959
 
1459
- // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1460
- // handler of `Readonly` then takes off as redundant.
1960
+ // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the handler
1961
+ // of `Readonly` then takes off as redundant.
1461
1962
  const underReadonly =
1462
1963
  readonlyContext.type === 'Readonly' &&
1463
1964
  readonlyContext.indexedAccessDepth === 0 &&
@@ -1602,297 +2103,14 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1602
2103
  );
1603
2104
  };
1604
2105
 
1605
- /**
1606
- * - `tr(DeepReadonly<A> | B | DeepReadonly<C>) |-> DeepReadonly<A | C> | B`
1607
- *
1608
- * The merged member takes the place of the first `DeepReadonly` member.
1609
- *
1610
- * Only a union is merged, although `Readonly<A> & Readonly<B>` is merged into
1611
- * `Readonly<A & B>`. The two differ in kind:
1612
- *
1613
- * - `Readonly` is a homomorphic mapped type, and over type literals
1614
- * `Readonly<A & B>` and `Readonly<A> & Readonly<B>` have the same properties
1615
- * with the same modifiers. That does not hold for an array `A`, so an
1616
- * intersection merges only `Readonly<T>` whose `T` is known to be a type
1617
- * literal; see `isMergeableReadonlyTypeReferenceNode`.
1618
- * - `DeepReadonly` is a conditional type that picks a branch by what `T` is
1619
- * (primitive, function, Map, Set, array, object). It distributes over a union
1620
- * because each member is tested on its own, but an intersection is tested as
1621
- * a whole and matches every branch any one member matches: with
1622
- * `F = () => void`, `F & { x: number[] }` extends a function type because `F`
1623
- * does, so `DeepReadonly<F & { x: number[] }>` hits that branch and returns
1624
- * the intersection unchanged, leaving `x` mutable, while `DeepReadonly<F> &
1625
- * DeepReadonly<{ x: number[] }>` is `F & { readonly x: readonly number[] }`.
1626
- * The codemod cannot tell whether a type argument is such a member, so it
1627
- * merges no intersection.
1628
- *
1629
- * The test "premises of merging DeepReadonly members" pins both facts against
1630
- * ts-type-forge's `DeepReadonly`.
1631
- */
1632
- const mergeDeepReadonlyUnionMembers = (
2106
+ /** Convert ((T)) -> (T) recursively */
2107
+ const transformParenthesizedTypeNode = (
1633
2108
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1634
- nodes: readonly tsm.TypeNode[],
1635
- deepReadonlyTypeName: string,
1636
- withLeadingComments: (
1637
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1638
- member: tsm.TypeNode,
1639
- memberText: string,
1640
- ) => string,
1641
- ): readonly string[] => {
1642
- const typeArgsOfDeepReadonly = nodes.map((n) =>
1643
- getDeepReadonlyTypeArg(n, deepReadonlyTypeName),
1644
- );
1645
-
1646
- const asIs = (
1647
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1648
- n: tsm.TypeNode,
1649
- ): string => withLeadingComments(n, wrapWithParentheses(n.getText()));
1650
-
1651
- if (typeArgsOfDeepReadonly.filter((t) => t !== undefined).length < 2) {
1652
- return nodes.map(asIs);
1653
- }
1654
-
1655
- const firstIndex = typeArgsOfDeepReadonly.findIndex((t) => t !== undefined);
1656
-
1657
- const merged = unionToString({
1658
- // A member's comments move inside, onto its type argument:
1659
- // `/** a */ DeepReadonly<A> | /** b */ DeepReadonly<B>`
1660
- // -> `DeepReadonly</** a */ A | /** b */ B>`
1661
- types: nodes.flatMap((n, i) => {
1662
- const t_ = typeArgsOfDeepReadonly[i];
1663
-
1664
- if (t_ === undefined) {
1665
- return [];
1666
- }
1667
-
1668
- const t = removeParentheses(t_);
1669
-
1670
- // NOTE: DeepReadonly<A | B> | DeepReadonly<C> -> DeepReadonly<A | B | C>
1671
- // NOTE: DeepReadonly<() => void> | DeepReadonly<B> -> DeepReadonly<(() => void) | B>
1672
- const tText = textWithoutParentheses(t_, t);
1673
-
1674
- return [
1675
- withLeadingComments(
1676
- n,
1677
- t.isKind(tsm.SyntaxKind.UnionType)
1678
- ? tText
1679
- : wrapWithParentheses(tText),
1680
- ),
1681
- ];
1682
- }),
1683
- op: '|',
1684
- wrapWithReadonly: deepReadonlyTypeName,
1685
- });
1686
-
1687
- return nodes.flatMap((n, i) =>
1688
- typeArgsOfDeepReadonly[i] === undefined
1689
- ? [asIs(n)]
1690
- : i === firstIndex
1691
- ? [merged]
1692
- : [],
1693
- );
1694
- };
1695
-
1696
- /** Whether `node` is the type argument of `Readonly<...>`, parentheses aside. */
1697
- const isTypeArgumentOfReadonly = (
1698
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1699
- node: tsm.TypeNode,
1700
- ): boolean => {
1701
- const parent = node.getParent();
1702
-
1703
- return (
1704
- parent !== undefined &&
1705
- (parent.isKind(tsm.SyntaxKind.ParenthesizedType)
1706
- ? isTypeArgumentOfReadonly(parent)
1707
- : isReadonlyTypeReferenceNode(parent))
1708
- );
1709
- };
1710
-
1711
- /**
1712
- * The comments in front of a union or intersection member, for the text that
1713
- * replaces the whole type.
1714
- *
1715
- * `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
1716
- * in front of that stays in the file by itself. With no leading `|` / `&`, the
1717
- * first member starts there too, and its comments are the type's: repeating
1718
- * them in the replacement would copy them once per run. Every other member
1719
- * has a separator in front of it, and a comment before the separator belongs
1720
- * to no member's full text, so it is gone once the separator is rewritten. It
1721
- * goes with the member after it, like the comments after the separator.
1722
- *
1723
- * `member` may be the type inside a member's parentheses; the comments inside
1724
- * them are collected too, and the parentheses are not.
1725
- */
1726
- const leadingCommentsOfMember = (
1727
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1728
- node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1729
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1730
- member: tsm.TypeNode,
1731
- ): string =>
1732
- commentsBetween(
1733
- node.getSourceFile(),
1734
- node
1735
- .getTypeNodes()
1736
- .findLast((m) => m.getEnd() <= member.getStart())
1737
- ?.getEnd() ?? node.getStart(),
1738
- member.getStart(),
1739
- );
1740
-
1741
- /**
1742
- * `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
1743
- * intersection (`&`) of the members it is merged with.
1744
- *
1745
- * `T` is wrapped in parentheses for precedence safety (a function type
1746
- * argument needs them), unless it is a union spliced into a union or an
1747
- * intersection spliced into an intersection, which needs none.
1748
- */
1749
- const readonlyTypeArgTextSplicedInto = (
1750
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1751
- node: ReadonlyTypeReferenceNode,
1752
- operator: '&' | '|',
1753
- ): string => {
1754
- const typeArgument = node.getTypeArguments()[0];
1755
-
1756
- const T = removeParentheses(typeArgument);
1757
-
1758
- return T.isKind(
1759
- operator === '|'
1760
- ? tsm.SyntaxKind.UnionType
1761
- : tsm.SyntaxKind.IntersectionType,
1762
- )
1763
- ? textWithoutParentheses(typeArgument, T)
1764
- : unwrapReadonlyTypeArgText(node);
1765
- };
1766
-
1767
- /**
1768
- * The text of `inner`, a type `removeParentheses` took out of `outer`, with
1769
- * the comments in `outer`'s full text in front of it — before the parentheses
1770
- * and inside them — and without the parentheses.
1771
- *
1772
- * `inner.getFullText()` would drop those before the parentheses, which are
1773
- * as much a part of `outer` as the rest of it.
1774
- */
1775
- const textWithoutParentheses = (
1776
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1777
- outer: tsm.TypeNode,
1778
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1779
- inner: tsm.TypeNode,
1780
- ): string =>
1781
- `${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
1782
-
1783
- /**
1784
- * The comments in `[from, to)` of a source file, each followed by what keeps
1785
- * the next token off it, and nothing else in that range — no whitespace, and
1786
- * no token such as a separator or a parenthesis.
1787
- */
1788
- const commentsBetween = (
1789
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1790
- sourceFile: tsm.SourceFile,
1791
- from: number,
1792
- to: number,
1793
- ): string => {
1794
- if (to <= from) {
1795
- return '';
1796
- }
1797
-
1798
- const scanner = tsm.ts.createScanner(
1799
- tsm.ts.ScriptTarget.Latest,
1800
- false,
1801
- tsm.ts.LanguageVariant.Standard,
1802
- sourceFile.getFullText(),
1803
- undefined,
1804
- from,
1805
- to - from,
1806
- );
1807
-
1808
- const mut_comments: string[] = [];
1809
-
1810
- while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
1811
- const token = scanner.getToken();
1812
-
1813
- // A line comment runs to the end of the line, so it takes one along.
1814
- const separator =
1815
- token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
1816
- ? '\n'
1817
- : token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
1818
- ? ' '
1819
- : undefined;
1820
-
1821
- if (separator !== undefined) {
1822
- mut_comments.push(`${scanner.getTokenText()}${separator}`);
1823
- }
1824
- }
1825
-
1826
- return mut_comments.join('');
1827
- };
1828
-
1829
- /** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
1830
- const getDeepReadonlyTypeArg = (
1831
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1832
- node: tsm.TypeNode,
1833
- deepReadonlyTypeName: string,
1834
- ): tsm.TypeNode | undefined => {
1835
- if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
1836
- return undefined;
1837
- }
1838
-
1839
- const typeName = node.getTypeName();
1840
-
1841
- const typeArguments = node.getTypeArguments();
1842
-
1843
- return typeName.isKind(tsm.SyntaxKind.Identifier) &&
1844
- typeName.getText() === deepReadonlyTypeName &&
1845
- Arr.isFixedLengthArray(1, typeArguments)
1846
- ? typeArguments[0]
1847
- : undefined;
1848
- };
1849
-
1850
- const unionToString = ({
1851
- types: typesRaw,
1852
- op,
1853
- wrapWithReadonly,
1854
- }: Readonly<{
1855
- types: readonly string[];
1856
- op: '&' | '|';
1857
- wrapWithReadonly: boolean | string;
1858
- }>): string => {
1859
- // The members are joined with a separator that supplies its own space, so
1860
- // whitespace left at a member's edge would accumulate: every run of the
1861
- // transformer would add one more space before every member but the first,
1862
- // and a file would never reach a fixed point. Trimming keeps the comments in
1863
- // front of a member, which are not whitespace.
1864
- const types = typesRaw.map((s) => s.trim());
1865
-
1866
- return unionToStringImpl({ types, op, wrapWithReadonly });
1867
- };
1868
-
1869
- const unionToStringImpl = ({
1870
- types,
1871
- op,
1872
- wrapWithReadonly,
1873
- }: Readonly<{
1874
- types: readonly string[];
1875
- op: '&' | '|';
1876
- wrapWithReadonly: boolean | string;
1877
- }>): string =>
1878
- Arr.isEmpty(types)
1879
- ? 'never'
1880
- : Arr.isFixedLengthArray(1, types)
1881
- ? wrapWithReadonly === false
1882
- ? wrapWithParentheses(types[0])
1883
- : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>` as const)
1884
- : wrapWithReadonly === false
1885
- ? wrapWithParentheses(types.join(` ${op} `))
1886
- : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>` as const);
1887
-
1888
- /** Convert ((T)) -> (T) recursively */
1889
- const transformParenthesizedTypeNode = (
1890
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1891
- node: tsm.ParenthesizedTypeNode,
1892
- readonlyContext: ReadonlyContext,
1893
- options: ReadonlyTransformerOptionsInternal,
1894
- ): void => {
1895
- const typeNode = node.getTypeNode();
2109
+ node: tsm.ParenthesizedTypeNode,
2110
+ readonlyContext: ReadonlyContext,
2111
+ options: ReadonlyTransformerOptionsInternal,
2112
+ ): void => {
2113
+ const typeNode = node.getTypeNode();
1896
2114
 
1897
2115
  if (typeNode.isKind(tsm.SyntaxKind.ParenthesizedType)) {
1898
2116
  // Recursive processing
@@ -1904,20 +2122,16 @@ const transformParenthesizedTypeNode = (
1904
2122
  const T = node.getTypeNode();
1905
2123
 
1906
2124
  if (
1907
- // remove () if T is TypeReferenceNode
1908
- // e.g. `(Readonly<A>) |-> Readonly<A>`
2125
+ // remove () if T is TypeReferenceNode e.g. `(Readonly<A>) |-> Readonly<A>`
1909
2126
  T.isKind(tsm.SyntaxKind.TypeReference) ||
1910
- // remove () if T is ArrayTypeNode
1911
- // e.g. `(A[]) |-> A[]`
2127
+ // remove () if T is ArrayTypeNode e.g. `(A[]) |-> A[]`
1912
2128
  T.isKind(tsm.SyntaxKind.ArrayType) ||
1913
- // remove () if T is TupleTypeNode
1914
- // e.g. `([A]) |-> [A]`
2129
+ // remove () if T is TupleTypeNode e.g. `([A]) |-> [A]`
1915
2130
  T.isKind(tsm.SyntaxKind.TupleType) ||
1916
- // remove () if T is PrimitiveTypeNode
1917
- // e.g. `(number) |-> number`
2131
+ // remove () if T is PrimitiveTypeNode e.g. `(number) |-> number`
1918
2132
  isAtomicTypeNode(T) ||
1919
- // remove () if T is TypeLiteralNode
1920
- // e.g. `({ member: V }) |-> { member: V }`
2133
+ // remove () if T is TypeLiteralNode e.g.
2134
+ // `({ member: V }) |-> { member: V }`
1921
2135
  T.isKind(tsm.SyntaxKind.TypeLiteral) ||
1922
2136
  // collapse redundant nested parentheses if T is itself a ParenthesizedType
1923
2137
  // e.g. `((X)) |-> (X)`
@@ -2074,138 +2288,3 @@ const transformIndexSignatureDeclaration = (
2074
2288
  }
2075
2289
  }
2076
2290
  };
2077
-
2078
- const checkIfPropertyNameShouldBeIgnored = (
2079
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2080
- nameNode: tsm.PropertyName,
2081
- options: ReadonlyTransformerOptionsInternal,
2082
- ): boolean => {
2083
- expectType<typeof nameNode, tsm.PropertyName>('=');
2084
-
2085
- expectType<
2086
- tsm.PropertyName,
2087
- | tsm.NumericLiteral
2088
- | tsm.BigIntLiteral
2089
- | tsm.NoSubstitutionTemplateLiteral
2090
- | tsm.Identifier
2091
- | tsm.StringLiteral
2092
- | tsm.PrivateIdentifier
2093
- | tsm.ComputedPropertyName // [`mut_x`]: number[]
2094
- >('=');
2095
-
2096
- return (
2097
- // mut_x: number[]
2098
- (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
2099
- pipe(nameNode.getText()).map((nm) =>
2100
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2101
- ).value) ||
2102
- // "mut_x": number[]
2103
- (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
2104
- pipe(nameNode.getLiteralValue()).map((nm) =>
2105
- options.ignoredPrefixes.some((p) => nm.startsWith(p)),
2106
- ).value) ||
2107
- // #memberName: number[] (class only)
2108
- (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
2109
- pipe(nameNode.getText()).map((nm) =>
2110
- options.ignoredPrefixes.some((p) => nm.startsWith(`#${p}`)),
2111
- ).value) ||
2112
- // ['mut_x']: number[], [`mut_x`]: number[]
2113
- (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
2114
- pipe(nameNode.getExpression()).map((exp) => {
2115
- if (
2116
- exp.isKind(tsm.SyntaxKind.StringLiteral) ||
2117
- exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
2118
- ) {
2119
- const nm = exp.getLiteralValue();
2120
-
2121
- return options.ignoredPrefixes.some((p) => nm.startsWith(p));
2122
- }
2123
-
2124
- return false;
2125
- }).value)
2126
- );
2127
- };
2128
-
2129
- /**
2130
- * Whether a type literal is left without `Readonly<...>` and marked readonly
2131
- * member by member, because it has a property, a method or an accessor of an
2132
- * ignored prefix (`{ mut_x: X[] }`), which `transformMembers` leaves mutable
2133
- * and `Readonly<T>` would not.
2134
- *
2135
- * It keeps its `Readonly<...>` when that would leave another member mutable,
2136
- * since only a property or index signature can be marked `readonly` itself:
2137
- *
2138
- * - a method signature or an accessor, which `Readonly<T>` makes readonly too,
2139
- * unless its name has an ignored prefix as well;
2140
- * - a property or index signature the ignore comment skips (as
2141
- * `transformMembers` does), which gets no `readonly` of its own, unless it
2142
- * is `readonly` already.
2143
- */
2144
- const isLeftUnwrapped = (
2145
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2146
- node: tsm.TypeLiteralNode,
2147
- options: ReadonlyTransformerOptionsInternal,
2148
- ): boolean => {
2149
- const members = node.getMembers();
2150
-
2151
- const isMethodOrAccessor = (
2152
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2153
- mb: tsm.TypeElementTypes,
2154
- ): mb is
2155
- | tsm.MethodSignature
2156
- | tsm.GetAccessorDeclaration
2157
- | tsm.SetAccessorDeclaration =>
2158
- mb.isKind(tsm.SyntaxKind.MethodSignature) ||
2159
- mb.isKind(tsm.SyntaxKind.GetAccessor) ||
2160
- mb.isKind(tsm.SyntaxKind.SetAccessor);
2161
-
2162
- const isIgnoredMember = (
2163
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2164
- mb: tsm.TypeElementTypes,
2165
- ): boolean =>
2166
- (mb.isKind(tsm.SyntaxKind.PropertySignature) || isMethodOrAccessor(mb)) &&
2167
- checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options);
2168
-
2169
- const needsReadonlyWrapper = (
2170
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2171
- mb: tsm.TypeElementTypes,
2172
- ): boolean =>
2173
- !isIgnoredMember(mb) &&
2174
- (isMethodOrAccessor(mb) ||
2175
- ((mb.isKind(tsm.SyntaxKind.PropertySignature) ||
2176
- mb.isKind(tsm.SyntaxKind.IndexSignature)) &&
2177
- !mb.hasModifier(tsm.SyntaxKind.ReadonlyKeyword) &&
2178
- hasDisableNextLineComment(mb)));
2179
-
2180
- return members.some(isIgnoredMember) && !members.some(needsReadonlyWrapper);
2181
- };
2182
-
2183
- /** Whether `node`, or a parenthesized type around it, carries the ignore comment. */
2184
- const hasDisableNextLineCommentAround = (node: tsm.Node): boolean => {
2185
- if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
2186
- return true;
2187
- }
2188
-
2189
- const parent = node.getParent();
2190
-
2191
- return (
2192
- parent?.isKind(tsm.SyntaxKind.ParenthesizedType) === true &&
2193
- hasDisableNextLineCommentAround(parent)
2194
- );
2195
- };
2196
-
2197
- /**
2198
- * Whether a type literal that is already wrapped, or would be merged into the
2199
- * `Readonly<...>` of a union or intersection, is taken out of it. See
2200
- * `isLeftUnwrapped`; in addition, the ignore comment leaves the literal where
2201
- * it is, and taking `Readonly` off one with a call or construct signature
2202
- * would bring back the signature it dropped.
2203
- */
2204
- const takesOffReadonly = (
2205
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2206
- node: tsm.TypeLiteralNode,
2207
- options: ReadonlyTransformerOptionsInternal,
2208
- ): boolean =>
2209
- isLeftUnwrapped(node, options) &&
2210
- !hasCallOrConstructSignature(node) &&
2211
- !hasDisableNextLineCommentAround(node);