ts-codemod-lib 2.0.4 → 2.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/dist/cmd/append-as-const.mjs +1 -1
- package/dist/cmd/convert-interface-to-type.mjs +1 -1
- package/dist/cmd/convert-to-readonly.mjs +1 -1
- package/dist/cmd/replace-any-with-unknown.mjs +1 -1
- package/dist/cmd/replace-record-with-unknown-record.mjs +1 -1
- package/dist/cmd/run-transformer-cli.mjs +3 -3
- package/dist/cmd/run-transformer-cli.mjs.map +1 -1
- package/dist/entry-point.mjs +2 -1
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +4 -4
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +31 -12
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +13 -12
- 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 +41 -13
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/constants/ignore-comment-text.d.mts +2 -2
- package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -1
- package/dist/functions/constants/ignore-comment-text.mjs +13 -3
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -1
- package/dist/functions/constants/index.mjs +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.d.mts +3 -2
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.mjs +9 -6
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/index.d.mts +1 -0
- package/dist/functions/functions/index.d.mts.map +1 -1
- package/dist/functions/functions/index.mjs +1 -0
- package/dist/functions/functions/index.mjs.map +1 -1
- package/dist/functions/functions/is-as-const-node.d.mts.map +1 -1
- package/dist/functions/functions/is-as-const-node.mjs +2 -1
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts +16 -0
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -0
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +32 -0
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -0
- package/dist/functions/functions/wrap-with-parentheses.d.mts +13 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +58 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/index.mjs +2 -1
- package/dist/functions/index.mjs.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -16
- package/src/cmd/append-as-const.mts +1 -1
- package/src/cmd/convert-interface-to-type.mts +1 -1
- package/src/cmd/convert-to-readonly.mts +1 -1
- package/src/cmd/replace-any-with-unknown.mts +1 -1
- package/src/cmd/replace-record-with-unknown-record.mts +1 -1
- package/src/cmd/run-transformer-cli.mts +3 -3
- package/src/functions/ast-transformers/convert-interface-to-type.mts +6 -7
- package/src/functions/ast-transformers/convert-to-readonly.mts +39 -13
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +79 -0
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +15 -12
- package/src/functions/ast-transformers/transform-source-code.mts +44 -15
- package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +129 -0
- package/src/functions/constants/ignore-comment-text.mts +12 -2
- package/src/functions/functions/has-disable-next-line-comment.mts +12 -6
- package/src/functions/functions/index.mts +1 -0
- package/src/functions/functions/is-as-const-node.mts +2 -1
- package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +32 -0
- package/src/functions/functions/wrap-with-parentheses.mts +75 -2
|
@@ -41,19 +41,18 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
|
|
|
41
41
|
const members = interfaceDecl.getMembers();
|
|
42
42
|
|
|
43
43
|
// Build type parameters string
|
|
44
|
-
const typeParamsStr =
|
|
45
|
-
typeParameters.
|
|
46
|
-
|
|
47
|
-
: '';
|
|
44
|
+
const typeParamsStr = Arr.isNonEmpty(typeParameters)
|
|
45
|
+
? `<${typeParameters.map((tp) => tp.getText()).join(', ')}>`
|
|
46
|
+
: '';
|
|
48
47
|
|
|
49
48
|
// Build type literal from members
|
|
50
49
|
const mut_typeBody: string = (() => {
|
|
51
|
-
if (extendsExpressions
|
|
50
|
+
if (Arr.isArrayOfLength(extendsExpressions, 0)) {
|
|
52
51
|
// No extends: simple type literal
|
|
53
52
|
return buildTypeLiteral(members);
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
if (members
|
|
55
|
+
if (Arr.isArrayOfLength(members, 0)) {
|
|
57
56
|
// Only extends, no own members: union of extended types
|
|
58
57
|
const extendedTypes = extendsExpressions.map((ext) =>
|
|
59
58
|
ext.getText(),
|
|
@@ -98,7 +97,7 @@ export const convertInterfaceToTypeTransformer = (): TsMorphTransformer =>
|
|
|
98
97
|
const buildTypeLiteral = (
|
|
99
98
|
members: readonly tsm.TypeElementTypes[],
|
|
100
99
|
): string => {
|
|
101
|
-
if (members
|
|
100
|
+
if (Arr.isArrayOfLength(members, 0)) {
|
|
102
101
|
return 'Record<string, never>';
|
|
103
102
|
}
|
|
104
103
|
|
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
isReadonlyTupleOrArrayTypeNode,
|
|
16
16
|
isReadonlyTupleTypeNode,
|
|
17
17
|
isReadonlyTypeReferenceNode,
|
|
18
|
-
type ReadonlyTypeReferenceNode,
|
|
19
18
|
removeParentheses,
|
|
19
|
+
shouldAvoidParenthesesForReadonly,
|
|
20
20
|
unwrapReadonlyTypeArgText,
|
|
21
21
|
wrapWithParentheses,
|
|
22
|
+
type ReadonlyTypeReferenceNode,
|
|
22
23
|
} from '../functions/index.mjs';
|
|
23
24
|
import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
|
|
24
25
|
import {
|
|
@@ -552,9 +553,13 @@ const transformTypeReferenceNode = (
|
|
|
552
553
|
T.isKind(tsm.SyntaxKind.ArrayType) ||
|
|
553
554
|
T.isKind(tsm.SyntaxKind.TupleType)
|
|
554
555
|
) {
|
|
556
|
+
const readonlyText = `readonly ${T.getFullText()}` as const;
|
|
557
|
+
|
|
555
558
|
options.replaceNode(
|
|
556
559
|
node,
|
|
557
|
-
|
|
560
|
+
shouldAvoidParenthesesForReadonly(node)
|
|
561
|
+
? readonlyText
|
|
562
|
+
: wrapWithParentheses(readonlyText),
|
|
558
563
|
);
|
|
559
564
|
|
|
560
565
|
return;
|
|
@@ -626,9 +631,13 @@ const transformTypeReferenceNode = (
|
|
|
626
631
|
T.isKind(tsm.SyntaxKind.ArrayType) &&
|
|
627
632
|
isAtomicTypeNode(T.getElementTypeNode())
|
|
628
633
|
) {
|
|
634
|
+
const readonlyText = `readonly ${T.getFullText()}` as const;
|
|
635
|
+
|
|
629
636
|
options.replaceNode(
|
|
630
637
|
node,
|
|
631
|
-
|
|
638
|
+
shouldAvoidParenthesesForReadonly(node)
|
|
639
|
+
? readonlyText
|
|
640
|
+
: wrapWithParentheses(readonlyText),
|
|
632
641
|
);
|
|
633
642
|
|
|
634
643
|
return;
|
|
@@ -640,9 +649,13 @@ const transformTypeReferenceNode = (
|
|
|
640
649
|
T.isKind(tsm.SyntaxKind.TupleType) &&
|
|
641
650
|
T.getElements().every(isAtomicTypeNode)
|
|
642
651
|
) {
|
|
652
|
+
const readonlyText = `readonly ${T.getFullText()}` as const;
|
|
653
|
+
|
|
643
654
|
options.replaceNode(
|
|
644
655
|
node,
|
|
645
|
-
|
|
656
|
+
shouldAvoidParenthesesForReadonly(node)
|
|
657
|
+
? readonlyText
|
|
658
|
+
: wrapWithParentheses(readonlyText),
|
|
646
659
|
);
|
|
647
660
|
|
|
648
661
|
return;
|
|
@@ -692,9 +705,13 @@ const transformArrayTypeNode = (
|
|
|
692
705
|
case 'Readonly':
|
|
693
706
|
case 'none':
|
|
694
707
|
if (readonlyContext.indexedAccessDepth === 0) {
|
|
708
|
+
const readonlyText = `readonly ${node.getFullText()}` as const;
|
|
709
|
+
|
|
695
710
|
options.replaceNode(
|
|
696
711
|
node,
|
|
697
|
-
|
|
712
|
+
shouldAvoidParenthesesForReadonly(node)
|
|
713
|
+
? readonlyText
|
|
714
|
+
: wrapWithParentheses(readonlyText),
|
|
698
715
|
);
|
|
699
716
|
}
|
|
700
717
|
}
|
|
@@ -735,9 +752,13 @@ const transformTupleTypeNode = (
|
|
|
735
752
|
case 'Readonly':
|
|
736
753
|
case 'none':
|
|
737
754
|
if (readonlyContext.indexedAccessDepth === 0) {
|
|
755
|
+
const readonlyText = `readonly ${node.getFullText()}` as const;
|
|
756
|
+
|
|
738
757
|
options.replaceNode(
|
|
739
758
|
node,
|
|
740
|
-
|
|
759
|
+
shouldAvoidParenthesesForReadonly(node)
|
|
760
|
+
? readonlyText
|
|
761
|
+
: wrapWithParentheses(readonlyText),
|
|
741
762
|
);
|
|
742
763
|
}
|
|
743
764
|
}
|
|
@@ -825,7 +846,10 @@ const transformTypeLiteralNode = (
|
|
|
825
846
|
>,
|
|
826
847
|
options: ReadonlyTransformerOptionsInternal,
|
|
827
848
|
): void => {
|
|
828
|
-
if (
|
|
849
|
+
if (
|
|
850
|
+
options.ignoreEmptyObjectTypes &&
|
|
851
|
+
Arr.isArrayOfLength(node.getMembers(), 0)
|
|
852
|
+
) {
|
|
829
853
|
return;
|
|
830
854
|
}
|
|
831
855
|
|
|
@@ -1130,7 +1154,8 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1130
1154
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
1131
1155
|
n: tsm.TypeLiteralNode | ReadonlyTypeReferenceNode,
|
|
1132
1156
|
): boolean =>
|
|
1133
|
-
n.isKind(tsm.SyntaxKind.TypeLiteral) &&
|
|
1157
|
+
n.isKind(tsm.SyntaxKind.TypeLiteral) &&
|
|
1158
|
+
Arr.isArrayOfLength(n.getMembers(), 0);
|
|
1134
1159
|
|
|
1135
1160
|
const nonEmptyTypeLiterals =
|
|
1136
1161
|
typeLiterals === undefined
|
|
@@ -1145,7 +1170,8 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1145
1170
|
: typeLiterals.nodes.filter((n) => isEmptyTypeLiteral(n));
|
|
1146
1171
|
|
|
1147
1172
|
const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
|
|
1148
|
-
nonEmptyTypeLiterals === undefined ||
|
|
1173
|
+
nonEmptyTypeLiterals === undefined ||
|
|
1174
|
+
Arr.isArrayOfLength(nonEmptyTypeLiterals, 0)
|
|
1149
1175
|
? ([] as const)
|
|
1150
1176
|
: ([
|
|
1151
1177
|
unionToString({
|
|
@@ -1172,13 +1198,13 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1172
1198
|
arraysAndTuples,
|
|
1173
1199
|
(a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
|
|
1174
1200
|
),
|
|
1175
|
-
nonEmptyTypeLiterals !== undefined && nonEmptyTypeLiterals
|
|
1201
|
+
nonEmptyTypeLiterals !== undefined && Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
1176
1202
|
? mapNullable(
|
|
1177
1203
|
typeLiterals,
|
|
1178
1204
|
(a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
|
|
1179
1205
|
)
|
|
1180
1206
|
: undefined,
|
|
1181
|
-
emptyTypeLiterals !== undefined && emptyTypeLiterals
|
|
1207
|
+
emptyTypeLiterals !== undefined && Arr.isNonEmpty(emptyTypeLiterals)
|
|
1182
1208
|
? mapNullable(
|
|
1183
1209
|
typeLiterals,
|
|
1184
1210
|
(a) =>
|
|
@@ -1187,7 +1213,7 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1187
1213
|
// Use a position after non-empty type literals if they exist
|
|
1188
1214
|
a.firstPosition +
|
|
1189
1215
|
(nonEmptyTypeLiterals !== undefined &&
|
|
1190
|
-
nonEmptyTypeLiterals
|
|
1216
|
+
Arr.isNonEmpty(nonEmptyTypeLiterals)
|
|
1191
1217
|
? 0.5
|
|
1192
1218
|
: 0),
|
|
1193
1219
|
] as const,
|
|
@@ -1226,7 +1252,7 @@ const unionToString = ({
|
|
|
1226
1252
|
op: '&' | '|';
|
|
1227
1253
|
wrapWithReadonly: boolean | string;
|
|
1228
1254
|
}>): string =>
|
|
1229
|
-
types
|
|
1255
|
+
Arr.isArrayOfLength(types, 0)
|
|
1230
1256
|
? 'never'
|
|
1231
1257
|
: Arr.isArrayOfLength(types, 1)
|
|
1232
1258
|
? wrapWithReadonly === false
|
|
@@ -3816,6 +3816,85 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
3816
3816
|
])('$name', testFn);
|
|
3817
3817
|
});
|
|
3818
3818
|
|
|
3819
|
+
describe('shouldAvoidParenthesesForReadonly', () => {
|
|
3820
|
+
test.each([
|
|
3821
|
+
{
|
|
3822
|
+
name: 'Type predicate with array - avoid parentheses',
|
|
3823
|
+
source: dedent`
|
|
3824
|
+
function isStringArray(x: unknown): x is string[] {
|
|
3825
|
+
return Array.isArray(x);
|
|
3826
|
+
}
|
|
3827
|
+
`,
|
|
3828
|
+
expected: dedent`
|
|
3829
|
+
function isStringArray(x: unknown): x is readonly string[] {
|
|
3830
|
+
return Array.isArray(x);
|
|
3831
|
+
}
|
|
3832
|
+
`,
|
|
3833
|
+
},
|
|
3834
|
+
{
|
|
3835
|
+
name: 'Type predicate with tuple - avoid parentheses',
|
|
3836
|
+
source: dedent`
|
|
3837
|
+
function isTuple(x: unknown): x is [string, number] {
|
|
3838
|
+
return Array.isArray(x) && x.length === 2;
|
|
3839
|
+
}
|
|
3840
|
+
`,
|
|
3841
|
+
expected: dedent`
|
|
3842
|
+
function isTuple(x: unknown): x is readonly [string, number] {
|
|
3843
|
+
return Array.isArray(x) && x.length === 2;
|
|
3844
|
+
}
|
|
3845
|
+
`,
|
|
3846
|
+
},
|
|
3847
|
+
{
|
|
3848
|
+
name: 'Type predicate with Readonly<T[]> - avoid parentheses',
|
|
3849
|
+
source: dedent`
|
|
3850
|
+
function isArray(x: unknown): x is Readonly<string[]> {
|
|
3851
|
+
return Array.isArray(x);
|
|
3852
|
+
}
|
|
3853
|
+
`,
|
|
3854
|
+
expected: dedent`
|
|
3855
|
+
function isArray(x: unknown): x is readonly string[] {
|
|
3856
|
+
return Array.isArray(x);
|
|
3857
|
+
}
|
|
3858
|
+
`,
|
|
3859
|
+
},
|
|
3860
|
+
{
|
|
3861
|
+
name: 'Type predicate with Readonly<[T, U]> - avoid parentheses',
|
|
3862
|
+
source: dedent`
|
|
3863
|
+
function isTuple(x: unknown): x is Readonly<[string, number]> {
|
|
3864
|
+
return Array.isArray(x);
|
|
3865
|
+
}
|
|
3866
|
+
`,
|
|
3867
|
+
expected: dedent`
|
|
3868
|
+
function isTuple(x: unknown): x is readonly [string, number] {
|
|
3869
|
+
return Array.isArray(x);
|
|
3870
|
+
}
|
|
3871
|
+
`,
|
|
3872
|
+
},
|
|
3873
|
+
{
|
|
3874
|
+
name: 'Non-type predicate context - keep parentheses (handled by formatter)',
|
|
3875
|
+
source: dedent`
|
|
3876
|
+
type T = Readonly<string[]>;
|
|
3877
|
+
`,
|
|
3878
|
+
expected: dedent`
|
|
3879
|
+
type T = readonly string[];
|
|
3880
|
+
`,
|
|
3881
|
+
},
|
|
3882
|
+
{
|
|
3883
|
+
name: 'Function return type - keep parentheses (handled by formatter)',
|
|
3884
|
+
source: dedent`
|
|
3885
|
+
function foo(): Readonly<number[]> {
|
|
3886
|
+
return [];
|
|
3887
|
+
}
|
|
3888
|
+
`,
|
|
3889
|
+
expected: dedent`
|
|
3890
|
+
function foo(): readonly number[] {
|
|
3891
|
+
return [];
|
|
3892
|
+
}
|
|
3893
|
+
`,
|
|
3894
|
+
},
|
|
3895
|
+
])('$name', testFn);
|
|
3896
|
+
});
|
|
3897
|
+
|
|
3819
3898
|
describe('Error Cases', () => {
|
|
3820
3899
|
test('Invalid DeepReadonlyTypeName', () => {
|
|
3821
3900
|
expect(() => {
|
|
@@ -68,7 +68,10 @@ const processDeclarations = (
|
|
|
68
68
|
if (hasStringUnknownSignature) {
|
|
69
69
|
const properties = interfaceDecl.getProperties();
|
|
70
70
|
|
|
71
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
Arr.isArrayOfLength(properties, 0) &&
|
|
73
|
+
Arr.isArrayOfLength(indexSignatures, 1)
|
|
74
|
+
) {
|
|
72
75
|
// Replace interface with type alias
|
|
73
76
|
const interfaceName = interfaceDecl.getName();
|
|
74
77
|
|
|
@@ -97,7 +100,7 @@ const visitTypeNode = (node: tsm.TypeNode): void => {
|
|
|
97
100
|
if (node.getTypeName().getText() === 'Readonly') {
|
|
98
101
|
const typeArgs = node.getTypeArguments();
|
|
99
102
|
|
|
100
|
-
if (typeArgs
|
|
103
|
+
if (Arr.isArrayOfLength(typeArgs, 1)) {
|
|
101
104
|
const typeArg = typeArgs[0];
|
|
102
105
|
|
|
103
106
|
if (tsm.Node.isTypeLiteral(typeArg)) {
|
|
@@ -109,7 +112,7 @@ const visitTypeNode = (node: tsm.TypeNode): void => {
|
|
|
109
112
|
|
|
110
113
|
// Check if it has only one index signature [k: string]: unknown
|
|
111
114
|
if (
|
|
112
|
-
members
|
|
115
|
+
Arr.isArrayOfLength(members, 1) &&
|
|
113
116
|
Arr.isArrayOfLength(indexSigs, 1) &&
|
|
114
117
|
isStringUnknownIndexSignature(indexSigs[0])
|
|
115
118
|
) {
|
|
@@ -139,7 +142,7 @@ const visitTypeNode = (node: tsm.TypeNode): void => {
|
|
|
139
142
|
|
|
140
143
|
// Check if it has only one index signature [k: string]: unknown
|
|
141
144
|
if (
|
|
142
|
-
members
|
|
145
|
+
Arr.isArrayOfLength(members, 1) &&
|
|
143
146
|
Arr.isArrayOfLength(indexSigs, 1) &&
|
|
144
147
|
isStringUnknownIndexSignature(indexSigs[0])
|
|
145
148
|
) {
|
|
@@ -204,9 +207,9 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): void => {
|
|
|
204
207
|
const typeArgs = node.getTypeArguments();
|
|
205
208
|
|
|
206
209
|
if (
|
|
207
|
-
typeArgs
|
|
208
|
-
typeArgs[0]
|
|
209
|
-
typeArgs[1]
|
|
210
|
+
Arr.isArrayOfLength(typeArgs, 2) &&
|
|
211
|
+
typeArgs[0].getText() === 'string' &&
|
|
212
|
+
typeArgs[1].getText() === 'unknown'
|
|
210
213
|
) {
|
|
211
214
|
node.replaceWithText('UnknownRecord');
|
|
212
215
|
}
|
|
@@ -217,19 +220,19 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): void => {
|
|
|
217
220
|
// Check if it's Readonly<Record<string, unknown>>
|
|
218
221
|
const typeArgs = node.getTypeArguments();
|
|
219
222
|
|
|
220
|
-
if (typeArgs
|
|
223
|
+
if (Arr.isArrayOfLength(typeArgs, 1)) {
|
|
221
224
|
const innerType = typeArgs[0];
|
|
222
225
|
|
|
223
|
-
if (
|
|
226
|
+
if (tsm.Node.isTypeReference(innerType)) {
|
|
224
227
|
const innerTypeName = innerType.getTypeName().getText();
|
|
225
228
|
|
|
226
229
|
if (innerTypeName === 'Record') {
|
|
227
230
|
const innerTypeArgs = innerType.getTypeArguments();
|
|
228
231
|
|
|
229
232
|
if (
|
|
230
|
-
innerTypeArgs
|
|
231
|
-
innerTypeArgs[0]
|
|
232
|
-
innerTypeArgs[1]
|
|
233
|
+
Arr.isArrayOfLength(innerTypeArgs, 2) &&
|
|
234
|
+
innerTypeArgs[0].getText() === 'string' &&
|
|
235
|
+
innerTypeArgs[1].getText() === 'unknown'
|
|
233
236
|
) {
|
|
234
237
|
node.replaceWithText('UnknownRecord');
|
|
235
238
|
}
|
|
@@ -1,22 +1,45 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
1
2
|
import * as tsm from 'ts-morph';
|
|
2
3
|
import { type TsMorphTransformer } from './types.mjs';
|
|
3
4
|
|
|
4
5
|
const extractFileIgnoreTransformers = (code: string): readonly string[] => {
|
|
5
|
-
|
|
6
|
+
// Try to find any of the supported file-level ignore comment prefixes
|
|
7
|
+
const patterns = [
|
|
8
|
+
{
|
|
9
|
+
prefix: 'transformer-ignore',
|
|
10
|
+
regex: /\/\*\s*transformer-ignore\s*(.*?)\s*\*\//u,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
prefix: 'ts-codemod-ignore',
|
|
14
|
+
regex: /\/\*\s*ts-codemod-ignore\s*(.*?)\s*\*\//u,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
prefix: 'codemod-ignore',
|
|
18
|
+
regex: /\/\*\s*codemod-ignore\s*(.*?)\s*\*\//u,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
prefix: 'transform-ignore',
|
|
22
|
+
regex: /\/\*\s*transform-ignore\s*(.*?)\s*\*\//u,
|
|
23
|
+
},
|
|
24
|
+
] as const;
|
|
6
25
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
26
|
+
for (const { regex } of patterns) {
|
|
27
|
+
const match = regex.exec(code);
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
if (match !== null) {
|
|
30
|
+
const targetTransformers = match[1]?.trim() ?? '';
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
// Empty means ignore all transformers
|
|
33
|
+
if (targetTransformers === '') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse comma-separated transformer names
|
|
38
|
+
return targetTransformers.split(',').map((name) => name.trim());
|
|
39
|
+
}
|
|
16
40
|
}
|
|
17
41
|
|
|
18
|
-
|
|
19
|
-
return targetTransformers.split(',').map((name) => name.trim());
|
|
42
|
+
return [];
|
|
20
43
|
};
|
|
21
44
|
|
|
22
45
|
const shouldSkipFile = (
|
|
@@ -26,15 +49,21 @@ const shouldSkipFile = (
|
|
|
26
49
|
const ignoredTransformers = extractFileIgnoreTransformers(code);
|
|
27
50
|
|
|
28
51
|
// If no file-level ignore comment found, don't skip
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
const patterns = [
|
|
53
|
+
/\/\*\s*transformer-ignore\s*.*?\s*\*\//u,
|
|
54
|
+
/\/\*\s*ts-codemod-ignore\s*.*?\s*\*\//u,
|
|
55
|
+
/\/\*\s*codemod-ignore\s*.*?\s*\*\//u,
|
|
56
|
+
/\/\*\s*transform-ignore\s*.*?\s*\*\//u,
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const hasFileIgnoreComment = patterns.some((regex) => regex.test(code));
|
|
60
|
+
|
|
61
|
+
if (Arr.isArrayOfLength(ignoredTransformers, 0) && !hasFileIgnoreComment) {
|
|
33
62
|
return false;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
// Empty array means ignore all transformers (file-level ignore without specific transformers)
|
|
37
|
-
if (ignoredTransformers
|
|
66
|
+
if (Arr.isArrayOfLength(ignoredTransformers, 0)) {
|
|
38
67
|
return true;
|
|
39
68
|
}
|
|
40
69
|
|
|
@@ -62,6 +62,45 @@ describe('Transformer-specific ignore comments (Integration)', () => {
|
|
|
62
62
|
type C = readonly number[]; // Should be readonly
|
|
63
63
|
`,
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: 'ts-codemod-ignore-next-line pattern',
|
|
67
|
+
source: dedent`
|
|
68
|
+
// ts-codemod-ignore-next-line replace-any-with-unknown
|
|
69
|
+
type A = any; // Should remain any
|
|
70
|
+
const b = [1, 2, 3]; // Should have as const
|
|
71
|
+
`,
|
|
72
|
+
expected: dedent`
|
|
73
|
+
// ts-codemod-ignore-next-line replace-any-with-unknown
|
|
74
|
+
type A = any; // Should remain any
|
|
75
|
+
const b = [1, 2, 3] as const; // Should have as const
|
|
76
|
+
`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'codemod-ignore-next-line pattern',
|
|
80
|
+
source: dedent`
|
|
81
|
+
// codemod-ignore-next-line replace-any-with-unknown
|
|
82
|
+
type A = any; // Should remain any
|
|
83
|
+
type B = number[];
|
|
84
|
+
`,
|
|
85
|
+
expected: dedent`
|
|
86
|
+
// codemod-ignore-next-line replace-any-with-unknown
|
|
87
|
+
type A = any; // Should remain any
|
|
88
|
+
type B = readonly number[];
|
|
89
|
+
`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'transform-ignore-next-line pattern',
|
|
93
|
+
source: dedent`
|
|
94
|
+
// transform-ignore-next-line append-as-const
|
|
95
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
96
|
+
type A = any;
|
|
97
|
+
`,
|
|
98
|
+
expected: dedent`
|
|
99
|
+
// transform-ignore-next-line append-as-const
|
|
100
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
101
|
+
type A = unknown;
|
|
102
|
+
`,
|
|
103
|
+
},
|
|
65
104
|
{
|
|
66
105
|
name: 'Ignore specific transformer for entire file',
|
|
67
106
|
source: dedent`
|
|
@@ -79,6 +118,51 @@ describe('Transformer-specific ignore comments (Integration)', () => {
|
|
|
79
118
|
type D = any; // Should remain any
|
|
80
119
|
`,
|
|
81
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: 'ts-codemod-ignore for entire file',
|
|
123
|
+
source: dedent`
|
|
124
|
+
/* ts-codemod-ignore replace-any-with-unknown */
|
|
125
|
+
type A = any; // Should remain any
|
|
126
|
+
const b = [1, 2, 3]; // Should have as const
|
|
127
|
+
type D = any; // Should remain any
|
|
128
|
+
`,
|
|
129
|
+
expected: dedent`
|
|
130
|
+
/* ts-codemod-ignore replace-any-with-unknown */
|
|
131
|
+
type A = any; // Should remain any
|
|
132
|
+
const b = [1, 2, 3] as const; // Should have as const
|
|
133
|
+
type D = any; // Should remain any
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'codemod-ignore for entire file',
|
|
138
|
+
source: dedent`
|
|
139
|
+
/* codemod-ignore convert-to-readonly */
|
|
140
|
+
type A = any;
|
|
141
|
+
const b = [1, 2, 3];
|
|
142
|
+
type C = number[]; // Should remain mutable
|
|
143
|
+
`,
|
|
144
|
+
expected: dedent`
|
|
145
|
+
/* codemod-ignore convert-to-readonly */
|
|
146
|
+
type A = unknown;
|
|
147
|
+
const b = [1, 2, 3] as const;
|
|
148
|
+
type C = number[]; // Should remain mutable
|
|
149
|
+
`,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'transform-ignore for entire file',
|
|
153
|
+
source: dedent`
|
|
154
|
+
/* transform-ignore append-as-const */
|
|
155
|
+
type A = any;
|
|
156
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
157
|
+
type C = number[];
|
|
158
|
+
`,
|
|
159
|
+
expected: dedent`
|
|
160
|
+
/* transform-ignore append-as-const */
|
|
161
|
+
type A = unknown;
|
|
162
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
163
|
+
type C = readonly number[];
|
|
164
|
+
`,
|
|
165
|
+
},
|
|
82
166
|
{
|
|
83
167
|
name: 'Ignore multiple specific transformers on next line',
|
|
84
168
|
source: dedent`
|
|
@@ -185,5 +269,50 @@ describe('Transformer-specific ignore comments (Integration)', () => {
|
|
|
185
269
|
type C = number[]; // Should remain mutable
|
|
186
270
|
`,
|
|
187
271
|
},
|
|
272
|
+
{
|
|
273
|
+
name: 'ts-codemod-ignore for entire file',
|
|
274
|
+
source: dedent`
|
|
275
|
+
/* ts-codemod-ignore replace-any-with-unknown */
|
|
276
|
+
type A = any; // Should remain any
|
|
277
|
+
const b = [1, 2, 3]; // Should have as const
|
|
278
|
+
type D = any; // Should remain any
|
|
279
|
+
`,
|
|
280
|
+
expected: dedent`
|
|
281
|
+
/* ts-codemod-ignore replace-any-with-unknown */
|
|
282
|
+
type A = any; // Should remain any
|
|
283
|
+
const b = [1, 2, 3] as const; // Should have as const
|
|
284
|
+
type D = any; // Should remain any
|
|
285
|
+
`,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: 'codemod-ignore for entire file',
|
|
289
|
+
source: dedent`
|
|
290
|
+
/* codemod-ignore convert-to-readonly */
|
|
291
|
+
type A = any;
|
|
292
|
+
const b = [1, 2, 3];
|
|
293
|
+
type C = number[]; // Should remain mutable
|
|
294
|
+
`,
|
|
295
|
+
expected: dedent`
|
|
296
|
+
/* codemod-ignore convert-to-readonly */
|
|
297
|
+
type A = unknown;
|
|
298
|
+
const b = [1, 2, 3] as const;
|
|
299
|
+
type C = number[]; // Should remain mutable
|
|
300
|
+
`,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'transform-ignore for entire file',
|
|
304
|
+
source: dedent`
|
|
305
|
+
/* transform-ignore append-as-const */
|
|
306
|
+
type A = any;
|
|
307
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
308
|
+
type C = number[];
|
|
309
|
+
`,
|
|
310
|
+
expected: dedent`
|
|
311
|
+
/* transform-ignore append-as-const */
|
|
312
|
+
type A = unknown;
|
|
313
|
+
const b = [1, 2, 3]; // Should remain without as const
|
|
314
|
+
type C = readonly number[];
|
|
315
|
+
`,
|
|
316
|
+
},
|
|
188
317
|
])('$name', testFn);
|
|
189
318
|
});
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const IGNORE_LINE_COMMENT_PREFIXES = [
|
|
2
|
+
'transformer-ignore-next-line',
|
|
3
|
+
'ts-codemod-ignore-next-line',
|
|
4
|
+
'codemod-ignore-next-line',
|
|
5
|
+
'transform-ignore-next-line',
|
|
6
|
+
] as const;
|
|
2
7
|
|
|
3
|
-
export const
|
|
8
|
+
export const IGNORE_FILE_COMMENT_PREFIXES = [
|
|
9
|
+
'transformer-ignore',
|
|
10
|
+
'ts-codemod-ignore',
|
|
11
|
+
'codemod-ignore',
|
|
12
|
+
'transform-ignore',
|
|
13
|
+
] as const;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as tsm from 'ts-morph';
|
|
2
|
-
import {
|
|
2
|
+
import { IGNORE_LINE_COMMENT_PREFIXES } from '../constants/index.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Checks if a given ts-morph Node is immediately preceded by
|
|
6
|
-
*
|
|
5
|
+
* Checks if a given ts-morph Node is immediately preceded by an ignore-next-line comment,
|
|
6
|
+
* optionally filtered by transformer name(s).
|
|
7
7
|
*
|
|
8
8
|
* @param node - The ts-morph Node to check.
|
|
9
9
|
* @param transformerName - Optional transformer name to check for specific ignore directive.
|
|
@@ -12,6 +12,7 @@ import { IGNORE_LINE_COMMENT_PREFIX } from '../constants/index.mjs';
|
|
|
12
12
|
* - `// transformer-ignore-next-line` (ignores all transformers)
|
|
13
13
|
* - `// transformer-ignore-next-line append-as-const` (specific transformer)
|
|
14
14
|
* - `// transformer-ignore-next-line append-as-const, replace-any-with-unknown` (multiple transformers)
|
|
15
|
+
* - Also accepts `ts-codemod-ignore-next-line`, `codemod-ignore-next-line`, `transform-ignore-next-line`
|
|
15
16
|
* @returns True if the node is preceded by the ignore comment on the immediately previous line, false otherwise.
|
|
16
17
|
*/
|
|
17
18
|
export const hasDisableNextLineComment = (
|
|
@@ -45,11 +46,16 @@ export const hasDisableNextLineComment = (
|
|
|
45
46
|
if (commentRange.getKind() === tsm.SyntaxKind.SingleLineCommentTrivia) {
|
|
46
47
|
const commentText = commentRange.getText().trim();
|
|
47
48
|
|
|
48
|
-
if
|
|
49
|
+
// Check if the comment contains any of the supported ignore prefixes
|
|
50
|
+
const matchedPrefix = IGNORE_LINE_COMMENT_PREFIXES.find((prefix) =>
|
|
51
|
+
commentText.includes(prefix),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (matchedPrefix !== undefined) {
|
|
49
55
|
// Extract the part after the prefix
|
|
50
56
|
const afterPrefix = commentText
|
|
51
|
-
.slice(commentText.indexOf(
|
|
52
|
-
.replace(
|
|
57
|
+
.slice(commentText.indexOf(matchedPrefix))
|
|
58
|
+
.replace(matchedPrefix, '')
|
|
53
59
|
.trim();
|
|
54
60
|
|
|
55
61
|
// If no transformer name specified, check if comment applies to all transformers
|
|
@@ -4,5 +4,6 @@ export * from './is-atomic-type-node.mjs';
|
|
|
4
4
|
export * from './is-readonly-node.mjs';
|
|
5
5
|
export * from './is-spread-parameter-node.mjs';
|
|
6
6
|
export * from './remove-parentheses.mjs';
|
|
7
|
+
export * from './should-avoid-parentheses-for-readonly.mjs';
|
|
7
8
|
export * from './unwrap-readonly.mjs';
|
|
8
9
|
export * from './wrap-with-parentheses.mjs';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
1
2
|
import * as ts from 'ts-morph';
|
|
2
3
|
|
|
3
4
|
export const isAsConstNode = (
|
|
@@ -42,6 +43,6 @@ export const isAsConstNode = (
|
|
|
42
43
|
// and that there are no type arguments (as const doesn't have them)
|
|
43
44
|
return (
|
|
44
45
|
typeNameNode.getText() === 'const' &&
|
|
45
|
-
typeNode.getTypeArguments()
|
|
46
|
+
Arr.isArrayOfLength(typeNode.getTypeArguments(), 0)
|
|
46
47
|
);
|
|
47
48
|
};
|