ts-codemod-lib 3.3.2 → 3.4.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 (26) hide show
  1. package/README.md +16 -16
  2. package/dist/functions/ast-transformers/append-as-const.mjs +11 -10
  3. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  4. package/dist/functions/ast-transformers/convert-to-readonly.mjs +74 -1
  5. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  6. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +10 -19
  7. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  8. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
  9. package/dist/functions/ast-transformers/transform-source-code.mjs +101 -49
  10. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
  11. package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -1
  12. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +1 -4
  13. package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
  14. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
  15. package/dist/functions/functions/wrap-with-parentheses.mjs +3 -4
  16. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
  17. package/package.json +42 -41
  18. package/src/functions/ast-transformers/append-as-const.mts +22 -20
  19. package/src/functions/ast-transformers/convert-to-readonly.mts +96 -1
  20. package/src/functions/ast-transformers/convert-to-readonly.test.mts +205 -10
  21. package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +1 -1
  22. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +13 -22
  23. package/src/functions/ast-transformers/transform-source-code.mts +158 -57
  24. package/src/functions/ast-transformers/transform-source-code.test.mts +123 -0
  25. package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +1 -5
  26. package/src/functions/functions/wrap-with-parentheses.mts +3 -5
@@ -6,7 +6,10 @@ import dedent from 'dedent';
6
6
  import * as prettierPluginEstree from 'prettier/plugins/estree';
7
7
  import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
8
8
  import * as prettier from 'prettier/standalone';
9
- import { isString } from 'ts-data-forge';
9
+ import { expectType } from 'ts-data-forge';
10
+ // Aliased: an import named `DeepReadonly` makes `vitest/prefer-describe-function-title`
11
+ // rewrite `describe('DeepReadonly', ...)` into a reference to this type.
12
+ import { type DeepReadonly as TsTypeForgeDeepReadonly } from 'ts-type-forge';
10
13
  import {
11
14
  convertToReadonlyTransformer,
12
15
  type ReadonlyTransformerOptions,
@@ -64,15 +67,8 @@ const normalizeWhitespaceForComparison = (code: string): string => {
64
67
  // 1. Protect newlines immediately following line comments
65
68
  const protectedCode = code.replaceAll(
66
69
  /(\/\/.*?)\r?\n/gu,
67
- // The strict standard library types a capture group as `unknown`: an
68
- // optional group is `undefined` when it does not participate, so `string`
69
- // is not something the signature can promise. This group is not optional,
70
- // so the narrowing never actually fails — it just has to be written.
71
- (match: string, ...args: readonly unknown[]): string => {
72
- const comment = args[0];
73
-
74
- return isString(comment) ? `${comment}${placeholder}` : match;
75
- },
70
+ (match: string, comment: string | undefined): string =>
71
+ comment === undefined ? match : `${comment}${placeholder}`,
76
72
  );
77
73
 
78
74
  // 2. Collapse multiple whitespace characters (including unprotected newlines) into a single space
@@ -2839,6 +2835,205 @@ describe(convertToReadonlyTransformer, () => {
2839
2835
  ])('$name', testFn);
2840
2836
  });
2841
2837
 
