ts-codemod-lib 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/functions/ast-transformers/append-as-const.d.mts +9 -10
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +123 -114
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +28 -7
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +452 -253
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +8 -8
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +103 -104
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +2 -2
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +51 -51
- 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 +69 -69
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -1
- package/dist/functions/constants/mutable-prefixes.d.mts +8 -8
- package/dist/functions/functions/has-disable-next-line-comment.mjs +23 -23
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts +2 -2
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.mjs +13 -13
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -2
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -1
- package/dist/functions/functions/wrap-with-parentheses.mjs +87 -32
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -1
- package/dist/functions/utils/replace-node-with-text.d.mts +10 -11
- package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -1
- package/dist/functions/utils/replace-node-with-text.mjs +0 -1
- package/dist/functions/utils/replace-node-with-text.mjs.map +1 -1
- package/package.json +9 -9
- package/src/functions/ast-transformers/append-as-const.mts +242 -231
- package/src/functions/ast-transformers/append-as-const.test.mts +39 -6
- package/src/functions/ast-transformers/convert-to-readonly-record-import.test.mts +458 -0
- package/src/functions/ast-transformers/convert-to-readonly.mts +1003 -694
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +80 -33
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +178 -179
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +29 -29
- package/src/functions/ast-transformers/replace-any-with-unknown.test.mts +6 -6
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +75 -75
- package/src/functions/ast-transformers/replace-record-with-unknown-record.test.mts +6 -6
- package/src/functions/ast-transformers/transform-source-code.mts +110 -110
- package/src/functions/ast-transformers/transformer-specific-ignore.test.mts +6 -6
- package/src/functions/constants/mutable-prefixes.mts +8 -8
- package/src/functions/functions/has-disable-next-line-comment.mts +35 -35
- package/src/functions/functions/is-readonly-node.mts +25 -25
- package/src/functions/functions/wrap-with-parentheses.mts +138 -52
- package/src/functions/functions/wrap-with-parentheses.test.mts +115 -0
- package/src/functions/utils/replace-node-with-text.mts +10 -11
|
@@ -14,39 +14,6 @@ import {
|
|
|
14
14
|
} from './convert-to-readonly.mjs';
|
|
15
15
|
import { transformSourceCode } from './transform-source-code.mjs';
|
|
16
16
|
|
|
17
|
-
const testFn = async ({
|
|
18
|
-
source,
|
|
19
|
-
expected,
|
|
20
|
-
debug,
|
|
21
|
-
options,
|
|
22
|
-
isTsx = false,
|
|
23
|
-
}: Readonly<{
|
|
24
|
-
source: string;
|
|
25
|
-
expected: string;
|
|
26
|
-
debug?: boolean;
|
|
27
|
-
options?: ReadonlyTransformerOptions;
|
|
28
|
-
isTsx?: boolean;
|
|
29
|
-
}>): Promise<void> => {
|
|
30
|
-
if (debug !== true) {
|
|
31
|
-
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
32
|
-
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
33
|
-
|
|
34
|
-
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
35
|
-
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
36
|
-
|
|
37
|
-
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
38
|
-
vi.spyOn(console, 'trace').mockImplementation(() => {});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const transformedCodeRaw = await formatter(
|
|
42
|
-
transformSourceCode(source, isTsx, [convertToReadonlyTransformer(options)]),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const expectedFormatted = await formatter(expected);
|
|
46
|
-
|
|
47
|
-
expect(transformedCodeRaw).toBe(expectedFormatted);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
17
|
/**
|
|
51
18
|
* Normalizes whitespace in a code string, primarily for comparing AST structure outputs.
|
|
52
19
|
* - Preserves newlines immediately following line comments (`//`).
|
|
@@ -93,6 +60,39 @@ const formatter = async (code: string): Promise<string> => {
|
|
|
93
60
|
});
|
|
94
61
|
};
|
|
95
62
|
|
|
63
|
+
const testFn = async ({
|
|
64
|
+
source,
|
|
65
|
+
expected,
|
|
66
|
+
debug,
|
|
67
|
+
options,
|
|
68
|
+
isTsx = false,
|
|
69
|
+
}: Readonly<{
|
|
70
|
+
source: string;
|
|
71
|
+
expected: string;
|
|
72
|
+
debug?: boolean;
|
|
73
|
+
options?: ReadonlyTransformerOptions;
|
|
74
|
+
isTsx?: boolean;
|
|
75
|
+
}>): Promise<void> => {
|
|
76
|
+
if (debug !== true) {
|
|
77
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
78
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
79
|
+
|
|
80
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
81
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
84
|
+
vi.spyOn(console, 'trace').mockImplementation(() => {});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const transformedCodeRaw = await formatter(
|
|
88
|
+
transformSourceCode(source, isTsx, [convertToReadonlyTransformer(options)]),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const expectedFormatted = await formatter(expected);
|
|
92
|
+
|
|
93
|
+
expect(transformedCodeRaw).toBe(expectedFormatted);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
96
|
describe(convertToReadonlyTransformer, () => {
|
|
97
97
|
describe('TypeReferences', () => {
|
|
98
98
|
describe('Sets', () => {
|
|
@@ -215,6 +215,13 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
215
215
|
|
|
216
216
|
describe('Records', () => {
|
|
217
217
|
describe('recordStyle: "ReadonlyRecord" (default)', () => {
|
|
218
|
+
// These sources are scripts, which get `Readonly<Record<K, V>>` unless
|
|
219
|
+
// the name is in scope; how it is brought into scope is tested in
|
|
220
|
+
// convert-to-readonly-record-import.test.mts.
|
|
221
|
+
const options = {
|
|
222
|
+
importStyle: 'global',
|
|
223
|
+
} as const satisfies ReadonlyTransformerOptions;
|
|
224
|
+
|
|
218
225
|
test.each([
|
|
219
226
|
{
|
|
220
227
|
name: 'Type alias',
|
|
@@ -224,6 +231,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
224
231
|
expected: dedent`
|
|
225
232
|
type Foo = ReadonlyRecord<string, number>
|
|
226
233
|
`,
|
|
234
|
+
options,
|
|
227
235
|
},
|
|
228
236
|
{
|
|
229
237
|
name: 'ReadonlyRecord<K, V> is unchanged',
|
|
@@ -233,6 +241,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
233
241
|
expected: dedent`
|
|
234
242
|
type Foo = ReadonlyRecord<string, number>
|
|
235
243
|
`,
|
|
244
|
+
options,
|
|
236
245
|
},
|
|
237
246
|
{
|
|
238
247
|
name: 'Readonly<Record<K, V>> is unified to ReadonlyRecord<K, V>',
|
|
@@ -242,6 +251,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
242
251
|
expected: dedent`
|
|
243
252
|
type Foo = ReadonlyRecord<string, number>
|
|
244
253
|
`,
|
|
254
|
+
options,
|
|
245
255
|
},
|
|
246
256
|
{
|
|
247
257
|
name: 'Redundant Readonly<ReadonlyRecord<K, V>> is normalized',
|
|
@@ -251,6 +261,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
251
261
|
expected: dedent`
|
|
252
262
|
type Foo = ReadonlyRecord<string, number>
|
|
253
263
|
`,
|
|
264
|
+
options,
|
|
254
265
|
},
|
|
255
266
|
{
|
|
256
267
|
name: 'Value type is converted recursively',
|
|
@@ -260,6 +271,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
260
271
|
expected: dedent`
|
|
261
272
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
262
273
|
`,
|
|
274
|
+
options,
|
|
263
275
|
},
|
|
264
276
|
{
|
|
265
277
|
name: 'Value type inside Readonly<Record<K, V>> is converted recursively',
|
|
@@ -269,6 +281,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
269
281
|
expected: dedent`
|
|
270
282
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
271
283
|
`,
|
|
284
|
+
options,
|
|
272
285
|
},
|
|
273
286
|
{
|
|
274
287
|
name: 'Value type inside ReadonlyRecord<K, V> is converted recursively',
|
|
@@ -278,6 +291,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
278
291
|
expected: dedent`
|
|
279
292
|
type Foo = ReadonlyRecord<string, readonly number[]>
|
|
280
293
|
`,
|
|
294
|
+
options,
|
|
281
295
|
},
|
|
282
296
|
{
|
|
283
297
|
name: 'In type literal member',
|
|
@@ -287,6 +301,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
287
301
|
expected: dedent`
|
|
288
302
|
type Foo = Readonly<{ a: ReadonlyRecord<string, number> }>
|
|
289
303
|
`,
|
|
304
|
+
options,
|
|
290
305
|
},
|
|
291
306
|
{
|
|
292
307
|
name: 'In function args',
|
|
@@ -296,6 +311,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
296
311
|
expected: dedent`
|
|
297
312
|
function foo(a: ReadonlyRecord<string, number>, b: Promise<ReadonlyRecord<string, number>>) {}
|
|
298
313
|
`,
|
|
314
|
+
options,
|
|
299
315
|
},
|
|
300
316
|
{
|
|
301
317
|
name: 'DeepReadonly<Record<K, V>> is unchanged',
|
|
@@ -305,6 +321,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
305
321
|
expected: dedent`
|
|
306
322
|
type Foo = DeepReadonly<Record<string, number[]>>
|
|
307
323
|
`,
|
|
324
|
+
options,
|
|
308
325
|
},
|
|
309
326
|
{
|
|
310
327
|
name: 'DeepReadonly<ReadonlyRecord<K, V>> is unchanged',
|
|
@@ -314,6 +331,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
314
331
|
expected: dedent`
|
|
315
332
|
type Foo = DeepReadonly<ReadonlyRecord<string, number>>
|
|
316
333
|
`,
|
|
334
|
+
options,
|
|
317
335
|
},
|
|
318
336
|
{
|
|
319
337
|
name: 'Record<K, V> under indexed access is kept as is',
|
|
@@ -323,6 +341,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
323
341
|
expected: dedent`
|
|
324
342
|
type Foo = Record<string, number>[string]
|
|
325
343
|
`,
|
|
344
|
+
options,
|
|
326
345
|
},
|
|
327
346
|
{
|
|
328
347
|
name: 'ReadonlyRecord<K, V> under indexed access is unchanged',
|
|
@@ -332,6 +351,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
332
351
|
expected: dedent`
|
|
333
352
|
type Foo = ReadonlyRecord<string, number>[string]
|
|
334
353
|
`,
|
|
354
|
+
options,
|
|
335
355
|
},
|
|
336
356
|
])('$name', testFn);
|
|
337
357
|
});
|
|
@@ -788,6 +808,33 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
788
808
|
function foo(a: readonly number[], b: Promise<readonly number[]>) {}
|
|
789
809
|
`,
|
|
790
810
|
},
|
|
811
|
+
{
|
|
812
|
+
name: 'Array<T> whose element type has a parenthesis inside a string literal type',
|
|
813
|
+
source: dedent`
|
|
814
|
+
type T = Array<(A | '(') | (B)>;
|
|
815
|
+
`,
|
|
816
|
+
expected: dedent`
|
|
817
|
+
type T = readonly ((A | '(') | (B))[];
|
|
818
|
+
`,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: 'Array<T> whose element type has a parenthesis inside a comment',
|
|
822
|
+
source: dedent`
|
|
823
|
+
type T = Array<(A | /* ( */ C) | (B)>;
|
|
824
|
+
`,
|
|
825
|
+
expected: dedent`
|
|
826
|
+
type T = readonly ((A | /* ( */ C) | (B))[];
|
|
827
|
+
`,
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
name: 'Array<T> whose element type has a parenthesis inside a template literal type',
|
|
831
|
+
source: dedent`
|
|
832
|
+
type T = Array<(\`(\${A}\` | C) | (B)>;
|
|
833
|
+
`,
|
|
834
|
+
expected: dedent`
|
|
835
|
+
type T = readonly ((\`(\${A}\` | C) | (B))[];
|
|
836
|
+
`,
|
|
837
|
+
},
|
|
791
838
|
{
|
|
792
839
|
name: 'Mutable array types in function args (generic)',
|
|
793
840
|
source: dedent`
|
|
@@ -9,15 +9,15 @@ const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
|
9
9
|
/**
|
|
10
10
|
* Appends `!` to the index accesses that turning on the
|
|
11
11
|
* [`noUncheckedIndexedAccess`](https://www.typescriptlang.org/tsconfig/#noUncheckedIndexedAccess)
|
|
12
|
-
* compiler option would turn into type errors, as a stopgap while the option
|
|
13
|
-
*
|
|
12
|
+
* compiler option would turn into type errors, as a stopgap while the option is
|
|
13
|
+
* being enabled.
|
|
14
14
|
*
|
|
15
15
|
* The rewrite is driven by the type checker rather than by syntax: the file is
|
|
16
16
|
* checked twice, once with `noUncheckedIndexedAccess` off and once with it on,
|
|
17
17
|
* and `!` is appended only where the option is what added `undefined` to the
|
|
18
18
|
* expression's type. Consequently an index whose presence the type already
|
|
19
|
-
* guarantees keeps its access untouched — `[T, T][1]`, `[T, T, ...T[]][0]`,
|
|
20
|
-
*
|
|
19
|
+
* guarantees keeps its access untouched — `[T, T][1]`, `[T, T, ...T[]][0]`, and
|
|
20
|
+
* `({ a: T })['a']` are all left as they are, as is an element type that
|
|
21
21
|
* contained `undefined` to begin with.
|
|
22
22
|
*
|
|
23
23
|
* Both spellings of an index read are covered, because the option widens both:
|
|
@@ -31,10 +31,10 @@ const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
|
31
31
|
* on its own — which is how {@link transformSourceCode} drives every
|
|
32
32
|
* transformer here — resolves neither a type from another module nor a
|
|
33
33
|
* narrowing that depends on an imported type guard, and the latter produces
|
|
34
|
-
* assertions a whole-program check does not need. Most of those are reported
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* assertions a whole-program check does not need. Most of those are reported by
|
|
35
|
+
* `@typescript-eslint/no-unnecessary-type-assertion`, whose fixer removes them;
|
|
36
|
+
* what no rule can see is an assertion that makes a later check dead, as the
|
|
37
|
+
* `if` of `const v = xs[0]!; if (v === undefined) …` now is.
|
|
38
38
|
*
|
|
39
39
|
* @example
|
|
40
40
|
*
|
|
@@ -91,9 +91,9 @@ export const enableNoUncheckedIndexedAccessTransformer = (
|
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// One text edit for the whole file: `insertText` forgets every
|
|
95
|
-
//
|
|
96
|
-
//
|
|
94
|
+
// One text edit for the whole file: `insertText` forgets every descendant
|
|
95
|
+
// node, so inserting one `!` at a time would reparse the file once per
|
|
96
|
+
// assertion.
|
|
97
97
|
sourceAst.replaceText(
|
|
98
98
|
[0, sourceAst.getEnd()],
|
|
99
99
|
insertNonNullAssertions(sourceAst.getFullText(), positions),
|
|
@@ -134,102 +134,6 @@ type EnableNoUncheckedIndexedAccessTransformerOptionsInternal = DeepReadonly<{
|
|
|
134
134
|
type IndexReadExpression =
|
|
135
135
|
tsm.ElementAccessExpression | tsm.PropertyAccessExpression;
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
* Returns the positions at which a `!` has to be inserted, i.e. the end
|
|
139
|
-
* offsets of the reads whose type `noUncheckedIndexedAccess` widens with
|
|
140
|
-
* `undefined`.
|
|
141
|
-
*/
|
|
142
|
-
const collectAssertionPositions = (
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
144
|
-
sourceAst: tsm.SourceFile,
|
|
145
|
-
options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,
|
|
146
|
-
): readonly number[] => {
|
|
147
|
-
const candidates: readonly IndexReadExpression[] = [
|
|
148
|
-
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),
|
|
149
|
-
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),
|
|
150
|
-
].filter(
|
|
151
|
-
(node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
if (Arr.isEmpty(candidates)) {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const project = sourceAst.getProject();
|
|
159
|
-
|
|
160
|
-
const originalOptions = project.getCompilerOptions();
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
// With the option on first, because that narrows the candidates down to
|
|
164
|
-
// the few reads worth checking a second time. `strictNullChecks` comes
|
|
165
|
-
// with it: `noUncheckedIndexedAccess` does nothing without it, and the
|
|
166
|
-
// project handed to the transformer may not have it on.
|
|
167
|
-
project.compilerOptions.set({
|
|
168
|
-
strictNullChecks: true,
|
|
169
|
-
noUncheckedIndexedAccess: true,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// The type asked is the access's own, so an `as`, `satisfies` or `!` on
|
|
173
|
-
// its object is already in it. That is the type the option's error is
|
|
174
|
-
// reported against, and the `!` appended has no run-time effect for the
|
|
175
|
-
// unwrapped object to contradict.
|
|
176
|
-
const widened = candidates.filter(
|
|
177
|
-
(node) =>
|
|
178
|
-
typeIncludesUndefined(node.getType()) &&
|
|
179
|
-
!(
|
|
180
|
-
options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&
|
|
181
|
-
contextAcceptsUndefined(node)
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
if (Arr.isEmpty(widened)) {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
project.compilerOptions.set({ noUncheckedIndexedAccess: false });
|
|
190
|
-
|
|
191
|
-
// What is possibly `undefined` without the option too is not the option's
|
|
192
|
-
// doing — the element type says so, or the expression is erroneous (an
|
|
193
|
-
// unresolved import resolves to `any`, which is never widened).
|
|
194
|
-
return widened
|
|
195
|
-
.filter((node) => !typeIncludesUndefined(node.getType()))
|
|
196
|
-
.map((node) => {
|
|
197
|
-
options.debugPrint(`${node.getText()} -> ${node.getText()}!`);
|
|
198
|
-
|
|
199
|
-
return node.getEnd();
|
|
200
|
-
});
|
|
201
|
-
} finally {
|
|
202
|
-
// `set` merges, so the two options overridden above have to be named
|
|
203
|
-
// explicitly: they are absent from `originalOptions` when the project did
|
|
204
|
-
// not carry them, and would otherwise stay overridden for whichever
|
|
205
|
-
// transformer runs next.
|
|
206
|
-
project.compilerOptions.set({
|
|
207
|
-
...originalOptions,
|
|
208
|
-
strictNullChecks: originalOptions.strictNullChecks,
|
|
209
|
-
noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const insertNonNullAssertions = (
|
|
215
|
-
fullText: string,
|
|
216
|
-
positions: readonly number[],
|
|
217
|
-
): string => {
|
|
218
|
-
const mut_parts: string[] = [];
|
|
219
|
-
|
|
220
|
-
let mut_restStart: number = 0;
|
|
221
|
-
|
|
222
|
-
for (const position of positions.toSorted((a, b) => a - b)) {
|
|
223
|
-
mut_parts.push(fullText.slice(mut_restStart, position), '!');
|
|
224
|
-
|
|
225
|
-
mut_restStart = position;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
mut_parts.push(fullText.slice(mut_restStart));
|
|
229
|
-
|
|
230
|
-
return mut_parts.join('');
|
|
231
|
-
};
|
|
232
|
-
|
|
233
137
|
const typeIncludesUndefined = (
|
|
234
138
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
235
139
|
type: tsm.Type,
|
|
@@ -238,9 +142,9 @@ const typeIncludesUndefined = (
|
|
|
238
142
|
(type.isUnion() && type.getUnionTypes().some((t) => t.isUndefined()));
|
|
239
143
|
|
|
240
144
|
/**
|
|
241
|
-
* Whether the position the access sits in already accepts `undefined`, in
|
|
242
|
-
*
|
|
243
|
-
*
|
|
145
|
+
* Whether the position the access sits in already accepts `undefined`, in which
|
|
146
|
+
* case `noUncheckedIndexedAccess` reports nothing there and the assertion would
|
|
147
|
+
* only be noise.
|
|
244
148
|
*/
|
|
245
149
|
const contextAcceptsUndefined = (
|
|
246
150
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -276,38 +180,6 @@ const isIgnoredByComment = (
|
|
|
276
180
|
);
|
|
277
181
|
};
|
|
278
182
|
|
|
279
|
-
/**
|
|
280
|
-
* Whether a `!` appended to this access would be both syntactically valid and
|
|
281
|
-
* meaningful.
|
|
282
|
-
*
|
|
283
|
-
* Note that most write positions need no rule of their own: TypeScript gives
|
|
284
|
-
* the target of an assignment the element type without `undefined`, so the
|
|
285
|
-
* type comparison in {@link collectAssertionPositions} already passes them
|
|
286
|
-
* over. The exceptions handled here are the ones it does widen.
|
|
287
|
-
*
|
|
288
|
-
* The position is that of the outermost parenthesis around the access:
|
|
289
|
-
* `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
|
|
290
|
-
* does, while the `!` still goes right after the access, inside them.
|
|
291
|
-
*/
|
|
292
|
-
const isAssertableReadPosition = (
|
|
293
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
294
|
-
node: IndexReadExpression,
|
|
295
|
-
): boolean => {
|
|
296
|
-
const outer = outermostParenthesized(node);
|
|
297
|
-
|
|
298
|
-
const parent = outer.getParent();
|
|
299
|
-
|
|
300
|
-
if (parent === undefined) {
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
!isAlreadyAssertedOrGuarded(node, outer, parent) &&
|
|
306
|
-
!isWriteTarget(outer, parent) &&
|
|
307
|
-
!isTestedForAbsence(outer, parent)
|
|
308
|
-
);
|
|
309
|
-
};
|
|
310
|
-
|
|
311
183
|
/** `node`, or the outermost of the parentheses written around it. */
|
|
312
184
|
const outermostParenthesized = (node: tsm.Node): tsm.Node => {
|
|
313
185
|
const parent = node.getParent();
|
|
@@ -317,12 +189,38 @@ const outermostParenthesized = (node: tsm.Node): tsm.Node => {
|
|
|
317
189
|
: node;
|
|
318
190
|
};
|
|
319
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Whether the read sits in an optional chain that can short-circuit before it,
|
|
194
|
+
* as the `xs[0]?.toFixed` of `xs[0]?.toFixed()` does. `undefined` in such a
|
|
195
|
+
* type comes from the chain rather than from the index, and the chain already
|
|
196
|
+
* accounts for it; asserting there would assert away the author's own `?.`.
|
|
197
|
+
*
|
|
198
|
+
* Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops
|
|
199
|
+
* at anything that is not a chain link.
|
|
200
|
+
*/
|
|
201
|
+
const isInOptionalChain = (node: tsm.Node): boolean => {
|
|
202
|
+
if (
|
|
203
|
+
node.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
|
|
204
|
+
node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
205
|
+
node.isKind(tsm.SyntaxKind.CallExpression)
|
|
206
|
+
) {
|
|
207
|
+
return (
|
|
208
|
+
node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
node.isKind(tsm.SyntaxKind.NonNullExpression) &&
|
|
214
|
+
isInOptionalChain(node.getExpression())
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
320
218
|
/**
|
|
321
219
|
* `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`.
|
|
322
220
|
*
|
|
323
|
-
* `outer` is `node` with its parentheses (see
|
|
324
|
-
*
|
|
325
|
-
*
|
|
221
|
+
* `outer` is `node` with its parentheses (see {@link outermostParenthesized});
|
|
222
|
+
* a chain continues from the access itself but is guarded from the outside of
|
|
223
|
+
* the parentheses (`(xs[0])?.foo`).
|
|
326
224
|
*/
|
|
327
225
|
const isAlreadyAssertedOrGuarded = (
|
|
328
226
|
node: tsm.Node,
|
|
@@ -341,8 +239,8 @@ const isAlreadyAssertedOrGuarded = (
|
|
|
341
239
|
return true;
|
|
342
240
|
}
|
|
343
241
|
|
|
344
|
-
// Only the object of the optional chain is guarded; an index argument such
|
|
345
|
-
//
|
|
242
|
+
// Only the object of the optional chain is guarded; an index argument such as
|
|
243
|
+
// the `xs[0]` of `m?.[xs[0]]` is not.
|
|
346
244
|
return (
|
|
347
245
|
(parent.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
|
|
348
246
|
parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
@@ -353,36 +251,19 @@ const isAlreadyAssertedOrGuarded = (
|
|
|
353
251
|
};
|
|
354
252
|
|
|
355
253
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* accounts for it; asserting there would assert away the author's own `?.`.
|
|
360
|
-
*
|
|
361
|
-
* Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops
|
|
362
|
-
* at anything that is not a chain link.
|
|
254
|
+
* `=` through `^=`, i.e. every assignment operator. TypeScript keeps them in
|
|
255
|
+
* one contiguous block of {@link tsm.SyntaxKind} and marks its ends, which is
|
|
256
|
+
* how the compiler itself recognizes them.
|
|
363
257
|
*/
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
368
|
-
node.isKind(tsm.SyntaxKind.CallExpression)
|
|
369
|
-
) {
|
|
370
|
-
return (
|
|
371
|
-
node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return (
|
|
376
|
-
node.isKind(tsm.SyntaxKind.NonNullExpression) &&
|
|
377
|
-
isInOptionalChain(node.getExpression())
|
|
378
|
-
);
|
|
379
|
-
};
|
|
258
|
+
const isAssignmentOperatorKind = (kind: tsm.SyntaxKind): boolean =>
|
|
259
|
+
tsm.SyntaxKind.FirstAssignment <= kind &&
|
|
260
|
+
kind <= tsm.SyntaxKind.LastAssignment;
|
|
380
261
|
|
|
381
262
|
/**
|
|
382
263
|
* Positions that write the value rather than read it, where `!` cannot be
|
|
383
|
-
* appended at all: an assignment target is not an expression (`xs[0]! = 1`
|
|
384
|
-
*
|
|
385
|
-
*
|
|
264
|
+
* appended at all: an assignment target is not an expression (`xs[0]! = 1` does
|
|
265
|
+
* not parse as an assignment), and `delete xs[0]!` and `xs[0]!++` are equally
|
|
266
|
+
* invalid.
|
|
386
267
|
*
|
|
387
268
|
* `xs[0] += 1` is the one case this leaves a type error behind: its left-hand
|
|
388
269
|
* side is read as well as written, so `noUncheckedIndexedAccess` reports it,
|
|
@@ -465,13 +346,131 @@ const isTestedForAbsence = (node: tsm.Node, parent: tsm.Node): boolean => {
|
|
|
465
346
|
};
|
|
466
347
|
|
|
467
348
|
/**
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
349
|
+
* Whether a `!` appended to this access would be both syntactically valid and
|
|
350
|
+
* meaningful.
|
|
351
|
+
*
|
|
352
|
+
* Note that most write positions need no rule of their own: TypeScript gives
|
|
353
|
+
* the target of an assignment the element type without `undefined`, so the type
|
|
354
|
+
* comparison in {@link collectAssertionPositions} already passes them over. The
|
|
355
|
+
* exceptions handled here are the ones it does widen.
|
|
356
|
+
*
|
|
357
|
+
* The position is that of the outermost parenthesis around the access:
|
|
358
|
+
* `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
|
|
359
|
+
* does, while the `!` still goes right after the access, inside them.
|
|
471
360
|
*/
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
361
|
+
const isAssertableReadPosition = (
|
|
362
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
363
|
+
node: IndexReadExpression,
|
|
364
|
+
): boolean => {
|
|
365
|
+
const outer = outermostParenthesized(node);
|
|
366
|
+
|
|
367
|
+
const parent = outer.getParent();
|
|
368
|
+
|
|
369
|
+
if (parent === undefined) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return (
|
|
374
|
+
!isAlreadyAssertedOrGuarded(node, outer, parent) &&
|
|
375
|
+
!isWriteTarget(outer, parent) &&
|
|
376
|
+
!isTestedForAbsence(outer, parent)
|
|
377
|
+
);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Returns the positions at which a `!` has to be inserted, i.e. the end offsets
|
|
382
|
+
* of the reads whose type `noUncheckedIndexedAccess` widens with `undefined`.
|
|
383
|
+
*/
|
|
384
|
+
const collectAssertionPositions = (
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
386
|
+
sourceAst: tsm.SourceFile,
|
|
387
|
+
options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,
|
|
388
|
+
): readonly number[] => {
|
|
389
|
+
const candidates: readonly IndexReadExpression[] = [
|
|
390
|
+
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),
|
|
391
|
+
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),
|
|
392
|
+
].filter(
|
|
393
|
+
(node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
if (Arr.isEmpty(candidates)) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const project = sourceAst.getProject();
|
|
401
|
+
|
|
402
|
+
const originalOptions = project.getCompilerOptions();
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// With the option on first, because that narrows the candidates down to the
|
|
406
|
+
// few reads worth checking a second time. `strictNullChecks` comes with it:
|
|
407
|
+
// `noUncheckedIndexedAccess` does nothing without it, and the project
|
|
408
|
+
// handed to the transformer may not have it on.
|
|
409
|
+
project.compilerOptions.set({
|
|
410
|
+
strictNullChecks: true,
|
|
411
|
+
noUncheckedIndexedAccess: true,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// The type asked is the access's own, so an `as`, `satisfies` or `!` on its
|
|
415
|
+
// object is already in it. That is the type the option's error is reported
|
|
416
|
+
// against, and the `!` appended has no run-time effect for the unwrapped
|
|
417
|
+
// object to contradict.
|
|
418
|
+
const widened = candidates.filter(
|
|
419
|
+
(node) =>
|
|
420
|
+
typeIncludesUndefined(node.getType()) &&
|
|
421
|
+
!(
|
|
422
|
+
options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&
|
|
423
|
+
contextAcceptsUndefined(node)
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
if (Arr.isEmpty(widened)) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
project.compilerOptions.set({ noUncheckedIndexedAccess: false });
|
|
432
|
+
|
|
433
|
+
// What is possibly `undefined` without the option too is not the option's
|
|
434
|
+
// doing — the element type says so, or the expression is erroneous (an
|
|
435
|
+
// unresolved import resolves to `any`, which is never widened).
|
|
436
|
+
return widened
|
|
437
|
+
.filter((node) => !typeIncludesUndefined(node.getType()))
|
|
438
|
+
.map((node) => {
|
|
439
|
+
options.debugPrint(`${node.getText()} -> ${node.getText()}!`);
|
|
440
|
+
|
|
441
|
+
return node.getEnd();
|
|
442
|
+
});
|
|
443
|
+
} finally {
|
|
444
|
+
// `set` merges, so the two options overridden above have to be named
|
|
445
|
+
// explicitly: they are absent from `originalOptions` when the project did
|
|
446
|
+
// not carry them, and would otherwise stay overridden for whichever
|
|
447
|
+
// transformer runs next.
|
|
448
|
+
project.compilerOptions.set({
|
|
449
|
+
...originalOptions,
|
|
450
|
+
strictNullChecks: originalOptions.strictNullChecks,
|
|
451
|
+
noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const insertNonNullAssertions = (
|
|
457
|
+
fullText: string,
|
|
458
|
+
positions: readonly number[],
|
|
459
|
+
): string => {
|
|
460
|
+
const mut_parts: string[] = [];
|
|
461
|
+
|
|
462
|
+
let mut_restStart: number = 0;
|
|
463
|
+
|
|
464
|
+
for (const position of positions.toSorted((a, b) => a - b)) {
|
|
465
|
+
mut_parts.push(fullText.slice(mut_restStart, position), '!');
|
|
466
|
+
|
|
467
|
+
mut_restStart = position;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
mut_parts.push(fullText.slice(mut_restStart));
|
|
471
|
+
|
|
472
|
+
return mut_parts.join('');
|
|
473
|
+
};
|
|
475
474
|
|
|
476
475
|
const equalityOperatorKinds: readonly tsm.SyntaxKind[] = [
|
|
477
476
|
tsm.SyntaxKind.EqualsEqualsToken,
|