ts-codemod-lib 4.0.0 → 4.1.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 +2 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +28 -7
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +452 -253
- 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/mutable-prefixes.d.mts +8 -8
- 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 +9 -9
- package/src/functions/ast-transformers/append-as-const.mts +242 -231
- package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
- package/src/functions/ast-transformers/convert-to-readonly-record-import.test.mts +458 -0
- package/src/functions/ast-transformers/convert-to-readonly.mts +1003 -694
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +80 -33
- 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/mutable-prefixes.mts +8 -8
- 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
|
@@ -49,6 +49,10 @@ import type { TsMorphTransformer } from './types.mjs';
|
|
|
49
49
|
|
|
50
50
|
const TRANSFORMER_NAME = 'convert-to-readonly';
|
|
51
51
|
|
|
52
|
+
const READONLY_RECORD_NAME = 'ReadonlyRecord';
|
|
53
|
+
|
|
54
|
+
const READONLY_RECORD_MODULE = 'ts-type-forge';
|
|
55
|
+
|
|
52
56
|
export const convertToReadonlyTransformer = (
|
|
53
57
|
options?: ReadonlyTransformerOptions,
|
|
54
58
|
): TsMorphTransformer => {
|
|
@@ -92,6 +96,7 @@ export const convertToReadonlyTransformer = (
|
|
|
92
96
|
options?.mutablePropertyPrefixes ?? DEFAULT_MUTABLE_PREFIXES,
|
|
93
97
|
),
|
|
94
98
|
recordStyle: options?.recordStyle ?? 'ReadonlyRecord',
|
|
99
|
+
importStyle: options?.importStyle ?? 'named',
|
|
95
100
|
debugPrint: options?.debug === true ? console.debug : () => {},
|
|
96
101
|
replaceNode:
|
|
97
102
|
options?.debug === true
|
|
@@ -102,8 +107,30 @@ export const convertToReadonlyTransformer = (
|
|
|
102
107
|
return {
|
|
103
108
|
name: TRANSFORMER_NAME,
|
|
104
109
|
transform: (sourceAst) => {
|
|
110
|
+
const scope =
|
|
111
|
+
optionsInternal.recordStyle === 'ReadonlyRecord' &&
|
|
112
|
+
optionsInternal.importStyle === 'named'
|
|
113
|
+
? readonlyRecordScope(sourceAst.compilerNode)
|
|
114
|
+
: 'in-scope';
|
|
115
|
+
|
|
116
|
+
// An import would turn a script into a module, and its declarations would
|
|
117
|
+
// stop being global (a `global.d.ts`); in a file whose top level declares
|
|
118
|
+
// the name as a value or a namespace, it could clash. Either gets the
|
|
119
|
+
// built-in style instead.
|
|
120
|
+
const mayImport =
|
|
121
|
+
scope === 'free' && tsm.ts.isExternalModule(sourceAst.compilerNode);
|
|
122
|
+
|
|
123
|
+
const fileOptions: ReadonlyTransformerOptionsInternal =
|
|
124
|
+
scope !== 'in-scope' && !mayImport
|
|
125
|
+
? ({ ...optionsInternal, recordStyle: 'Readonly<Record>' } as const)
|
|
126
|
+
: optionsInternal;
|
|
127
|
+
|
|
105
128
|
for (const node of sourceAst.getChildren()) {
|
|
106
|
-
transformNode(node, initialReadonlyContext,
|
|
129
|
+
transformNode(node, initialReadonlyContext, fileOptions);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (mayImport && referencesReadonlyRecord(sourceAst.compilerNode)) {
|
|
133
|
+
addReadonlyRecordImport(sourceAst);
|
|
107
134
|
}
|
|
108
135
|
},
|
|
109
136
|
};
|
|
@@ -121,7 +148,7 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
121
148
|
*/
|
|
122
149
|
typeName?: string;
|
|
123
150
|
|
|
124
|
-
|
|
151
|
+
/*
|
|
125
152
|
* Whether to apply `DeepReadonly` aggressively or remove `DeepReadonly` as
|
|
126
153
|
* possible.
|
|
127
154
|
*
|
|
@@ -157,8 +184,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
157
184
|
|
|
158
185
|
/**
|
|
159
186
|
* Prefixes of the names of variables, parameters (but for a constructor
|
|
160
|
-
* parameter property, which `mutablePropertyPrefixes` decides) and
|
|
161
|
-
*
|
|
187
|
+
* parameter property, which `mutablePropertyPrefixes` decides) and functions
|
|
188
|
+
* that are left mutable: their types are not made readonly.
|
|
162
189
|
*
|
|
163
190
|
* @default ['mut_']
|
|
164
191
|
*/
|
|
@@ -181,8 +208,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
181
208
|
* both `mut_` and `#mut_` cover `#mut_x`. Of a named tuple member with such a prefix, only the type
|
|
182
209
|
* is kept; the tuple is still made `readonly`.
|
|
183
210
|
*
|
|
184
|
-
* A type literal with a property, a method or an accessor of such a prefix
|
|
185
|
-
*
|
|
211
|
+
* A type literal with a property, a method or an accessor of such a prefix is
|
|
212
|
+
* not wrapped with `Readonly<...>`, which would make that member readonly
|
|
186
213
|
* too; its other members are marked `readonly` one by one instead, and a
|
|
187
214
|
* `Readonly<...>` written around it is taken off. It is wrapped as before
|
|
188
215
|
* when it also has a member that cannot be marked `readonly` by itself (a
|
|
@@ -201,9 +228,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
201
228
|
* - `"ReadonlyRecord"`: Uses the `ReadonlyRecord` type utility provided by
|
|
202
229
|
* `ts-type-forge` (as the `ts-type-forge/prefer-readonly-or-mutable-record`
|
|
203
230
|
* ESLint rule suggests). Occurrences of `Readonly<Record<K, V>>` are also
|
|
204
|
-
* unified to `ReadonlyRecord<K, V>`.
|
|
205
|
-
*
|
|
206
|
-
* (e.g. globally via `ts-type-forge`'s `global.d.mts`).
|
|
231
|
+
* unified to `ReadonlyRecord<K, V>`. How the name is brought into scope
|
|
232
|
+
* is set by `importStyle`.
|
|
207
233
|
* - `"Readonly<Record>"`: Uses only built-in utility types
|
|
208
234
|
* (`Readonly<Record<K, V>>`). Occurrences of `ReadonlyRecord<K, V>` are
|
|
209
235
|
* also unified to `Readonly<Record<K, V>>`.
|
|
@@ -215,6 +241,29 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
215
241
|
*/
|
|
216
242
|
recordStyle?: 'Readonly<Record>' | 'ReadonlyRecord';
|
|
217
243
|
|
|
244
|
+
/**
|
|
245
|
+
* How `ReadonlyRecord` is brought into scope with `recordStyle:
|
|
246
|
+
* "ReadonlyRecord"` (the same choice as the `importStyle` option of
|
|
247
|
+
* `eslint-plugin-ts-type-forge`).
|
|
248
|
+
*
|
|
249
|
+
* - `"named"`: When the transformer writes `ReadonlyRecord` into a file that
|
|
250
|
+
* does not mention the name yet, it also imports it — `import type {
|
|
251
|
+
* ReadonlyRecord } from 'ts-type-forge';`, merged into an existing named
|
|
252
|
+
* import of `ts-type-forge` where there is one. A file that already
|
|
253
|
+
* imports, declares or uses the name (as a global from
|
|
254
|
+
* `ts-type-forge/global`) is left to resolve it as before. A script (a
|
|
255
|
+
* file with no import or export), which an import would turn into a
|
|
256
|
+
* module, and a file whose top level declares the name as a value only,
|
|
257
|
+
* which an import could clash with, get `Readonly<Record<K, V>>` instead.
|
|
258
|
+
* - `"global"`: Imports are never touched, so `ReadonlyRecord` has to be in
|
|
259
|
+
* scope already: globally via `ts-type-forge/global`, or by an import the
|
|
260
|
+
* caller writes itself (as when transforming a file one statement at a
|
|
261
|
+
* time, where each statement would otherwise get an import of its own).
|
|
262
|
+
*
|
|
263
|
+
* @default 'named'
|
|
264
|
+
*/
|
|
265
|
+
importStyle?: 'global' | 'named';
|
|
266
|
+
|
|
218
267
|
debug?: boolean;
|
|
219
268
|
}>;
|
|
220
269
|
|
|
@@ -236,6 +285,8 @@ type ReadonlyTransformerOptionsInternal = Readonly<{
|
|
|
236
285
|
|
|
237
286
|
recordStyle: 'Readonly<Record>' | 'ReadonlyRecord';
|
|
238
287
|
|
|
288
|
+
importStyle: 'global' | 'named';
|
|
289
|
+
|
|
239
290
|
debugPrint: (...args: readonly unknown[]) => void;
|
|
240
291
|
replaceNode: (node: tsm.Node, newNodeText: string) => void;
|
|
241
292
|
}>;
|
|
@@ -245,6 +296,27 @@ const initialReadonlyContext: ReadonlyContext = {
|
|
|
245
296
|
indexedAccessDepth: 0,
|
|
246
297
|
} as const;
|
|
247
298
|
|
|
299
|
+
/** Whether a type alias, interface or class name starts with a `mutableTypePrefixes` entry. */
|
|
300
|
+
const hasMutableTypeName = (
|
|
301
|
+
name: string | undefined,
|
|
302
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
303
|
+
): boolean =>
|
|
304
|
+
name !== undefined &&
|
|
305
|
+
options.mutableTypePrefixes.some((p) => name.startsWith(p));
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* A parameter property is a property of the class, as a `PropertyDeclaration`
|
|
309
|
+
* is; any other parameter is a variable.
|
|
310
|
+
*/
|
|
311
|
+
const mutablePrefixesOfParameter = (
|
|
312
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
313
|
+
param: tsm.ParameterDeclaration,
|
|
314
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
315
|
+
): ISet<string> =>
|
|
316
|
+
param.isParameterProperty()
|
|
317
|
+
? options.mutablePropertyPrefixes
|
|
318
|
+
: options.mutableVariablePrefixes;
|
|
319
|
+
|
|
248
320
|
const transformNode = (
|
|
249
321
|
node: tsm.Node,
|
|
250
322
|
readonlyContext: ReadonlyContext,
|
|
@@ -480,229 +552,705 @@ const transformNode = (
|
|
|
480
552
|
}
|
|
481
553
|
};
|
|
482
554
|
|
|
483
|
-
//
|
|
484
|
-
// Transformer implementation for each node type
|
|
485
|
-
//
|
|
486
|
-
|
|
487
555
|
/**
|
|
488
|
-
* -
|
|
489
|
-
*
|
|
490
|
-
* - `tr(DeepReadonly<Readonly<E>>) |-> DeepReadonly<tr(E)>`
|
|
491
|
-
* - `tr(Readonly<number>) |-> number`
|
|
492
|
-
* - `tr(Readonly<{ mut_x: X; y: Y }>) |-> { mut_x: X; readonly y: tr(Y) }` (see
|
|
493
|
-
* `isLeftUnwrapped`)
|
|
494
|
-
* - `tr(Readonly<E[]>) |-> readonly tr(E)[]`
|
|
495
|
-
* - `tr(Readonly<[E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
|
|
496
|
-
* - `tr(Readonly<readonly E[]>) |-> readonly tr(E)[]`
|
|
497
|
-
* - `tr(Readonly<readonly [E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
|
|
498
|
-
* - `tr(Readonly<A | readonly E[]>) |-> Readonly<tr(A)> | readonly tr(E)[]>`
|
|
499
|
-
* - `tr(Record<K, V>) |-> Readonly<Record<tr(K), tr(V)>>` (or
|
|
500
|
-
* `ReadonlyRecord<tr(K), tr(V)>`, depending on the `recordStyle` option;
|
|
501
|
-
* `Readonly<Record<K, V>>`, `ReadonlyRecord<K, V>` and
|
|
502
|
-
* `Readonly<ReadonlyRecord<K, V>>` are unified to the same style)
|
|
556
|
+
* The names a top-level statement binds, and whether each can name a type (an
|
|
557
|
+
* import can).
|
|
503
558
|
*/
|
|
504
|
-
const
|
|
559
|
+
const topLevelBindings = (
|
|
505
560
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
>,
|
|
511
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
512
|
-
): void => {
|
|
513
|
-
const typeNameStr = node.getTypeName().getText();
|
|
514
|
-
|
|
515
|
-
const typeArguments = node.getTypeArguments();
|
|
561
|
+
statement: tsm.ts.Statement,
|
|
562
|
+
): readonly Readonly<{ name: string; isType: boolean }>[] => {
|
|
563
|
+
if (tsm.ts.isImportDeclaration(statement)) {
|
|
564
|
+
const clause = statement.importClause;
|
|
516
565
|
|
|
517
|
-
|
|
518
|
-
if (typeNameStr === 'Array' || typeNameStr === 'ReadonlyArray') {
|
|
519
|
-
if (!Arr.isFixedLengthArray(1, typeArguments)) {
|
|
520
|
-
throw new Error(
|
|
521
|
-
`Unexpected number of type arguments "${typeArguments.length}" for ${typeNameStr}.`,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
566
|
+
const bindings = clause?.namedBindings;
|
|
524
567
|
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
indexedAccessDepth: readonlyContext.indexedAccessDepth,
|
|
531
|
-
},
|
|
532
|
-
options,
|
|
533
|
-
);
|
|
568
|
+
// A namespace import cannot name a type.
|
|
569
|
+
const namespaceImport =
|
|
570
|
+
bindings !== undefined && tsm.ts.isNamespaceImport(bindings)
|
|
571
|
+
? ([{ name: bindings.name.text, isType: false }] as const)
|
|
572
|
+
: ([] as const);
|
|
534
573
|
|
|
535
|
-
|
|
536
|
-
|
|
574
|
+
const named =
|
|
575
|
+
bindings !== undefined && tsm.ts.isNamedImports(bindings)
|
|
576
|
+
? bindings.elements.map((element) => ({
|
|
577
|
+
name: element.name.text,
|
|
578
|
+
isType: true,
|
|
579
|
+
}))
|
|
580
|
+
: ([] as const);
|
|
537
581
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
: wrapWithParentheses(
|
|
543
|
-
`readonly ${wrapWithParentheses(T.getFullText())}[]`,
|
|
544
|
-
),
|
|
545
|
-
);
|
|
582
|
+
const defaultImport =
|
|
583
|
+
clause?.name === undefined
|
|
584
|
+
? ([] as const)
|
|
585
|
+
: ([{ name: clause.name.text, isType: true }] as const);
|
|
546
586
|
|
|
547
|
-
return;
|
|
587
|
+
return [...defaultImport, ...namespaceImport, ...named];
|
|
548
588
|
}
|
|
549
589
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
throw new Error(
|
|
554
|
-
`Unexpected number of type arguments "${typeArguments.length}" for Set.`,
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Recursive processing
|
|
559
|
-
transformNode(
|
|
560
|
-
typeArguments[0],
|
|
561
|
-
{
|
|
562
|
-
type: 'none',
|
|
563
|
-
indexedAccessDepth: readonlyContext.indexedAccessDepth,
|
|
564
|
-
},
|
|
565
|
-
options,
|
|
566
|
-
);
|
|
590
|
+
if (tsm.ts.isImportEqualsDeclaration(statement)) {
|
|
591
|
+
return [{ name: statement.name.text, isType: true }];
|
|
592
|
+
}
|
|
567
593
|
|
|
568
|
-
|
|
569
|
-
|
|
594
|
+
if (
|
|
595
|
+
tsm.ts.isTypeAliasDeclaration(statement) ||
|
|
596
|
+
tsm.ts.isInterfaceDeclaration(statement) ||
|
|
597
|
+
tsm.ts.isClassDeclaration(statement) ||
|
|
598
|
+
tsm.ts.isEnumDeclaration(statement)
|
|
599
|
+
) {
|
|
600
|
+
return statement.name === undefined
|
|
601
|
+
? []
|
|
602
|
+
: [{ name: statement.name.text, isType: true }];
|
|
603
|
+
}
|
|
570
604
|
|
|
571
|
-
|
|
605
|
+
if (
|
|
606
|
+
(tsm.ts.isFunctionDeclaration(statement) ||
|
|
607
|
+
tsm.ts.isModuleDeclaration(statement)) &&
|
|
608
|
+
statement.name !== undefined &&
|
|
609
|
+
tsm.ts.isIdentifier(statement.name)
|
|
610
|
+
) {
|
|
611
|
+
return [{ name: statement.name.text, isType: false }];
|
|
612
|
+
}
|
|
572
613
|
|
|
573
|
-
|
|
614
|
+
if (tsm.ts.isVariableStatement(statement)) {
|
|
615
|
+
return statement.declarationList.declarations.flatMap((declaration) =>
|
|
616
|
+
tsm.ts.isIdentifier(declaration.name)
|
|
617
|
+
? [{ name: declaration.name.text, isType: false }]
|
|
618
|
+
: [],
|
|
619
|
+
);
|
|
574
620
|
}
|
|
575
621
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
579
|
-
throw new Error(
|
|
580
|
-
`Unexpected number of type arguments "${typeArguments.length}" for Map.`,
|
|
581
|
-
);
|
|
582
|
-
}
|
|
622
|
+
return [];
|
|
623
|
+
};
|
|
583
624
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Whether the file has a type reference to `ReadonlyRecord` (an `extends` or
|
|
627
|
+
* `implements` clause included).
|
|
628
|
+
*/
|
|
629
|
+
const referencesReadonlyRecord = (node: tsm.ts.Node): boolean =>
|
|
630
|
+
(tsm.ts.isTypeReferenceNode(node) &&
|
|
631
|
+
tsm.ts.isIdentifier(node.typeName) &&
|
|
632
|
+
node.typeName.text === READONLY_RECORD_NAME) ||
|
|
633
|
+
(tsm.ts.isExpressionWithTypeArguments(node) &&
|
|
634
|
+
tsm.ts.isIdentifier(node.expression) &&
|
|
635
|
+
node.expression.text === READONLY_RECORD_NAME) ||
|
|
636
|
+
(tsm.ts.forEachChild(
|
|
637
|
+
node,
|
|
638
|
+
(child) => referencesReadonlyRecord(child) || undefined,
|
|
639
|
+
) ??
|
|
640
|
+
false);
|
|
589
641
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
642
|
+
/**
|
|
643
|
+
* Whether `ReadonlyRecord` can be written in the file as it is, with
|
|
644
|
+
* `importStyle: "named"`:
|
|
645
|
+
*
|
|
646
|
+
* - `"in-scope"`: the top level imports it or declares it as a type, or the
|
|
647
|
+
* file has a type reference to it, which without either relies on a global.
|
|
648
|
+
* - `"taken"`: the top level declares it as a value or a namespace only, which
|
|
649
|
+
* an import of the name could clash with (an exported one does).
|
|
650
|
+
* - `"free"`: neither; it can be imported.
|
|
651
|
+
*
|
|
652
|
+
* A qualified name (`tf.ReadonlyRecord`), a property name, the original name of
|
|
653
|
+
* an aliased import (`ReadonlyRecord as RR`) and a declaration in an inner
|
|
654
|
+
* scope bind nothing at the top level, so they do not count.
|
|
655
|
+
*/
|
|
656
|
+
const readonlyRecordScope = (
|
|
657
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
658
|
+
sourceFile: tsm.ts.SourceFile,
|
|
659
|
+
): 'free' | 'in-scope' | 'taken' => {
|
|
660
|
+
const topLevelNames = sourceFile.statements.flatMap(topLevelBindings);
|
|
661
|
+
|
|
662
|
+
return topLevelNames.some(
|
|
663
|
+
({ name, isType }) => isType && name === READONLY_RECORD_NAME,
|
|
664
|
+
) || referencesReadonlyRecord(sourceFile)
|
|
665
|
+
? 'in-scope'
|
|
666
|
+
: topLevelNames.some(({ name }) => name === READONLY_RECORD_NAME)
|
|
667
|
+
? 'taken'
|
|
668
|
+
: 'free';
|
|
669
|
+
};
|
|
594
670
|
|
|
595
|
-
|
|
671
|
+
/**
|
|
672
|
+
* `end`, moved past the comments that start on the same line after it: one such
|
|
673
|
+
* as `// eslint-disable-line` is about that line.
|
|
674
|
+
*/
|
|
675
|
+
const endOfLineComments = (text: string, end: number): number =>
|
|
676
|
+
tsm.ts.getTrailingCommentRanges(text, end)?.at(-1)?.end ?? end;
|
|
596
677
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
678
|
+
/**
|
|
679
|
+
* Where a new import goes in a file with no import and no directive prologue:
|
|
680
|
+
* before the first statement, and before the run of JSDoc and `//` comments
|
|
681
|
+
* right above it with no blank line in between (its documentation, an
|
|
682
|
+
* `eslint-disable-next-line`), which belong to it. A comment separated from it
|
|
683
|
+
* by a blank line, a `/* ... *\/` comment (a file-wide `eslint-disable`, a
|
|
684
|
+
* license), a `///` directive, a pragma (`@ts-nocheck`, `@jsxImportSource`),
|
|
685
|
+
* which TypeScript reads only above the first statement, and a shebang stay
|
|
686
|
+
* above the import.
|
|
687
|
+
*/
|
|
688
|
+
const newImportPosition = (
|
|
689
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
690
|
+
sourceFile: tsm.SourceFile,
|
|
691
|
+
): number => {
|
|
692
|
+
const text = sourceFile.getFullText();
|
|
602
693
|
|
|
603
|
-
|
|
694
|
+
const first = sourceFile.getStatements().at(0);
|
|
695
|
+
|
|
696
|
+
if (first === undefined) {
|
|
697
|
+
return text.length;
|
|
604
698
|
}
|
|
605
699
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
609
|
-
throw new Error(
|
|
610
|
-
`Unexpected number of type arguments "${typeArguments.length}" for Record.`,
|
|
611
|
-
);
|
|
612
|
-
}
|
|
700
|
+
const comments =
|
|
701
|
+
tsm.ts.getLeadingCommentRanges(text, first.getFullStart()) ?? [];
|
|
613
702
|
|
|
614
|
-
|
|
615
|
-
currentReadonlyContext: readonlyContext,
|
|
616
|
-
nextReadonlyContextType: 'none',
|
|
617
|
-
indexedAccessDepthChange: 'keep',
|
|
618
|
-
});
|
|
703
|
+
let mut_position = first.getStart();
|
|
619
704
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
transformNode(t, nextReadonlyContextValue, options);
|
|
623
|
-
}
|
|
705
|
+
for (const comment of comments.toReversed()) {
|
|
706
|
+
const commentText = text.slice(comment.pos, comment.end);
|
|
624
707
|
|
|
625
|
-
// The bare Record is kept as is in the following cases:
|
|
626
|
-
// - DeepReadonly<Record<K, V>> already applies readonly recursively.
|
|
627
|
-
// - Readonly<Record<K, V>> is unified to the configured record style by
|
|
628
|
-
// the surrounding `Readonly` handler.
|
|
629
|
-
// - Record<K, V>[I] does not need readonly, just like
|
|
630
|
-
// `Readonly<{ ... }>[I] -> { ... }[I]`.
|
|
631
708
|
if (
|
|
632
|
-
|
|
633
|
-
|
|
709
|
+
/\n[^\S\n]*\n/u.test(text.slice(comment.end, mut_position)) ||
|
|
710
|
+
commentText.startsWith('///') ||
|
|
711
|
+
(commentText.startsWith('/*') && !commentText.startsWith('/**')) ||
|
|
712
|
+
/@(?:ts-nocheck|ts-check|jsx)/u.test(commentText)
|
|
634
713
|
) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
options.replaceNode(
|
|
638
|
-
node,
|
|
639
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
640
|
-
readonlyRecordText(K!.getFullText(), V!.getFullText(), options),
|
|
641
|
-
);
|
|
714
|
+
break;
|
|
642
715
|
}
|
|
643
716
|
|
|
644
|
-
|
|
717
|
+
mut_position = comment.pos;
|
|
645
718
|
}
|
|
646
719
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
650
|
-
throw new Error(
|
|
651
|
-
`Unexpected number of type arguments "${typeArguments.length}" for ReadonlyRecord.`,
|
|
652
|
-
);
|
|
653
|
-
}
|
|
720
|
+
return mut_position;
|
|
721
|
+
};
|
|
654
722
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
723
|
+
/** The leading `'use client';`-like statements of the file. */
|
|
724
|
+
const directivePrologue = (
|
|
725
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
726
|
+
sourceFile: tsm.SourceFile,
|
|
727
|
+
): readonly tsm.ts.Statement[] => {
|
|
728
|
+
const statements = sourceFile.compilerNode.statements;
|
|
729
|
+
|
|
730
|
+
const end = statements.findIndex(
|
|
731
|
+
(statement) =>
|
|
732
|
+
!(
|
|
733
|
+
tsm.ts.isExpressionStatement(statement) &&
|
|
734
|
+
tsm.ts.isStringLiteral(statement.expression)
|
|
735
|
+
),
|
|
736
|
+
);
|
|
660
737
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
transformNode(t, nextReadonlyContextValue, options);
|
|
664
|
-
}
|
|
738
|
+
return statements.slice(0, end === -1 ? statements.length : end);
|
|
739
|
+
};
|
|
665
740
|
|
|
666
|
-
|
|
741
|
+
const insertReadonlyRecordImport = (
|
|
742
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
743
|
+
sourceFile: tsm.SourceFile,
|
|
744
|
+
text: string,
|
|
745
|
+
): void => {
|
|
746
|
+
const imports = sourceFile.getImportDeclarations();
|
|
667
747
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
748
|
+
const hosts = imports.filter(
|
|
749
|
+
(decl) =>
|
|
750
|
+
decl.getModuleSpecifierValue() === READONLY_RECORD_MODULE &&
|
|
751
|
+
tsm.Node.isNamedImports(decl.getImportClause()?.getNamedBindings()),
|
|
752
|
+
);
|
|
671
753
|
|
|
672
|
-
|
|
673
|
-
// (the surrounding `Readonly` handler then unifies it to the configured
|
|
674
|
-
// record style, so the bare Record never survives into the output)
|
|
675
|
-
if (
|
|
676
|
-
readonlyContext.type === 'Readonly' &&
|
|
677
|
-
readonlyContext.indexedAccessDepth === 0
|
|
678
|
-
) {
|
|
679
|
-
options.replaceNode(node, bareRecordText);
|
|
754
|
+
const host = hosts.find((decl) => decl.isTypeOnly()) ?? hosts.at(0);
|
|
680
755
|
|
|
681
|
-
|
|
682
|
-
}
|
|
756
|
+
if (host !== undefined) {
|
|
757
|
+
// `import type { ..., type X }` is an error: the declaration already makes
|
|
758
|
+
// every name type-only.
|
|
759
|
+
host.addNamedImport({
|
|
760
|
+
name: READONLY_RECORD_NAME,
|
|
761
|
+
isTypeOnly: !host.isTypeOnly(),
|
|
762
|
+
});
|
|
683
763
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// - DeepReadonly<ReadonlyRecord<K, V>> -> DeepReadonly<Record<K, V>>
|
|
687
|
-
// - ReadonlyRecord<K, V>[I] -> Record<K, V>[I]
|
|
688
|
-
// (readonly is unnecessary under indexed access, as with
|
|
689
|
-
// Readonly<{ ... }>[I] -> { ... }[I])
|
|
690
|
-
if (
|
|
691
|
-
readonlyContext.type !== 'none' ||
|
|
692
|
-
readonlyContext.indexedAccessDepth > 0
|
|
693
|
-
) {
|
|
694
|
-
options.replaceNode(node, bareRecordText);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
695
766
|
|
|
696
|
-
|
|
697
|
-
|
|
767
|
+
// `import type`, not `import { type X }`, which `verbatimModuleSyntax` keeps
|
|
768
|
+
// as `import {} from 'ts-type-forge'`, a module with nothing to run.
|
|
769
|
+
const declaration =
|
|
770
|
+
`import type { ${READONLY_RECORD_NAME} } from '${READONLY_RECORD_MODULE}';` as const;
|
|
698
771
|
|
|
699
|
-
|
|
772
|
+
const anchorEnd =
|
|
773
|
+
imports.at(-1)?.getEnd() ?? directivePrologue(sourceFile).at(-1)?.end;
|
|
700
774
|
|
|
701
|
-
|
|
702
|
-
}
|
|
775
|
+
if (anchorEnd === undefined) {
|
|
776
|
+
sourceFile.insertText(newImportPosition(sourceFile), `${declaration}\n\n`);
|
|
777
|
+
} else {
|
|
778
|
+
sourceFile.insertText(
|
|
779
|
+
endOfLineComments(text, anchorEnd),
|
|
780
|
+
`\n${declaration}`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
703
784
|
|
|
704
|
-
|
|
705
|
-
|
|
785
|
+
/**
|
|
786
|
+
* Imports `ReadonlyRecord` from `ts-type-forge`: into the braces of an existing
|
|
787
|
+
* import of the module where there is one, or else as a new declaration —
|
|
788
|
+
* after the last import, else after the directive prologue, else before the
|
|
789
|
+
* first statement and the comments attached to it (see
|
|
790
|
+
* {@link newImportPosition}). The line breaks follow the file's.
|
|
791
|
+
*/
|
|
792
|
+
const addReadonlyRecordImport = (
|
|
793
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
794
|
+
sourceFile: tsm.SourceFile,
|
|
795
|
+
): void => {
|
|
796
|
+
const text = sourceFile.getFullText();
|
|
797
|
+
|
|
798
|
+
// ts-morph writes its line breaks as the project's setting says, so the
|
|
799
|
+
// setting follows the file's for the edit.
|
|
800
|
+
const { manipulationSettings } = sourceFile.getProject();
|
|
801
|
+
|
|
802
|
+
const newLineKind = manipulationSettings.getNewLineKind();
|
|
803
|
+
|
|
804
|
+
manipulationSettings.set({
|
|
805
|
+
newLineKind: text.includes('\r\n')
|
|
806
|
+
? tsm.NewLineKind.CarriageReturnLineFeed
|
|
807
|
+
: tsm.NewLineKind.LineFeed,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
insertReadonlyRecordImport(sourceFile, text);
|
|
812
|
+
} finally {
|
|
813
|
+
manipulationSettings.set({ newLineKind });
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
//
|
|
818
|
+
// Transformer implementation for each node type
|
|
819
|
+
//
|
|
820
|
+
|
|
821
|
+
/** Renders a readonly `Record<K, V>` in the configured record style. */
|
|
822
|
+
const readonlyRecordText = (
|
|
823
|
+
keyText: string,
|
|
824
|
+
valueText: string,
|
|
825
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
826
|
+
): string =>
|
|
827
|
+
options.recordStyle === 'ReadonlyRecord'
|
|
828
|
+
? (`ReadonlyRecord<${keyText}, ${valueText}>` as const)
|
|
829
|
+
: (`Readonly<Record<${keyText}, ${valueText}>>` as const);
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* The comments in `[from, to)` of a source file, each followed by what keeps
|
|
833
|
+
* the next token off it, and nothing else in that range — no whitespace, and no
|
|
834
|
+
* token such as a separator or a parenthesis.
|
|
835
|
+
*/
|
|
836
|
+
const commentsBetween = (
|
|
837
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
838
|
+
sourceFile: tsm.SourceFile,
|
|
839
|
+
from: number,
|
|
840
|
+
to: number,
|
|
841
|
+
): string => {
|
|
842
|
+
if (to <= from) {
|
|
843
|
+
return '';
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const scanner = tsm.ts.createScanner(
|
|
847
|
+
tsm.ts.ScriptTarget.Latest,
|
|
848
|
+
false,
|
|
849
|
+
tsm.ts.LanguageVariant.Standard,
|
|
850
|
+
sourceFile.getFullText(),
|
|
851
|
+
undefined,
|
|
852
|
+
from,
|
|
853
|
+
to - from,
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
const mut_comments: string[] = [];
|
|
857
|
+
|
|
858
|
+
while (scanner.scan() !== tsm.ts.SyntaxKind.EndOfFileToken) {
|
|
859
|
+
const token = scanner.getToken();
|
|
860
|
+
|
|
861
|
+
// A line comment runs to the end of the line, so it takes one along.
|
|
862
|
+
const separator =
|
|
863
|
+
token === tsm.ts.SyntaxKind.SingleLineCommentTrivia
|
|
864
|
+
? '\n'
|
|
865
|
+
: token === tsm.ts.SyntaxKind.MultiLineCommentTrivia
|
|
866
|
+
? ' '
|
|
867
|
+
: undefined;
|
|
868
|
+
|
|
869
|
+
if (separator !== undefined) {
|
|
870
|
+
mut_comments.push(`${scanner.getTokenText()}${separator}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return mut_comments.join('');
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
/** `DeepReadonly<T>` |-> `T`, anything else |-> `undefined` */
|
|
878
|
+
const getDeepReadonlyTypeArg = (
|
|
879
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
880
|
+
node: tsm.TypeNode,
|
|
881
|
+
deepReadonlyTypeName: string,
|
|
882
|
+
): tsm.TypeNode | undefined => {
|
|
883
|
+
if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const typeName = node.getTypeName();
|
|
888
|
+
|
|
889
|
+
const typeArguments = node.getTypeArguments();
|
|
890
|
+
|
|
891
|
+
return typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
892
|
+
typeName.getText() === deepReadonlyTypeName &&
|
|
893
|
+
Arr.isFixedLengthArray(1, typeArguments)
|
|
894
|
+
? typeArguments[0]
|
|
895
|
+
: undefined;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const checkIfPropertyNameShouldBeIgnored = (
|
|
899
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
900
|
+
nameNode: tsm.PropertyName,
|
|
901
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
902
|
+
): boolean => {
|
|
903
|
+
expectType<typeof nameNode, tsm.PropertyName>('=');
|
|
904
|
+
|
|
905
|
+
expectType<
|
|
906
|
+
tsm.PropertyName,
|
|
907
|
+
| tsm.NumericLiteral
|
|
908
|
+
| tsm.BigIntLiteral
|
|
909
|
+
| tsm.NoSubstitutionTemplateLiteral
|
|
910
|
+
| tsm.Identifier
|
|
911
|
+
| tsm.StringLiteral
|
|
912
|
+
| tsm.PrivateIdentifier
|
|
913
|
+
| tsm.ComputedPropertyName // [`mut_x`]: number[]
|
|
914
|
+
>('=');
|
|
915
|
+
|
|
916
|
+
return (
|
|
917
|
+
// mut_x: number[]
|
|
918
|
+
(nameNode.isKind(tsm.SyntaxKind.Identifier) &&
|
|
919
|
+
pipe(nameNode.getText()).map((nm) =>
|
|
920
|
+
options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
|
|
921
|
+
).value) ||
|
|
922
|
+
// "mut_x": number[]
|
|
923
|
+
(nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
|
|
924
|
+
pipe(nameNode.getLiteralValue()).map((nm) =>
|
|
925
|
+
options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
|
|
926
|
+
).value) ||
|
|
927
|
+
// #memberName: number[] (class only), matched with or after its `#`
|
|
928
|
+
(nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
|
|
929
|
+
pipe(nameNode.getText()).map((nm) =>
|
|
930
|
+
options.mutablePropertyPrefixes.some(
|
|
931
|
+
(p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
|
|
932
|
+
),
|
|
933
|
+
).value) ||
|
|
934
|
+
// ['mut_x']: number[], [`mut_x`]: number[]
|
|
935
|
+
(nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
|
|
936
|
+
pipe(nameNode.getExpression()).map((exp) => {
|
|
937
|
+
if (
|
|
938
|
+
exp.isKind(tsm.SyntaxKind.StringLiteral) ||
|
|
939
|
+
exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
940
|
+
) {
|
|
941
|
+
const nm = exp.getLiteralValue();
|
|
942
|
+
|
|
943
|
+
return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return false;
|
|
947
|
+
}).value)
|
|
948
|
+
);
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Whether a type literal is left without `Readonly<...>` and marked readonly
|
|
953
|
+
* member by member, because it has a property, a method or an accessor of an
|
|
954
|
+
* ignored prefix (`{ mut_x: X[] }`), which `transformMembers` leaves mutable
|
|
955
|
+
* and `Readonly<T>` would not.
|
|
956
|
+
*
|
|
957
|
+
* It keeps its `Readonly<...>` when that would leave another member mutable,
|
|
958
|
+
* since only a property or index signature can be marked `readonly` itself:
|
|
959
|
+
*
|
|
960
|
+
* - a method signature or an accessor, which `Readonly<T>` makes readonly too,
|
|
961
|
+
* unless its name has an ignored prefix as well;
|
|
962
|
+
* - a property or index signature the ignore comment skips (as
|
|
963
|
+
* `transformMembers` does), which gets no `readonly` of its own, unless it
|
|
964
|
+
* is `readonly` already.
|
|
965
|
+
*/
|
|
966
|
+
const isLeftUnwrapped = (
|
|
967
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
968
|
+
node: tsm.TypeLiteralNode,
|
|
969
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
970
|
+
): boolean => {
|
|
971
|
+
const members = node.getMembers();
|
|
972
|
+
|
|
973
|
+
const isMethodOrAccessor = (
|
|
974
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
975
|
+
mb: tsm.TypeElementTypes,
|
|
976
|
+
): mb is
|
|
977
|
+
| tsm.MethodSignature
|
|
978
|
+
| tsm.GetAccessorDeclaration
|
|
979
|
+
| tsm.SetAccessorDeclaration =>
|
|
980
|
+
mb.isKind(tsm.SyntaxKind.MethodSignature) ||
|
|
981
|
+
mb.isKind(tsm.SyntaxKind.GetAccessor) ||
|
|
982
|
+
mb.isKind(tsm.SyntaxKind.SetAccessor);
|
|
983
|
+
|
|
984
|
+
const isIgnoredMember = (
|
|
985
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
986
|
+
mb: tsm.TypeElementTypes,
|
|
987
|
+
): boolean =>
|
|
988
|
+
(mb.isKind(tsm.SyntaxKind.PropertySignature) || isMethodOrAccessor(mb)) &&
|
|
989
|
+
checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options);
|
|
990
|
+
|
|
991
|
+
const needsReadonlyWrapper = (
|
|
992
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
993
|
+
mb: tsm.TypeElementTypes,
|
|
994
|
+
): boolean =>
|
|
995
|
+
!isIgnoredMember(mb) &&
|
|
996
|
+
(isMethodOrAccessor(mb) ||
|
|
997
|
+
((mb.isKind(tsm.SyntaxKind.PropertySignature) ||
|
|
998
|
+
mb.isKind(tsm.SyntaxKind.IndexSignature)) &&
|
|
999
|
+
!mb.hasModifier(tsm.SyntaxKind.ReadonlyKeyword) &&
|
|
1000
|
+
hasDisableNextLineComment(mb)));
|
|
1001
|
+
|
|
1002
|
+
return members.some(isIgnoredMember) && !members.some(needsReadonlyWrapper);
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/** Whether `node`, or a parenthesized type around it, carries the ignore comment. */
|
|
1006
|
+
const hasDisableNextLineCommentAround = (node: tsm.Node): boolean => {
|
|
1007
|
+
if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const parent = node.getParent();
|
|
1012
|
+
|
|
1013
|
+
return (
|
|
1014
|
+
parent?.isKind(tsm.SyntaxKind.ParenthesizedType) === true &&
|
|
1015
|
+
hasDisableNextLineCommentAround(parent)
|
|
1016
|
+
);
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Whether a type literal that is already wrapped, or would be merged into the
|
|
1021
|
+
* `Readonly<...>` of a union or intersection, is taken out of it. See
|
|
1022
|
+
* `isLeftUnwrapped`; in addition, the ignore comment leaves the literal where
|
|
1023
|
+
* it is, and taking `Readonly` off one with a call or construct signature would
|
|
1024
|
+
* bring back the signature it dropped.
|
|
1025
|
+
*/
|
|
1026
|
+
const takesOffReadonly = (
|
|
1027
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1028
|
+
node: tsm.TypeLiteralNode,
|
|
1029
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
1030
|
+
): boolean =>
|
|
1031
|
+
isLeftUnwrapped(node, options) &&
|
|
1032
|
+
!hasCallOrConstructSignature(node) &&
|
|
1033
|
+
!hasDisableNextLineCommentAround(node);
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* - `tr(ReadonlyArray<E>) |-> readonly tr(E)[]`
|
|
1037
|
+
* - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
|
|
1038
|
+
* - `tr(DeepReadonly<Readonly<E>>) |-> DeepReadonly<tr(E)>`
|
|
1039
|
+
* - `tr(Readonly<number>) |-> number`
|
|
1040
|
+
* - `tr(Readonly<{ mut_x: X; y: Y }>) |-> { mut_x: X; readonly y: tr(Y) }` (see
|
|
1041
|
+
* `isLeftUnwrapped`)
|
|
1042
|
+
* - `tr(Readonly<E[]>) |-> readonly tr(E)[]`
|
|
1043
|
+
* - `tr(Readonly<[E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
|
|
1044
|
+
* - `tr(Readonly<readonly E[]>) |-> readonly tr(E)[]`
|
|
1045
|
+
* - `tr(Readonly<readonly [E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
|
|
1046
|
+
* - `tr(Readonly<A | readonly E[]>) |-> Readonly<tr(A)> | readonly tr(E)[]>`
|
|
1047
|
+
* - `tr(Record<K, V>) |-> Readonly<Record<tr(K), tr(V)>>` (or
|
|
1048
|
+
* `ReadonlyRecord<tr(K), tr(V)>`, depending on the `recordStyle` option;
|
|
1049
|
+
* `Readonly<Record<K, V>>`, `ReadonlyRecord<K, V>` and
|
|
1050
|
+
* `Readonly<ReadonlyRecord<K, V>>` are unified to the same style)
|
|
1051
|
+
*/
|
|
1052
|
+
const transformTypeReferenceNode = (
|
|
1053
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1054
|
+
node: tsm.TypeReferenceNode,
|
|
1055
|
+
readonlyContext: StrictExclude<
|
|
1056
|
+
ReadonlyContext,
|
|
1057
|
+
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1058
|
+
>,
|
|
1059
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
1060
|
+
): void => {
|
|
1061
|
+
const typeNameStr = node.getTypeName().getText();
|
|
1062
|
+
|
|
1063
|
+
const typeArguments = node.getTypeArguments();
|
|
1064
|
+
|
|
1065
|
+
// Array<T> / ReadonlyArray<T> to readonly T[]
|
|
1066
|
+
if (typeNameStr === 'Array' || typeNameStr === 'ReadonlyArray') {
|
|
1067
|
+
if (!Arr.isFixedLengthArray(1, typeArguments)) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`Unexpected number of type arguments "${typeArguments.length}" for ${typeNameStr}.`,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Recursive processing
|
|
1074
|
+
transformNode(
|
|
1075
|
+
typeArguments[0],
|
|
1076
|
+
{
|
|
1077
|
+
type: 'none',
|
|
1078
|
+
indexedAccessDepth: readonlyContext.indexedAccessDepth,
|
|
1079
|
+
},
|
|
1080
|
+
options,
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1084
|
+
const T = node.getTypeArguments()[0]!;
|
|
1085
|
+
|
|
1086
|
+
options.replaceNode(
|
|
1087
|
+
node,
|
|
1088
|
+
readonlyContext.type === 'DeepReadonly'
|
|
1089
|
+
? wrapWithParentheses(`${wrapWithParentheses(T.getFullText())}[]`)
|
|
1090
|
+
: wrapWithParentheses(
|
|
1091
|
+
`readonly ${wrapWithParentheses(T.getFullText())}[]`,
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Set<T> to ReadonlySet<T>
|
|
1099
|
+
if (typeNameStr === 'Set') {
|
|
1100
|
+
if (!Arr.isFixedLengthArray(1, typeArguments)) {
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`Unexpected number of type arguments "${typeArguments.length}" for Set.`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Recursive processing
|
|
1107
|
+
transformNode(
|
|
1108
|
+
typeArguments[0],
|
|
1109
|
+
{
|
|
1110
|
+
type: 'none',
|
|
1111
|
+
indexedAccessDepth: readonlyContext.indexedAccessDepth,
|
|
1112
|
+
},
|
|
1113
|
+
options,
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1117
|
+
const T = node.getTypeArguments()[0]!;
|
|
1118
|
+
|
|
1119
|
+
options.replaceNode(node, `ReadonlySet<${T.getFullText()}>`);
|
|
1120
|
+
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Map<T> to ReadonlyMap<T>
|
|
1125
|
+
if (typeNameStr === 'Map') {
|
|
1126
|
+
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
1127
|
+
throw new Error(
|
|
1128
|
+
`Unexpected number of type arguments "${typeArguments.length}" for Map.`,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const nextReadonlyContextValue = nextReadonlyContext({
|
|
1133
|
+
currentReadonlyContext: readonlyContext,
|
|
1134
|
+
nextReadonlyContextType: 'none',
|
|
1135
|
+
indexedAccessDepthChange: 'keep',
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// Recursive processing
|
|
1139
|
+
for (const t of typeArguments) {
|
|
1140
|
+
transformNode(t, nextReadonlyContextValue, options);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const [K, V] = node.getTypeArguments();
|
|
1144
|
+
|
|
1145
|
+
options.replaceNode(
|
|
1146
|
+
node,
|
|
1147
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1148
|
+
`ReadonlyMap<${K!.getFullText()}, ${V!.getFullText()}>`,
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Record<K, V> to ReadonlyRecord<K, V> / Readonly<Record<K, V>>
|
|
1155
|
+
if (typeNameStr === 'Record') {
|
|
1156
|
+
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`Unexpected number of type arguments "${typeArguments.length}" for Record.`,
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const nextReadonlyContextValue = nextReadonlyContext({
|
|
1163
|
+
currentReadonlyContext: readonlyContext,
|
|
1164
|
+
nextReadonlyContextType: 'none',
|
|
1165
|
+
indexedAccessDepthChange: 'keep',
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
// Recursive processing
|
|
1169
|
+
for (const t of typeArguments) {
|
|
1170
|
+
transformNode(t, nextReadonlyContextValue, options);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// The bare Record is kept as is in the following cases:
|
|
1174
|
+
// - DeepReadonly<Record<K, V>> already applies readonly recursively.
|
|
1175
|
+
// - Readonly<Record<K, V>> is unified to the configured record style by
|
|
1176
|
+
// the surrounding `Readonly` handler.
|
|
1177
|
+
// - Record<K, V>[I] does not need readonly, just like
|
|
1178
|
+
// `Readonly<{ ... }>[I] -> { ... }[I]`.
|
|
1179
|
+
if (
|
|
1180
|
+
readonlyContext.type === 'none' &&
|
|
1181
|
+
readonlyContext.indexedAccessDepth === 0
|
|
1182
|
+
) {
|
|
1183
|
+
const [K, V] = node.getTypeArguments();
|
|
1184
|
+
|
|
1185
|
+
options.replaceNode(
|
|
1186
|
+
node,
|
|
1187
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1188
|
+
readonlyRecordText(K!.getFullText(), V!.getFullText(), options),
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Unify ReadonlyRecord<K, V> to the configured record style
|
|
1196
|
+
if (typeNameStr === 'ReadonlyRecord') {
|
|
1197
|
+
if (!Arr.isFixedLengthArray(2, typeArguments)) {
|
|
1198
|
+
throw new Error(
|
|
1199
|
+
`Unexpected number of type arguments "${typeArguments.length}" for ReadonlyRecord.`,
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const nextReadonlyContextValue = nextReadonlyContext({
|
|
1204
|
+
currentReadonlyContext: readonlyContext,
|
|
1205
|
+
nextReadonlyContextType: 'none',
|
|
1206
|
+
indexedAccessDepthChange: 'keep',
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// Recursive processing
|
|
1210
|
+
for (const t of typeArguments) {
|
|
1211
|
+
transformNode(t, nextReadonlyContextValue, options);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const [K, V] = node.getTypeArguments();
|
|
1215
|
+
|
|
1216
|
+
const bareRecordText =
|
|
1217
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1218
|
+
`Record<${K!.getFullText()}, ${V!.getFullText()}>` as const;
|
|
1219
|
+
|
|
1220
|
+
// Readonly<ReadonlyRecord<K, V>> -> Readonly<Record<K, V>>
|
|
1221
|
+
// (the surrounding `Readonly` handler then unifies it to the configured
|
|
1222
|
+
// record style, so the bare Record never survives into the output)
|
|
1223
|
+
if (
|
|
1224
|
+
readonlyContext.type === 'Readonly' &&
|
|
1225
|
+
readonlyContext.indexedAccessDepth === 0
|
|
1226
|
+
) {
|
|
1227
|
+
options.replaceNode(node, bareRecordText);
|
|
1228
|
+
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (options.recordStyle === 'Readonly<Record>') {
|
|
1233
|
+
// The ReadonlyRecord name is eliminated everywhere:
|
|
1234
|
+
// - DeepReadonly<ReadonlyRecord<K, V>> -> DeepReadonly<Record<K, V>>
|
|
1235
|
+
// - ReadonlyRecord<K, V>[I] -> Record<K, V>[I]
|
|
1236
|
+
// (readonly is unnecessary under indexed access, as with
|
|
1237
|
+
// Readonly<{ ... }>[I] -> { ... }[I])
|
|
1238
|
+
if (
|
|
1239
|
+
readonlyContext.type !== 'none' ||
|
|
1240
|
+
readonlyContext.indexedAccessDepth > 0
|
|
1241
|
+
) {
|
|
1242
|
+
options.replaceNode(node, bareRecordText);
|
|
1243
|
+
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
options.replaceNode(node, `Readonly<${bareRecordText}>`);
|
|
1248
|
+
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// With `recordStyle: "ReadonlyRecord"`, ReadonlyRecord is already the
|
|
1253
|
+
// configured style. It is deliberately kept even under DeepReadonly or an
|
|
706
1254
|
// indexed access — stripping it to the bare built-in `Record` there would
|
|
707
1255
|
// conflict with the `ts-type-forge/prefer-readonly-or-mutable-record`
|
|
708
1256
|
// ESLint rule.
|
|
@@ -786,9 +1334,9 @@ const transformTypeReferenceNode = (
|
|
|
786
1334
|
takesOffWrapper ||
|
|
787
1335
|
(T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
|
|
788
1336
|
) {
|
|
789
|
-
// The comments after the literal, which nothing else carries over, go
|
|
790
|
-
//
|
|
791
|
-
//
|
|
1337
|
+
// The comments after the literal, which nothing else carries over, go in
|
|
1338
|
+
// front of it: a line comment after it would end the type with a line
|
|
1339
|
+
// break, which a following `[]` cannot be parsed across.
|
|
792
1340
|
const commentsAfterT = commentsBetween(
|
|
793
1341
|
node.getSourceFile(),
|
|
794
1342
|
T.getEnd(),
|
|
@@ -985,16 +1533,6 @@ const transformTypeReferenceNode = (
|
|
|
985
1533
|
}
|
|
986
1534
|
};
|
|
987
1535
|
|
|
988
|
-
/** Renders a readonly `Record<K, V>` in the configured record style. */
|
|
989
|
-
const readonlyRecordText = (
|
|
990
|
-
keyText: string,
|
|
991
|
-
valueText: string,
|
|
992
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
993
|
-
): string =>
|
|
994
|
-
options.recordStyle === 'ReadonlyRecord'
|
|
995
|
-
? (`ReadonlyRecord<${keyText}, ${valueText}>` as const)
|
|
996
|
-
: (`Readonly<Record<${keyText}, ${valueText}>>` as const);
|
|
997
|
-
|
|
998
1536
|
/** `tr(E[]) |-> tr(E)[]` */
|
|
999
1537
|
const transformArrayTypeNode = (
|
|
1000
1538
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1445,221 +1983,94 @@ const transformConstructorParameter = (
|
|
|
1445
1983
|
if (type !== undefined) {
|
|
1446
1984
|
transformNode(type, initialReadonlyContext, options);
|
|
1447
1985
|
}
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
|
-
/**
|
|
1451
|
-
* - `tr(A & B) -> tr(A) & tr(B)`
|
|
1452
|
-
* - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`, when `A` and `B`
|
|
1453
|
-
* are type literals (see `isMergeableReadonlyTypeReferenceNode`)
|
|
1454
|
-
*/
|
|
1455
|
-
const transformIntersectionTypeNode = (
|
|
1456
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1457
|
-
node: tsm.IntersectionTypeNode,
|
|
1458
|
-
readonlyContext: StrictExclude<
|
|
1459
|
-
ReadonlyContext,
|
|
1460
|
-
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1461
|
-
>,
|
|
1462
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
1463
|
-
): void => {
|
|
1464
|
-
transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '&');
|
|
1465
|
-
};
|
|
1466
|
-
|
|
1467
|
-
/**
|
|
1468
|
-
* - `tr(A | B) |-> tr(A) | tr(B)`
|
|
1469
|
-
* - `tr(Readonly<A> | Readonly<B>) |-> Readonly<tr(A) | tr(B)>`
|
|
1470
|
-
* - `tr(DeepReadonly<A> | DeepReadonly<B>) |-> DeepReadonly<tr(A) | tr(B)>`
|
|
1471
|
-
*/
|
|
1472
|
-
const transformUnionTypeNode = (
|
|
1473
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1474
|
-
node: tsm.UnionTypeNode,
|
|
1475
|
-
readonlyContext: StrictExclude<
|
|
1476
|
-
ReadonlyContext,
|
|
1477
|
-
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1478
|
-
>,
|
|
1479
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
1480
|
-
): void => {
|
|
1481
|
-
transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
|
|
1482
|
-
};
|
|
1483
|
-
|
|
1484
|
-
const transformUnionOrIntersectionTypeNodeImpl = (
|
|
1485
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1486
|
-
node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
|
|
1487
|
-
readonlyContext: StrictExclude<
|
|
1488
|
-
ReadonlyContext,
|
|
1489
|
-
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1490
|
-
>,
|
|
1491
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
1492
|
-
operator: '&' | '|',
|
|
1493
|
-
): void => {
|
|
1494
|
-
const nextReadonlyContextValue = nextReadonlyContext({
|
|
1495
|
-
currentReadonlyContext: readonlyContext,
|
|
1496
|
-
nextReadonlyContextType: 'none',
|
|
1497
|
-
indexedAccessDepthChange: 'keep',
|
|
1498
|
-
});
|
|
1499
|
-
|
|
1500
|
-
// Recursive processing
|
|
1501
|
-
for (const n of node.getTypeNodes() /* = [A, B] */) {
|
|
1502
|
-
transformNode(n, nextReadonlyContextValue, options);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
const newTypes = node.getTypeNodes();
|
|
1506
|
-
|
|
1507
|
-
const withLeadingComments = (
|
|
1508
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1509
|
-
member: tsm.TypeNode,
|
|
1510
|
-
memberText: string,
|
|
1511
|
-
): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
|
|
1512
|
-
|
|
1513
|
-
// Whether a type literal member is wrapped at all; under `DeepReadonly` or an
|
|
1514
|
-
// indexed access none is.
|
|
1515
|
-
const wrapsTypeLiterals =
|
|
1516
|
-
nextReadonlyContextValue.type !== 'DeepReadonly' &&
|
|
1517
|
-
nextReadonlyContextValue.indexedAccessDepth === 0;
|
|
1518
|
-
|
|
1519
|
-
// `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the
|
|
1520
|
-
// handler of `Readonly` then takes off as redundant.
|
|
1521
|
-
const underReadonly =
|
|
1522
|
-
readonlyContext.type === 'Readonly' &&
|
|
1523
|
-
readonlyContext.indexedAccessDepth === 0 &&
|
|
1524
|
-
isTypeArgumentOfReadonly(node);
|
|
1525
|
-
|
|
1526
|
-
const { primitives, arraysAndTuples, typeLiterals, others } =
|
|
1527
|
-
groupUnionIntersectionTypes(newTypes, {
|
|
1528
|
-
operator,
|
|
1529
|
-
underReadonly,
|
|
1530
|
-
deepReadonlyTypeName: options.DeepReadonly.typeName,
|
|
1531
|
-
keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
|
|
1535
|
-
|
|
1536
|
-
// Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
|
|
1537
|
-
const isEmptyTypeLiteral = (
|
|
1538
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1539
|
-
n: tsm.TypeNode,
|
|
1540
|
-
): boolean =>
|
|
1541
|
-
n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
|
|
1542
|
-
|
|
1543
|
-
const nonEmptyTypeLiterals =
|
|
1544
|
-
typeLiterals === undefined
|
|
1545
|
-
? undefined
|
|
1546
|
-
: options.ignoreEmptyObjectTypes
|
|
1547
|
-
? typeLiterals.nodes.filter((n) => !isEmptyTypeLiteral(n))
|
|
1548
|
-
: typeLiterals.nodes;
|
|
1549
|
-
|
|
1550
|
-
const emptyTypeLiterals =
|
|
1551
|
-
typeLiterals === undefined || !options.ignoreEmptyObjectTypes
|
|
1552
|
-
? undefined
|
|
1553
|
-
: typeLiterals.nodes.filter(isEmptyTypeLiteral);
|
|
1554
|
-
|
|
1555
|
-
const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
|
|
1556
|
-
nonEmptyTypeLiterals === undefined || Arr.isEmpty(nonEmptyTypeLiterals)
|
|
1557
|
-
? ([] as const)
|
|
1558
|
-
: ([
|
|
1559
|
-
unionToString({
|
|
1560
|
-
types: nonEmptyTypeLiterals.map((n) =>
|
|
1561
|
-
withLeadingComments(
|
|
1562
|
-
n,
|
|
1563
|
-
isMergeableReadonlyTypeReferenceNode(n, operator)
|
|
1564
|
-
? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
|
|
1565
|
-
// NOTE: Readonly<A | B> -> A | B (spliced into `|`)
|
|
1566
|
-
readonlyTypeArgTextSplicedInto(n, operator)
|
|
1567
|
-
: n.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
1568
|
-
? n.getText()
|
|
1569
|
-
: // A member kept under `Readonly`; see `underReadonly`
|
|
1570
|
-
wrapWithParentheses(n.getText()),
|
|
1571
|
-
),
|
|
1572
|
-
),
|
|
1573
|
-
op: operator,
|
|
1574
|
-
wrapWithReadonly: wrapsTypeLiterals,
|
|
1575
|
-
}),
|
|
1576
|
-
] as const);
|
|
1577
|
-
|
|
1578
|
-
// Sort by first occurrence (preserving the original union order as much as possible)
|
|
1579
|
-
const sorted: readonly string[] = [
|
|
1580
|
-
mapNullable(
|
|
1581
|
-
primitives,
|
|
1582
|
-
(a) =>
|
|
1583
|
-
[
|
|
1584
|
-
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
1585
|
-
a.firstPosition,
|
|
1586
|
-
] as const,
|
|
1587
|
-
),
|
|
1588
|
-
mapNullable(
|
|
1589
|
-
arraysAndTuples,
|
|
1590
|
-
(a) =>
|
|
1591
|
-
[
|
|
1592
|
-
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
1593
|
-
a.firstPosition,
|
|
1594
|
-
] as const,
|
|
1595
|
-
),
|
|
1596
|
-
nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
1597
|
-
? mapNullable(
|
|
1598
|
-
typeLiterals,
|
|
1599
|
-
(a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
|
|
1600
|
-
)
|
|
1601
|
-
: undefined,
|
|
1602
|
-
emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
|
|
1603
|
-
? mapNullable(
|
|
1604
|
-
typeLiterals,
|
|
1605
|
-
(a) =>
|
|
1606
|
-
[
|
|
1607
|
-
emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
|
|
1608
|
-
// Use a position after non-empty type literals if they exist
|
|
1609
|
-
a.firstPosition +
|
|
1610
|
-
(nonEmptyTypeLiterals !== undefined &&
|
|
1611
|
-
Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
1612
|
-
? 0.5
|
|
1613
|
-
: 0),
|
|
1614
|
-
] as const,
|
|
1615
|
-
)
|
|
1616
|
-
: undefined,
|
|
1617
|
-
mapNullable(
|
|
1618
|
-
others,
|
|
1619
|
-
(a) =>
|
|
1620
|
-
[
|
|
1621
|
-
// `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
|
|
1622
|
-
// above. See `mergeDeepReadonlyUnionMembers` for why.
|
|
1623
|
-
operator === '|'
|
|
1624
|
-
? mergeDeepReadonlyUnionMembers(
|
|
1625
|
-
a.nodes,
|
|
1626
|
-
options.DeepReadonly.typeName,
|
|
1627
|
-
withLeadingComments,
|
|
1628
|
-
)
|
|
1629
|
-
: a.nodes.map((n) =>
|
|
1630
|
-
withLeadingComments(n, wrapWithParentheses(n.getText())),
|
|
1631
|
-
),
|
|
1632
|
-
a.firstPosition,
|
|
1633
|
-
] as const,
|
|
1634
|
-
),
|
|
1635
|
-
]
|
|
1636
|
-
.filter((x) => x !== undefined)
|
|
1637
|
-
.toSorted((a, b) => a[1] - b[1])
|
|
1638
|
-
.flatMap((a) => a[0]);
|
|
1986
|
+
};
|
|
1639
1987
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* - `tr(A & B) -> tr(A) & tr(B)`
|
|
1990
|
+
* - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`, when `A` and `B`
|
|
1991
|
+
* are type literals (see `isMergeableReadonlyTypeReferenceNode`)
|
|
1992
|
+
*/
|
|
1993
|
+
const transformIntersectionTypeNode = (
|
|
1994
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1995
|
+
node: tsm.IntersectionTypeNode,
|
|
1996
|
+
readonlyContext: StrictExclude<
|
|
1997
|
+
ReadonlyContext,
|
|
1998
|
+
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1999
|
+
>,
|
|
2000
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
2001
|
+
): void => {
|
|
2002
|
+
transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '&');
|
|
2003
|
+
};
|
|
1642
2004
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2005
|
+
/**
|
|
2006
|
+
* - `tr(A | B) |-> tr(A) | tr(B)`
|
|
2007
|
+
* - `tr(Readonly<A> | Readonly<B>) |-> Readonly<tr(A) | tr(B)>`
|
|
2008
|
+
* - `tr(DeepReadonly<A> | DeepReadonly<B>) |-> DeepReadonly<tr(A) | tr(B)>`
|
|
2009
|
+
*/
|
|
2010
|
+
const transformUnionTypeNode = (
|
|
2011
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2012
|
+
node: tsm.UnionTypeNode,
|
|
2013
|
+
readonlyContext: StrictExclude<
|
|
2014
|
+
ReadonlyContext,
|
|
2015
|
+
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
2016
|
+
>,
|
|
2017
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
2018
|
+
): void => {
|
|
2019
|
+
transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
|
|
2020
|
+
};
|
|
1647
2021
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
//
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
);
|
|
2022
|
+
/**
|
|
2023
|
+
* The text of `inner`, a type `removeParentheses` took out of `outer`, with the
|
|
2024
|
+
* comments in `outer`'s full text in front of it — before the parentheses and
|
|
2025
|
+
* inside them — and without the parentheses.
|
|
2026
|
+
*
|
|
2027
|
+
* `inner.getFullText()` would drop those before the parentheses, which are as
|
|
2028
|
+
* much a part of `outer` as the rest of it.
|
|
2029
|
+
*/
|
|
2030
|
+
const textWithoutParentheses = (
|
|
2031
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2032
|
+
outer: tsm.TypeNode,
|
|
2033
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2034
|
+
inner: tsm.TypeNode,
|
|
2035
|
+
): string =>
|
|
2036
|
+
`${commentsBetween(outer.getSourceFile(), outer.getPos(), inner.getStart())}${inner.getText()}` as const;
|
|
2037
|
+
|
|
2038
|
+
const unionToStringImpl = ({
|
|
2039
|
+
types,
|
|
2040
|
+
op,
|
|
2041
|
+
wrapWithReadonly,
|
|
2042
|
+
}: Readonly<{
|
|
2043
|
+
types: readonly string[];
|
|
2044
|
+
op: '&' | '|';
|
|
2045
|
+
wrapWithReadonly: boolean | string;
|
|
2046
|
+
}>): string =>
|
|
2047
|
+
Arr.isEmpty(types)
|
|
2048
|
+
? 'never'
|
|
2049
|
+
: Arr.isFixedLengthArray(1, types)
|
|
2050
|
+
? wrapWithReadonly === false
|
|
2051
|
+
? wrapWithParentheses(types[0])
|
|
2052
|
+
: (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>` as const)
|
|
2053
|
+
: wrapWithReadonly === false
|
|
2054
|
+
? wrapWithParentheses(types.join(` ${op} `))
|
|
2055
|
+
: (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>` as const);
|
|
2056
|
+
|
|
2057
|
+
const unionToString = ({
|
|
2058
|
+
types: typesRaw,
|
|
2059
|
+
op,
|
|
2060
|
+
wrapWithReadonly,
|
|
2061
|
+
}: Readonly<{
|
|
2062
|
+
types: readonly string[];
|
|
2063
|
+
op: '&' | '|';
|
|
2064
|
+
wrapWithReadonly: boolean | string;
|
|
2065
|
+
}>): string => {
|
|
2066
|
+
// The members are joined with a separator that supplies its own space, so
|
|
2067
|
+
// whitespace left at a member's edge would accumulate: every run of the
|
|
2068
|
+
// transformer would add one more space before every member but the first, and
|
|
2069
|
+
// a file would never reach a fixed point. Trimming keeps the comments in
|
|
2070
|
+
// front of a member, which are not whitespace.
|
|
2071
|
+
const types = typesRaw.map((s) => s.trim());
|
|
2072
|
+
|
|
2073
|
+
return unionToStringImpl({ types, op, wrapWithReadonly });
|
|
1663
2074
|
};
|
|
1664
2075
|
|
|
1665
2076
|
/**
|
|
@@ -1775,10 +2186,10 @@ const isTypeArgumentOfReadonly = (
|
|
|
1775
2186
|
* `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
|
|
1776
2187
|
* in front of that stays in the file by itself. With no leading `|` / `&`, the
|
|
1777
2188
|
* first member starts there too, and its comments are the type's: repeating
|
|
1778
|
-
* them in the replacement would copy them once per run. Every other member
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
2189
|
+
* them in the replacement would copy them once per run. Every other member has
|
|
2190
|
+
* a separator in front of it, and a comment before the separator belongs to no
|
|
2191
|
+
* member's full text, so it is gone once the separator is rewritten. It goes
|
|
2192
|
+
* with the member after it, like the comments after the separator.
|
|
1782
2193
|
*
|
|
1783
2194
|
* `member` may be the type inside a member's parentheses; the comments inside
|
|
1784
2195
|
* them are collected too, and the parentheses are not.
|
|
@@ -1802,9 +2213,9 @@ const leadingCommentsOfMember = (
|
|
|
1802
2213
|
* `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
|
|
1803
2214
|
* intersection (`&`) of the members it is merged with.
|
|
1804
2215
|
*
|
|
1805
|
-
* `T` is wrapped in parentheses for precedence safety (a function type
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
2216
|
+
* `T` is wrapped in parentheses for precedence safety (a function type argument
|
|
2217
|
+
* needs them), unless it is a union spliced into a union or an intersection
|
|
2218
|
+
* spliced into an intersection, which needs none.
|
|
1808
2219
|
*/
|
|
1809
2220
|
const readonlyTypeArgTextSplicedInto = (
|
|
1810
2221
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1824,127 +2235,187 @@ const readonlyTypeArgTextSplicedInto = (
|
|
|
1824
2235
|
: unwrapReadonlyTypeArgText(node);
|
|
1825
2236
|
};
|
|
1826
2237
|
|
|
1827
|
-
|
|
1828
|
-
* The text of `inner`, a type `removeParentheses` took out of `outer`, with
|
|
1829
|
-
* the comments in `outer`'s full text in front of it — before the parentheses
|
|
1830
|
-
* and inside them — and without the parentheses.
|
|
1831
|
-
*
|
|
1832
|
-
* `inner.getFullText()` would drop those before the parentheses, which are
|
|
1833
|
-
* as much a part of `outer` as the rest of it.
|
|
1834
|
-
*/
|
|
1835
|
-
const textWithoutParentheses = (
|
|
1836
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1837
|
-
outer: tsm.TypeNode,
|
|
2238
|
+
const transformUnionOrIntersectionTypeNodeImpl = (
|
|
1838
2239
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2240
|
+
node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
|
|
2241
|
+
readonlyContext: StrictExclude<
|
|
2242
|
+
ReadonlyContext,
|
|
2243
|
+
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
2244
|
+
>,
|
|
2245
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
2246
|
+
operator: '&' | '|',
|
|
2247
|
+
): void => {
|
|
2248
|
+
const nextReadonlyContextValue = nextReadonlyContext({
|
|
2249
|
+
currentReadonlyContext: readonlyContext,
|
|
2250
|
+
nextReadonlyContextType: 'none',
|
|
2251
|
+
indexedAccessDepthChange: 'keep',
|
|
2252
|
+
});
|
|
1842
2253
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
* no token such as a separator or a parenthesis.
|
|
1847
|
-
*/
|
|
1848
|
-
const commentsBetween = (
|
|
1849
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1850
|
-
sourceFile: tsm.SourceFile,
|
|
1851
|
-
from: number,
|
|
1852
|
-
to: number,
|
|
1853
|
-
): string => {
|
|
1854
|
-
if (to <= from) {
|
|
1855
|
-
return '';
|
|
2254
|
+
// Recursive processing
|
|
2255
|
+
for (const n of node.getTypeNodes() /* = [A, B] */) {
|
|
2256
|
+
transformNode(n, nextReadonlyContextValue, options);
|
|
1856
2257
|
}
|
|
1857
2258
|
|
|
1858
|
-
const
|
|
1859
|
-
tsm.ts.ScriptTarget.Latest,
|
|
1860
|
-
false,
|
|
1861
|
-
tsm.ts.LanguageVariant.Standard,
|
|
1862
|
-
sourceFile.getFullText(),
|
|
1863
|
-
undefined,
|
|
1864
|
-
from,
|
|
1865
|
-
to - from,
|
|
1866
|
-
);
|
|
2259
|
+
const newTypes = node.getTypeNodes();
|
|
1867
2260
|
|
|
1868
|
-
const
|
|
2261
|
+
const withLeadingComments = (
|
|
2262
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2263
|
+
member: tsm.TypeNode,
|
|
2264
|
+
memberText: string,
|
|
2265
|
+
): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
|
|
1869
2266
|
|
|
1870
|
-
|
|
1871
|
-
|
|
2267
|
+
// Whether a type literal member is wrapped at all; under `DeepReadonly` or an
|
|
2268
|
+
// indexed access none is.
|
|
2269
|
+
const wrapsTypeLiterals =
|
|
2270
|
+
nextReadonlyContextValue.type !== 'DeepReadonly' &&
|
|
2271
|
+
nextReadonlyContextValue.indexedAccessDepth === 0;
|
|
1872
2272
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
: undefined;
|
|
2273
|
+
// `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the handler
|
|
2274
|
+
// of `Readonly` then takes off as redundant.
|
|
2275
|
+
const underReadonly =
|
|
2276
|
+
readonlyContext.type === 'Readonly' &&
|
|
2277
|
+
readonlyContext.indexedAccessDepth === 0 &&
|
|
2278
|
+
isTypeArgumentOfReadonly(node);
|
|
1880
2279
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2280
|
+
const { primitives, arraysAndTuples, typeLiterals, others } =
|
|
2281
|
+
groupUnionIntersectionTypes(newTypes, {
|
|
2282
|
+
operator,
|
|
2283
|
+
underReadonly,
|
|
2284
|
+
deepReadonlyTypeName: options.DeepReadonly.typeName,
|
|
2285
|
+
keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
|
|
2286
|
+
});
|
|
1885
2287
|
|
|
1886
|
-
|
|
1887
|
-
};
|
|
2288
|
+
options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
|
|
1888
2289
|
|
|
1889
|
-
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
1896
|
-
return undefined;
|
|
1897
|
-
}
|
|
2290
|
+
// Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
|
|
2291
|
+
const isEmptyTypeLiteral = (
|
|
2292
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2293
|
+
n: tsm.TypeNode,
|
|
2294
|
+
): boolean =>
|
|
2295
|
+
n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
|
|
1898
2296
|
|
|
1899
|
-
const
|
|
2297
|
+
const nonEmptyTypeLiterals =
|
|
2298
|
+
typeLiterals === undefined
|
|
2299
|
+
? undefined
|
|
2300
|
+
: options.ignoreEmptyObjectTypes
|
|
2301
|
+
? typeLiterals.nodes.filter((n) => !isEmptyTypeLiteral(n))
|
|
2302
|
+
: typeLiterals.nodes;
|
|
1900
2303
|
|
|
1901
|
-
const
|
|
2304
|
+
const emptyTypeLiterals =
|
|
2305
|
+
typeLiterals === undefined || !options.ignoreEmptyObjectTypes
|
|
2306
|
+
? undefined
|
|
2307
|
+
: typeLiterals.nodes.filter(isEmptyTypeLiteral);
|
|
2308
|
+
|
|
2309
|
+
const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
|
|
2310
|
+
nonEmptyTypeLiterals === undefined || Arr.isEmpty(nonEmptyTypeLiterals)
|
|
2311
|
+
? ([] as const)
|
|
2312
|
+
: ([
|
|
2313
|
+
unionToString({
|
|
2314
|
+
types: nonEmptyTypeLiterals.map((n) =>
|
|
2315
|
+
withLeadingComments(
|
|
2316
|
+
n,
|
|
2317
|
+
isMergeableReadonlyTypeReferenceNode(n, operator)
|
|
2318
|
+
? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
|
|
2319
|
+
// NOTE: Readonly<A | B> -> A | B (spliced into `|`)
|
|
2320
|
+
readonlyTypeArgTextSplicedInto(n, operator)
|
|
2321
|
+
: n.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
2322
|
+
? n.getText()
|
|
2323
|
+
: // A member kept under `Readonly`; see `underReadonly`
|
|
2324
|
+
wrapWithParentheses(n.getText()),
|
|
2325
|
+
),
|
|
2326
|
+
),
|
|
2327
|
+
op: operator,
|
|
2328
|
+
wrapWithReadonly: wrapsTypeLiterals,
|
|
2329
|
+
}),
|
|
2330
|
+
] as const);
|
|
2331
|
+
|
|
2332
|
+
// Sort by first occurrence (preserving the original union order as much as possible)
|
|
2333
|
+
const sorted: readonly string[] = [
|
|
2334
|
+
mapNullable(
|
|
2335
|
+
primitives,
|
|
2336
|
+
(a) =>
|
|
2337
|
+
[
|
|
2338
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
2339
|
+
a.firstPosition,
|
|
2340
|
+
] as const,
|
|
2341
|
+
),
|
|
2342
|
+
mapNullable(
|
|
2343
|
+
arraysAndTuples,
|
|
2344
|
+
(a) =>
|
|
2345
|
+
[
|
|
2346
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
2347
|
+
a.firstPosition,
|
|
2348
|
+
] as const,
|
|
2349
|
+
),
|
|
2350
|
+
nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
2351
|
+
? mapNullable(
|
|
2352
|
+
typeLiterals,
|
|
2353
|
+
(a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
|
|
2354
|
+
)
|
|
2355
|
+
: undefined,
|
|
2356
|
+
emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
|
|
2357
|
+
? mapNullable(
|
|
2358
|
+
typeLiterals,
|
|
2359
|
+
(a) =>
|
|
2360
|
+
[
|
|
2361
|
+
emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
|
|
2362
|
+
// Use a position after non-empty type literals if they exist
|
|
2363
|
+
a.firstPosition +
|
|
2364
|
+
(nonEmptyTypeLiterals !== undefined &&
|
|
2365
|
+
Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
2366
|
+
? 0.5
|
|
2367
|
+
: 0),
|
|
2368
|
+
] as const,
|
|
2369
|
+
)
|
|
2370
|
+
: undefined,
|
|
2371
|
+
mapNullable(
|
|
2372
|
+
others,
|
|
2373
|
+
(a) =>
|
|
2374
|
+
[
|
|
2375
|
+
// `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
|
|
2376
|
+
// above. See `mergeDeepReadonlyUnionMembers` for why.
|
|
2377
|
+
operator === '|'
|
|
2378
|
+
? mergeDeepReadonlyUnionMembers(
|
|
2379
|
+
a.nodes,
|
|
2380
|
+
options.DeepReadonly.typeName,
|
|
2381
|
+
withLeadingComments,
|
|
2382
|
+
)
|
|
2383
|
+
: a.nodes.map((n) =>
|
|
2384
|
+
withLeadingComments(n, wrapWithParentheses(n.getText())),
|
|
2385
|
+
),
|
|
2386
|
+
a.firstPosition,
|
|
2387
|
+
] as const,
|
|
2388
|
+
),
|
|
2389
|
+
]
|
|
2390
|
+
.filter((x) => x !== undefined)
|
|
2391
|
+
.toSorted((a, b) => a[1] - b[1])
|
|
2392
|
+
.flatMap((a) => a[0]);
|
|
1902
2393
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
Arr.isFixedLengthArray(1, typeArguments)
|
|
1906
|
-
? typeArguments[0]
|
|
1907
|
-
: undefined;
|
|
1908
|
-
};
|
|
2394
|
+
// Readonly<number & { x: X } & { y: Y } & readonly E[]>
|
|
2395
|
+
// -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
|
|
1909
2396
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}: Readonly<{
|
|
1915
|
-
types: readonly string[];
|
|
1916
|
-
op: '&' | '|';
|
|
1917
|
-
wrapWithReadonly: boolean | string;
|
|
1918
|
-
}>): string => {
|
|
1919
|
-
// The members are joined with a separator that supplies its own space, so
|
|
1920
|
-
// whitespace left at a member's edge would accumulate: every run of the
|
|
1921
|
-
// transformer would add one more space before every member but the first,
|
|
1922
|
-
// and a file would never reach a fixed point. Trimming keeps the comments in
|
|
1923
|
-
// front of a member, which are not whitespace.
|
|
1924
|
-
const types = typesRaw.map((s) => s.trim());
|
|
2397
|
+
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
2398
|
+
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
2399
|
+
// Readonly<number & { x: X } & { y: Y } & readonly E[]>
|
|
2400
|
+
// -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
|
|
1925
2401
|
|
|
1926
|
-
|
|
2402
|
+
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
2403
|
+
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
2404
|
+
//
|
|
2405
|
+
// NOTE: The result is wrapped in parentheses (via `unionToString`). This is
|
|
2406
|
+
// intentional and required for precedence safety: without it, a member such
|
|
2407
|
+
// as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
|
|
2408
|
+
// operator would produce invalid syntax when this node is spliced back in.
|
|
2409
|
+
// Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
|
|
2410
|
+
// collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
|
|
2411
|
+
// — see the note there. That prevents the parentheses from growing without
|
|
2412
|
+
// bound when a union/intersection sits inside a `ParenthesizedType`.
|
|
2413
|
+
options.replaceNode(
|
|
2414
|
+
node,
|
|
2415
|
+
unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
|
|
2416
|
+
);
|
|
1927
2417
|
};
|
|
1928
2418
|
|
|
1929
|
-
const unionToStringImpl = ({
|
|
1930
|
-
types,
|
|
1931
|
-
op,
|
|
1932
|
-
wrapWithReadonly,
|
|
1933
|
-
}: Readonly<{
|
|
1934
|
-
types: readonly string[];
|
|
1935
|
-
op: '&' | '|';
|
|
1936
|
-
wrapWithReadonly: boolean | string;
|
|
1937
|
-
}>): string =>
|
|
1938
|
-
Arr.isEmpty(types)
|
|
1939
|
-
? 'never'
|
|
1940
|
-
: Arr.isFixedLengthArray(1, types)
|
|
1941
|
-
? wrapWithReadonly === false
|
|
1942
|
-
? wrapWithParentheses(types[0])
|
|
1943
|
-
: (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>` as const)
|
|
1944
|
-
: wrapWithReadonly === false
|
|
1945
|
-
? wrapWithParentheses(types.join(` ${op} `))
|
|
1946
|
-
: (`${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>` as const);
|
|
1947
|
-
|
|
1948
2419
|
/** Convert ((T)) -> (T) recursively */
|
|
1949
2420
|
const transformParenthesizedTypeNode = (
|
|
1950
2421
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1964,20 +2435,16 @@ const transformParenthesizedTypeNode = (
|
|
|
1964
2435
|
const T = node.getTypeNode();
|
|
1965
2436
|
|
|
1966
2437
|
if (
|
|
1967
|
-
// remove () if T is TypeReferenceNode
|
|
1968
|
-
// e.g. `(Readonly<A>) |-> Readonly<A>`
|
|
2438
|
+
// remove () if T is TypeReferenceNode e.g. `(Readonly<A>) |-> Readonly<A>`
|
|
1969
2439
|
T.isKind(tsm.SyntaxKind.TypeReference) ||
|
|
1970
|
-
// remove () if T is ArrayTypeNode
|
|
1971
|
-
// e.g. `(A[]) |-> A[]`
|
|
2440
|
+
// remove () if T is ArrayTypeNode e.g. `(A[]) |-> A[]`
|
|
1972
2441
|
T.isKind(tsm.SyntaxKind.ArrayType) ||
|
|
1973
|
-
// remove () if T is TupleTypeNode
|
|
1974
|
-
// e.g. `([A]) |-> [A]`
|
|
2442
|
+
// remove () if T is TupleTypeNode e.g. `([A]) |-> [A]`
|
|
1975
2443
|
T.isKind(tsm.SyntaxKind.TupleType) ||
|
|
1976
|
-
// remove () if T is PrimitiveTypeNode
|
|
1977
|
-
// e.g. `(number) |-> number`
|
|
2444
|
+
// remove () if T is PrimitiveTypeNode e.g. `(number) |-> number`
|
|
1978
2445
|
isAtomicTypeNode(T) ||
|
|
1979
|
-
// remove () if T is TypeLiteralNode
|
|
1980
|
-
//
|
|
2446
|
+
// remove () if T is TypeLiteralNode e.g.
|
|
2447
|
+
// `({ member: V }) |-> { member: V }`
|
|
1981
2448
|
T.isKind(tsm.SyntaxKind.TypeLiteral) ||
|
|
1982
2449
|
// collapse redundant nested parentheses if T is itself a ParenthesizedType
|
|
1983
2450
|
// e.g. `((X)) |-> (X)`
|
|
@@ -2134,161 +2601,3 @@ const transformIndexSignatureDeclaration = (
|
|
|
2134
2601
|
}
|
|
2135
2602
|
}
|
|
2136
2603
|
};
|
|
2137
|
-
|
|
2138
|
-
const checkIfPropertyNameShouldBeIgnored = (
|
|
2139
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2140
|
-
nameNode: tsm.PropertyName,
|
|
2141
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
2142
|
-
): boolean => {
|
|
2143
|
-
expectType<typeof nameNode, tsm.PropertyName>('=');
|
|
2144
|
-
|
|
2145
|
-
expectType<
|
|
2146
|
-
tsm.PropertyName,
|
|
2147
|
-
| tsm.NumericLiteral
|
|
2148
|
-
| tsm.BigIntLiteral
|
|
2149
|
-
| tsm.NoSubstitutionTemplateLiteral
|
|
2150
|
-
| tsm.Identifier
|
|
2151
|
-
| tsm.StringLiteral
|
|
2152
|
-
| tsm.PrivateIdentifier
|
|
2153
|
-
| tsm.ComputedPropertyName // [`mut_x`]: number[]
|
|
2154
|
-
>('=');
|
|
2155
|
-
|
|
2156
|
-
return (
|
|
2157
|
-
// mut_x: number[]
|
|
2158
|
-
(nameNode.isKind(tsm.SyntaxKind.Identifier) &&
|
|
2159
|
-
pipe(nameNode.getText()).map((nm) =>
|
|
2160
|
-
options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
|
|
2161
|
-
).value) ||
|
|
2162
|
-
// "mut_x": number[]
|
|
2163
|
-
(nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
|
|
2164
|
-
pipe(nameNode.getLiteralValue()).map((nm) =>
|
|
2165
|
-
options.mutablePropertyPrefixes.some((p) => nm.startsWith(p)),
|
|
2166
|
-
).value) ||
|
|
2167
|
-
// #memberName: number[] (class only), matched with or after its `#`
|
|
2168
|
-
(nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
|
|
2169
|
-
pipe(nameNode.getText()).map((nm) =>
|
|
2170
|
-
options.mutablePropertyPrefixes.some(
|
|
2171
|
-
(p) => nm.startsWith(p) || nm.startsWith(`#${p}`),
|
|
2172
|
-
),
|
|
2173
|
-
).value) ||
|
|
2174
|
-
// ['mut_x']: number[], [`mut_x`]: number[]
|
|
2175
|
-
(nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
|
|
2176
|
-
pipe(nameNode.getExpression()).map((exp) => {
|
|
2177
|
-
if (
|
|
2178
|
-
exp.isKind(tsm.SyntaxKind.StringLiteral) ||
|
|
2179
|
-
exp.isKind(tsm.SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
2180
|
-
) {
|
|
2181
|
-
const nm = exp.getLiteralValue();
|
|
2182
|
-
|
|
2183
|
-
return options.mutablePropertyPrefixes.some((p) => nm.startsWith(p));
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
return false;
|
|
2187
|
-
}).value)
|
|
2188
|
-
);
|
|
2189
|
-
};
|
|
2190
|
-
|
|
2191
|
-
/**
|
|
2192
|
-
* Whether a type literal is left without `Readonly<...>` and marked readonly
|
|
2193
|
-
* member by member, because it has a property, a method or an accessor of an
|
|
2194
|
-
* ignored prefix (`{ mut_x: X[] }`), which `transformMembers` leaves mutable
|
|
2195
|
-
* and `Readonly<T>` would not.
|
|
2196
|
-
*
|
|
2197
|
-
* It keeps its `Readonly<...>` when that would leave another member mutable,
|
|
2198
|
-
* since only a property or index signature can be marked `readonly` itself:
|
|
2199
|
-
*
|
|
2200
|
-
* - a method signature or an accessor, which `Readonly<T>` makes readonly too,
|
|
2201
|
-
* unless its name has an ignored prefix as well;
|
|
2202
|
-
* - a property or index signature the ignore comment skips (as
|
|
2203
|
-
* `transformMembers` does), which gets no `readonly` of its own, unless it
|
|
2204
|
-
* is `readonly` already.
|
|
2205
|
-
*/
|
|
2206
|
-
const isLeftUnwrapped = (
|
|
2207
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2208
|
-
node: tsm.TypeLiteralNode,
|
|
2209
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
2210
|
-
): boolean => {
|
|
2211
|
-
const members = node.getMembers();
|
|
2212
|
-
|
|
2213
|
-
const isMethodOrAccessor = (
|
|
2214
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2215
|
-
mb: tsm.TypeElementTypes,
|
|
2216
|
-
): mb is
|
|
2217
|
-
| tsm.MethodSignature
|
|
2218
|
-
| tsm.GetAccessorDeclaration
|
|
2219
|
-
| tsm.SetAccessorDeclaration =>
|
|
2220
|
-
mb.isKind(tsm.SyntaxKind.MethodSignature) ||
|
|
2221
|
-
mb.isKind(tsm.SyntaxKind.GetAccessor) ||
|
|
2222
|
-
mb.isKind(tsm.SyntaxKind.SetAccessor);
|
|
2223
|
-
|
|
2224
|
-
const isIgnoredMember = (
|
|
2225
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2226
|
-
mb: tsm.TypeElementTypes,
|
|
2227
|
-
): boolean =>
|
|
2228
|
-
(mb.isKind(tsm.SyntaxKind.PropertySignature) || isMethodOrAccessor(mb)) &&
|
|
2229
|
-
checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options);
|
|
2230
|
-
|
|
2231
|
-
const needsReadonlyWrapper = (
|
|
2232
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2233
|
-
mb: tsm.TypeElementTypes,
|
|
2234
|
-
): boolean =>
|
|
2235
|
-
!isIgnoredMember(mb) &&
|
|
2236
|
-
(isMethodOrAccessor(mb) ||
|
|
2237
|
-
((mb.isKind(tsm.SyntaxKind.PropertySignature) ||
|
|
2238
|
-
mb.isKind(tsm.SyntaxKind.IndexSignature)) &&
|
|
2239
|
-
!mb.hasModifier(tsm.SyntaxKind.ReadonlyKeyword) &&
|
|
2240
|
-
hasDisableNextLineComment(mb)));
|
|
2241
|
-
|
|
2242
|
-
return members.some(isIgnoredMember) && !members.some(needsReadonlyWrapper);
|
|
2243
|
-
};
|
|
2244
|
-
|
|
2245
|
-
/** Whether `node`, or a parenthesized type around it, carries the ignore comment. */
|
|
2246
|
-
const hasDisableNextLineCommentAround = (node: tsm.Node): boolean => {
|
|
2247
|
-
if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
|
|
2248
|
-
return true;
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
const parent = node.getParent();
|
|
2252
|
-
|
|
2253
|
-
return (
|
|
2254
|
-
parent?.isKind(tsm.SyntaxKind.ParenthesizedType) === true &&
|
|
2255
|
-
hasDisableNextLineCommentAround(parent)
|
|
2256
|
-
);
|
|
2257
|
-
};
|
|
2258
|
-
|
|
2259
|
-
/**
|
|
2260
|
-
* Whether a type literal that is already wrapped, or would be merged into the
|
|
2261
|
-
* `Readonly<...>` of a union or intersection, is taken out of it. See
|
|
2262
|
-
* `isLeftUnwrapped`; in addition, the ignore comment leaves the literal where
|
|
2263
|
-
* it is, and taking `Readonly` off one with a call or construct signature
|
|
2264
|
-
* would bring back the signature it dropped.
|
|
2265
|
-
*/
|
|
2266
|
-
const takesOffReadonly = (
|
|
2267
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2268
|
-
node: tsm.TypeLiteralNode,
|
|
2269
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
2270
|
-
): boolean =>
|
|
2271
|
-
isLeftUnwrapped(node, options) &&
|
|
2272
|
-
!hasCallOrConstructSignature(node) &&
|
|
2273
|
-
!hasDisableNextLineCommentAround(node);
|
|
2274
|
-
|
|
2275
|
-
/** Whether a type alias, interface or class name starts with a `mutableTypePrefixes` entry. */
|
|
2276
|
-
const hasMutableTypeName = (
|
|
2277
|
-
name: string | undefined,
|
|
2278
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
2279
|
-
): boolean =>
|
|
2280
|
-
name !== undefined &&
|
|
2281
|
-
options.mutableTypePrefixes.some((p) => name.startsWith(p));
|
|
2282
|
-
|
|
2283
|
-
/**
|
|
2284
|
-
* A parameter property is a property of the class, as a `PropertyDeclaration`
|
|
2285
|
-
* is; any other parameter is a variable.
|
|
2286
|
-
*/
|
|
2287
|
-
const mutablePrefixesOfParameter = (
|
|
2288
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
2289
|
-
param: tsm.ParameterDeclaration,
|
|
2290
|
-
options: ReadonlyTransformerOptionsInternal,
|
|
2291
|
-
): ISet<string> =>
|
|
2292
|
-
param.isParameterProperty()
|
|
2293
|
-
? options.mutablePropertyPrefixes
|
|
2294
|
-
: options.mutableVariablePrefixes;
|