ts-codemod-lib 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
  2. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  3. package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
  4. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  5. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +4 -4
  6. package/dist/functions/ast-transformers/convert-to-readonly.mjs +248 -252
  7. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  8. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
  9. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
  10. package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
  11. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
  12. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
  13. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  14. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  15. package/dist/functions/ast-transformers/transform-source-code.mjs +69 -69
  16. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  17. package/dist/functions/constants/mutable-prefixes.d.mts +8 -8
  18. package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
  19. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  20. package/dist/functions/functions/is-readonly-node.d.mts +2 -2
  21. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
  22. package/dist/functions/functions/is-readonly-node.mjs +13 -13
  23. package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
  24. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
  25. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  26. package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
  27. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  28. package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
  29. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
  30. package/dist/functions/utils/replace-node-with-text.mjs +0 -1
  31. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
  32. package/package.json +6 -6
  33. package/src/functions/ast-transformers/append-as-const.mts +242 -231
  34. package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
  35. package/src/functions/ast-transformers/convert-to-readonly.mts +467 -471
  36. package/src/functions/ast-transformers/convert-to-readonly.test.mts +60 -33
  37. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
  38. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
  39. package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
  40. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
  41. package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
  42. package/src/functions/ast-transformers/transform-source-code.mts +110 -110
  43. package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
  44. package/src/functions/constants/mutable-prefixes.mts +8 -8
  45. package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
  46. package/src/functions/functions/is-readonly-node.mts +25 -25
  47. package/src/functions/functions/wrap-with-parentheses.mts +138 -52
  48. package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
  49. package/src/functions/utils/replace-node-with-text.mts +10 -11
@@ -157,8 +157,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
157
157
 
158
158
  /**
159
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.
160
+ * parameter property, which `mutablePropertyPrefixes` decides) and functions
161
+ * that are left mutable: their types are not made readonly.
162
162
  *
163
163
  * @default ['mut_']
164
164
  */
@@ -181,8 +181,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
181
181
  * both `mut_` and `#mut_` cover `#mut_x`. Of a named tuple member with such a prefix, only the type
182
182
  * is kept; the tuple is still made `readonly`.
183
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
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
186
186
  * too; its other members are marked `readonly` one by one instead, and a
187
187
  * `Readonly<...>` written around it is taken off. It is wrapped as before
