ts-codemod-lib 4.0.0 → 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/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 +4 -4
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +248 -252
- 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 +6 -6
- 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.mts +467 -471
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +60 -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
|
@@ -157,8 +157,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
157
157
|
|
|
158
158
|
/**
|
|
159
159
|
* Prefixes of the names of variables, parameters (but for a constructor
|
|
160
|
-
* parameter property, which `mutablePropertyPrefixes` decides) and
|
|
161
|
-
*
|
|
160
|
+
* parameter property, which `mutablePropertyPrefixes` decides) and functions
|
|
161
|
+
* that are left mutable: their types are not made readonly.
|
|
162
162
|
*
|
|
163
163
|
* @default ['mut_']
|
|
164
164
|
*/
|
|
@@ -181,8 +181,8 @@ export type ReadonlyTransformerOptions = DeepReadonly<{
|
|
|
181
181
|
* both `mut_` and `#mut_` cover `#mut_x`. Of a named tuple member with such a prefix, only the type
|
|
182
182
|
* is kept; the tuple is still made `readonly`.
|
|
183
183
|
*
|
|
184
|
-
* A type literal with a property, a method or an accessor of such a prefix
|
|
185
|
-
*
|
|
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
|
|
186
186
|
* too; its other members are marked `readonly` one by one instead, and a
|
|
187
187
|
* `Readonly<...>` written around it is taken off. It is wrapped as before
|
|
188
188
|
* when it also has a member that cannot be marked `readonly` by itself (a
|
|
@@ -245,6 +245,27 @@ const initialReadonlyContext: ReadonlyContext = {
|
|
|
245
245
|
indexedAccessDepth: 0,
|
|
246
246
|
} as const;
|
|
247
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
|
+
|
|
248
269
|
const transformNode = (
|
|
249
270
|
node: tsm.Node,
|
|
250
271
|
readonlyContext: ReadonlyContext,
|
|
@@ -484,6 +505,220 @@ const transformNode = (
|
|
|
484
505
|
// Transformer implementation for each node type
|
|
485
506
|
//
|
|
486
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
|
+
|
|
487
722
|
/**
|
|
488
723
|
* - `tr(ReadonlyArray<E>) |-> readonly tr(E)[]`
|
|
489
724
|
* - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
|
|
@@ -786,9 +1021,9 @@ const transformTypeReferenceNode = (
|
|
|
786
1021
|
takesOffWrapper ||
|
|
787
1022
|
(T.isKind(tsm.SyntaxKind.TypeLiteral) && takesOffReadonly(T, options))
|
|
788
1023
|
) {
|
|
789
|
-
// The comments after the literal, which nothing else carries over, go
|
|
790
|
-
//
|
|
791
|
-
//
|
|
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.
|
|
792
1027
|
const commentsAfterT = commentsBetween(
|
|
793
1028
|
node.getSourceFile(),
|
|
794
1029
|
T.getEnd(),
|
|
@@ -985,16 +1220,6 @@ const transformTypeReferenceNode = (
|
|
|
985
1220
|
}
|
|
986
1221
|
};
|
|
987
1222
|
|
|
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
1223
|
/** `tr(E[]) |-> tr(E)[]` */
|
|
999
1224
|
const transformArrayTypeNode = (
|
|
1000
1225
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1481,185 +1706,58 @@ const transformUnionTypeNode = (
|
|
|
1481
1706
|
transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
|
|
1482
1707
|
};
|
|
1483
1708
|
|
|
1484
|
-
|
|
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 = (
|
|
1485
1718
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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]);
|
|
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;
|
|
1639
1724
|
|
|
1640
|
-
|
|
1641
|
-
|
|
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);
|
|
1642
1743
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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());
|
|
1647
1759
|
|
|
1648
|
-
|
|
1649
|
-
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
1650
|
-
//
|
|
1651
|
-
// NOTE: The result is wrapped in parentheses (via `unionToString`). This is
|
|
1652
|
-
// intentional and required for precedence safety: without it, a member such
|
|
1653
|
-
// as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
|
|
1654
|
-
// operator would produce invalid syntax when this node is spliced back in.
|
|
1655
|
-
// Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
|
|
1656
|
-
// collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
|
|
1657
|
-
// — see the note there. That prevents the parentheses from growing without
|
|
1658
|
-
// bound when a union/intersection sits inside a `ParenthesizedType`.
|
|
1659
|
-
options.replaceNode(
|
|
1660
|
-
node,
|
|
1661
|
-
unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
|
|
1662
|
-
);
|
|
1760
|
+
return unionToStringImpl({ types, op, wrapWithReadonly });
|
|
1663
1761
|
};
|
|
1664
1762
|
|
|
1665
1763
|
/**
|
|
@@ -1775,10 +1873,10 @@ const isTypeArgumentOfReadonly = (
|
|
|
1775
1873
|
* `replaceWithText` overwrites a node from its `getStart()`, so whatever lies
|
|
1776
1874
|
* in front of that stays in the file by itself. With no leading `|` / `&`, the
|
|
1777
1875
|
* 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
|
-
*
|
|
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.
|
|
1782
1880
|
*
|
|
1783
1881
|
* `member` may be the type inside a member's parentheses; the comments inside
|
|
1784
1882
|
* them are collected too, and the parentheses are not.
|
|
@@ -1802,9 +1900,9 @@ const leadingCommentsOfMember = (
|
|
|
1802
1900
|
* `Readonly<T>` |-> the text of `T`, to be spliced into a union (`|`) or an
|
|
1803
1901
|
* intersection (`&`) of the members it is merged with.
|
|
1804
1902
|
*
|
|
1805
|
-
* `T` is wrapped in parentheses for precedence safety (a function type
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
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.
|
|
1808
1906
|
*/
|
|
1809
1907
|
const readonlyTypeArgTextSplicedInto = (
|
|
1810
1908
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1824,127 +1922,187 @@ const readonlyTypeArgTextSplicedInto = (
|
|
|
1824
1922
|
: unwrapReadonlyTypeArgText(node);
|
|
1825
1923
|
};
|
|
1826
1924
|
|
|
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,
|
|
1925
|
+
const transformUnionOrIntersectionTypeNodeImpl = (
|
|
1838
1926
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1927
|
+
node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
|
|
1928
|
+
readonlyContext: StrictExclude<
|
|
1929
|
+
ReadonlyContext,
|
|
1930
|
+
Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
|
|
1931
|
+
>,
|
|
1932
|
+
options: ReadonlyTransformerOptionsInternal,
|
|
1933
|
+
operator: '&' | '|',
|
|
1934
|
+
): void => {
|
|
1935
|
+
const nextReadonlyContextValue = nextReadonlyContext({
|
|
1936
|
+
currentReadonlyContext: readonlyContext,
|
|
1937
|
+
nextReadonlyContextType: 'none',
|
|
1938
|
+
indexedAccessDepthChange: 'keep',
|
|
1939
|
+
});
|
|
1842
1940
|
|
|
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 '';
|
|
1941
|
+
// Recursive processing
|
|
1942
|
+
for (const n of node.getTypeNodes() /* = [A, B] */) {
|
|
1943
|
+
transformNode(n, nextReadonlyContextValue, options);
|
|
1856
1944
|
}
|
|
1857
1945
|
|
|
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
|
-
);
|
|
1946
|
+
const newTypes = node.getTypeNodes();
|
|
1867
1947
|
|
|
1868
|
-
const
|
|
1948
|
+
const withLeadingComments = (
|
|
1949
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1950
|
+
member: tsm.TypeNode,
|
|
1951
|
+
memberText: string,
|
|
1952
|
+
): string => `${leadingCommentsOfMember(node, member)}${memberText}` as const;
|
|
1869
1953
|
|
|
1870
|
-
|
|
1871
|
-
|
|
1954
|
+
// Whether a type literal member is wrapped at all; under `DeepReadonly` or an
|
|
1955
|
+
// indexed access none is.
|
|
1956
|
+
const wrapsTypeLiterals =
|
|
1957
|
+
nextReadonlyContextValue.type !== 'DeepReadonly' &&
|
|
1958
|
+
nextReadonlyContextValue.indexedAccessDepth === 0;
|
|
1872
1959
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
: undefined;
|
|
1960
|
+
// `Readonly<A & { b: B }>`: `A` stays under the `Readonly`, which the handler
|
|
1961
|
+
// of `Readonly` then takes off as redundant.
|
|
1962
|
+
const underReadonly =
|
|
1963
|
+
readonlyContext.type === 'Readonly' &&
|
|
1964
|
+
readonlyContext.indexedAccessDepth === 0 &&
|
|
1965
|
+
isTypeArgumentOfReadonly(node);
|
|
1880
1966
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1967
|
+
const { primitives, arraysAndTuples, typeLiterals, others } =
|
|
1968
|
+
groupUnionIntersectionTypes(newTypes, {
|
|
1969
|
+
operator,
|
|
1970
|
+
underReadonly,
|
|
1971
|
+
deepReadonlyTypeName: options.DeepReadonly.typeName,
|
|
1972
|
+
keptUnwrapped: (t) => wrapsTypeLiterals && takesOffReadonly(t, options),
|
|
1973
|
+
});
|
|
1885
1974
|
|
|
1886
|
-
|
|
1887
|
-
};
|
|
1975
|
+
options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
|
|
1888
1976
|
|
|
1889
|
-
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
if (!node.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
1896
|
-
return undefined;
|
|
1897
|
-
}
|
|
1977
|
+
// Separate empty type literals from non-empty ones when ignoreEmptyObjectTypes is true
|
|
1978
|
+
const isEmptyTypeLiteral = (
|
|
1979
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1980
|
+
n: tsm.TypeNode,
|
|
1981
|
+
): boolean =>
|
|
1982
|
+
n.isKind(tsm.SyntaxKind.TypeLiteral) && Arr.isEmpty(n.getMembers());
|
|
1898
1983
|
|
|
1899
|
-
const
|
|
1984
|
+
const nonEmptyTypeLiterals =
|
|
1985
|
+
typeLiterals === undefined
|
|
1986
|
+
? undefined
|
|
1987
|
+
: options.ignoreEmptyObjectTypes
|
|
1988
|
+
? typeLiterals.nodes.filter((n) => !isEmptyTypeLiteral(n))
|
|
1989
|
+
: typeLiterals.nodes;
|
|
1900
1990
|
|
|
1901
|
-
const
|
|
1991
|
+
const emptyTypeLiterals =
|
|
1992
|
+
typeLiterals === undefined || !options.ignoreEmptyObjectTypes
|
|
1993
|
+
? undefined
|
|
1994
|
+
: typeLiterals.nodes.filter(isEmptyTypeLiteral);
|
|
1995
|
+
|
|
1996
|
+
const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
|
|
1997
|
+
nonEmptyTypeLiterals === undefined || Arr.isEmpty(nonEmptyTypeLiterals)
|
|
1998
|
+
? ([] as const)
|
|
1999
|
+
: ([
|
|
2000
|
+
unionToString({
|
|
2001
|
+
types: nonEmptyTypeLiterals.map((n) =>
|
|
2002
|
+
withLeadingComments(
|
|
2003
|
+
n,
|
|
2004
|
+
isMergeableReadonlyTypeReferenceNode(n, operator)
|
|
2005
|
+
? // NOTE: Readonly<A & B> -> (A & B) (spliced into `|`)
|
|
2006
|
+
// NOTE: Readonly<A | B> -> A | B (spliced into `|`)
|
|
2007
|
+
readonlyTypeArgTextSplicedInto(n, operator)
|
|
2008
|
+
: n.isKind(tsm.SyntaxKind.TypeLiteral)
|
|
2009
|
+
? n.getText()
|
|
2010
|
+
: // A member kept under `Readonly`; see `underReadonly`
|
|
2011
|
+
wrapWithParentheses(n.getText()),
|
|
2012
|
+
),
|
|
2013
|
+
),
|
|
2014
|
+
op: operator,
|
|
2015
|
+
wrapWithReadonly: wrapsTypeLiterals,
|
|
2016
|
+
}),
|
|
2017
|
+
] as const);
|
|
2018
|
+
|
|
2019
|
+
// Sort by first occurrence (preserving the original union order as much as possible)
|
|
2020
|
+
const sorted: readonly string[] = [
|
|
2021
|
+
mapNullable(
|
|
2022
|
+
primitives,
|
|
2023
|
+
(a) =>
|
|
2024
|
+
[
|
|
2025
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
2026
|
+
a.firstPosition,
|
|
2027
|
+
] as const,
|
|
2028
|
+
),
|
|
2029
|
+
mapNullable(
|
|
2030
|
+
arraysAndTuples,
|
|
2031
|
+
(a) =>
|
|
2032
|
+
[
|
|
2033
|
+
a.nodes.map((n) => withLeadingComments(n, n.getText())),
|
|
2034
|
+
a.firstPosition,
|
|
2035
|
+
] as const,
|
|
2036
|
+
),
|
|
2037
|
+
nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
2038
|
+
? mapNullable(
|
|
2039
|
+
typeLiterals,
|
|
2040
|
+
(a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
|
|
2041
|
+
)
|
|
2042
|
+
: undefined,
|
|
2043
|
+
emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
|
|
2044
|
+
? mapNullable(
|
|
2045
|
+
typeLiterals,
|
|
2046
|
+
(a) =>
|
|
2047
|
+
[
|
|
2048
|
+
emptyTypeLiterals.map((n) => withLeadingComments(n, n.getText())),
|
|
2049
|
+
// Use a position after non-empty type literals if they exist
|
|
2050
|
+
a.firstPosition +
|
|
2051
|
+
(nonEmptyTypeLiterals !== undefined &&
|
|
2052
|
+
Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
2053
|
+
? 0.5
|
|
2054
|
+
: 0),
|
|
2055
|
+
] as const,
|
|
2056
|
+
)
|
|
2057
|
+
: undefined,
|
|
2058
|
+
mapNullable(
|
|
2059
|
+
others,
|
|
2060
|
+
(a) =>
|
|
2061
|
+
[
|
|
2062
|
+
// `&` is deliberately not merged, unlike `Readonly<A> & Readonly<B>`
|
|
2063
|
+
// above. See `mergeDeepReadonlyUnionMembers` for why.
|
|
2064
|
+
operator === '|'
|
|
2065
|
+
? mergeDeepReadonlyUnionMembers(
|
|
2066
|
+
a.nodes,
|
|
2067
|
+
options.DeepReadonly.typeName,
|
|
2068
|
+
withLeadingComments,
|
|
2069
|
+
)
|
|
2070
|
+
: a.nodes.map((n) =>
|
|
2071
|
+
withLeadingComments(n, wrapWithParentheses(n.getText())),
|
|
2072
|
+
),
|
|
2073
|
+
a.firstPosition,
|
|
2074
|
+
] as const,
|
|
2075
|
+
),
|
|
2076
|
+
]
|
|
2077
|
+
.filter((x) => x !== undefined)
|
|
2078
|
+
.toSorted((a, b) => a[1] - b[1])
|
|
2079
|
+
.flatMap((a) => a[0]);
|
|
1902
2080
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
Arr.isFixedLengthArray(1, typeArguments)
|
|
1906
|
-
? typeArguments[0]
|
|
1907
|
-
: undefined;
|
|
1908
|
-
};
|
|
2081
|
+
// Readonly<number & { x: X } & { y: Y } & readonly E[]>
|
|
2082
|
+
// -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
|
|
1909
2083
|
|
|
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());
|
|
2084
|
+
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
2085
|
+
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
2086
|
+
// Readonly<number & { x: X } & { y: Y } & readonly E[]>
|
|
2087
|
+
// -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
|
|
1925
2088
|
|
|
1926
|
-
|
|
2089
|
+
// Readonly<number | { x: X } | { y: Y } | readonly E[]>
|
|
2090
|
+
// -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
|
|
2091
|
+
//
|
|
2092
|
+
// NOTE: The result is wrapped in parentheses (via `unionToString`). This is
|
|
2093
|
+
// intentional and required for precedence safety: without it, a member such
|
|
2094
|
+
// as a function type (`() => T`), or a mix of `&` and `|`, or a `readonly`
|
|
2095
|
+
// operator would produce invalid syntax when this node is spliced back in.
|
|
2096
|
+
// Idempotency is guaranteed by `transformParenthesizedTypeNode`, which
|
|
2097
|
+
// collapses the redundant nesting (`((X))` -> `(X)`) this wrapping can create
|
|
2098
|
+
// — see the note there. That prevents the parentheses from growing without
|
|
2099
|
+
// bound when a union/intersection sits inside a `ParenthesizedType`.
|
|
2100
|
+
options.replaceNode(
|
|
2101
|
+
node,
|
|
2102
|
+
unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
|
|
2103
|
+
);
|
|
1927
2104
|
};
|
|
1928
2105
|
|
|
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
2106
|
/** Convert ((T)) -> (T) recursively */
|
|
1949
2107
|
const transformParenthesizedTypeNode = (
|
|
1950
2108
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -1964,20 +2122,16 @@ const transformParenthesizedTypeNode = (
|
|
|
1964
2122
|
const T = node.getTypeNode();
|
|
1965
2123
|
|
|
1966
2124
|
if (
|
|
1967
|
-
// remove () if T is TypeReferenceNode
|
|
1968
|
-
// e.g. `(Readonly<A>) |-> Readonly<A>`
|
|
2125
|
+
// remove () if T is TypeReferenceNode e.g. `(Readonly<A>) |-> Readonly<A>`
|
|
1969
2126
|
T.isKind(tsm.SyntaxKind.TypeReference) ||
|
|
1970
|
-
// remove () if T is ArrayTypeNode
|
|
1971
|
-
// e.g. `(A[]) |-> A[]`
|
|
2127
|
+
// remove () if T is ArrayTypeNode e.g. `(A[]) |-> A[]`
|
|
1972
2128
|
T.isKind(tsm.SyntaxKind.ArrayType) ||
|
|
1973
|
-
// remove () if T is TupleTypeNode
|
|
1974
|
-
// e.g. `([A]) |-> [A]`
|
|
2129
|
+
// remove () if T is TupleTypeNode e.g. `([A]) |-> [A]`
|
|
1975
2130
|
T.isKind(tsm.SyntaxKind.TupleType) ||
|
|
1976
|
-
// remove () if T is PrimitiveTypeNode
|
|
1977
|
-
// e.g. `(number) |-> number`
|
|
2131
|
+
// remove () if T is PrimitiveTypeNode e.g. `(number) |-> number`
|
|
1978
2132
|
isAtomicTypeNode(T) ||
|
|
1979
|
-
// remove () if T is TypeLiteralNode
|
|
1980
|
-
//
|
|
2133
|
+
// remove () if T is TypeLiteralNode e.g.
|
|
2134
|
+
// `({ member: V }) |-> { member: V }`
|
|
1981
2135
|
T.isKind(tsm.SyntaxKind.TypeLiteral) ||
|
|
1982
2136
|
// collapse redundant nested parentheses if T is itself a ParenthesizedType
|
|
1983
2137
|
// e.g. `((X)) |-> (X)`
|
|
@@ -2134,161 +2288,3 @@ const transformIndexSignatureDeclaration = (
|
|
|
2134
2288
|
}
|
|
2135
2289
|
}
|
|
2136
2290
|
};
|
|
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;
|