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