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.
- package/README.md +26 -7
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +6 -3
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +2 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +10 -0
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +120 -16
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +7 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +25 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +3 -2
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +6 -5
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.mjs +56 -4
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/utils/index.d.mts +1 -0
- package/dist/functions/utils/index.d.mts.map +1 -1
- package/dist/functions/utils/index.mjs +1 -0
- package/dist/functions/utils/index.mjs.map +1 -1
- package/dist/functions/utils/replace-node-with-text.d.mts +21 -0
- package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -0
- package/dist/functions/utils/replace-node-with-text.mjs +22 -0
- package/dist/functions/utils/replace-node-with-text.mjs.map +1 -0
- package/dist/functions/utils/replace-with-debug.d.mts.map +1 -1
- package/dist/functions/utils/replace-with-debug.mjs +2 -1
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
- package/package.json +25 -25
- package/src/functions/ast-transformers/append-as-const.mts +9 -3
- package/src/functions/ast-transformers/convert-interface-to-type.mts +2 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +187 -22
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +430 -32
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +36 -5
- package/src/functions/ast-transformers/replace-any-with-unknown.mts +3 -2
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +7 -5
- package/src/functions/functions/has-disable-next-line-comment.mts +79 -6
- package/src/functions/functions/has-disable-next-line-comment.test.mts +96 -0
- package/src/functions/utils/index.mts +1 -0
- package/src/functions/utils/replace-node-with-text.mts +37 -0
- package/src/functions/utils/replace-node-with-text.test.mts +89 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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>`,
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
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
|
-
|
|
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 (
|
|
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);
|