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