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,438 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
2
|
+
import * as tsm from 'ts-morph';
|
|
3
|
+
import { type DeepReadonly } from 'ts-type-forge';
|
|
4
|
+
import { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
5
|
+
import { type TsMorphTransformer } from './types.mjs';
|
|
6
|
+
|
|
7
|
+
const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Appends `!` to the index accesses that turning on the
|
|
11
|
+
* [`noUncheckedIndexedAccess`](https://www.typescriptlang.org/tsconfig/#noUncheckedIndexedAccess)
|
|
12
|
+
* compiler option would turn into type errors, as a stopgap while the option
|
|
13
|
+
* is being enabled.
|
|
14
|
+
*
|
|
15
|
+
* The rewrite is driven by the type checker rather than by syntax: the file is
|
|
16
|
+
* checked twice, once with `noUncheckedIndexedAccess` off and once with it on,
|
|
17
|
+
* and `!` is appended only where the option is what added `undefined` to the
|
|
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
|
+
* and `({ a: T })['a']` are all left as they are, as is an element type that
|
|
21
|
+
* contained `undefined` to begin with.
|
|
22
|
+
*
|
|
23
|
+
* Both spellings of an index read are covered, because the option widens both:
|
|
24
|
+
* `rec['a']` and the dotted `rec.a` of a type carrying an index signature.
|
|
25
|
+
*
|
|
26
|
+
* `strictNullChecks` is turned on for both checks, because
|
|
27
|
+
* `noUncheckedIndexedAccess` does nothing without it and the project the
|
|
28
|
+
* transformer is handed may not have it on.
|
|
29
|
+
*
|
|
30
|
+
* The result is a stopgap, not a fix, and wants reviewing. A file transformed
|
|
31
|
+
* on its own — which is how {@link transformSourceCode} drives every
|
|
32
|
+
* transformer here — resolves neither a type from another module nor a
|
|
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
|
+
* by `@typescript-eslint/no-unnecessary-type-assertion`, whose fixer removes
|
|
36
|
+
* them; what no rule can see is an assertion that makes a later check dead,
|
|
37
|
+
* as the `if` of `const v = xs[0]!; if (v === undefined) …` now is.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* declare const xs: readonly number[];
|
|
42
|
+
* declare const pair: readonly [number, number];
|
|
43
|
+
* declare const rec: Record<string, number>;
|
|
44
|
+
* declare const known: { a: number };
|
|
45
|
+
*
|
|
46
|
+
* xs[0].toFixed(); // -> xs[0]!.toFixed();
|
|
47
|
+
* rec['a'].toFixed(); // -> rec['a']!.toFixed();
|
|
48
|
+
* rec.a.toFixed(); // -> rec.a!.toFixed();
|
|
49
|
+
* pair[1].toFixed(); // -> pair[1].toFixed(); (guaranteed to exist)
|
|
50
|
+
* known.a.toFixed(); // -> known.a.toFixed(); (guaranteed to exist)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const enableNoUncheckedIndexedAccessTransformer = (
|
|
54
|
+
options?: EnableNoUncheckedIndexedAccessTransformerOptions,
|
|
55
|
+
): TsMorphTransformer => {
|
|
56
|
+
const optionsInternal: EnableNoUncheckedIndexedAccessTransformerOptionsInternal =
|
|
57
|
+
{
|
|
58
|
+
applyLevel: options?.applyLevel ?? 'all',
|
|
59
|
+
debugPrint: options?.debug === true ? console.debug : () => {},
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: TRANSFORMER_NAME,
|
|
64
|
+
transform: (sourceAst) => {
|
|
65
|
+
const positions = collectAssertionPositions(sourceAst, optionsInternal);
|
|
66
|
+
|
|
67
|
+
if (Arr.isEmpty(positions)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One text edit for the whole file: `insertText` forgets every
|
|
72
|
+
// descendant node, so inserting one `!` at a time would reparse the
|
|
73
|
+
// file once per assertion.
|
|
74
|
+
sourceAst.replaceText(
|
|
75
|
+
[0, sourceAst.getEnd()],
|
|
76
|
+
insertNonNullAssertions(sourceAst.getFullText(), positions),
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type EnableNoUncheckedIndexedAccessTransformerOptions = DeepReadonly<{
|
|
83
|
+
/**
|
|
84
|
+
* How eagerly `!` is appended.
|
|
85
|
+
*
|
|
86
|
+
* - `"all"`: append it to every read whose type `noUncheckedIndexedAccess`
|
|
87
|
+
* widens with `undefined`, so that as few type errors as possible remain.
|
|
88
|
+
* - `"avoidWhereUndefinedIsAllowed"`: additionally leave the access alone
|
|
89
|
+
* where its contextual type already accepts `undefined` (e.g.
|
|
90
|
+
* `const x: number | undefined = xs[0];` or an argument of a
|
|
91
|
+
* `(x: number | undefined) => void` parameter), which produces fewer but
|
|
92
|
+
* only load-bearing assertions.
|
|
93
|
+
*
|
|
94
|
+
* @default "all"
|
|
95
|
+
*/
|
|
96
|
+
applyLevel?: 'all' | 'avoidWhereUndefinedIsAllowed';
|
|
97
|
+
|
|
98
|
+
debug?: boolean;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
type EnableNoUncheckedIndexedAccessTransformerOptionsInternal = DeepReadonly<{
|
|
102
|
+
applyLevel: 'all' | 'avoidWhereUndefinedIsAllowed';
|
|
103
|
+
debugPrint: (...args: readonly unknown[]) => void;
|
|
104
|
+
}>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* An index read that `noUncheckedIndexedAccess` can widen: an element access,
|
|
108
|
+
* and the dotted spelling of the same thing on a type with an index signature
|
|
109
|
+
* (`rec.a`, which the option widens exactly as it widens `rec['a']`).
|
|
110
|
+
*/
|
|
111
|
+
type IndexReadExpression =
|
|
112
|
+
tsm.ElementAccessExpression | tsm.PropertyAccessExpression;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the positions at which a `!` has to be inserted, i.e. the end
|
|
116
|
+
* offsets of the reads whose type `noUncheckedIndexedAccess` widens with
|
|
117
|
+
* `undefined`.
|
|
118
|
+
*/
|
|
119
|
+
const collectAssertionPositions = (
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
121
|
+
sourceAst: tsm.SourceFile,
|
|
122
|
+
options: EnableNoUncheckedIndexedAccessTransformerOptionsInternal,
|
|
123
|
+
): readonly number[] => {
|
|
124
|
+
const candidates: readonly IndexReadExpression[] = [
|
|
125
|
+
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.ElementAccessExpression),
|
|
126
|
+
...sourceAst.getDescendantsOfKind(tsm.SyntaxKind.PropertyAccessExpression),
|
|
127
|
+
].filter(
|
|
128
|
+
(node) => isAssertableReadPosition(node) && !isIgnoredByComment(node),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (Arr.isEmpty(candidates)) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const project = sourceAst.getProject();
|
|
136
|
+
|
|
137
|
+
const originalOptions = project.getCompilerOptions();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// With the option on first, because that narrows the candidates down to
|
|
141
|
+
// the few reads worth checking a second time. `strictNullChecks` comes
|
|
142
|
+
// with it: `noUncheckedIndexedAccess` does nothing without it, and the
|
|
143
|
+
// project handed to the transformer may not have it on.
|
|
144
|
+
project.compilerOptions.set({
|
|
145
|
+
strictNullChecks: true,
|
|
146
|
+
noUncheckedIndexedAccess: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const widened = candidates.filter(
|
|
150
|
+
(node) =>
|
|
151
|
+
typeIncludesUndefined(node.getType()) &&
|
|
152
|
+
!(
|
|
153
|
+
options.applyLevel === 'avoidWhereUndefinedIsAllowed' &&
|
|
154
|
+
contextAcceptsUndefined(node)
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (Arr.isEmpty(widened)) {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
project.compilerOptions.set({ noUncheckedIndexedAccess: false });
|
|
163
|
+
|
|
164
|
+
// What is possibly `undefined` without the option too is not the option's
|
|
165
|
+
// doing — the element type says so, or the expression is erroneous (an
|
|
166
|
+
// unresolved import resolves to `any`, which is never widened).
|
|
167
|
+
return widened
|
|
168
|
+
.filter((node) => !typeIncludesUndefined(node.getType()))
|
|
169
|
+
.map((node) => {
|
|
170
|
+
options.debugPrint(`${node.getText()} -> ${node.getText()}!`);
|
|
171
|
+
|
|
172
|
+
return node.getEnd();
|
|
173
|
+
});
|
|
174
|
+
} finally {
|
|
175
|
+
// `set` merges, so the two options overridden above have to be named
|
|
176
|
+
// explicitly: they are absent from `originalOptions` when the project did
|
|
177
|
+
// not carry them, and would otherwise stay overridden for whichever
|
|
178
|
+
// transformer runs next.
|
|
179
|
+
project.compilerOptions.set({
|
|
180
|
+
...originalOptions,
|
|
181
|
+
strictNullChecks: originalOptions.strictNullChecks,
|
|
182
|
+
noUncheckedIndexedAccess: originalOptions.noUncheckedIndexedAccess,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const insertNonNullAssertions = (
|
|
188
|
+
fullText: string,
|
|
189
|
+
positions: readonly number[],
|
|
190
|
+
): string => {
|
|
191
|
+
const mut_parts: string[] = [];
|
|
192
|
+
|
|
193
|
+
let mut_restStart: number = 0;
|
|
194
|
+
|
|
195
|
+
for (const position of positions.toSorted((a, b) => a - b)) {
|
|
196
|
+
mut_parts.push(fullText.slice(mut_restStart, position), '!');
|
|
197
|
+
|
|
198
|
+
mut_restStart = position;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
mut_parts.push(fullText.slice(mut_restStart));
|
|
202
|
+
|
|
203
|
+
return mut_parts.join('');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const typeIncludesUndefined = (
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
208
|
+
type: tsm.Type,
|
|
209
|
+
): boolean =>
|
|
210
|
+
type.isUndefined() ||
|
|
211
|
+
(type.isUnion() && type.getUnionTypes().some((t) => t.isUndefined()));
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Whether the position the access sits in already accepts `undefined`, in
|
|
215
|
+
* which case `noUncheckedIndexedAccess` reports nothing there and the
|
|
216
|
+
* assertion would only be noise.
|
|
217
|
+
*/
|
|
218
|
+
const contextAcceptsUndefined = (
|
|
219
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
220
|
+
node: IndexReadExpression,
|
|
221
|
+
): boolean => {
|
|
222
|
+
const contextualType = node.getContextualType();
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
contextualType !== undefined &&
|
|
226
|
+
(contextualType.isAny() ||
|
|
227
|
+
contextualType.isUnknown() ||
|
|
228
|
+
typeIncludesUndefined(contextualType))
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const isIgnoredByComment = (
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
234
|
+
node: IndexReadExpression,
|
|
235
|
+
): boolean => {
|
|
236
|
+
if (hasDisableNextLineComment(node, TRANSFORMER_NAME)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// An index access is rarely the first thing on its own line, so the comment
|
|
241
|
+
// that covers it is the one above the statement it appears in.
|
|
242
|
+
const statement = node.getFirstAncestor((ancestor) =>
|
|
243
|
+
tsm.Node.isStatement(ancestor),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
statement !== undefined &&
|
|
248
|
+
hasDisableNextLineComment(statement, TRANSFORMER_NAME)
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Whether a `!` appended to this access would be both syntactically valid and
|
|
254
|
+
* meaningful.
|
|
255
|
+
*
|
|
256
|
+
* Note that most write positions need no rule of their own: TypeScript gives
|
|
257
|
+
* the target of an assignment the element type without `undefined`, so the
|
|
258
|
+
* type comparison in {@link collectAssertionPositions} already passes them
|
|
259
|
+
* over. The exceptions handled here are the ones it does widen.
|
|
260
|
+
*/
|
|
261
|
+
const isAssertableReadPosition = (
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
263
|
+
node: IndexReadExpression,
|
|
264
|
+
): boolean => {
|
|
265
|
+
const parent = node.getParent();
|
|
266
|
+
|
|
267
|
+
if (parent === undefined) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
!isAlreadyAssertedOrGuarded(node, parent) &&
|
|
273
|
+
!isWriteTarget(node, parent) &&
|
|
274
|
+
!isTestedForAbsence(node, parent)
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`. */
|
|
279
|
+
const isAlreadyAssertedOrGuarded = (
|
|
280
|
+
node: tsm.Node,
|
|
281
|
+
parent: tsm.Node,
|
|
282
|
+
): boolean => {
|
|
283
|
+
if (
|
|
284
|
+
parent.isKind(tsm.SyntaxKind.NonNullExpression) ||
|
|
285
|
+
parent.isKind(tsm.SyntaxKind.AsExpression) ||
|
|
286
|
+
parent.isKind(tsm.SyntaxKind.TypeAssertionExpression)
|
|
287
|
+
) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (isInOptionalChain(node)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Only the object of the optional chain is guarded; an index argument such
|
|
296
|
+
// as the `xs[0]` of `m?.[xs[0]]` is not.
|
|
297
|
+
return (
|
|
298
|
+
(parent.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
|
|
299
|
+
parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
300
|
+
parent.isKind(tsm.SyntaxKind.CallExpression)) &&
|
|
301
|
+
parent.hasQuestionDotToken() &&
|
|
302
|
+
parent.getExpression() === node
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Whether the read sits in an optional chain that can short-circuit before it,
|
|
308
|
+
* as the `xs[0]?.toFixed` of `xs[0]?.toFixed()` does. `undefined` in such a
|
|
309
|
+
* type comes from the chain rather than from the index, and the chain already
|
|
310
|
+
* accounts for it; asserting there would assert away the author's own `?.`.
|
|
311
|
+
*
|
|
312
|
+
* Parentheses end a chain (`(a?.b).c` is not one), which is why the walk stops
|
|
313
|
+
* at anything that is not a chain link.
|
|
314
|
+
*/
|
|
315
|
+
const isInOptionalChain = (node: tsm.Node): boolean => {
|
|
316
|
+
if (
|
|
317
|
+
node.isKind(tsm.SyntaxKind.PropertyAccessExpression) ||
|
|
318
|
+
node.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
319
|
+
node.isKind(tsm.SyntaxKind.CallExpression)
|
|
320
|
+
) {
|
|
321
|
+
return (
|
|
322
|
+
node.hasQuestionDotToken() || isInOptionalChain(node.getExpression())
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
node.isKind(tsm.SyntaxKind.NonNullExpression) &&
|
|
328
|
+
isInOptionalChain(node.getExpression())
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Positions that write the value rather than read it, where `!` cannot be
|
|
334
|
+
* appended at all: an assignment target is not an expression (`xs[0]! = 1`
|
|
335
|
+
* does not parse as an assignment), and `delete xs[0]!` and `xs[0]!++` are
|
|
336
|
+
* equally invalid.
|
|
337
|
+
*
|
|
338
|
+
* `xs[0] += 1` is the one case this leaves a type error behind: its left-hand
|
|
339
|
+
* side is read as well as written, so `noUncheckedIndexedAccess` reports it,
|
|
340
|
+
* but no assertion can be spelled there.
|
|
341
|
+
*/
|
|
342
|
+
const isWriteTarget = (node: tsm.Node, parent: tsm.Node): boolean => {
|
|
343
|
+
if (
|
|
344
|
+
parent.isKind(tsm.SyntaxKind.DeleteExpression) ||
|
|
345
|
+
parent.isKind(tsm.SyntaxKind.PostfixUnaryExpression)
|
|
346
|
+
) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (parent.isKind(tsm.SyntaxKind.PrefixUnaryExpression)) {
|
|
351
|
+
const operator = parent.getOperatorToken();
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
operator === tsm.SyntaxKind.PlusPlusToken ||
|
|
355
|
+
operator === tsm.SyntaxKind.MinusMinusToken
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
parent.isKind(tsm.SyntaxKind.ForOfStatement) ||
|
|
361
|
+
parent.isKind(tsm.SyntaxKind.ForInStatement)
|
|
362
|
+
) {
|
|
363
|
+
return parent.getInitializer() === node;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
parent.isKind(tsm.SyntaxKind.BinaryExpression) &&
|
|
368
|
+
parent.getLeft() === node &&
|
|
369
|
+
isAssignmentOperatorKind(parent.getOperatorToken().getKind())
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Positions that read the access precisely to find out whether the value is
|
|
375
|
+
* there — a truthiness check, an equality comparison, `??`, `typeof`. None of
|
|
376
|
+
* them is an error under `noUncheckedIndexedAccess`, and asserting inside one
|
|
377
|
+
* would defeat the check it performs (`case undefined:` would stop compiling
|
|
378
|
+
* against an asserted `switch` subject).
|
|
379
|
+
*/
|
|
380
|
+
const isTestedForAbsence = (node: tsm.Node, parent: tsm.Node): boolean => {
|
|
381
|
+
if (parent.isKind(tsm.SyntaxKind.TypeOfExpression)) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (parent.isKind(tsm.SyntaxKind.PrefixUnaryExpression)) {
|
|
386
|
+
return parent.getOperatorToken() === tsm.SyntaxKind.ExclamationToken;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (parent.isKind(tsm.SyntaxKind.BinaryExpression)) {
|
|
390
|
+
const operator = parent.getOperatorToken().getKind();
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
equalityOperatorKinds.includes(operator) ||
|
|
394
|
+
(parent.getLeft() === node &&
|
|
395
|
+
shortCircuitOperatorKinds.includes(operator))
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (
|
|
400
|
+
parent.isKind(tsm.SyntaxKind.IfStatement) ||
|
|
401
|
+
parent.isKind(tsm.SyntaxKind.WhileStatement) ||
|
|
402
|
+
parent.isKind(tsm.SyntaxKind.DoStatement) ||
|
|
403
|
+
parent.isKind(tsm.SyntaxKind.SwitchStatement)
|
|
404
|
+
) {
|
|
405
|
+
return parent.getExpression() === node;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (parent.isKind(tsm.SyntaxKind.ForStatement)) {
|
|
409
|
+
return parent.getCondition() === node;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
parent.isKind(tsm.SyntaxKind.ConditionalExpression) &&
|
|
414
|
+
parent.getCondition() === node
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* `=` through `^=`, i.e. every assignment operator. TypeScript keeps them in
|
|
420
|
+
* one contiguous block of {@link tsm.SyntaxKind} and marks its ends, which is
|
|
421
|
+
* how the compiler itself recognizes them.
|
|
422
|
+
*/
|
|
423
|
+
const isAssignmentOperatorKind = (kind: tsm.SyntaxKind): boolean =>
|
|
424
|
+
tsm.SyntaxKind.FirstAssignment <= kind &&
|
|
425
|
+
kind <= tsm.SyntaxKind.LastAssignment;
|
|
426
|
+
|
|
427
|
+
const equalityOperatorKinds: readonly tsm.SyntaxKind[] = [
|
|
428
|
+
tsm.SyntaxKind.EqualsEqualsToken,
|
|
429
|
+
tsm.SyntaxKind.EqualsEqualsEqualsToken,
|
|
430
|
+
tsm.SyntaxKind.ExclamationEqualsToken,
|
|
431
|
+
tsm.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
432
|
+
] as const;
|
|
433
|
+
|
|
434
|
+
const shortCircuitOperatorKinds: readonly tsm.SyntaxKind[] = [
|
|
435
|
+
tsm.SyntaxKind.QuestionQuestionToken,
|
|
436
|
+
tsm.SyntaxKind.AmpersandAmpersandToken,
|
|
437
|
+
tsm.SyntaxKind.BarBarToken,
|
|
438
|
+
] as const;
|