2838
+ describe('Union of DeepReadonly members', () => {
2839
+ test.each([
2840
+ {
2841
+ name: 'DeepReadonly<A> | DeepReadonly<B> -> DeepReadonly<A | B>',
2842
+ source: dedent`
2843
+ type T = DeepReadonly<{ a: number[] }> | DeepReadonly<{ b: string[] }>;
2844
+ `,
2845
+ expected: dedent`
2846
+ type T = DeepReadonly<{ a: number[] } | { b: string[] }>;
2847
+ `,
2848
+ },
2849
+ {
2850
+ name: 'Three DeepReadonly members',
2851
+ source: dedent`
2852
+ type T<A, B> = DeepReadonly<A> | DeepReadonly<B> | DeepReadonly<{ c: boolean[] }>;
2853
+ `,
2854
+ expected: dedent`
2855
+ type T<A, B> = DeepReadonly<A | B | { c: boolean[] }>;
2856
+ `,
2857
+ },
2858
+ {
2859
+ name: 'DeepReadonly members separated by other members',
2860
+ source: dedent`
2861
+ type T = DeepReadonly<{ a: number[] }> | string | Map<string, number> | DeepReadonly<{ b: string[] }>;
2862
+ `,
2863
+ expected: dedent`
2864
+ type T = DeepReadonly<{ a: number[] } | { b: string[] }> | ReadonlyMap<string, number> | string;
2865
+ `,
2866
+ },
2867
+ {
2868
+ name: 'A DeepReadonly member whose argument is itself a union',
2869
+ source: dedent`
2870
+ type T = DeepReadonly<{ a: number[] } | { b: string[] }> | DeepReadonly<{ c: boolean[] }>;
2871
+ `,
2872
+ expected: dedent`
2873
+ type T = DeepReadonly<{ a: number[] } | { b: string[] } | { c: boolean[] }>;
2874
+ `,
2875
+ },
2876
+ {
2877
+ // `() => void | X` would parse as a function returning `void | X`.
2878
+ name: 'A function type argument keeps its parentheses',
2879
+ source: dedent`
2880
+ type T = DeepReadonly<() => void> | DeepReadonly<{ b: string[] }>;
2881
+ `,
2882
+ expected: dedent`
2883
+ type T = DeepReadonly<(() => void) | { b: string[] }>;
2884
+ `,
2885
+ },
2886
+ {
2887
+ name: 'Nested in a type literal',
2888
+ source: dedent`
2889
+ type T = { x: DeepReadonly<{ a: number[] }> | DeepReadonly<{ b: string[] }> };
2890
+ `,
2891
+ expected: dedent`
2892
+ type T = Readonly<{ x: DeepReadonly<{ a: number[] } | { b: string[] }> }>;
2893
+ `,
2894
+ },
2895
+ {
2896
+ name: 'A single DeepReadonly member is left as written',
2897
+ source: dedent`
2898
+ type T = DeepReadonly<{ a: number[] }> | { b: string[] };
2899
+ `,
2900
+ expected: dedent`
2901
+ type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: readonly string[] }>;
2902
+ `,
2903
+ },
2904
+ {
2905
+ name: 'DeepReadonly and Readonly members are not merged',
2906
+ source: dedent`
2907
+ type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: string[] }>;
2908
+ `,
2909
+ expected: dedent`
2910
+ type T = DeepReadonly<{ a: number[] }> | Readonly<{ b: readonly string[] }>;
2911
+ `,
2912
+ },
2913
+ // Unlike `Readonly<A> & Readonly<B>`, an intersection of DeepReadonly
2914
+ // members is never merged; see `mergeDeepReadonlyUnionMembers` and the
2915
+ // premises test below.
2916
+ {
2917
+ name: 'An intersection of DeepReadonly members is left as written',
2918
+ source: dedent`
2919
+ type T = DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>;
2920
+ `,
2921
+ expected: dedent`
2922
+ type T = DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>;
2923
+ `,
2924
+ },
2925
+ {
2926
+ // The case merging would get wrong: `DeepReadonly<F & X>` is `F & X`.
2927
+ name: 'An intersection with a function type argument is left as written',
2928
+ source: dedent`
2929
+ type T = DeepReadonly<() => void> & DeepReadonly<{ x: number[] }>;
2930
+ `,
2931
+ expected: dedent`
2932
+ type T = DeepReadonly<() => void> & DeepReadonly<{ x: number[] }>;
2933
+ `,
2934
+ },
2935
+ {
2936
+ name: 'An intersection of generic DeepReadonly members is left as written',
2937
+ source: dedent`
2938
+ type T<A, B> = DeepReadonly<A> & DeepReadonly<B>;
2939
+ `,
2940
+ expected: dedent`
2941
+ type T<A, B> = DeepReadonly<A> & DeepReadonly<B>;
2942
+ `,
2943
+ },
2944
+ {
2945
+ name: 'An intersection under a custom typeName is left as written',
2946
+ source: dedent`
2947
+ type T = DeepRO<{ a: number[] }> & DeepRO<{ b: string[] }>;
2948
+ `,
2949
+ expected: dedent`
2950
+ type T = DeepRO<{ a: number[] }> & DeepRO<{ b: string[] }>;
2951
+ `,
2952
+ options: { DeepReadonly: { typeName: 'DeepRO' } },
2953
+ },
2954
+ {
2955
+ // The union around it has one DeepReadonly member, so nothing merges.
2956
+ name: 'An intersection of DeepReadonly members inside a union is left as written',
2957
+ source: dedent`
2958
+ type T = (DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>) | DeepReadonly<{ c: boolean[] }>;
2959
+ `,
2960
+ expected: dedent`
2961
+ type T = (DeepReadonly<{ a: number[] }> & DeepReadonly<{ b: string[] }>) | DeepReadonly<{ c: boolean[] }>;
2962
+ `,
2963
+ },
2964
+ {
2965
+ name: 'Custom DeepReadonly typeName',
2966
+ source: dedent`
2967
+ type T = DeepRO<{ a: number[] }> | DeepRO<{ b: string[] }>;
2968
+ `,
2969
+ expected: dedent`
2970
+ type T = DeepRO<{ a: number[] } | { b: string[] }>;
2971
+ `,
2972
+ options: { DeepReadonly: { typeName: 'DeepRO' } },
2973
+ },
2974
+ {
2975
+ name: 'Other generic types are not merged',
2976
+ source: dedent`
2977
+ type T<A, B> = Partial<A> | Partial<B>;
2978
+ `,
2979
+ expected: dedent`
2980
+ type T<A, B> = Partial<A> | Partial<B>;
2981
+ `,
2982
+ },
2983
+ ])('$name', testFn);
2984
+
2985
+ // What `mergeDeepReadonlyUnionMembers` relies on. If ts-type-forge's
2986
+ // `DeepReadonly` changes so that one of these no longer holds, revisit
2987
+ // which operators the codemod merges.
2988
+ test('premises of merging DeepReadonly members', () => {
2989
+ type F = () => void;
2990
+
2991
+ type X = Readonly<{ x: number[] }>;
2992
+
2993
+ type A = Readonly<{ a: number[] }>;
2994
+
2995
+ type B = Readonly<{ b: string[] }>;
2996
+
2997
+ // `|` commutes with DeepReadonly, so merging a union is safe.
2998
+ expectType<
2999
+ TsTypeForgeDeepReadonly<A | B>,
3000
+ TsTypeForgeDeepReadonly<A> | TsTypeForgeDeepReadonly<B>
3001
+ >('=');
3002
+
3003
+ expectType<
3004
+ TsTypeForgeDeepReadonly<F | X>,
3005
+ TsTypeForgeDeepReadonly<F> | TsTypeForgeDeepReadonly<X>
3006
+ >('=');
3007
+
3008
+ expectType<
3009
+ TsTypeForgeDeepReadonly<readonly string[] | X>,
3010
+ TsTypeForgeDeepReadonly<readonly string[]> | TsTypeForgeDeepReadonly<X>
3011
+ >('=');
3012
+
3013
+ // `&` does not: an intersection is tested as a whole and matches every
3014
+ // branch any one member matches. `F & X` hits the function branch and
3015
+ // comes back unchanged, leaving `x` mutable.
3016
+ expectType<TsTypeForgeDeepReadonly<F & X>, F & X>('=');
3017
+
3018
+ expectType<
3019
+ TsTypeForgeDeepReadonly<F & X>,
3020
+ TsTypeForgeDeepReadonly<F> & TsTypeForgeDeepReadonly<X>
3021
+ >('!=');
3022
+
3023
+ expectType<
3024
+ TsTypeForgeDeepReadonly<readonly string[] & X>,
3025
+ TsTypeForgeDeepReadonly<readonly string[]> & TsTypeForgeDeepReadonly<X>
3026
+ >('!=');
3027
+
3028
+ // `Readonly` over type literals does commute with `&`, which is what
3029
+ // merging `{ a: A } & Readonly<{ b: B }>` into one `Readonly` relies on.
3030
+ expectType<
3031
+ Readonly<{ a: number[] } & { b: string[] }>,
3032
+ Readonly<{ a: number[] }> & Readonly<{ b: string[] }>
3033
+ >('~=');
3034
+ });
3035
+ });
3036
+
2842
3037
  describe('Parenthesized types', () => {
2843
3038
  test.each([
2844
3039
  {
@@ -84,7 +84,7 @@ const transformInProject = (
84
84
  * off. A case that went through it therefore paid for the whole default
85
85
  * library, `lib.esnext.full.d.ts` with the DOM inside it, being parsed and
86
86
  * checked from scratch: ~300 ms a case, of which the file under test was a
87
- * fraction of a millisecond. Sixty of those took `test:cov` past Vitest's
87
+ * fraction of a millisecond. Sixty of those took `check:test:cov` past Vitest's
88
88
  * timeout on CI.
89
89
  *
90
90
  * Reusing the project is half of the answer and the smaller half — measured,
@@ -44,9 +44,7 @@ const processDeclarations = (
44
44
 
45
45
  const typeNode = typeAlias.getTypeNode();
46
46
 
47
- if (typeNode === undefined) return false;
48
-
49
- return visitTypeNode(typeNode);
47
+ return typeNode !== undefined && visitTypeNode(typeNode);
50
48
  });
51
49
 
52
50
  const interfaceModifications = container
@@ -103,9 +101,7 @@ const processInterfaceDeclaration = (
103
101
  .map((property) => {
104
102
  const typeNode = property.getTypeNode();
105
103
 
106
- if (typeNode === undefined) return false;
107
-
108
- return visitTypeNode(typeNode);
104
+ return typeNode !== undefined && visitTypeNode(typeNode);
109
105
  })
110
106
  .includes(true);
111
107
  };
@@ -170,13 +166,13 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
170
166
  if (tsm.Node.isPropertySignature(member)) {
171
167
  const typeNode = member.getTypeNode();
172
168
 
173
- return typeNode === undefined ? false : visitTypeNode(typeNode);
169
+ return typeNode !== undefined && visitTypeNode(typeNode);
174
170
  }
175
171
 
176
172
  if (tsm.Node.isIndexSignatureDeclaration(member)) {
177
173
  const typeNode = member.getReturnTypeNode();
178
174
 
179
- return typeNode === undefined ? false : visitTypeNode(typeNode);
175
+ return typeNode !== undefined && visitTypeNode(typeNode);
180
176
  }
181
177
 
182
178
  return false;
@@ -202,19 +198,12 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
202
198
  }
203
199
 
204
200
  // Handles `readonly T[]`, `readonly [...]`, `keyof T`
205
- if (tsm.Node.isTypeOperatorTypeNode(node)) {
206
- return visitTypeNode(node.getTypeNode());
207
- }
208
-
209
- if (tsm.Node.isRestTypeNode(node)) {
210
- return visitTypeNode(node.getTypeNode());
211
- }
212
-
213
- if (tsm.Node.isNamedTupleMember(node)) {
214
- return visitTypeNode(node.getTypeNode());
215
- }
216
-
217
- return false;
201
+ return (
202
+ (tsm.Node.isTypeOperatorTypeNode(node) ||
203
+ tsm.Node.isRestTypeNode(node) ||
204
+ tsm.Node.isNamedTupleMember(node)) &&
205
+ visitTypeNode(node.getTypeNode())
206
+ );
218
207
  };
219
208
 
220
209
  const isStringUnknownIndexSignature = (
@@ -305,7 +294,9 @@ const ensureUnknownRecordImport = (sourceFile: tsm.SourceFile): void => {
305
294
  .getNamedImports()
306
295
  .some((ni) => ni.getName() === UNKNOWN_RECORD_NAME);
307
296
 
308
- if (alreadyImported) return;
297
+ if (alreadyImported) {
298
+ return;
299
+ }
309
300
 
310
301
  existingImport.addNamedImport({
311
302
  name: UNKNOWN_RECORD_NAME,
@@ -2,80 +2,179 @@ import { Arr } from 'ts-data-forge';
2
2
  import * as tsm from 'ts-morph';
3
3
  import { type TsMorphTransformer } from './types.mjs';
4
4
 
5
- const extractFileIgnoreTransformers = (code: string): readonly string[] => {
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;
5
+ const fileIgnorePrefixes = [
6
+ 'transformer-ignore',
7
+ 'ts-codemod-ignore',
8
+ 'codemod-ignore',
9
+ 'transform-ignore',
10
+ ] as const;
11
+
12
+ /**
13
+ * Finds the file-level ignore comment `/* <prefix> name, name *\/` and returns
14
+ * the transformer names it lists (empty for all transformers), or `undefined`
15
+ * when the file has none. Whitespace, line breaks included, may surround the
16
+ * prefix and the names, but the names themselves must stay on one line. The
17
+ * prefixes are tried in order, each against the whole file.
18
+ *
19
+ * The scan runs in time linear in the length of `code`, whatever it contains.
20
+ */
21
+ const findFileIgnoreTransformers = (
22
+ code: string,
23
+ ): readonly string[] | undefined => {
24
+ for (const prefix of fileIgnorePrefixes) {
25
+ const names = findFileIgnoreComment(code, prefix);
26
+
27
+ if (names !== undefined) {
28
+ return names;
29
+ }
30
+ }
25
31
 
26
- for (const { regex } of patterns) {
27
- const match = regex.exec(code);
32
+ return undefined;
33
+ };
28
34
 
29
- if (match !== null) {
30
- const targetTransformers = match[1]?.trim() ?? '';
35
+ const findFileIgnoreComment = (
36
+ code: string,
37
+ prefix: string,
38
+ ): readonly string[] | undefined => {
39
+ // Queries into these searches only move forward, so each part of `code` is
40
+ // scanned a bounded number of times.
41
+ const findCommentEnd = createForwardSearch((from) =>
42
+ code.indexOf('*/', from),
43
+ );
31
44
 
32
- // Empty means ignore all transformers
33
- if (targetTransformers === '') {
34
- return [];
35
- }
45
+ const findLineTerminator = createForwardSearch((from) =>
46
+ indexOfLineTerminator(code, from),
47
+ );
48
+
49
+ const trimEndBefore = createLastValueCache((commentEnd: number) =>
50
+ skipWhitespaceBackward(code, commentEnd),
51
+ );
52
+
53
+ for (
54
+ let mut_open = code.indexOf('/*');
55
+ mut_open !== -1;
56
+ mut_open = code.indexOf('/*', mut_open + 2)
57
+ ) {
58
+ const prefixStart = skipWhitespace(code, mut_open + 2);
59
+
60
+ if (!code.startsWith(prefix, prefixStart)) {
61
+ continue;
62
+ }
63
+
64
+ const namesStart = skipWhitespace(code, prefixStart + prefix.length);
65
+
66
+ const commentEnd = findCommentEnd(namesStart);
67
+
68
+ // Nothing after this point is closed, so no later comment can match.
69
+ if (commentEnd === -1) {
70
+ return undefined;
71
+ }
72
+
73
+ const namesEnd = Math.max(namesStart, trimEndBefore(commentEnd));
36
74
 
37
- // Parse comma-separated transformer names
38
- return targetTransformers.split(',').map((name) => name.trim());
75
+ const lineTerminator = findLineTerminator(namesStart);
76
+
77
+ if (lineTerminator === -1 || lineTerminator >= namesEnd) {
78
+ const names = code.slice(namesStart, namesEnd);
79
+
80
+ // Empty means ignore all transformers; otherwise parse comma-separated
81
+ // transformer names
82
+ return names === '' ? [] : names.split(',').map((name) => name.trim());
39
83
  }
40
84
  }
41
85
 
42
- return [];
86
+ return undefined;
43
87
  };
44
88
 
45
89
  const shouldSkipFile = (
46
- code: string,
47
- transformerName: string | undefined,
48
- ): boolean => {
49
- const ignoredTransformers = extractFileIgnoreTransformers(code);
50
-
51
- // If no file-level ignore comment found, don't skip
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 (!hasFileIgnoreComment && Arr.isEmpty(ignoredTransformers)) {
62
- return false;
90
+ ignoredTransformers: readonly string[] | undefined,
91
+ transformerName: string,
92
+ ): boolean =>
93
+ ignoredTransformers !== undefined &&
94
+ // Empty array means ignore all transformers (file-level ignore without
95
+ // specific transformers); otherwise check if the transformer is in the
96
+ // ignore list
97
+ (Arr.isEmpty(ignoredTransformers) ||
98
+ ignoredTransformers.includes(transformerName));
99
+
100
+ /**
101
+ * Wraps a search returning the first index at or after `from` (or -1), reusing
102
+ * the previous answer while it still holds for a later `from`.
103
+ */
104
+ const createForwardSearch = (
105
+ search: (from: number) => number,
106
+ ): ((from: number) => number) => {
107
+ let mut_from = Number.POSITIVE_INFINITY;
108
+
109
+ let mut_found = -1;
110
+
111
+ return (from) => {
112
+ if (from < mut_from || (mut_found !== -1 && from > mut_found)) {
113
+ mut_from = from;
114
+
115
+ mut_found = search(from);
116
+ }
117
+
118
+ return mut_found;
119
+ };
120
+ };
121
+
122
+ const createLastValueCache = (
123
+ fn: (arg: number) => number,
124
+ ): ((arg: number) => number) => {
125
+ let mut_arg: number | undefined = undefined;
126
+
127
+ let mut_value = 0;
128
+
129
+ return (arg) => {
130
+ if (arg !== mut_arg) {
131
+ mut_arg = arg;
132
+
133
+ mut_value = fn(arg);
134
+ }
135
+
136
+ return mut_value;
137
+ };
138
+ };
139
+
140
+ const skipWhitespace = (code: string, from: number): number => {
141
+ let mut_index = from;
142
+
143
+ while (mut_index < code.length && isWhitespace(code[mut_index])) {
144
+ mut_index += 1;
63
145
  }
64
146
 
65
- // Empty array means ignore all transformers (file-level ignore without specific transformers)
66
- if (Arr.isEmpty(ignoredTransformers)) {
67
- return true;
147
+ return mut_index;
148
+ };
149
+
150
+ const skipWhitespaceBackward = (code: string, end: number): number => {
151
+ let mut_index = end;
152
+
153
+ while (mut_index > 0 && isWhitespace(code[mut_index - 1])) {
154
+ mut_index -= 1;
68
155
  }
69
156
 
70
- // If transformer name is not specified, don't skip
71
- if (transformerName === undefined) {
72
- return false;
157
+ return mut_index;
158
+ };
159
+
160
+ const indexOfLineTerminator = (code: string, from: number): number => {
161
+ let mut_index = from;
162
+
163
+ while (mut_index < code.length && !isLineTerminator(code[mut_index])) {
164
+ mut_index += 1;
73
165
  }
74
166
 
75
- // Check if the transformer is in the ignore list
76
- return ignoredTransformers.includes(transformerName);
167
+ return mut_index < code.length ? mut_index : -1;
77
168
  };
78
169
 
170
+ // Same set as `\s` and `String.prototype.trim`
171
+ const isWhitespace = (char: string | undefined): boolean =>
172
+ char !== undefined && /^\s$/u.test(char);
173
+
174
+ // The characters `.` does not match
175
+ const isLineTerminator = (char: string | undefined): boolean =>
176
+ char !== undefined && /^[\n\r\u{2028}\u{2029}]$/u.test(char);
177
+
79
178
  export const transformSourceCode = (
80
179
  code: string,
81
180
  isTsx: boolean,
@@ -96,10 +195,12 @@ export const transformSourceCode = (
96
195
  code,
97
196
  );
98
197
 
198
+ const ignoredTransformers = findFileIgnoreTransformers(code);
199
+
99
200
  for (const transformer of transformers) {
100
201
  const transformerName = transformer.name;
101
202
 
102
- if (shouldSkipFile(code, transformerName)) {
203
+ if (shouldSkipFile(ignoredTransformers, transformerName)) {
103
204
  if (debug) {
104
205
  console.debug(
105
206
  `skipped by ignore-file comment for transformer: ${transformerName}`,