ts-codemod-lib 3.4.4 → 3.5.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/README.md +26 -7
  2. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  3. package/dist/functions/ast-transformers/append-as-const.mjs +6 -3
  4. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  5. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
  6. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +2 -1
  7. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
  8. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +10 -0
  9. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  10. package/dist/functions/ast-transformers/convert-to-readonly.mjs +120 -16
  11. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  12. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +7 -1
  13. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
  14. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +25 -2
  15. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
  17. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +3 -2
  18. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
  19. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
  20. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +6 -5
  21. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  22. package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
  23. package/dist/functions/functions/has-disable-next-line-comment.mjs +56 -4
  24. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  25. package/dist/functions/utils/index.d.mts +1 -0
  26. package/dist/functions/utils/index.d.mts.map +1 -1
  27. package/dist/functions/utils/index.mjs +1 -0
  28. package/dist/functions/utils/index.mjs.map +1 -1
  29. package/dist/functions/utils/replace-node-with-text.d.mts +21 -0
  30. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -0
  31. package/dist/functions/utils/replace-node-with-text.mjs +22 -0
  32. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -0
  33. package/dist/functions/utils/replace-with-debug.d.mts.map +1 -1
  34. package/dist/functions/utils/replace-with-debug.mjs +2 -1
  35. package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
  36. package/package.json +25 -25
  37. package/src/functions/ast-transformers/append-as-const.mts +9 -3
  38. package/src/functions/ast-transformers/convert-interface-to-type.mts +2 -1
  39. package/src/functions/ast-transformers/convert-to-readonly.mts +187 -22
  40. package/src/functions/ast-transformers/convert-to-readonly.test.mts +430 -32
  41. package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +36 -5
  42. package/src/functions/ast-transformers/replace-any-with-unknown.mts +3 -2
  43. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +7 -5
  44. package/src/functions/functions/has-disable-next-line-comment.mts +79 -6
  45. package/src/functions/functions/has-disable-next-line-comment.test.mts +96 -0
  46. package/src/functions/utils/index.mts +1 -0
  47. package/src/functions/utils/replace-node-with-text.mts +37 -0
  48. package/src/functions/utils/replace-node-with-text.test.mts +89 -0
  49. package/src/functions/utils/replace-with-debug.mts +2 -1
@@ -29,7 +29,10 @@ import {
29
29
  wrapWithParentheses,
30
30
  type ReadonlyTypeReferenceNode,
31
31
  } from '../functions/index.mjs';
