ts-codemod-lib 3.2.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/entry-point.mjs +2 -23
- package/dist/entry-point.mjs.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +330 -292
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +85 -70
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +969 -840
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +65 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts.map +1 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +297 -0
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -0
- package/dist/functions/ast-transformers/index.d.mts +1 -0
- package/dist/functions/ast-transformers/index.d.mts.map +1 -1
- package/dist/functions/ast-transformers/index.mjs +10 -11
- package/dist/functions/ast-transformers/index.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +14 -7
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +12 -15
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +63 -58
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -5
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +15 -20
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +30 -34
- package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +197 -156
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.mjs +74 -75
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/ast-transformers/types.mjs +2 -2
- package/dist/functions/ast-transformers/types.mjs.map +1 -1
- package/dist/functions/constants/ignore-comment-text.mjs +11 -13
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -1
- package/dist/functions/constants/index.mjs +2 -2
- package/dist/functions/constants/index.mjs.map +1 -1
- package/dist/functions/functions/has-disable-next-line-comment.mjs +67 -38
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/index.mjs +11 -11
- package/dist/functions/functions/index.mjs.map +1 -1
- package/dist/functions/functions/is-as-const-node.mjs +25 -20
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.mjs +245 -25
- package/dist/functions/functions/is-atomic-type-node.mjs.map +1 -1
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs +34 -13
- package/dist/functions/functions/is-conditional-type-distribution-guard.mjs.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +120 -14
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/is-spread-parameter-node.mjs +5 -6
- package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -1
- package/dist/functions/functions/remove-parentheses.mjs +4 -4
- package/dist/functions/functions/remove-parentheses.mjs.map +1 -1
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs +23 -9
- package/dist/functions/functions/should-avoid-parentheses-for-readonly.mjs.map +1 -1
- package/dist/functions/functions/unwrap-readonly.mjs +3 -4
- package/dist/functions/functions/unwrap-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 +54 -29
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/index.mjs +5 -23
- package/dist/functions/index.mjs.map +1 -1
- package/dist/functions/utils/index.mjs +2 -2
- package/dist/functions/utils/index.mjs.map +1 -1
- package/dist/functions/utils/replace-with-debug.mjs +4 -6
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
- package/dist/index.mjs +2 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -12
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +10 -1
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +438 -0
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +619 -0
- package/src/functions/ast-transformers/index.mts +1 -0
- package/src/functions/functions/wrap-with-parentheses.mts +5 -2
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/* eslint-disable tree-shakable/import-star */
|
|
2
|
+
/* eslint-disable vitest/expect-expect */
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
5
|
+
import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
|
|
6
|
+
import * as prettier from 'prettier/standalone';
|
|
7
|
+
import * as tsm from 'ts-morph';
|
|
8
|
+
import { enableNoUncheckedIndexedAccessTransformer } from './enable-no-unchecked-indexed-access.mjs';
|
|
9
|
+
import { transformSourceCode } from './transform-source-code.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Declarations every case is prefixed with, so that each `source` can be a
|
|
13
|
+
* single expression statement. `declare const` keeps them free of index
|
|
14
|
+
* accesses of their own.
|
|
15
|
+
*
|
|
16
|
+
* `console` is among them because {@link project} loads only the libraries
|
|
17
|
+
* these cases need, and the one that declares `console` is not one of them.
|
|
18
|
+
*/
|
|
19
|
+
const preamble = dedent`
|
|
20
|
+
declare const console: { log: (...args: readonly unknown[]) => void };
|
|
21
|
+
declare const xs: readonly number[];
|
|
22
|
+
declare const mut_ys: number[];
|
|
23
|
+
declare const pair: readonly [number, number];
|
|
24
|
+
declare const rest: readonly [number, number, ...(readonly number[])];
|
|
25
|
+
declare const known: { readonly a: number };
|
|
26
|
+
declare const rec: Record<string, number>;
|
|
27
|
+
declare const loose: { readonly a: number; readonly [k: string]: number };
|
|
28
|
+
declare const nested: Record<string, readonly number[]>;
|
|
29
|
+
declare const sparse: readonly (number | undefined)[];
|
|
30
|
+
declare const fns: readonly ((x: number) => void)[];
|
|
31
|
+
declare const ctors: readonly (new () => object)[];
|
|
32
|
+
declare const str: string;
|
|
33
|
+
declare const i: number;
|
|
34
|
+
declare const k: string;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const testFn = async ({
|
|
38
|
+
source,
|
|
39
|
+
expected,
|
|
40
|
+
options,
|
|
41
|
+
debug,
|
|
42
|
+
}: Readonly<{
|
|
43
|
+
source: string;
|
|
44
|
+
expected: string;
|
|
45
|
+
options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0];
|
|
46
|
+
debug?: boolean;
|
|
47
|
+
}>): Promise<void> => {
|
|
48
|
+
if (debug !== true) {
|
|
49
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
50
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transformed = await formatter(
|
|
54
|
+
transformInProject(`${preamble}\n\n${source}`, { ...options, debug }),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const expectedFormatted = await formatter(`${preamble}\n\n${expected}`);
|
|
58
|
+
|
|
59
|
+
expect(transformed).toBe(expectedFormatted);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Runs the transformer over `code` in {@link project}, and returns what it
|
|
64
|
+
* made of it.
|
|
65
|
+
*/
|
|
66
|
+
const transformInProject = (
|
|
67
|
+
code: string,
|
|
68
|
+
options?: Parameters<typeof enableNoUncheckedIndexedAccessTransformer>[0],
|
|
69
|
+
): string => {
|
|
70
|
+
const sourceAst = project.createSourceFile('source.ts', code, {
|
|
71
|
+
overwrite: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
enableNoUncheckedIndexedAccessTransformer(options).transform(sourceAst);
|
|
75
|
+
|
|
76
|
+
return sourceAst.getFullText();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The one project every case is checked in, rather than one project per case.
|
|
81
|
+
*
|
|
82
|
+
* `transformSourceCode` builds a project per call, and this transformer checks
|
|
83
|
+
* the file twice — once with `noUncheckedIndexedAccess` on and once with it
|
|
84
|
+
* off. A case that went through it therefore paid for the whole default
|
|
85
|
+
* library, `lib.esnext.full.d.ts` with the DOM inside it, being parsed and
|
|
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
|
|
88
|
+
* timeout on CI.
|
|
89
|
+
*
|
|
90
|
+
* Reusing the project is half of the answer and the smaller half — measured,
|
|
91
|
+
* it takes a case from ~300 ms to ~260 ms, because what an edit invalidates is
|
|
92
|
+
* the program, not the parsed library behind it. Naming the libraries the
|
|
93
|
+
* cases actually need is the rest: together they bring a case to ~25 ms.
|
|
94
|
+
*
|
|
95
|
+
* What that leaves out — `transformSourceCode` itself, and the transformer
|
|
96
|
+
* against the full library — is what the tests at the bottom of this file
|
|
97
|
+
* still cover.
|
|
98
|
+
*/
|
|
99
|
+
const project = new tsm.Project({
|
|
100
|
+
useInMemoryFileSystem: true,
|
|
101
|
+
compilerOptions: {
|
|
102
|
+
target: tsm.ts.ScriptTarget.ESNext,
|
|
103
|
+
module: tsm.ts.ModuleKind.ESNext,
|
|
104
|
+
// `es2015` rather than `es5` for `Symbol.iterator`, which the spread and
|
|
105
|
+
// `for-of` cases need at this target.
|
|
106
|
+
lib: ['lib.es2015.d.ts'],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const formatter = (code: string): Promise<string> =>
|
|
111
|
+
prettier.format(code, {
|
|
112
|
+
parser: 'typescript',
|
|
113
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
117
|
+
describe('accesses the option widens with undefined', () => {
|
|
118
|
+
test.each([
|
|
119
|
+
{
|
|
120
|
+
name: 'array with a literal index',
|
|
121
|
+
source: 'console.log(xs[0].toFixed());',
|
|
122
|
+
expected: 'console.log(xs[0]!.toFixed());',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'array with a computed index',
|
|
126
|
+
source: 'console.log(xs[i] + 1);',
|
|
127
|
+
expected: 'console.log(xs[i]! + 1);',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'index signature record',
|
|
131
|
+
source: "console.log(rec['a'].toFixed());",
|
|
132
|
+
expected: "console.log(rec['a']!.toFixed());",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'index signature record with a computed key',
|
|
136
|
+
source: 'console.log(rec[k].toFixed());',
|
|
137
|
+
expected: 'console.log(rec[k]!.toFixed());',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'dotted read of an index signature',
|
|
141
|
+
source: 'console.log(rec.a.toFixed());',
|
|
142
|
+
expected: 'console.log(rec.a!.toFixed());',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'dotted read of an undeclared key of a type with an index signature',
|
|
146
|
+
source: 'console.log(loose.b.toFixed());',
|
|
147
|
+
expected: 'console.log(loose.b!.toFixed());',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'string index',
|
|
151
|
+
source: 'console.log(str[0].charCodeAt(0));',
|
|
152
|
+
expected: 'console.log(str[0]!.charCodeAt(0));',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'tuple index past its fixed part',
|
|
156
|
+
source: 'console.log(rest[9].toFixed());',
|
|
157
|
+
expected: 'console.log(rest[9]!.toFixed());',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'chained accesses are asserted at every step',
|
|
161
|
+
source: "console.log(nested['k'][0].toFixed());",
|
|
162
|
+
expected: "console.log(nested['k']![0]!.toFixed());",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'callee position',
|
|
166
|
+
source: 'fns[0](1);',
|
|
167
|
+
expected: 'fns[0]!(1);',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'constructor position',
|
|
171
|
+
source: 'console.log(new ctors[0]());',
|
|
172
|
+
expected: 'console.log(new ctors[0]!());',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'spread element',
|
|
176
|
+
source: "console.log(...nested['k']);",
|
|
177
|
+
expected: "console.log(...nested['k']!);",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'for-of iterable',
|
|
181
|
+
source: "for (const v of nested['k']) { console.log(v); }",
|
|
182
|
+
expected: "for (const v of nested['k']!) { console.log(v); }",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'unary minus operand',
|
|
186
|
+
source: 'console.log(-xs[0]);',
|
|
187
|
+
expected: 'console.log(-xs[0]!);',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'index argument of another access',
|
|
191
|
+
source: 'console.log(rec[String(xs[0])].toFixed());',
|
|
192
|
+
expected: 'console.log(rec[String(xs[0]!)]!.toFixed());',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'initializer of an untyped const, where the error surfaces later',
|
|
196
|
+
source: dedent`
|
|
197
|
+
const head = xs[0];
|
|
198
|
+
console.log(head.toFixed());
|
|
199
|
+
`,
|
|
200
|
+
expected: dedent`
|
|
201
|
+
const head = xs[0]!;
|
|
202
|
+
console.log(head.toFixed());
|
|
203
|
+
`,
|
|
204
|
+
},
|
|
205
|
+
])('$name', testFn);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('indices whose presence the type guarantees', () => {
|
|
209
|
+
test.each([
|
|
210
|
+
{
|
|
211
|
+
name: 'tuple index within its length',
|
|
212
|
+
source: 'console.log(pair[1].toFixed());',
|
|
213
|
+
expected: 'console.log(pair[1].toFixed());',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'fixed part of a tuple with a rest element',
|
|
217
|
+
source: 'console.log(rest[1].toFixed());',
|
|
218
|
+
expected: 'console.log(rest[1].toFixed());',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'declared property of an object without an index signature',
|
|
222
|
+
source: "console.log(known['a'].toFixed());",
|
|
223
|
+
expected: "console.log(known['a'].toFixed());",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'declared property of a type that also has an index signature',
|
|
227
|
+
source: 'console.log(loose.a.toFixed());',
|
|
228
|
+
expected: 'console.log(loose.a.toFixed());',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'ordinary property access',
|
|
232
|
+
source: 'console.log(known.a.toFixed());',
|
|
233
|
+
expected: 'console.log(known.a.toFixed());',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'element type that already contained undefined',
|
|
237
|
+
source: 'console.log(sparse[0]?.toFixed());',
|
|
238
|
+
expected: 'console.log(sparse[0]?.toFixed());',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'access already narrowed by a guard',
|
|
242
|
+
source: dedent`
|
|
243
|
+
if (xs[0] !== undefined) {
|
|
244
|
+
console.log(xs[0].toFixed());
|
|
245
|
+
}
|
|
246
|
+
`,
|
|
247
|
+
expected: dedent`
|
|
248
|
+
if (xs[0] !== undefined) {
|
|
249
|
+
console.log(xs[0].toFixed());
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'value of unresolvable type',
|
|
255
|
+
source: dedent`
|
|
256
|
+
import { imported } from './other.mjs';
|
|
257
|
+
|
|
258
|
+
console.log(imported[0].toFixed());
|
|
259
|
+
`,
|
|
260
|
+
expected: dedent`
|
|
261
|
+
import { imported } from './other.mjs';
|
|
262
|
+
|
|
263
|
+
console.log(imported[0].toFixed());
|
|
264
|
+
`,
|
|
265
|
+
},
|
|
266
|
+
])('$name', testFn);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('positions that cannot take an assertion', () => {
|
|
270
|
+
test.each([
|
|
271
|
+
{
|
|
272
|
+
name: 'assignment target',
|
|
273
|
+
source: 'mut_ys[0] = 1;',
|
|
274
|
+
expected: 'mut_ys[0] = 1;',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'compound assignment target',
|
|
278
|
+
source: 'mut_ys[0] += 1;',
|
|
279
|
+
expected: 'mut_ys[0] += 1;',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'logical assignment target',
|
|
283
|
+
source: 'mut_ys[0] ??= 1;',
|
|
284
|
+
expected: 'mut_ys[0] ??= 1;',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'increment target',
|
|
288
|
+
source: 'mut_ys[0]++;',
|
|
289
|
+
expected: 'mut_ys[0]++;',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'decrement target',
|
|
293
|
+
source: '--mut_ys[0];',
|
|
294
|
+
expected: '--mut_ys[0];',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'delete target',
|
|
298
|
+
source: "delete rec['a'];",
|
|
299
|
+
expected: "delete rec['a'];",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'dotted delete target',
|
|
303
|
+
source: 'delete rec.a;',
|
|
304
|
+
expected: 'delete rec.a;',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'destructuring assignment target',
|
|
308
|
+
source: '[mut_ys[0]] = [1];',
|
|
309
|
+
expected: '[mut_ys[0]] = [1];',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'for-of loop target',
|
|
313
|
+
source: 'for (mut_ys[0] of xs) { break; }',
|
|
314
|
+
expected: 'for (mut_ys[0] of xs) { break; }',
|
|
315
|
+
},
|
|
316
|
+
])('$name', testFn);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('positions that already account for undefined', () => {
|
|
320
|
+
test.each([
|
|
321
|
+
{
|
|
322
|
+
name: 'existing non-null assertion',
|
|
323
|
+
source: 'console.log(xs[0]!.toFixed());',
|
|
324
|
+
expected: 'console.log(xs[0]!.toFixed());',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'type assertion',
|
|
328
|
+
source: 'console.log((xs[0] as number).toFixed());',
|
|
329
|
+
expected: 'console.log((xs[0] as number).toFixed());',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'optional property access',
|
|
333
|
+
source: 'console.log(xs[0]?.toFixed());',
|
|
334
|
+
expected: 'console.log(xs[0]?.toFixed());',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'property access further along an optional chain',
|
|
338
|
+
source: 'console.log(nested?.k[0]);',
|
|
339
|
+
expected: 'console.log(nested?.k[0]);',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'optional call',
|
|
343
|
+
source: 'fns[0]?.(1);',
|
|
344
|
+
expected: 'fns[0]?.(1);',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'nullish coalescing',
|
|
348
|
+
source: 'console.log(xs[0] ?? 0);',
|
|
349
|
+
expected: 'console.log(xs[0] ?? 0);',
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'logical and',
|
|
353
|
+
source: 'console.log(xs[0] && 1);',
|
|
354
|
+
expected: 'console.log(xs[0] && 1);',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: 'typeof',
|
|
358
|
+
source: 'console.log(typeof xs[0]);',
|
|
359
|
+
expected: 'console.log(typeof xs[0]);',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'equality against undefined',
|
|
363
|
+
source: 'console.log(xs[0] === undefined);',
|
|
364
|
+
expected: 'console.log(xs[0] === undefined);',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'if condition',
|
|
368
|
+
source: 'if (xs[0]) { console.log(1); }',
|
|
369
|
+
expected: 'if (xs[0]) { console.log(1); }',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: 'while condition',
|
|
373
|
+
source: 'while (xs[0]) { break; }',
|
|
374
|
+
expected: 'while (xs[0]) { break; }',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: 'negation',
|
|
378
|
+
source: 'console.log(!xs[0]);',
|
|
379
|
+
expected: 'console.log(!xs[0]);',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'switch subject, where an asserted subject would reject `case undefined`',
|
|
383
|
+
source: 'switch (xs[0]) { case undefined: break; default: break; }',
|
|
384
|
+
expected: 'switch (xs[0]) { case undefined: break; default: break; }',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'ternary condition',
|
|
388
|
+
source: 'console.log(xs[0] ? 1 : 2);',
|
|
389
|
+
expected: 'console.log(xs[0] ? 1 : 2);',
|
|
390
|
+
},
|
|
391
|
+
])('$name', testFn);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe('applyLevel: "avoidWhereUndefinedIsAllowed"', () => {
|
|
395
|
+
test.each([
|
|
396
|
+
{
|
|
397
|
+
name: 'skips an annotation that accepts undefined',
|
|
398
|
+
source: 'const a: number | undefined = xs[0];',
|
|
399
|
+
expected: 'const a: number | undefined = xs[0];',
|
|
400
|
+
options: { applyLevel: 'avoidWhereUndefinedIsAllowed' },
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'keeps an annotation that does not',
|
|
404
|
+
source: 'const a: number = xs[0];',
|
|
405
|
+
expected: 'const a: number = xs[0]!;',
|
|
406
|
+
options: { applyLevel: 'avoidWhereUndefinedIsAllowed' },
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'keeps a position with no contextual type',
|
|
410
|
+
source: 'console.log(xs[0].toFixed());',
|
|
411
|
+
expected: 'console.log(xs[0]!.toFixed());',
|
|
412
|
+
options: { applyLevel: 'avoidWhereUndefinedIsAllowed' },
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'the default level asserts even where undefined is allowed',
|
|
416
|
+
source: 'const a: number | undefined = xs[0];',
|
|
417
|
+
expected: 'const a: number | undefined = xs[0]!;',
|
|
418
|
+
},
|
|
419
|
+
] as const)('$name', testFn);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('ignore comments', () => {
|
|
423
|
+
test.each([
|
|
424
|
+
{
|
|
425
|
+
name: 'transformer-ignore-next-line above the statement',
|
|
426
|
+
source: dedent`
|
|
427
|
+
// transformer-ignore-next-line
|
|
428
|
+
console.log(xs[0].toFixed());
|
|
429
|
+
`,
|
|
430
|
+
expected: dedent`
|
|
431
|
+
// transformer-ignore-next-line
|
|
432
|
+
console.log(xs[0].toFixed());
|
|
433
|
+
`,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'transformer-ignore-next-line naming this transformer',
|
|
437
|
+
source: dedent`
|
|
438
|
+
// transformer-ignore-next-line enable-no-unchecked-indexed-access
|
|
439
|
+
console.log(xs[0].toFixed());
|
|
440
|
+
`,
|
|
441
|
+
expected: dedent`
|
|
442
|
+
// transformer-ignore-next-line enable-no-unchecked-indexed-access
|
|
443
|
+
console.log(xs[0].toFixed());
|
|
444
|
+
`,
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'transformer-ignore-next-line naming a different transformer',
|
|
448
|
+
source: dedent`
|
|
449
|
+
// transformer-ignore-next-line append-as-const
|
|
450
|
+
console.log(xs[0].toFixed());
|
|
451
|
+
`,
|
|
452
|
+
expected: dedent`
|
|
453
|
+
// transformer-ignore-next-line append-as-const
|
|
454
|
+
console.log(xs[0]!.toFixed());
|
|
455
|
+
`,
|
|
456
|
+
},
|
|
457
|
+
])('$name', testFn);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('a file-level transformer-ignore comment skips the file', () => {
|
|
461
|
+
const source = dedent`
|
|
462
|
+
/* transformer-ignore */
|
|
463
|
+
declare const xs: readonly number[];
|
|
464
|
+
|
|
465
|
+
console.log(xs[0].toFixed());
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
expect(
|
|
469
|
+
transformSourceCode(source, false, [
|
|
470
|
+
enableNoUncheckedIndexedAccessTransformer(),
|
|
471
|
+
]),
|
|
472
|
+
).toBe(source);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('the surrounding formatting is left byte-for-byte alone', () => {
|
|
476
|
+
const source = [
|
|
477
|
+
'declare const xs: readonly number[];',
|
|
478
|
+
'',
|
|
479
|
+
'const value = xs[ 0 ] . toFixed( ) ; // trailing comment',
|
|
480
|
+
'',
|
|
481
|
+
'/* a block comment */',
|
|
482
|
+
'console.log(value);',
|
|
483
|
+
'',
|
|
484
|
+
].join('\n');
|
|
485
|
+
|
|
486
|
+
expect(
|
|
487
|
+
transformSourceCode(source, false, [
|
|
488
|
+
enableNoUncheckedIndexedAccessTransformer(),
|
|
489
|
+
]),
|
|
490
|
+
).toBe(source.replace('xs[ 0 ]', 'xs[ 0 ]!'));
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('JSX files are transformed as well', () => {
|
|
494
|
+
const source = dedent`
|
|
495
|
+
declare const xs: readonly (readonly number[])[];
|
|
496
|
+
|
|
497
|
+
export const C = (): JSX.Element => <div>{xs[0].length}</div>;
|
|
498
|
+
`;
|
|
499
|
+
|
|
500
|
+
expect(
|
|
501
|
+
transformSourceCode(source, true, [
|
|
502
|
+
enableNoUncheckedIndexedAccessTransformer(),
|
|
503
|
+
]),
|
|
504
|
+
).toBe(source.replace('xs[0]', 'xs[0]!'));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('the errors the option introduces are gone from the result', () => {
|
|
508
|
+
const source = dedent`
|
|
509
|
+
declare const console: { log: (...args: readonly unknown[]) => void };
|
|
510
|
+
|
|
511
|
+
const parseRoute = (path: string): string => {
|
|
512
|
+
const parts = path.split('/');
|
|
513
|
+
|
|
514
|
+
const head = parts[0];
|
|
515
|
+
|
|
516
|
+
return head.toUpperCase() + parts[parts.length - 1].toLowerCase();
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const counts: Record<string, number> = {};
|
|
520
|
+
|
|
521
|
+
const bump = (key: string): void => {
|
|
522
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const table: readonly (readonly string[])[] = [['a']];
|
|
526
|
+
|
|
527
|
+
const cell = (r: number, c: number): string => table[r][c];
|
|
528
|
+
|
|
529
|
+
const pair: readonly [number, number] = [1, 2];
|
|
530
|
+
|
|
531
|
+
const sum = pair[0] + pair[1];
|
|
532
|
+
|
|
533
|
+
console.log(parseRoute, bump, cell, sum);
|
|
534
|
+
`;
|
|
535
|
+
|
|
536
|
+
expect(diagnosticsUnderTheOption(source).length).toBeGreaterThan(0);
|
|
537
|
+
|
|
538
|
+
assert.deepStrictEqual(
|
|
539
|
+
diagnosticsUnderTheOption(transformInProject(source)),
|
|
540
|
+
[],
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('array destructuring is left alone, since `!` has nowhere to go there', () => {
|
|
545
|
+
const source = dedent`
|
|
546
|
+
declare const xs: readonly number[];
|
|
547
|
+
|
|
548
|
+
const [head] = xs;
|
|
549
|
+
|
|
550
|
+
console.log(head);
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
expect(
|
|
554
|
+
transformSourceCode(source, false, [
|
|
555
|
+
enableNoUncheckedIndexedAccessTransformer(),
|
|
556
|
+
]),
|
|
557
|
+
).toBe(source);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test('the compiler options it overrides are restored afterwards', () => {
|
|
561
|
+
const mut_seen: {
|
|
562
|
+
noUncheckedIndexedAccess: boolean | undefined;
|
|
563
|
+
strictNullChecks: boolean | undefined;
|
|
564
|
+
}[] = [];
|
|
565
|
+
|
|
566
|
+
transformSourceCode(
|
|
567
|
+
'declare const xs: readonly number[];\nconsole.log(xs[0]);\n',
|
|
568
|
+
false,
|
|
569
|
+
[
|
|
570
|
+
enableNoUncheckedIndexedAccessTransformer(),
|
|
571
|
+
{
|
|
572
|
+
name: 'record-compiler-options',
|
|
573
|
+
transform: (sourceAst) => {
|
|
574
|
+
const compilerOptions = sourceAst.getProject().getCompilerOptions();
|
|
575
|
+
|
|
576
|
+
mut_seen.push({
|
|
577
|
+
noUncheckedIndexedAccess:
|
|
578
|
+
compilerOptions.noUncheckedIndexedAccess,
|
|
579
|
+
strictNullChecks: compilerOptions.strictNullChecks,
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
assert.deepStrictEqual(mut_seen, [
|
|
587
|
+
{ noUncheckedIndexedAccess: undefined, strictNullChecks: undefined },
|
|
588
|
+
]);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
/** The compiler's own verdict on the code, with the option this transformer prepares for turned on. */
|
|
593
|
+
const diagnosticsUnderTheOption = (code: string): readonly string[] =>
|
|
594
|
+
diagnosticsProject
|
|
595
|
+
.createSourceFile('source.ts', code, { overwrite: true })
|
|
596
|
+
.getPreEmitDiagnostics()
|
|
597
|
+
.map((diagnostic) => diagnostic.getMessageText())
|
|
598
|
+
.map((messageText) =>
|
|
599
|
+
typeof messageText === 'string'
|
|
600
|
+
? messageText
|
|
601
|
+
: messageText.getMessageText(),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* A second project, because {@link project} has to start with the option off,
|
|
606
|
+
* and reused for the reason that one is. `getPreEmitDiagnostics` checks the
|
|
607
|
+
* whole program rather than one expression, so the library it is pointed at
|
|
608
|
+
* costs more here than anywhere else in this file.
|
|
609
|
+
*/
|
|
610
|
+
const diagnosticsProject = new tsm.Project({
|
|
611
|
+
useInMemoryFileSystem: true,
|
|
612
|
+
compilerOptions: {
|
|
613
|
+
target: tsm.ts.ScriptTarget.ESNext,
|
|
614
|
+
module: tsm.ts.ModuleKind.ESNext,
|
|
615
|
+
lib: ['lib.es2015.d.ts'],
|
|
616
|
+
strict: true,
|
|
617
|
+
noUncheckedIndexedAccess: true,
|
|
618
|
+
},
|
|
619
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './append-as-const.mjs';
|
|
2
2
|
export * from './convert-interface-to-type.mjs';
|
|
3
3
|
export * from './convert-to-readonly.mjs';
|
|
4
|
+
export * from './enable-no-unchecked-indexed-access.mjs';
|
|
4
5
|
export * from './readonly-transformer-helpers/index.mjs';
|
|
5
6
|
export * from './replace-any-with-unknown.mjs';
|
|
6
7
|
export * from './replace-record-with-unknown-record.mjs';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { asUint32, range } from 'ts-data-forge';
|
|
12
12
|
|
|
13
|
-
const parenDepthDelta = (char: string): number => {
|
|
13
|
+
const parenDepthDelta = (char: string | undefined): number => {
|
|
14
14
|
switch (char) {
|
|
15
15
|
case '(':
|
|
16
16
|
return 1;
|
|
@@ -18,6 +18,9 @@ const parenDepthDelta = (char: string): number => {
|
|
|
18
18
|
case ')':
|
|
19
19
|
return -1;
|
|
20
20
|
|
|
21
|
+
// `at` returns `undefined` past the end of the string. The loop below
|
|
22
|
+
// never asks for such an index, but the type says it can.
|
|
23
|
+
case undefined:
|
|
21
24
|
default:
|
|
22
25
|
return 0;
|
|
23
26
|
}
|
|
@@ -35,7 +38,7 @@ const isWrappedWithParentheses = (str: string): boolean => {
|
|
|
35
38
|
let mut_depth = 0;
|
|
36
39
|
|
|
37
40
|
for (const mut_i of range(0, asUint32(trimmed.length))) {
|
|
38
|
-
mut_depth += parenDepthDelta(trimmed.
|
|
41
|
+
mut_depth += parenDepthDelta(trimmed.at(mut_i));
|
|
39
42
|
|
|
40
43
|
// If we reach depth 0 before the end, the outer parentheses don't wrap everything
|
|
41
44
|
if (mut_depth === 0 && mut_i < trimmed.length - 1) {
|