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.
- package/README.md +16 -16
- package/dist/functions/ast-transformers/append-as-const.mjs +11 -10
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +74 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +10 -19
- 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 +101 -49
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.d.mts.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +1 -4
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +3 -4
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/package.json +42 -41
- package/src/functions/ast-transformers/append-as-const.mts +22 -20
- package/src/functions/ast-transformers/convert-to-readonly.mts +96 -1
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +205 -10
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +1 -1
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +13 -22
- package/src/functions/ast-transformers/transform-source-code.mts +158 -57
- package/src/functions/ast-transformers/transform-source-code.test.mts +123 -0
- package/src/functions/functions/should-avoid-parentheses-for-readonly.mts +1 -5
- 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 {
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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)
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
47
|
-
transformerName: string
|
|
48
|
-
): boolean =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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(
|
|
203
|
+
if (shouldSkipFile(ignoredTransformers, transformerName)) {
|
|
103
204
|
if (debug) {
|
|
104
205
|
console.debug(
|
|
105
206
|
`skipped by ignore-file comment for transformer: ${transformerName}`,
|