32
- import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
32
+ import {
33
+ replaceNodeWithDebugPrint,
34
+ replaceNodeWithText,
35
+ } from '../utils/index.mjs';
33
36
  import {
34
37
  groupUnionIntersectionTypes,
35
38
  invalidDeepReadonlyTypeName,
@@ -56,6 +59,10 @@ export const convertToReadonlyTransformer = (
56
59
  const DeepReadonlyTypeName =
57
60
  options?.DeepReadonly?.typeName ?? 'DeepReadonly';
58
61
 
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.)
59
66
  const ignorePrefixes = ISet.create(
60
67
  options?.ignorePrefixes ?? ['mut_', '#mut_', '_mut_', 'draft'],
61
68
  );
@@ -72,7 +79,7 @@ export const convertToReadonlyTransformer = (
72
79
  replaceNode:
73
80
  options?.debug === true
74
81
  ? replaceNodeWithDebugPrint
75
- : (node, newNodeText) => node.replaceWithText(newNodeText),
82
+ : (node, newNodeText) => replaceNodeWithText(node, newNodeText),
76
83
  } as const;
77
84
 
78
85
  return {
@@ -136,6 +143,16 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
136
143
  *
137
144
  * (e.g. `"mut_"`)
138
145
  *
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
148
+ * too; its other members are marked `readonly` one by one instead, and a
149
+ * `Readonly<...>` written around it is taken off. It is wrapped as before
150
+ * when it also has a member that cannot be marked `readonly` by itself (a
151
+ * method signature or an accessor without such a prefix, or a property or
152
+ * index signature not yet `readonly` that an ignore comment skips), or when
153
+ * it is a member of a `Readonly<...>` intersection beside a type that may be
154
+ * an array.
155
+ *
139
156
  * @default ['mut_', '#mut_', '_mut_', 'draft']
140
157
  */
141
158
  ignorePrefixes?: string[];
@@ -418,6 +435,8 @@ const transformNode = (
418
435
  * - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
419
436
  * - `tr(DeepReadonly<Readonly<E>>) |-> DeepReadonly<tr(E)>`
420
437
  * - `tr(Readonly<number>) |-> number`
438
+ * - `tr(Readonly<{ mut_x: X; y: Y }>) |-> { mut_x: X; readonly y: tr(Y) }` (see
439
+ * `isLeftUnwrapped`)
421
440
  * - `tr(Readonly<E[]>) |-> readonly tr(E)[]`
422
441
  * - `tr(Readonly<[E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
423
442
  * - `tr(Readonly<readonly E[]>) |-> readonly tr(E)[]`
@@ -644,14 +663,29 @@ const transformTypeReferenceNode = (
644
663
  );
645
664
  }
646
665
 
666
+ // Read before the recursion, which may replace the node.
667
+ const typeArgumentBefore = removeParentheses(typeArguments[0]);
668
+
669
+ // Readonly<{ mut_x: X; y: Y }> -> { mut_x: X; readonly y: Y }
670
+ // `Readonly` would make the property of an ignored prefix readonly too, so
671
+ // it is taken off, and the literal is transformed where nothing wraps it,
672
+ // which marks its other members readonly in place. See `takesOffReadonly`.
673
+ const takesOffWrapper =
674
+ readonlyContext.type !== 'DeepReadonly' &&
675
+ readonlyContext.indexedAccessDepth === 0 &&
676
+ typeArgumentBefore.isKind(tsm.SyntaxKind.TypeLiteral) &&
677
+ takesOffReadonly(typeArgumentBefore, options);
678
+
647
679
  // Recursive processing
648
680
  transformNode(
649
681
  typeArguments[0],
650
- nextReadonlyContext({
651
- currentReadonlyContext: readonlyContext,
652
- nextReadonlyContextType: 'Readonly',
653
- indexedAccessDepthChange: 'keep',
654
- }),
682
+ takesOffWrapper
683
+ ? initialReadonlyContext
684
+ : nextReadonlyContext({
685
+ currentReadonlyContext: readonlyContext,
686
+ nextReadonlyContextType: 'Readonly',
687
+ indexedAccessDepthChange: 'keep',
688
+ }),
655
689
  options,
656
690
  );
657
691
 
@@ -692,6 +726,29 @@ const transformTypeReferenceNode = (
692
726
  return;
693
727
  }
694
728
 
729
+ // Readonly<Readonly<{ mut_x: X }>> -> { mut_x: X } as well: the inner
730
+ // `Readonly` has already been taken off the literal.
731
+ if (
732
+ takesOffWrapper ||
733
+ (T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
734
+ ) {
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.
738
+ const commentsAfterT = commentsBetween(
739
+ node.getSourceFile(),
740
+ T.getEnd(),
741
+ node.getEnd(),
742
+ );
743
+
744
+ options.replaceNode(
745
+ node,
746
+ `${commentsInFrontOfT}${commentsAfterT}${T.getText()}`,
747
+ );
748
+
749
+ return;
750
+ }
751
+
695
752
  // Readonly<Readonly<T>> -> Readonly<T>
696
753
  if (isReadonlyTypeReferenceNode(T)) {
697
754
  options.replaceNode(node, TText);
@@ -1082,10 +1139,13 @@ const transformTypeLiteralNode = (
1082
1139
  const wrappingApplies =
1083
1140
  readonlyContext.type === 'none' && readonlyContext.indexedAccessDepth === 0;
1084
1141
 
1085
- // A call or construct signature does not survive `Readonly<T>`, so a literal
1086
- // carrying one is left unwrapped and marked readonly member by member
1087
- // instead. See `hasCallOrConstructSignature`.
1088
- const skipWrapping = wrappingApplies && hasCallOrConstructSignature(node);
1142
+ // A call or construct signature does not survive `Readonly<T>`, and a
1143
+ // property of an ignored prefix (`mut_`) must stay mutable, so a literal
1144
+ // carrying either is left unwrapped and marked readonly member by member
1145
+ // instead. See `hasCallOrConstructSignature` and `isLeftUnwrapped`.
1146
+ const skipWrapping =
1147
+ wrappingApplies &&
1148
+ (hasCallOrConstructSignature(node) || isLeftUnwrapped(node, options));
1089
1149
 
1090
1150
  // Recursive processing
1091
1151
  transformMembers(
@@ -1390,16 +1450,25 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1390
1450
  memberText: string,
1391
1451
  ): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
1392
1452
 
1453
+ // Whether a type literal member is wrapped at all; under `DeepReadonly` or an
1454
+ // indexed access none is.
1455
+ const wrapsTypeLiterals =
1456
+ nextReadonlyContextValue.type !== 'DeepReadonly' &&
1457
+ nextReadonlyContextValue.indexedAccessDepth === 0;
1458
+
1459
+ // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1460
+ // handler of `Readonly` then takes off as redundant.
1461
+ const underReadonly =
1462
+ readonlyContext.type === 'Readonly' &&
1463
+ readonlyContext.indexedAccessDepth === 0 &&
1464
+ isTypeArgumentOfReadonly(node);
1465
+
1393
1466
  const { primitives, arraysAndTuples, typeLiterals, others } =
1394
1467
  groupUnionIntersectionTypes(newTypes, {
1395
1468
  operator,
1396
- // `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
1397
- // handler of `Readonly` then takes off as redundant.
1398
- underReadonly:
1399
- readonlyContext.type === 'Readonly' &&
1400
- readonlyContext.indexedAccessDepth === 0 &&
1401
- isTypeArgumentOfReadonly(node),
1469
+ underReadonly,
1402
1470
  deepReadonlyTypeName: options.DeepReadonly.typeName,
1471
+ keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
1403
1472
  });
1404
1473
 
1405
1474
  options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
@@ -1442,9 +1511,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
1442
1511
  ),