188
188
  * when it also has a member that cannot be marked `readonly` by itself (a
@@ -245,6 +245,27 @@ const initialReadonlyContext: ReadonlyContext = {
245
245
  indexedAccessDepth: 0,
246
246
  } as const;
247
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
+
248
269
  const transformNode = (
249
270
  node: tsm.Node,
250
271
  readonlyContext: ReadonlyContext,
@@ -484,6 +505,220 @@ const transformNode = (
484
505
  // Transformer implementation for each node type
485
506
  //
486
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
+
487
722
  /**
488
723
  * - `tr(ReadonlyArray<E>) |-> readonly tr(E)[]`
489
724
  * - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
@@ -786,9 +1021,9 @@ const transformTypeReferenceNode = (
786
1021
  takesOffWrapper ||
787
1022
  (T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
788
1023
  ) {
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.
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.
792
1027
  const commentsAfterT = commentsBetween(
793
1028
  node.getSourceFile(),
794
1029
  T.getEnd(),
@@ -985,16 +1220,6 @@ const transformTypeReferenceNode = (
985
1220
  }
986
1221
  };
987
1222
 
988
- /** Renders a readonly `Record<K, V>` in the configured record style. */
989
- const readonlyRecordText = (
990
- keyText: string,
991
- valueText: string,
992
- options: ReadonlyTransformerOptionsInternal,
993
- ): string =>
994
- options.recordStyle === 'ReadonlyRecord'
995
- ? (`ReadonlyRecord<${keyText}, ${valueText}>` as const)
996
- : (`Readonly<Record<${keyText}, ${valueText}>>` as const);
997
-
998
1223
  /** `tr(E[]) |-> tr(E)[]` */
999
1224
  const transformArrayTypeNode = (
1000
1225
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1481,185 +1706,58 @@ const transformUnionTypeNode = (
1481
1706
  transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
1482
1707
  };
1483
1708
 
1484
- const transformUnionOrIntersectionTypeNodeImpl = (
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 = (
1485
1718
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1486
- node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1487
- readonlyContext: StrictExclude<
1488
- ReadonlyContext,
1489
- Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
1490
- >,
1491
- options: ReadonlyTransformerOptionsInternal,
1492
- operator: '&' | '|',
1493
- ): void => {
1494
- const nextReadonlyContextValue = nextReadonlyContext({
1495
- currentReadonlyContext: readonlyContext,
1496
- nextReadonlyContextType: 'none',
1497
- indexedAccessDepthChange: 'keep',
1498
- });
1499
-
1500
- // Recursive processing
1501
- for (const n of node.getTypeNodes() /* = [A, B] */) {
1502
- transformNode(n, nextReadonlyContextValue, options);
1503
- }
1504
-
1505
- const newTypes = node.getTypeNodes();
1506
-
1507
- const withLeadingComments = (
1508
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1509
- member: tsm.TypeNode,
1510
- memberText: string,
1511
- ): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
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
-
1526
- const { primitives, arraysAndTuples, typeLiterals, others } =
1527
- groupUnionIntersectionTypes(newTypes, {
1528
- operator,
1529
- underReadonly,
1530
- deepReadonlyTypeName: options.DeepReadonly.typeName,
1531
- keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
1532
- });
1533
-
1534
- options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
1535
-
1536
- // Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
1537
- const isEmptyTypeLiteral = (
1538
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1539
- n: tsm.TypeNode,
1540
- ): boolean =>
1541
- n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
1542
-
1543
- const nonEmptyTypeLiterals =
1544
- typeLiterals === undefined
1545
- ? undefined
1546
- : options.ignoreEmptyObjectTypes
1547
- ? typeLiterals.nodes.filter((n) => !isEmptyTypeLiteral(n))
1548
- : typeLiterals.nodes;
1549
-
1550
- const emptyTypeLiterals =
1551
- typeLiterals === undefined || !options.ignoreEmptyObjectTypes
1552
- ? undefined
1553
- : typeLiterals.nodes.filter(isEmptyTypeLiteral);
1554
-
1555
- const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
1556
- nonEmptyTypeLiterals === undefined || Arr.isEmpty(nonEmptyTypeLiterals)
1557
- ? ([] as const)
1558
- : ([
1559
- unionToString({
1560
- types: nonEmptyTypeLiterals.map((n) =>
1561
- withLeadingComments(
1562
- n,
1563
- isMergeableReadonlyTypeReferenceNode(n, operator)
1564
- ? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
1565
- // NOTE: Readonly<A | B> -> A | B (spliced into `|`)
1566
- readonlyTypeArgTextSplicedInto(n, operator)
1567
- : n.isKind(tsm.SyntaxKind.TypeLiteral)
1568
- ? n.getText()
1569
- : // A member kept under `Readonly`; see `underReadonly`
1570
- wrapWithParentheses(n.getText()),
1571
- ),
1572
- ),
1573
- op: operator,
1574
- wrapWithReadonly: wrapsTypeLiterals,
1575
- }),
1576
- ] as const);
1577
-
1578
- // Sort by first occurrence (preserving the original union order as much as possible)
1579
- const sorted: readonly string[] = [
1580
- mapNullable(
1581
- primitives,
1582
- (a) =>
1583
- [
1584
- a.nodes.map((n) => withLeadingComments(n, n.getText())),
1585
- a.firstPosition,
1586
- ] as const,
1587
- ),
1588
- mapNullable(
1589
- arraysAndTuples,
1590
- (a) =>
1591
- [
1592
- a.nodes.map((n) => withLeadingComments(n, n.getText())),
1593
- a.firstPosition,
1594
- ] as const,
1595
- ),
1596
- nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
1597
- ? mapNullable(
1598
- typeLiterals,
1599
- (a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
1600
- )
1601
- : undefined,
1602
- emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
1603
- ? mapNullable(
1604
- typeLiterals,
1605
- (a) =>
1606
- [
1607
- emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
1608
- // Use a position after non-empty type literals if they exist
1609
- a.firstPosition +
1610
- (nonEmptyTypeLiterals !== undefined &&
1611
- Arr.isNonEmpty(nonEmptyTypeLiterals)
1612
- ? 0.5
1613
- : 0),
1614
- ] as const,
1615
- )
1616
- : undefined,
1617
- mapNullable(
1618
- others,
1619
- (a) =>
1620
- [
1621
- // `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
1622
- // above. See `mergeDeepReadonlyUnionMembers` for why.
1623
- operator === '|'
1624
- ? mergeDeepReadonlyUnionMembers(
1625
- a.nodes,
1626
- options.DeepReadonly.typeName,
1627
- withLeadingComments,
1628
- )
1629
- : a.nodes.map((n) =>
1630
- withLeadingComments(n, wrapWithParentheses(n.getText())),
1631
- ),
1632
- a.firstPosition,
1633
- ] as const,
1634
- ),
1635
- ]
1636
- .filter((x) => x !== undefined)
1637
- .toSorted((a, b) => a[1] - b[1])
1638
- .flatMap((a) => a[0]);
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;
1639
1724
 
1640
- // Readonly<number & { x: X } & { y: Y } & readonly E[]>
1641
- // -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
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);
1642
1743
 
1643
- // Readonly<number | { x: X } | { y: Y } | readonly E[]>
1644
- // -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
1645
- // Readonly<number & { x: X } & { y: Y } & readonly E[]>
1646
- // -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
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());
1647
1759
 
1648
- // Readonly<number | { x: X } | { y: Y } | readonly E[]>
1649
- // -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
1650
- //
1651
- // NOTE: The result is wrapped in parentheses (via `unionToString`). This is
1652
- // intentional and required for precedence safety: without it, a member such
1653
- // as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
1654
- // operator would produce invalid syntax when this node is spliced back in.
1655
- // Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
1656
- // collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
1657
- // — see the note there. That prevents the parentheses from growing without
1658
- // bound when a union/intersection sits inside a `ParenthesizedType`.
1659
- options.replaceNode(
1660
- node,
1661
- unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
1662
- );
1760
+ return unionToStringImpl({ types, op, wrapWithReadonly });
1663
1761
  };
1664
1762
 
1665
1763
  /**
@@ -1775,10 +1873,10 @@ const isTypeArgumentOfReadonly = (
1775
1873
  * `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
1776
1874
  * in front of that stays in the file by itself. With no leading `|` / `&`, the
1777
1875
  * first member starts there too, and its comments are the type's: repeating
1778
- * them in the replacement would copy them once per run. Every other member
1779
- * has a separator in front of it, and a comment before the separator belongs
1780
- * to no member's full text, so it is gone once the separator is rewritten. It
1781
- * goes with the member after it, like the comments after the separator.
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.
1782
1880
  *
1783
1881
  * `member` may be the type inside a member's parentheses; the comments inside
1784
1882
  * them are collected too, and the parentheses are not.
@@ -1802,9 +1900,9 @@ const leadingCommentsOfMember = (
1802
1900
  * `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
1803
1901
  * intersection (`&`) of the members it is merged with.
1804
1902
  *
1805
- * `T` is wrapped in parentheses for precedence safety (a function type
1806
- * argument needs them), unless it is a union spliced into a union or an
1807
- * intersection spliced into an intersection, which needs none.
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.
1808
1906
  */
1809
1907
  const readonlyTypeArgTextSplicedInto = (
1810
1908
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1824,127 +1922,187 @@ const readonlyTypeArgTextSplicedInto = (
1824
1922
  : unwrapReadonlyTypeArgText(node);
1825
1923
  };
1826
1924
 
1827
- /**
1828
- * The text of `inner`, a type `removeParentheses` took out of `outer`, with
1829
- * the comments in `outer`'s full text in front of it — before the parentheses
1830
- * and inside them — and without the parentheses.
1831
- *
1832
- * `inner.getFullText()` would drop those before the parentheses, which are
1833
- * as much a part of `outer` as the rest of it.
1834
- */
1835
- const textWithoutParentheses = (
1836
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1837
- outer: tsm.TypeNode,
1925
+ const transformUnionOrIntersectionTypeNodeImpl = (
1838
1926
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1839
- inner: tsm.TypeNode,
1840
- ): string =>
1841
- `${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
1927
+ node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1928
+ readonlyContext: StrictExclude<
1929
+ ReadonlyContext,
1930
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
1931
+ >,
1932
+ options: ReadonlyTransformerOptionsInternal,
1933
+ operator: '&' | '|',
1934
+ ): void => {
1935
+ const nextReadonlyContextValue = nextReadonlyContext({
1936
+ currentReadonlyContext: readonlyContext,
1937
+ nextReadonlyContextType: 'none',
1938
+ indexedAccessDepthChange: 'keep',
1939
+ });
1842
1940
 
1843
- /**
1844
- * The comments in `[from, to)` of a source file, each followed by what keeps
1845
- * the next token off it, and nothing else in that range — no whitespace, and
1846
- * no token such as a separator or a parenthesis.
1847
- */
1848
- const commentsBetween = (
1849
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1850
- sourceFile: tsm.SourceFile,
1851
- from: number,
1852
- to: number,
1853
- ): string => {
1854
- if (to <= from) {
1855
- return '';
1941
+ // Recursive processing
1942
+ for (const n of node.getTypeNodes() /* = [A, B] */) {
1943
+ transformNode(n, nextReadonlyContextValue, options);
1856
1944
  }
1857
1945
 
1858
- const scanner = tsm.ts.createScanner(
1859
- tsm.ts.ScriptTarget.Latest,
1860
- false,
1861
- tsm.ts.LanguageVariant.Standard,
1862
- sourceFile.getFullText(),
1863
- undefined,
1864
- from,
1865
- to - from,
1866
- );
1946
+ const newTypes = node.getTypeNodes();
1867
1947
 
1868
- const mut_comments: string[] = [];
1948
+ const withLeadingComments = (
1949
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1950
+ member: tsm.TypeNode,
1951
+ memberText: string,
1952
+ ): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
1869
1953
 
1870
- while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
1871
- const token = scanner.getToken();
1954
+ // Whether a type literal member is wrapped at all; under `DeepReadonly` or an
1955
+ // indexed access none is.
1956
+ const wrapsTypeLiterals =
1957
+ nextReadonlyContextValue.type !== 'DeepReadonly' &&
1958
+ nextReadonlyContextValue.indexedAccessDepth === 0;
1872
1959
 
1873
- // A line comment runs to the end of the line, so it takes one along.
1874
- const separator =
1875
- token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
1876
- ? '\n'
1877
- : token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
1878
- ? ' '
1879
- : undefined;
1960
+ // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the handler
1961
+ // of `Readonly` then takes off as redundant.
1962
+ const underReadonly =
1963
+ readonlyContext.type === 'Readonly' &&
1964
+ readonlyContext.indexedAccessDepth === 0 &&
1965
+ isTypeArgumentOfReadonly(node);
1880
1966
 
1881
- if (separator !== undefined) {
1882
- mut_comments.push(`${scanner.getTokenText()}${separator}`);
1883
- }
1884
- }
1967
+ const { primitives, arraysAndTuples, typeLiterals, others } =
1968
+ groupUnionIntersectionTypes(newTypes, {
1969
+ operator,
1970
+ underReadonly,
1971
+ deepReadonlyTypeName: options.DeepReadonly.typeName,
1972
+ keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
1973
+ });
1885
1974
 
1886
- return mut_comments.join('');
1887
- };
1975
+ options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
1888
1976
 
1889
- /** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
1890
- const getDeepReadonlyTypeArg = (
1891
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1892
- node: tsm.TypeNode,
1893
- deepReadonlyTypeName: string,
1894
- ): tsm.TypeNode | undefined => {
1895
- if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
1896
- return undefined;
1897
- }
1977
+ // Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
1978
+ const isEmptyTypeLiteral = (
1979
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1980
+ n: tsm.TypeNode,
1981
+ ): boolean =>
1982
+ n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
1898
1983
 
1899
- const typeName = node.getTypeName();
1984
+ const nonEmptyTypeLiterals =
1985
+ typeLiterals === undefined
1986
+ ? undefined
1987
+ : options.ignoreEmptyObjectTypes
1988
+ ? typeLiterals.nodes.filter((n) => !isEmptyTypeLiteral(n))
1989
+ : typeLiterals.nodes;
1900
1990
 
1901
- const typeArguments = node.getTypeArguments();
1991
+ const emptyTypeLiterals =
1992
+ typeLiterals === undefined || !options.ignoreEmptyObjectTypes
1993
+ ? undefined
1994
+ : typeLiterals.nodes.filter(isEmptyTypeLiteral);
1995
+
1996
+ const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
1997
+ nonEmptyTypeLiterals === undefined || Arr.isEmpty(nonEmptyTypeLiterals)
1998
+ ? ([] as const)
1999
+ : ([
2000
+ unionToString({
2001
+ types: nonEmptyTypeLiterals.map((n) =>
2002
+ withLeadingComments(
2003
+ n,
2004
+ isMergeableReadonlyTypeReferenceNode(n, operator)
2005
+ ? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
2006
+ // NOTE: Readonly<A | B> -> A | B (spliced into `|`)
2007
+ readonlyTypeArgTextSplicedInto(n, operator)
2008
+ : n.isKind(tsm.SyntaxKind.TypeLiteral)
2009
+ ? n.getText()
2010
+ : // A member kept under `Readonly`; see `underReadonly`
2011
+ wrapWithParentheses(n.getText()),
2012
+ ),
2013
+ ),
2014
+ op: operator,
2015
+ wrapWithReadonly: wrapsTypeLiterals,
2016
+ }),
2017
+ ] as const);
2018
+
2019
+ // Sort by first occurrence (preserving the original union order as much as possible)
2020
+ const sorted: readonly string[] = [
2021
+ mapNullable(
2022
+ primitives,
2023
+ (a) =>
2024
+ [
2025
+ a.nodes.map((n) => withLeadingComments(n, n.getText())),
2026
+ a.firstPosition,
2027
+ ] as const,
2028
+ ),
2029
+ mapNullable(
2030
+ arraysAndTuples,
2031
+ (a) =>
2032
+ [
2033
+ a.nodes.map((n) => withLeadingComments(n, n.getText())),
2034
+ a.firstPosition,
2035
+ ] as const,
2036
+ ),
2037
+ nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
2038
+ ? mapNullable(
2039
+ typeLiterals,
2040
+ (a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
2041
+ )
2042
+ : undefined,
2043
+ emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
2044
+ ? mapNullable(
2045
+ typeLiterals,
2046
+ (a) =>
2047
+ [
2048
+ emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
2049
+ // Use a position after non-empty type literals if they exist
2050
+ a.firstPosition +
2051
+ (nonEmptyTypeLiterals !== undefined &&
2052
+ Arr.isNonEmpty(nonEmptyTypeLiterals)
2053
+ ? 0.5
2054
+ : 0),
2055
+ ] as const,
2056
+ )
2057
+ : undefined,
2058
+ mapNullable(
2059
+ others,
2060
+ (a) =>
2061
+ [
2062
+ // `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
2063
+ // above. See `mergeDeepReadonlyUnionMembers` for why.
2064
+ operator === '|'
2065
+ ? mergeDeepReadonlyUnionMembers(
2066
+ a.nodes,
2067
+ options.DeepReadonly.typeName,
2068
+ withLeadingComments,
2069
+ )
2070
+ : a.nodes.map((n) =>
2071
+ withLeadingComments(n, wrapWithParentheses(n.getText())),
2072
+ ),
2073
+ a.firstPosition,
2074
+ ] as const,
2075
+ ),
2076
+ ]
2077
+ .filter((x) => x !== undefined)
2078
+ .toSorted((a, b) => a[1] - b[1])
2079
+ .flatMap((a) => a[0]);
1902
2080
 
1903
- return typeName.isKind(tsm.SyntaxKind.Identifier) &&
1904
- typeName.getText() === deepReadonlyTypeName &&
1905
- Arr.isFixedLengthArray(1, typeArguments)
1906
- ? typeArguments[0]
1907
- : undefined;
1908
- };
2081
+ // Readonly<number & { x: X } & { y: Y } & readonly E[]>
2082
+ // -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
1909
2083
 
1910
- const unionToString = ({
1911
- types: typesRaw,
1912
- op,
1913
- wrapWithReadonly,
1914
- }: Readonly<{
1915
- types: readonly string[];
1916
- op: '&' | '|';
1917
- wrapWithReadonly: boolean | string;
1918
- }>): string => {
1919
- // The members are joined with a separator that supplies its own space, so
1920
- // whitespace left at a member's edge would accumulate: every run of the
1921
- // transformer would add one more space before every member but the first,
1922
- // and a file would never reach a fixed point. Trimming keeps the comments in
1923
- // front of a member, which are not whitespace.
1924
- const types = typesRaw.map((s) => s.trim());
2084
+ // Readonly<number | { x: X } | { y: Y } | readonly E[]>
2085
+ // -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
2086
+ // Readonly<number & { x: X } & { y: Y } & readonly E[]>
2087
+ // -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
1925
2088
 
1926
- return unionToStringImpl({ types, op, wrapWithReadonly });
2089
+ // Readonly<number | { x: X } | { y: Y } | readonly E[]>
2090
+ // -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
2091
+ //
2092
+ // NOTE: The result is wrapped in parentheses (via `unionToString`). This is
2093
+ // intentional and required for precedence safety: without it, a member such
2094
+ // as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
2095
+ // operator would produce invalid syntax when this node is spliced back in.
2096
+ // Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
2097
+ // collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
2098
+ // — see the note there. That prevents the parentheses from growing without
2099
+ // bound when a union/intersection sits inside a `ParenthesizedType`.
2100
+ options.replaceNode(
2101
+ node,
2102
+ unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
2103
+ );
1927
2104
  };
1928
2105
 
1929
- const unionToStringImpl = ({
1930
- types,
1931
- op,
1932
- wrapWithReadonly,
1933
- }: Readonly<{
1934
- types: readonly string[];
1935
- op: '&' | '|';
1936
- wrapWithReadonly: boolean | string;
1937
- }>): string =>
1938
- Arr.isEmpty(types)
1939
- ? 'never'
1940
- : Arr.isFixedLengthArray(1, types)
1941
- ? wrapWithReadonly === false
1942
- ? wrapWithParentheses(types[0])
1943
- : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>` as const)
1944
- : wrapWithReadonly === false
1945
- ? wrapWithParentheses(types.join(` ${op} `))
1946
- : (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>` as const);
1947
-
1948
2106
  /** Convert ((T)) -> (T) recursively */
1949
2107
  const transformParenthesizedTypeNode = (
1950
2108
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -1964,20 +2122,16 @@ const transformParenthesizedTypeNode = (
1964
2122
  const T = node.getTypeNode();
1965
2123
 
1966
2124
  if (
1967
- // remove () if T is TypeReferenceNode
1968
- // e.g. `(Readonly<A>) |-> Readonly<A>`
2125
+ // remove () if T is TypeReferenceNode e.g. `(Readonly<A>) |-> Readonly<A>`
1969
2126
  T.isKind(tsm.SyntaxKind.TypeReference) ||
1970
- // remove () if T is ArrayTypeNode
1971
- // e.g. `(A[]) |-> A[]`
2127
+ // remove () if T is ArrayTypeNode e.g. `(A[]) |-> A[]`
1972
2128
  T.isKind(tsm.SyntaxKind.ArrayType) ||
1973
- // remove () if T is TupleTypeNode
1974
- // e.g. `([A]) |-> [A]`
2129
+ // remove () if T is TupleTypeNode e.g. `([A]) |-> [A]`
1975
2130
  T.isKind(tsm.SyntaxKind.TupleType) ||
1976
- // remove () if T is PrimitiveTypeNode
1977
- // e.g. `(number) |-> number`
2131
+ // remove () if T is PrimitiveTypeNode e.g. `(number) |-> number`
1978
2132
  isAtomicTypeNode(T) ||
1979
- // remove () if T is TypeLiteralNode
1980
- // e.g. `({ member: V }) |-> { member: V }`
2133
+ // remove () if T is TypeLiteralNode e.g.
2134
+ // `({ member: V }) |-> { member: V }`
1981
2135
  T.isKind(tsm.SyntaxKind.TypeLiteral) ||
1982
2136
  // collapse redundant nested parentheses if T is itself a ParenthesizedType
1983
2137
  // e.g. `((X)) |-> (X)`
@@ -2134,161 +2288,3 @@ const transformIndexSignatureDeclaration = (
2134
2288
  }
2135
2289
  }
2136
2290
  };
2137
-
2138
- const checkIfPropertyNameShouldBeIgnored = (
2139
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
2140
- nameNode: tsm.PropertyName,
2141
- options: ReadonlyTransformerOptionsInternal,
2142
- ): boolean => {
2143
- expectType<typeof nameNode, tsm.PropertyName>('=');
2144
-
2145
- expectType<
2146
- tsm.PropertyName,
2147
- | tsm.NumericLiteral
2148
- | tsm.BigIntLiteral
2149
- | tsm.NoSubstitutionTemplateLiteral
2150
- | tsm.Identifier
2151
- | tsm.StringLiteral
2152
- | tsm.PrivateIdentifier
2153
- | tsm.ComputedPropertyName // [`mut_x`]: number[]
2154
- >('=');
2155
-
2156
- return (
2157
- // mut_x: number[]
2158
- (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
2159
- pipe(nameNode.getText()).map((nm) =>
2160
- options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2161
- ).value) ||
2162
- // "mut_x": number[]
2163
- (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
2164
- pipe(nameNode.getLiteralValue()).map((nm) =>
2165
- options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
2166
- ).value) ||
2167
- // #memberName: number[] (class only), matched with or after its `#`
2168
- (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
2169
- pipe(nameNode.getText()).map((nm) =>
2170
- options.mutablePropertyPrefixes.some(
2171
- (p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
2172
- ),
2173
- ).value) ||
2174
- // ['mut_x']: number[], [`mut_x`]: number[]
2175
- (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
2176
- pipe(nameNode.getExpression()).map((exp) => {
2177
- if (
2178
- exp.isKind(tsm.SyntaxKind.StringLiteral) ||
2179
- exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
2180
- ) {
2181
- const nm = exp.getLiteralValue();
2182
-
2183
- return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
2184
- }
2185
-
2186
- return false;
2187
- }).value)
2188
- );
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;