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.
Files changed (72) hide show
  1. package/dist/cmd/append-as-const.mjs +1 -1
  2. package/dist/cmd/convert-interface-to-type.mjs +1 -1
  3. package/dist/cmd/convert-to-readonly.mjs +1 -1
  4. package/dist/cmd/replace-any-with-unknown.mjs +1 -1
  5. package/dist/cmd/replace-record-with-unknown-record.mjs +1 -1
  6. package/dist/cmd/run-transformer-cli.mjs +3 -3
  7. package/dist/cmd/run-transformer-cli.mjs.map +1 -1
  8. package/dist/entry-point.mjs +2 -1
  9. package/dist/entry-point.mjs.map +1 -1
  10. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
  11. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +4 -4
  12. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
  13. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  14. package/dist/functions/ast-transformers/convert-to-readonly.mjs +31 -12
  15. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +1 -0
  17. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
  18. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +1 -0
  19. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
  20. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +1 -1
  21. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +13 -12
  22. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  23. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  24. package/dist/functions/ast-transformers/transform-source-code.mjs +41 -13
  25. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  26. package/dist/functions/constants/ignore-comment-text.d.mts +2 -2
  27. package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -1
  28. package/dist/functions/constants/ignore-comment-text.mjs +13 -3
  29. package/dist/functions/constants/ignore-comment-text.mjs.map +1 -1
  30. package/dist/functions/constants/index.mjs +1 -1
  31. package/dist/functions/functions/has-disable-next-line-comment.d.mts +3 -2
  32. package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
  33. package/dist/functions/functions/has-disable-next-line-comment.mjs +9 -6
  34. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  35. package/dist/functions/functions/index.d.mts +1 -0
  36. package/dist/functions/functions/index.d.mts.map +1 -1
  37. package/dist/functions/functions/index.mjs +1 -0
  38. package/dist/functions/functions/index.mjs.map +1 -1
  39. package/dist/functions/functions/is-as-const-node.d.mts.map +1 -1
  40. package/dist/functions/functions/is-as-const-node.mjs +2 -1
  41. package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
  42. package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts +16 -0
  43. package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -0
  44. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +32 -0
  45. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -0
  46. package/dist/functions/functions/wrap-with-parentheses.d.mts +13 -1
  47. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  48. package/dist/functions/functions/wrap-with-parentheses.mjs +58 -1
  49. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  50. package/dist/functions/index.mjs +2 -1
  51. package/dist/functions/index.mjs.map +1 -1
  52. package/dist/index.mjs +2 -1
  53. package/dist/index.mjs.map +1 -1
  54. package/package.json +16 -16
  55. package/src/cmd/append-as-const.mts +1 -1
  56. package/src/cmd/convert-interface-to-type.mts +1 -1
  57. package/src/cmd/convert-to-readonly.mts +1 -1
  58. package/src/cmd/replace-any-with-unknown.mts +1 -1
  59. package/src/cmd/replace-record-with-unknown-record.mts +1 -1
  60. package/src/cmd/run-transformer-cli.mts +3 -3
  61. package/src/functions/ast-transformers/convert-interface-to-type.mts +6 -7
  62. package/src/functions/ast-transformers/convert-to-readonly.mts +39 -13
  63. package/src/functions/ast-transformers/convert-to-readonly.test.mts +79 -0
  64. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +15 -12
  65. package/src/functions/ast-transformers/transform-source-code.mts +44 -15
  66. package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +129 -0
  67. package/src/functions/constants/ignore-comment-text.mts +12 -2
  68. package/src/functions/functions/has-disable-next-line-comment.mts +12 -6
  69. package/src/functions/functions/index.mts +1 -0
  70. package/src/functions/functions/is-as-const-node.mts +2 -1
  71. package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +32 -0
  72. 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.length > 0
46
- ? `<${typeParameters.map((tp) => tp.getText()).join(', ')}>`
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.length === 0) {
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.length === 0) {
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.length === 0) {
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
- wrapWithParentheses(`readonly ${T.getFullText()}`),
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
- wrapWithParentheses(`readonly ${T.getFullText()}`),
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
- wrapWithParentheses(`readonly ${T.getFullText()}`),
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
- wrapWithParentheses(`readonly ${node.getFullText()}`),
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
- wrapWithParentheses(`readonly ${node.getFullText()}`),
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 (options.ignoreEmptyObjectTypes && node.getMembers().length === 0) {
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) && n.getMembers().length === 0;
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 || nonEmptyTypeLiterals.length === 0
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.length > 0
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.length > 0
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.length > 0
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.length === 0
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 (properties.length === 0 && indexSignatures.length === 1) {
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.length === 1) {
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.length === 1 &&
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.length === 1 &&
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.length === 2 &&
208
- typeArgs[0]?.getText() === 'string' &&
209
- typeArgs[1]?.getText() === 'unknown'
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.length === 1) {
223
+ if (Arr.isArrayOfLength(typeArgs, 1)) {
221
224
  const innerType = typeArgs[0];
222
225
 
223
- if (innerType !== undefined && tsm.Node.isTypeReference(innerType)) {
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.length === 2 &&
231
- innerTypeArgs[0]?.getText() === 'string' &&
232
- innerTypeArgs[1]?.getText() === 'unknown'
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
- const match = /\/\*\s*transformer-ignore\s*(.*?)\s*\*\//u.exec(code);
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
- if (match === null) {
8
- return [];
9
- }
26
+ for (const { regex } of patterns) {
27
+ const match = regex.exec(code);
10
28
 
11
- const targetTransformers = match[1]?.trim() ?? '';
29
+ if (match !== null) {
30
+ const targetTransformers = match[1]?.trim() ?? '';
12
31
 
13
- // Empty means ignore all transformers
14
- if (targetTransformers === '') {
15
- return [];
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
- // Parse comma-separated transformer names
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
- if (
30
- ignoredTransformers.length === 0 &&
31
- !/\/\*\s*transformer-ignore\s*.*?\s*\*\//u.test(code)
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.length === 0) {
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 IGNORE_LINE_COMMENT_PREFIX = 'transformer-ignore-next-line';
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 IGNORE_FILE_COMMENT_PREFIX = 'transformer-ignore';
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 { IGNORE_LINE_COMMENT_PREFIX } from '../constants/index.mjs';
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 a
6
- * '// transformer-ignore-next-line' comment, optionally filtered by transformer name(s).
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 (commentText.includes(IGNORE_LINE_COMMENT_PREFIX)) {
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(IGNORE_LINE_COMMENT_PREFIX))
52
- .replace(IGNORE_LINE_COMMENT_PREFIX, '')
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().length === 0
46
+ Arr.isArrayOfLength(typeNode.getTypeArguments(), 0)
46
47
  );
47
48
  };