1443
1512
  ),
1444
1513
  op: operator,
1445
- wrapWithReadonly:
1446
- nextReadonlyContextValue.type !== 'DeepReadonly' &&
1447
- nextReadonlyContextValue.indexedAccessDepth === 0,
1514
+ wrapWithReadonly: wrapsTypeLiterals,
1448
1515
  }),
1449
1516
  ] as const);
1450
1517
 
@@ -1906,6 +1973,17 @@ const transformMembers = (
1906
1973
  continue;
1907
1974
  }
1908
1975
 
1976
+ // A method or an accessor of an ignored prefix is left as written, as in a
1977
+ // class.
1978
+ if (
1979
+ (mb.isKind(tsm.SyntaxKind.MethodSignature) ||
1980
+ mb.isKind(tsm.SyntaxKind.GetAccessor) ||
1981
+ mb.isKind(tsm.SyntaxKind.SetAccessor)) &&
1982
+ checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options)
1983
+ ) {
1984
+ continue;
1985
+ }
1986
+
1909
1987
  if (mb.isKind(tsm.SyntaxKind.IndexSignature)) {
1910
1988
  transformIndexSignatureDeclaration(
1911
1989
  mb,
@@ -2031,10 +2109,13 @@ const checkIfPropertyNameShouldBeIgnored = (
2031
2109
  pipe(nameNode.getText()).map((nm) =>
2032
2110
  options.ignoredPrefixes.some((p) => nm.startsWith(`#${p}`)),
2033
2111
  ).value) ||
2034
- // [`mut_x`]: number[]
2112
+ // ['mut_x']: number[], [`mut_x`]: number[]
2035
2113
  (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
2036
2114
  pipe(nameNode.getExpression()).map((exp) => {
2037
- if (exp.isKind(tsm.SyntaxKind.StringLiteral)) {
2115
+ if (
2116
+ exp.isKind(tsm.SyntaxKind.StringLiteral) ||
2117
+ exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
2118
+ ) {
2038
2119
  const nm = exp.getLiteralValue();
2039
2120
 
2040
2121
  return options.ignoredPrefixes.some((p) => nm.startsWith(p));
@@ -2044,3 +2125,87 @@ const checkIfPropertyNameShouldBeIgnored = (
2044
2125
  }).value)
2045
2126
  );
2046
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);