sourceverity 1.0.1 → 1.0.3
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/dist/analysis/controlFlow.d.ts +5 -0
- package/dist/analysis/controlFlow.d.ts.map +1 -1
- package/dist/analysis/controlFlow.js +2388 -74
- package/dist/analysis/controlFlow.js.map +1 -1
- package/dist/analysis/frameworks.d.ts.map +1 -1
- package/dist/analysis/frameworks.js +136 -12
- package/dist/analysis/frameworks.js.map +1 -1
- package/dist/analysis/functions.d.ts.map +1 -1
- package/dist/analysis/functions.js +95 -19
- package/dist/analysis/functions.js.map +1 -1
- package/dist/analysis/promiseFlow.d.ts.map +1 -1
- package/dist/analysis/promiseFlow.js +85 -11
- package/dist/analysis/promiseFlow.js.map +1 -1
- package/dist/baseline/manager.js +1 -1
- package/dist/baseline/manager.js.map +1 -1
- package/dist/baseline/types.d.ts +1 -1
- package/dist/baseline/types.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +4 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/core/paths.d.ts +0 -4
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +0 -6
- package/dist/core/paths.js.map +1 -1
- package/dist/core/version.d.ts +9 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +51 -0
- package/dist/core/version.js.map +1 -0
- package/dist/engine/runner.d.ts +6 -1
- package/dist/engine/runner.d.ts.map +1 -1
- package/dist/engine/runner.js +37 -20
- package/dist/engine/runner.js.map +1 -1
- package/dist/engine/symbols.d.ts +17 -4
- package/dist/engine/symbols.d.ts.map +1 -1
- package/dist/engine/symbols.js +267 -34
- package/dist/engine/symbols.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/reporters/agent.d.ts +1 -1
- package/dist/reporters/agent.d.ts.map +1 -1
- package/dist/reporters/agent.js +2 -1
- package/dist/reporters/agent.js.map +1 -1
- package/dist/reporters/sarif.d.ts +2 -0
- package/dist/reporters/sarif.d.ts.map +1 -1
- package/dist/reporters/sarif.js +38 -8
- package/dist/reporters/sarif.js.map +1 -1
- package/dist/reporters/terminal.d.ts.map +1 -1
- package/dist/reporters/terminal.js +2 -1
- package/dist/reporters/terminal.js.map +1 -1
- package/dist/rules/async/floatingPromise.d.ts.map +1 -1
- package/dist/rules/async/floatingPromise.js +4 -2
- package/dist/rules/async/floatingPromise.js.map +1 -1
- package/dist/rules/browser/eventListenerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/eventListenerCleanup.js +2 -9
- package/dist/rules/browser/eventListenerCleanup.js.map +1 -1
- package/dist/rules/browser/observerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/observerCleanup.js +2 -9
- package/dist/rules/browser/observerCleanup.js.map +1 -1
- package/dist/rules/browser/timerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/timerCleanup.js +194 -27
- package/dist/rules/browser/timerCleanup.js.map +1 -1
- package/dist/rules/network/fetchStatusUnchecked.d.ts.map +1 -1
- package/dist/rules/network/fetchStatusUnchecked.js +245 -46
- package/dist/rules/network/fetchStatusUnchecked.js.map +1 -1
- package/dist/rules/react/asyncEffectCallback.d.ts.map +1 -1
- package/dist/rules/react/asyncEffectCallback.js +2 -9
- package/dist/rules/react/asyncEffectCallback.js.map +1 -1
- package/dist/rules/react/derivedStateEffect.d.ts.map +1 -1
- package/dist/rules/react/derivedStateEffect.js +2 -9
- package/dist/rules/react/derivedStateEffect.js.map +1 -1
- package/dist/rules/react/missingEffectCleanup.d.ts.map +1 -1
- package/dist/rules/react/missingEffectCleanup.js +102 -10
- package/dist/rules/react/missingEffectCleanup.js.map +1 -1
- package/dist/rules/typescript/uncheckedIndexAccess.d.ts.map +1 -1
- package/dist/rules/typescript/uncheckedIndexAccess.js +55 -0
- package/dist/rules/typescript/uncheckedIndexAccess.js.map +1 -1
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.d.ts.map +1 -1
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.js +58 -21
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { unwrapExpression } from "./values.js";
|
|
2
|
+
import { resolveReactHook } from "../engine/symbols.js";
|
|
2
3
|
/**
|
|
3
4
|
* Checks if a statement definitely exits control flow (return, throw, break, continue).
|
|
4
5
|
*/
|
|
5
|
-
function statementExitsControlFlow(stmt, ts) {
|
|
6
|
+
export function statementExitsControlFlow(stmt, ts) {
|
|
6
7
|
if (ts.isReturnStatement(stmt) ||
|
|
7
8
|
ts.isThrowStatement(stmt) ||
|
|
8
9
|
ts.isBreakStatement(stmt) ||
|
|
@@ -18,6 +19,249 @@ function statementExitsControlFlow(stmt, ts) {
|
|
|
18
19
|
}
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
22
|
+
function isAssignmentOperator(kind, ts) {
|
|
23
|
+
return (kind === ts.SyntaxKind.EqualsToken ||
|
|
24
|
+
kind === ts.SyntaxKind.PlusEqualsToken ||
|
|
25
|
+
kind === ts.SyntaxKind.MinusEqualsToken ||
|
|
26
|
+
kind === ts.SyntaxKind.AsteriskEqualsToken ||
|
|
27
|
+
kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken ||
|
|
28
|
+
kind === ts.SyntaxKind.SlashEqualsToken ||
|
|
29
|
+
kind === ts.SyntaxKind.PercentEqualsToken ||
|
|
30
|
+
kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||
|
|
31
|
+
kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||
|
|
32
|
+
kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken ||
|
|
33
|
+
kind === ts.SyntaxKind.AmpersandEqualsToken ||
|
|
34
|
+
kind === ts.SyntaxKind.BarEqualsToken ||
|
|
35
|
+
kind === ts.SyntaxKind.CaretEqualsToken ||
|
|
36
|
+
kind === ts.SyntaxKind.BarBarEqualsToken ||
|
|
37
|
+
kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
|
|
38
|
+
kind === ts.SyntaxKind.QuestionQuestionEqualsToken);
|
|
39
|
+
}
|
|
40
|
+
export function hasVariableReassignment(node, targetName, targetSymbol, ts, checker, beforePos, afterPos) {
|
|
41
|
+
let reassigned = false;
|
|
42
|
+
function checkTargetMatch(ident) {
|
|
43
|
+
if (checker && targetSymbol) {
|
|
44
|
+
let sym = checker.getSymbolAtLocation(ident);
|
|
45
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
46
|
+
try {
|
|
47
|
+
sym = checker.getAliasedSymbol(sym);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (sym) {
|
|
54
|
+
return sym === targetSymbol;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return targetSymbol === undefined && ident.text === targetName;
|
|
58
|
+
}
|
|
59
|
+
function checkLeftAssignment(leftExpr) {
|
|
60
|
+
const unwrapped = unwrapExpression(leftExpr, ts);
|
|
61
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
62
|
+
return checkTargetMatch(unwrapped);
|
|
63
|
+
}
|
|
64
|
+
if (ts.isArrayLiteralExpression(unwrapped)) {
|
|
65
|
+
return unwrapped.elements.some((el) => {
|
|
66
|
+
if (ts.isOmittedExpression(el))
|
|
67
|
+
return false;
|
|
68
|
+
if (ts.isSpreadElement(el)) {
|
|
69
|
+
return checkLeftAssignment(el.expression);
|
|
70
|
+
}
|
|
71
|
+
return checkLeftAssignment(el);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
75
|
+
return unwrapped.properties.some((prop) => {
|
|
76
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
77
|
+
return checkTargetMatch(prop.name);
|
|
78
|
+
}
|
|
79
|
+
if (ts.isPropertyAssignment(prop) && ts.isExpression(prop.initializer)) {
|
|
80
|
+
return checkLeftAssignment(prop.initializer);
|
|
81
|
+
}
|
|
82
|
+
if (ts.isSpreadAssignment(prop)) {
|
|
83
|
+
return checkLeftAssignment(prop.expression);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function walk(n) {
|
|
91
|
+
if (reassigned)
|
|
92
|
+
return;
|
|
93
|
+
const nStart = n.getStart();
|
|
94
|
+
const nEnd = n.getEnd();
|
|
95
|
+
if (beforePos !== undefined && nStart > beforePos)
|
|
96
|
+
return;
|
|
97
|
+
if (afterPos !== undefined && nEnd < afterPos)
|
|
98
|
+
return;
|
|
99
|
+
const inRange = (afterPos === undefined || nStart >= afterPos) && (beforePos === undefined || nStart <= beforePos);
|
|
100
|
+
if (inRange && ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind, ts)) {
|
|
101
|
+
if (checkLeftAssignment(n.left)) {
|
|
102
|
+
reassigned = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (inRange &&
|
|
107
|
+
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
|
|
108
|
+
(n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
109
|
+
const operand = unwrapExpression(n.operand, ts);
|
|
110
|
+
if (ts.isIdentifier(operand) && checkTargetMatch(operand)) {
|
|
111
|
+
reassigned = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
n.forEachChild(walk);
|
|
116
|
+
}
|
|
117
|
+
walk(node);
|
|
118
|
+
return reassigned;
|
|
119
|
+
}
|
|
120
|
+
function isStateUpperBoundGuard(cond, stateName, maxExclusive, ts, checker) {
|
|
121
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
122
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
123
|
+
const op = unwrapped.operatorToken.kind;
|
|
124
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
125
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
126
|
+
if (ts.isIdentifier(left) && left.text === stateName) {
|
|
127
|
+
if (ts.isNumericLiteral(right)) {
|
|
128
|
+
const limit = Number(right.text);
|
|
129
|
+
if (op === ts.SyntaxKind.LessThanToken && limit <= maxExclusive)
|
|
130
|
+
return true;
|
|
131
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && limit <= maxExclusive - 1)
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (ts.isBinaryExpression(right) && right.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
135
|
+
const rLeft = unwrapExpression(right.left, ts);
|
|
136
|
+
const rRight = unwrapExpression(right.right, ts);
|
|
137
|
+
if (ts.isPropertyAccessExpression(rLeft) && rLeft.name.text === "length" && ts.isNumericLiteral(rRight)) {
|
|
138
|
+
const minusNum = Number(rRight.text);
|
|
139
|
+
if (op === ts.SyntaxKind.LessThanToken && minusNum >= 1)
|
|
140
|
+
return true;
|
|
141
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && minusNum >= 2)
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function isStateLowerBoundGuard(cond, stateName, minInclusive, ts, checker) {
|
|
150
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
151
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
152
|
+
const op = unwrapped.operatorToken.kind;
|
|
153
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
154
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
155
|
+
if (ts.isIdentifier(left) && left.text === stateName) {
|
|
156
|
+
if (ts.isNumericLiteral(right)) {
|
|
157
|
+
const limit = Number(right.text);
|
|
158
|
+
if (op === ts.SyntaxKind.GreaterThanToken && limit >= minInclusive - 1)
|
|
159
|
+
return true;
|
|
160
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken && limit >= minInclusive)
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
function isParamBoundedCondition(cond, paramName, arrayLength, ts, isExitGuard) {
|
|
168
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
169
|
+
if (isExitGuard) {
|
|
170
|
+
// Exit guard disjunction: param < 0 || param >= arrayLength (or param > arrayLength - 1)
|
|
171
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
172
|
+
let hasLowerExit = false;
|
|
173
|
+
let hasUpperExit = false;
|
|
174
|
+
const disjuncts = [];
|
|
175
|
+
function collectOrs(e) {
|
|
176
|
+
const u = unwrapExpression(e, ts);
|
|
177
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
178
|
+
collectOrs(u.left);
|
|
179
|
+
collectOrs(u.right);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
disjuncts.push(u);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
collectOrs(unwrapped);
|
|
186
|
+
for (const d of disjuncts) {
|
|
187
|
+
if (ts.isBinaryExpression(d)) {
|
|
188
|
+
const op = d.operatorToken.kind;
|
|
189
|
+
const left = unwrapExpression(d.left, ts);
|
|
190
|
+
const right = unwrapExpression(d.right, ts);
|
|
191
|
+
if (ts.isIdentifier(left) && left.text === paramName) {
|
|
192
|
+
if (ts.isNumericLiteral(right)) {
|
|
193
|
+
const n = Number(right.text);
|
|
194
|
+
if ((op === ts.SyntaxKind.LessThanToken && n === 0) || (op === ts.SyntaxKind.LessThanEqualsToken && n === -1)) {
|
|
195
|
+
hasLowerExit = true;
|
|
196
|
+
}
|
|
197
|
+
if ((op === ts.SyntaxKind.GreaterThanEqualsToken && n >= arrayLength) || (op === ts.SyntaxKind.GreaterThanToken && n >= arrayLength - 1)) {
|
|
198
|
+
hasUpperExit = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (ts.isPropertyAccessExpression(right) && right.name.text === "length") {
|
|
202
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken || op === ts.SyntaxKind.GreaterThanToken) {
|
|
203
|
+
hasUpperExit = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (ts.isBinaryExpression(right) && right.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
207
|
+
const rLeft = unwrapExpression(right.left, ts);
|
|
208
|
+
if (ts.isPropertyAccessExpression(rLeft) && rLeft.name.text === "length") {
|
|
209
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
210
|
+
hasUpperExit = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return hasLowerExit && hasUpperExit;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Entry guard conjunction: param >= 0 && param < arrayLength
|
|
222
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
223
|
+
let hasLowerEntry = false;
|
|
224
|
+
let hasUpperEntry = false;
|
|
225
|
+
const conjuncts = [];
|
|
226
|
+
function collectAnds(e) {
|
|
227
|
+
const u = unwrapExpression(e, ts);
|
|
228
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
229
|
+
collectAnds(u.left);
|
|
230
|
+
collectAnds(u.right);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
conjuncts.push(u);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
collectAnds(unwrapped);
|
|
237
|
+
for (const c of conjuncts) {
|
|
238
|
+
if (ts.isBinaryExpression(c)) {
|
|
239
|
+
const op = c.operatorToken.kind;
|
|
240
|
+
const left = unwrapExpression(c.left, ts);
|
|
241
|
+
const right = unwrapExpression(c.right, ts);
|
|
242
|
+
if (ts.isIdentifier(left) && left.text === paramName) {
|
|
243
|
+
if (ts.isNumericLiteral(right)) {
|
|
244
|
+
const n = Number(right.text);
|
|
245
|
+
if ((op === ts.SyntaxKind.GreaterThanEqualsToken && n === 0) || (op === ts.SyntaxKind.GreaterThanToken && n === -1)) {
|
|
246
|
+
hasLowerEntry = true;
|
|
247
|
+
}
|
|
248
|
+
if ((op === ts.SyntaxKind.LessThanToken && n <= arrayLength) || (op === ts.SyntaxKind.LessThanEqualsToken && n <= arrayLength - 1)) {
|
|
249
|
+
hasUpperEntry = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (ts.isPropertyAccessExpression(right) && right.name.text === "length") {
|
|
253
|
+
if (op === ts.SyntaxKind.LessThanToken || op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
254
|
+
hasUpperEntry = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return hasLowerEntry && hasUpperEntry;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
21
265
|
function normalizeIndexExpression(expr, ts, checker) {
|
|
22
266
|
const unwrapped = unwrapExpression(expr, ts);
|
|
23
267
|
if (ts.isIdentifier(unwrapped)) {
|
|
@@ -42,6 +286,22 @@ function normalizeIndexExpression(expr, ts, checker) {
|
|
|
42
286
|
}
|
|
43
287
|
return undefined;
|
|
44
288
|
}
|
|
289
|
+
function arraysMatch(a, b) {
|
|
290
|
+
if (!a || !b)
|
|
291
|
+
return false;
|
|
292
|
+
if (a.symbol && b.symbol && a.symbol === b.symbol)
|
|
293
|
+
return true;
|
|
294
|
+
return a.text === b.text;
|
|
295
|
+
}
|
|
296
|
+
function isNodeDescendantOf(child, parent) {
|
|
297
|
+
let cur = child;
|
|
298
|
+
while (cur) {
|
|
299
|
+
if (cur === parent)
|
|
300
|
+
return true;
|
|
301
|
+
cur = cur.parent;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
45
305
|
function normalizeArrayTarget(expr, ts, checker) {
|
|
46
306
|
const unwrapped = unwrapExpression(expr, ts);
|
|
47
307
|
if (ts.isIdentifier(unwrapped)) {
|
|
@@ -94,12 +354,13 @@ function isLowerBoundNegativeGuard(cond, indexInfo, ts) {
|
|
|
94
354
|
/**
|
|
95
355
|
* Checks if an expression represents `array.length` or `array.length - K`.
|
|
96
356
|
*/
|
|
97
|
-
function parseArrayLengthOffset(expr, arrayInfo, ts) {
|
|
357
|
+
function parseArrayLengthOffset(expr, arrayInfo, ts, checker) {
|
|
98
358
|
const unwrapped = unwrapExpression(expr, ts);
|
|
359
|
+
const targets = Array.isArray(arrayInfo) ? arrayInfo : [arrayInfo];
|
|
99
360
|
// array.length
|
|
100
361
|
if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "length") {
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
362
|
+
const objTarget = normalizeArrayTarget(unwrapped.expression, ts, checker);
|
|
363
|
+
if (objTarget && targets.some((t) => arraysMatch(objTarget, t))) {
|
|
103
364
|
return 0;
|
|
104
365
|
}
|
|
105
366
|
}
|
|
@@ -108,8 +369,8 @@ function parseArrayLengthOffset(expr, arrayInfo, ts) {
|
|
|
108
369
|
const left = unwrapExpression(unwrapped.left, ts);
|
|
109
370
|
const right = unwrapExpression(unwrapped.right, ts);
|
|
110
371
|
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
372
|
+
const objTarget = normalizeArrayTarget(left.expression, ts, checker);
|
|
373
|
+
if (objTarget && targets.some((t) => arraysMatch(objTarget, t)) && ts.isNumericLiteral(right)) {
|
|
113
374
|
const k = Number(right.text);
|
|
114
375
|
if (!Number.isNaN(k))
|
|
115
376
|
return k;
|
|
@@ -118,6 +379,133 @@ function parseArrayLengthOffset(expr, arrayInfo, ts) {
|
|
|
118
379
|
}
|
|
119
380
|
return undefined;
|
|
120
381
|
}
|
|
382
|
+
function parseLengthPropertyTarget(expr, ts, checker) {
|
|
383
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
384
|
+
if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "length") {
|
|
385
|
+
return normalizeArrayTarget(unwrapped.expression, ts, checker);
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
function parseGuardedLengthCondition(expr, arrayTarget, ts, checker) {
|
|
390
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
391
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
392
|
+
const op = unwrapped.operatorToken.kind;
|
|
393
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
394
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
395
|
+
// Conjunction: A && B
|
|
396
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
397
|
+
const leftFact = parseGuardedLengthCondition(unwrapped.left, arrayTarget, ts, checker);
|
|
398
|
+
const rightFact = parseGuardedLengthCondition(unwrapped.right, arrayTarget, ts, checker);
|
|
399
|
+
if (leftFact && rightFact) {
|
|
400
|
+
return {
|
|
401
|
+
minLength: Math.max(leftFact.minLength, rightFact.minLength),
|
|
402
|
+
exactLength: leftFact.exactLength ?? rightFact.exactLength,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return leftFact ?? rightFact;
|
|
406
|
+
}
|
|
407
|
+
const leftTarget = parseLengthPropertyTarget(left, ts, checker);
|
|
408
|
+
const rightTarget = parseLengthPropertyTarget(right, ts, checker);
|
|
409
|
+
// Left is arr.length, right is number literal
|
|
410
|
+
if (arraysMatch(leftTarget, arrayTarget) && ts.isNumericLiteral(right)) {
|
|
411
|
+
const num = Number(right.text);
|
|
412
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
413
|
+
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) {
|
|
414
|
+
return { minLength: num, exactLength: num };
|
|
415
|
+
}
|
|
416
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
417
|
+
return { minLength: num };
|
|
418
|
+
}
|
|
419
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
420
|
+
return { minLength: num + 1 };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Right is arr.length, left is number literal
|
|
425
|
+
if (arraysMatch(rightTarget, arrayTarget) && ts.isNumericLiteral(left)) {
|
|
426
|
+
const num = Number(left.text);
|
|
427
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
428
|
+
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) {
|
|
429
|
+
return { minLength: num, exactLength: num };
|
|
430
|
+
}
|
|
431
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
432
|
+
return { minLength: num };
|
|
433
|
+
}
|
|
434
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
435
|
+
return { minLength: num + 1 };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
function parseExitGuardedLengthCondition(expr, arrayTarget, ts, checker) {
|
|
443
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
444
|
+
// Disjunction: A || B
|
|
445
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
446
|
+
const leftFact = parseExitGuardedLengthCondition(unwrapped.left, arrayTarget, ts, checker);
|
|
447
|
+
const rightFact = parseExitGuardedLengthCondition(unwrapped.right, arrayTarget, ts, checker);
|
|
448
|
+
if (leftFact && rightFact) {
|
|
449
|
+
return {
|
|
450
|
+
minLength: Math.max(leftFact.minLength, rightFact.minLength),
|
|
451
|
+
exactLength: leftFact.exactLength ?? rightFact.exactLength,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return leftFact ?? rightFact;
|
|
455
|
+
}
|
|
456
|
+
// !arr.length or !arr
|
|
457
|
+
if (ts.isPrefixUnaryExpression(unwrapped) && unwrapped.operator === ts.SyntaxKind.ExclamationToken) {
|
|
458
|
+
const inner = unwrapExpression(unwrapped.operand, ts);
|
|
459
|
+
const target = parseLengthPropertyTarget(inner, ts, checker);
|
|
460
|
+
if (arraysMatch(target, arrayTarget)) {
|
|
461
|
+
return { minLength: 1 };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
465
|
+
const op = unwrapped.operatorToken.kind;
|
|
466
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
467
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
468
|
+
const leftTarget = parseLengthPropertyTarget(left, ts, checker);
|
|
469
|
+
const rightTarget = parseLengthPropertyTarget(right, ts, checker);
|
|
470
|
+
// Left is arr.length, right is number
|
|
471
|
+
if (arraysMatch(leftTarget, arrayTarget) && ts.isNumericLiteral(right)) {
|
|
472
|
+
const num = Number(right.text);
|
|
473
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
474
|
+
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
475
|
+
return { minLength: num, exactLength: num };
|
|
476
|
+
}
|
|
477
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
478
|
+
return { minLength: num };
|
|
479
|
+
}
|
|
480
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
481
|
+
return { minLength: num + 1 };
|
|
482
|
+
}
|
|
483
|
+
if ((op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) && num === 0) {
|
|
484
|
+
return { minLength: 1 };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Right is arr.length, left is number
|
|
489
|
+
if (arraysMatch(rightTarget, arrayTarget) && ts.isNumericLiteral(left)) {
|
|
490
|
+
const num = Number(left.text);
|
|
491
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
492
|
+
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
493
|
+
return { minLength: num, exactLength: num };
|
|
494
|
+
}
|
|
495
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
496
|
+
return { minLength: num };
|
|
497
|
+
}
|
|
498
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
499
|
+
return { minLength: num + 1 };
|
|
500
|
+
}
|
|
501
|
+
if ((op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) && num === 0) {
|
|
502
|
+
return { minLength: 1 };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
121
509
|
/**
|
|
122
510
|
* Checks if a binary condition expression guards an upper bound against array.length.
|
|
123
511
|
* e.g. `index >= array.length` or `index >= array.length - 1` or `index > array.length - 1`.
|
|
@@ -185,87 +573,2013 @@ function isCompleteBoundsExitGuard(cond, indexInfo, arrayInfo, ts) {
|
|
|
185
573
|
return false;
|
|
186
574
|
}
|
|
187
575
|
/**
|
|
188
|
-
* Checks if a non-
|
|
576
|
+
* Checks if an entry condition contains both a lower-bound non-negative check and an upper-bound check against array.length.
|
|
577
|
+
* e.g. `if (i >= 0 && i < array.length - 1)`
|
|
189
578
|
*/
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
579
|
+
function isPositiveBoundsEntryGuard(cond, indexInfo, arrayInfo, ts) {
|
|
580
|
+
const conjuncts = [];
|
|
581
|
+
function collectAnds(e) {
|
|
582
|
+
const u = unwrapExpression(e, ts);
|
|
583
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
584
|
+
collectAnds(u.left);
|
|
585
|
+
collectAnds(u.right);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
conjuncts.push(u);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
collectAnds(cond);
|
|
592
|
+
let hasLower = false;
|
|
593
|
+
let maxUpperOffset = -1;
|
|
594
|
+
for (const c of conjuncts) {
|
|
595
|
+
const u = unwrapExpression(c, ts);
|
|
596
|
+
if (!ts.isBinaryExpression(u))
|
|
597
|
+
continue;
|
|
598
|
+
const left = unwrapExpression(u.left, ts);
|
|
599
|
+
const right = unwrapExpression(u.right, ts);
|
|
600
|
+
const op = u.operatorToken.kind;
|
|
601
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken && ts.isIdentifier(left) && left.text === indexInfo.baseText && ts.isNumericLiteral(right) && right.text === "0") {
|
|
602
|
+
hasLower = true;
|
|
603
|
+
}
|
|
604
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && ts.isNumericLiteral(left) && left.text === "0" && ts.isIdentifier(right) && right.text === indexInfo.baseText) {
|
|
605
|
+
hasLower = true;
|
|
606
|
+
}
|
|
607
|
+
if (op === ts.SyntaxKind.GreaterThanToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
608
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
609
|
+
hasLower = true;
|
|
201
610
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
611
|
+
}
|
|
612
|
+
if (op === ts.SyntaxKind.LessThanToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
613
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts);
|
|
614
|
+
if (lengthMinus !== undefined) {
|
|
615
|
+
maxUpperOffset = Math.max(maxUpperOffset, lengthMinus);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
619
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts);
|
|
620
|
+
if (lengthMinus !== undefined && lengthMinus >= 1) {
|
|
621
|
+
maxUpperOffset = Math.max(maxUpperOffset, lengthMinus - 1);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (hasLower && indexInfo.offset >= 0 && indexInfo.offset <= maxUpperOffset) {
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Checks if an array target is a static, non-empty const array initialized with an array literal.
|
|
632
|
+
*/
|
|
633
|
+
function resolveStaticConstArrayLiteral(arrayExpr, ts, checker) {
|
|
634
|
+
if (!checker)
|
|
635
|
+
return undefined;
|
|
636
|
+
const unwrapped = unwrapExpression(arrayExpr, ts);
|
|
637
|
+
let symbol = checker.getSymbolAtLocation(unwrapped);
|
|
638
|
+
if (!symbol)
|
|
639
|
+
return undefined;
|
|
640
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
641
|
+
try {
|
|
642
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// ignore
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const decls = symbol.getDeclarations();
|
|
649
|
+
if (!decls || decls.length === 0)
|
|
650
|
+
return undefined;
|
|
651
|
+
for (const decl of decls) {
|
|
652
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
653
|
+
const init = unwrapExpression(decl.initializer, ts);
|
|
654
|
+
if (ts.isArrayLiteralExpression(init)) {
|
|
655
|
+
const isConst = decl.parent &&
|
|
656
|
+
ts.isVariableDeclarationList(decl.parent) &&
|
|
657
|
+
Boolean(decl.parent.flags & ts.NodeFlags.Const);
|
|
658
|
+
if (isConst) {
|
|
659
|
+
const length = init.elements.length;
|
|
660
|
+
if (length > 0) {
|
|
661
|
+
return {
|
|
662
|
+
elements: init.elements,
|
|
663
|
+
length,
|
|
664
|
+
name: ts.isIdentifier(decl.name) ? decl.name.text : unwrapped.getText(),
|
|
665
|
+
};
|
|
222
666
|
}
|
|
223
667
|
}
|
|
224
668
|
}
|
|
225
669
|
}
|
|
226
670
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Checks if a static array access has a literal integer index guaranteed to be in-bounds.
|
|
675
|
+
*/
|
|
676
|
+
function isStaticArrayLiteralBoundedIndex(operand, ts, checker) {
|
|
677
|
+
const staticArray = resolveStaticConstArrayLiteral(operand.expression, ts, checker);
|
|
678
|
+
if (!staticArray)
|
|
679
|
+
return undefined;
|
|
680
|
+
const indexExpr = unwrapExpression(operand.argumentExpression, ts);
|
|
681
|
+
// Case A: Literal numeric integer index e.g. tiers[0]! or colW[2]!
|
|
682
|
+
if (ts.isNumericLiteral(indexExpr)) {
|
|
683
|
+
const k = Number(indexExpr.text);
|
|
684
|
+
if (Number.isInteger(k) && k >= 0 && k < staticArray.length) {
|
|
685
|
+
return {
|
|
686
|
+
isGuarded: true,
|
|
687
|
+
evidence: `Literal index ${k} is statically in-bounds for constant array '${staticArray.name}' of length ${staticArray.length}.`,
|
|
688
|
+
};
|
|
233
689
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
690
|
+
}
|
|
691
|
+
// Case B: array[array.length - 1]!
|
|
692
|
+
if (ts.isBinaryExpression(indexExpr) && indexExpr.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
693
|
+
const left = unwrapExpression(indexExpr.left, ts);
|
|
694
|
+
const right = unwrapExpression(indexExpr.right, ts);
|
|
695
|
+
if (ts.isPropertyAccessExpression(left) &&
|
|
696
|
+
left.name.text === "length" &&
|
|
697
|
+
ts.isNumericLiteral(right) &&
|
|
698
|
+
right.text === "1") {
|
|
699
|
+
return {
|
|
700
|
+
isGuarded: true,
|
|
701
|
+
evidence: `Index 'length - 1' is statically in-bounds for non-empty constant array '${staticArray.name}' of length ${staticArray.length}.`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Checks if a literal index access is proven in-bounds by a dominating fixed-length equality or comparison guard.
|
|
709
|
+
* e.g. `if (arr.length === 2) arr[0]!` or `if (arr.length >= 2) arr[1]!`
|
|
710
|
+
*/
|
|
711
|
+
function isFixedLengthGuardedIndex(node, operand, ts, checker) {
|
|
712
|
+
const arrayTarget = normalizeArrayTarget(operand.expression, ts, checker);
|
|
713
|
+
if (!arrayTarget)
|
|
714
|
+
return undefined;
|
|
715
|
+
const indexExpr = unwrapExpression(operand.argumentExpression, ts);
|
|
716
|
+
if (!ts.isNumericLiteral(indexExpr))
|
|
717
|
+
return undefined;
|
|
718
|
+
const k = Number(indexExpr.text);
|
|
719
|
+
if (!Number.isInteger(k) || k < 0)
|
|
720
|
+
return undefined;
|
|
721
|
+
// 1. Walk up enclosing AST nodes (if statements, && expressions, ternary expressions)
|
|
722
|
+
let curr = node;
|
|
723
|
+
while (curr.parent &&
|
|
724
|
+
!ts.isFunctionDeclaration(curr.parent) &&
|
|
725
|
+
!ts.isFunctionExpression(curr.parent) &&
|
|
726
|
+
!ts.isArrowFunction(curr.parent) &&
|
|
727
|
+
!ts.isSourceFile(curr.parent)) {
|
|
728
|
+
// Case A: Enclosing IfStatement
|
|
729
|
+
if (ts.isIfStatement(curr.parent)) {
|
|
730
|
+
const ifStmt = curr.parent;
|
|
731
|
+
if (ifStmt.thenStatement === curr ||
|
|
732
|
+
(ts.isBlock(ifStmt.thenStatement) && ifStmt.thenStatement.statements.includes(curr)) ||
|
|
733
|
+
isNodeDescendantOf(curr, ifStmt.thenStatement)) {
|
|
734
|
+
const fact = parseGuardedLengthCondition(ifStmt.expression, arrayTarget, ts, checker);
|
|
735
|
+
if (fact) {
|
|
736
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
737
|
+
if (k < maxAllowed) {
|
|
738
|
+
if (!hasMutationBeforeNode(ifStmt.thenStatement, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
739
|
+
return {
|
|
740
|
+
isGuarded: true,
|
|
741
|
+
evidence: `Enclosing guard '${ifStmt.expression.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
else if (ifStmt.elseStatement &&
|
|
748
|
+
(ifStmt.elseStatement === curr ||
|
|
749
|
+
(ts.isBlock(ifStmt.elseStatement) && ifStmt.elseStatement.statements.includes(curr)) ||
|
|
750
|
+
isNodeDescendantOf(curr, ifStmt.elseStatement))) {
|
|
751
|
+
const fact = parseExitGuardedLengthCondition(ifStmt.expression, arrayTarget, ts, checker);
|
|
752
|
+
if (fact) {
|
|
753
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
754
|
+
if (k < maxAllowed) {
|
|
755
|
+
if (!hasMutationBeforeNode(ifStmt.elseStatement, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
756
|
+
return {
|
|
757
|
+
isGuarded: true,
|
|
758
|
+
evidence: `Enclosing guard 'else' of '${ifStmt.expression.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// Case B: Logical AND expression: arr.length === 2 && arr[0]!
|
|
766
|
+
if (ts.isBinaryExpression(curr.parent) &&
|
|
767
|
+
curr.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
768
|
+
(curr.parent.right === curr || isNodeDescendantOf(curr, curr.parent.right))) {
|
|
769
|
+
const fact = parseGuardedLengthCondition(curr.parent.left, arrayTarget, ts, checker);
|
|
770
|
+
if (fact) {
|
|
771
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
772
|
+
if (k < maxAllowed) {
|
|
773
|
+
if (!hasMutationBeforeNode(curr.parent.right, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
774
|
+
return {
|
|
775
|
+
isGuarded: true,
|
|
776
|
+
evidence: `Logical AND guard '${curr.parent.left.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Case C: ConditionalExpression: arr.length === 2 ? arr[0]! : fallback
|
|
783
|
+
if (ts.isConditionalExpression(curr.parent)) {
|
|
784
|
+
const condExpr = curr.parent;
|
|
785
|
+
if (condExpr.whenTrue === curr || isNodeDescendantOf(curr, condExpr.whenTrue)) {
|
|
786
|
+
const fact = parseGuardedLengthCondition(condExpr.condition, arrayTarget, ts, checker);
|
|
787
|
+
if (fact) {
|
|
788
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
789
|
+
if (k < maxAllowed) {
|
|
790
|
+
if (!hasMutationBeforeNode(condExpr.whenTrue, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
791
|
+
return {
|
|
792
|
+
isGuarded: true,
|
|
793
|
+
evidence: `Ternary guard '${condExpr.condition.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else if (condExpr.whenFalse === curr || isNodeDescendantOf(curr, condExpr.whenFalse)) {
|
|
800
|
+
const fact = parseExitGuardedLengthCondition(condExpr.condition, arrayTarget, ts, checker);
|
|
801
|
+
if (fact) {
|
|
802
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
803
|
+
if (k < maxAllowed) {
|
|
804
|
+
if (!hasMutationBeforeNode(condExpr.whenFalse, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
805
|
+
return {
|
|
806
|
+
isGuarded: true,
|
|
807
|
+
evidence: `Ternary false branch of '${condExpr.condition.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
curr = curr.parent;
|
|
815
|
+
}
|
|
816
|
+
// 2. Check dominating early-exit guards in prior sibling statements
|
|
817
|
+
let stmt = node;
|
|
818
|
+
while (stmt.parent &&
|
|
819
|
+
!ts.isBlock(stmt.parent) &&
|
|
820
|
+
!ts.isSourceFile(stmt.parent) &&
|
|
821
|
+
!ts.isCaseClause(stmt.parent) &&
|
|
822
|
+
!ts.isDefaultClause(stmt.parent)) {
|
|
823
|
+
stmt = stmt.parent;
|
|
824
|
+
}
|
|
825
|
+
const container = stmt.parent;
|
|
826
|
+
if (container &&
|
|
827
|
+
(ts.isBlock(container) ||
|
|
828
|
+
ts.isSourceFile(container) ||
|
|
829
|
+
ts.isCaseClause(container) ||
|
|
830
|
+
ts.isDefaultClause(container))) {
|
|
831
|
+
const statements = container.statements;
|
|
832
|
+
const targetIndex = statements.indexOf(stmt);
|
|
833
|
+
if (targetIndex > 0) {
|
|
834
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
835
|
+
const prior = statements[i];
|
|
836
|
+
if (!prior)
|
|
837
|
+
continue;
|
|
838
|
+
if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
839
|
+
const fact = parseExitGuardedLengthCondition(prior.expression, arrayTarget, ts, checker);
|
|
840
|
+
if (fact) {
|
|
841
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
842
|
+
if (k < maxAllowed) {
|
|
843
|
+
let hasInterveningMutation = false;
|
|
844
|
+
for (let m = i + 1; m < targetIndex; m++) {
|
|
845
|
+
const s = statements[m];
|
|
846
|
+
if (s && hasArrayMutation(s, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
847
|
+
hasInterveningMutation = true;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (!hasInterveningMutation &&
|
|
852
|
+
!hasMutationBeforeNode(stmt, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
248
853
|
return {
|
|
249
854
|
isGuarded: true,
|
|
250
|
-
evidence: `Dominating guard '
|
|
855
|
+
evidence: `Dominating guard '${prior.expression.getText()}' proves '${arrayTarget.text}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`,
|
|
251
856
|
};
|
|
252
857
|
}
|
|
253
858
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Checks if a call expression is a genuine Array.prototype method call (e.g. map or forEach).
|
|
868
|
+
*/
|
|
869
|
+
function isBuiltinArrayMethodCall(call, methodName, ts, checker) {
|
|
870
|
+
if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== methodName) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
const callerExpr = unwrapExpression(call.expression.expression, ts);
|
|
874
|
+
// Array literals: [1, 2].map(...)
|
|
875
|
+
if (ts.isArrayLiteralExpression(callerExpr)) {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
if (checker) {
|
|
879
|
+
const callerType = checker.getTypeAtLocation(callerExpr);
|
|
880
|
+
if (checker.isArrayType(callerType) || checker.isTupleType(callerType)) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
const methodSym = checker.getSymbolAtLocation(call.expression);
|
|
884
|
+
if (methodSym) {
|
|
885
|
+
const decls = methodSym.getDeclarations();
|
|
886
|
+
if (decls && decls.length > 0) {
|
|
887
|
+
for (const d of decls) {
|
|
888
|
+
const sf = d.getSourceFile();
|
|
889
|
+
if (sf.isDeclarationFile &&
|
|
890
|
+
(sf.fileName.includes("lib.es") ||
|
|
891
|
+
sf.fileName.includes("lib.core") ||
|
|
892
|
+
sf.fileName.includes("lib.d.ts") ||
|
|
893
|
+
sf.fileName.includes("typescript/lib"))) {
|
|
894
|
+
const parentName = d.parent && ts.isInterfaceDeclaration(d.parent) ? d.parent.name.text : undefined;
|
|
895
|
+
if (parentName === "Array" ||
|
|
896
|
+
parentName === "ReadonlyArray" ||
|
|
897
|
+
parentName === "ConcatArray" ||
|
|
898
|
+
parentName?.includes("Array")) {
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Checks if an element access index is bounded by an enclosing for-loop or array iteration callback.
|
|
911
|
+
*/
|
|
912
|
+
function isLoopOrCallbackBoundedIndex(node, operand, ts, checker) {
|
|
913
|
+
const indexInfo = normalizeIndexExpression(operand.argumentExpression, ts, checker);
|
|
914
|
+
if (!indexInfo)
|
|
915
|
+
return undefined;
|
|
916
|
+
const arrayTarget = normalizeArrayTarget(operand.expression, ts, checker);
|
|
917
|
+
if (!arrayTarget)
|
|
918
|
+
return undefined;
|
|
919
|
+
let curr = node;
|
|
920
|
+
while (curr.parent && !ts.isSourceFile(curr.parent)) {
|
|
921
|
+
// 1. Enclosing classic ForStatement: for (let i = 0; i < arr.length - K; i++)
|
|
922
|
+
if (ts.isForStatement(curr.parent)) {
|
|
923
|
+
const forStmt = curr.parent;
|
|
924
|
+
if (forStmt.condition) {
|
|
925
|
+
const conjuncts = [];
|
|
926
|
+
function collectConjuncts(e) {
|
|
927
|
+
const u = unwrapExpression(e, ts);
|
|
928
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
929
|
+
collectConjuncts(u.left);
|
|
930
|
+
collectConjuncts(u.right);
|
|
931
|
+
}
|
|
932
|
+
else if (ts.isBinaryExpression(u)) {
|
|
933
|
+
conjuncts.push(u);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
collectConjuncts(forStmt.condition);
|
|
937
|
+
for (const cond of conjuncts) {
|
|
938
|
+
const left = unwrapExpression(cond.left, ts);
|
|
939
|
+
if (ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
940
|
+
const op = cond.operatorToken.kind;
|
|
941
|
+
const right = unwrapExpression(cond.right, ts);
|
|
942
|
+
// Parse loop start index (e.g. let i = 0 or let i = 1)
|
|
943
|
+
let initVal = 0;
|
|
944
|
+
if (forStmt.initializer && ts.isVariableDeclarationList(forStmt.initializer)) {
|
|
945
|
+
const decl = forStmt.initializer.declarations[0];
|
|
946
|
+
if (decl && decl.initializer && ts.isNumericLiteral(decl.initializer)) {
|
|
947
|
+
initVal = Number(decl.initializer.text);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
// Condition: i < array.length (K = 0)
|
|
951
|
+
if (op === ts.SyntaxKind.LessThanToken &&
|
|
952
|
+
ts.isPropertyAccessExpression(right) &&
|
|
953
|
+
right.name.text === "length") {
|
|
954
|
+
const condArr = normalizeArrayTarget(right.expression, ts, checker);
|
|
955
|
+
if (condArr && condArr.text === arrayTarget.text) {
|
|
956
|
+
if (initVal + indexInfo.offset >= 0 && indexInfo.offset <= 0) {
|
|
957
|
+
return {
|
|
958
|
+
isGuarded: true,
|
|
959
|
+
evidence: `Index '${indexInfo.baseText}' is bounded by enclosing loop condition '${cond.getText()}'.`,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Condition: i < array.length - K (allows 0 <= offset <= K)
|
|
965
|
+
if (op === ts.SyntaxKind.LessThanToken &&
|
|
966
|
+
ts.isBinaryExpression(right) &&
|
|
967
|
+
right.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
968
|
+
const rightLeft = unwrapExpression(right.left, ts);
|
|
969
|
+
const rightRight = unwrapExpression(right.right, ts);
|
|
970
|
+
if (ts.isPropertyAccessExpression(rightLeft) &&
|
|
971
|
+
rightLeft.name.text === "length" &&
|
|
972
|
+
ts.isNumericLiteral(rightRight)) {
|
|
973
|
+
const k = Number(rightRight.text);
|
|
974
|
+
const condArr = normalizeArrayTarget(rightLeft.expression, ts, checker);
|
|
975
|
+
if (condArr && condArr.text === arrayTarget.text) {
|
|
976
|
+
if (initVal + indexInfo.offset >= 0 && indexInfo.offset >= 0 && indexInfo.offset <= k) {
|
|
977
|
+
return {
|
|
978
|
+
isGuarded: true,
|
|
979
|
+
evidence: `Index '${indexInfo.baseText}${indexInfo.offset ? ` + ${indexInfo.offset}` : ""}' is bounded by enclosing loop condition '${cond.getText()}'.`,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Condition: i <= array.length - 1 - K (allows 0 <= offset <= K)
|
|
986
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken &&
|
|
987
|
+
ts.isBinaryExpression(right) &&
|
|
988
|
+
right.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
989
|
+
const rightLeft = unwrapExpression(right.left, ts);
|
|
990
|
+
const rightRight = unwrapExpression(right.right, ts);
|
|
991
|
+
if (ts.isPropertyAccessExpression(rightLeft) &&
|
|
992
|
+
rightLeft.name.text === "length" &&
|
|
993
|
+
ts.isNumericLiteral(rightRight)) {
|
|
994
|
+
const minusVal = Number(rightRight.text);
|
|
995
|
+
const k = minusVal - 1;
|
|
996
|
+
const condArr = normalizeArrayTarget(rightLeft.expression, ts, checker);
|
|
997
|
+
if (condArr && condArr.text === arrayTarget.text) {
|
|
998
|
+
if (initVal + indexInfo.offset >= 0 && indexInfo.offset >= 0 && indexInfo.offset <= k) {
|
|
999
|
+
return {
|
|
1000
|
+
isGuarded: true,
|
|
1001
|
+
evidence: `Index '${indexInfo.baseText}${indexInfo.offset ? ` + ${indexInfo.offset}` : ""}' is bounded by enclosing loop condition '${cond.getText()}'.`,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// 2. Enclosing .map(...) or .forEach(...) callback
|
|
1012
|
+
if ((ts.isArrowFunction(curr.parent) || ts.isFunctionExpression(curr.parent)) &&
|
|
1013
|
+
curr.parent.parent &&
|
|
1014
|
+
ts.isCallExpression(curr.parent.parent)) {
|
|
1015
|
+
const call = curr.parent.parent;
|
|
1016
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
1017
|
+
const method = call.expression.name.text;
|
|
1018
|
+
if (method === "map" || method === "forEach") {
|
|
1019
|
+
if (isBuiltinArrayMethodCall(call, method, ts, checker)) {
|
|
1020
|
+
const callback = curr.parent;
|
|
1021
|
+
const indexParam = callback.parameters[1];
|
|
1022
|
+
const arrayParam = callback.parameters[2];
|
|
1023
|
+
if (indexParam && ts.isIdentifier(indexParam.name) && indexParam.name.text === indexInfo.baseText) {
|
|
1024
|
+
const callerExpr = call.expression.expression;
|
|
1025
|
+
const directCallerArr = normalizeArrayTarget(callerExpr, ts, checker);
|
|
1026
|
+
const arrayParamTarget = arrayParam && ts.isIdentifier(arrayParam.name)
|
|
1027
|
+
? normalizeArrayTarget(arrayParam.name, ts, checker)
|
|
1028
|
+
: undefined;
|
|
1029
|
+
const collectionTargets = [];
|
|
1030
|
+
if (directCallerArr)
|
|
1031
|
+
collectionTargets.push(directCallerArr);
|
|
1032
|
+
if (arrayParamTarget)
|
|
1033
|
+
collectionTargets.push(arrayParamTarget);
|
|
1034
|
+
const isIteratedCollectionAccess = collectionTargets.some((t) => arraysMatch(arrayTarget, t));
|
|
1035
|
+
if (isIteratedCollectionAccess) {
|
|
1036
|
+
// Check if any mutation occurred before node in the callback
|
|
1037
|
+
let hasMutation = false;
|
|
1038
|
+
for (const target of collectionTargets) {
|
|
1039
|
+
if (hasMutationBeforeNode(callback, node, target.text, ts, checker, target.symbol)) {
|
|
1040
|
+
hasMutation = true;
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (!hasMutation) {
|
|
1045
|
+
// Sub-case 2a: Direct index access (offset 0): arr[i]! or array[i]!
|
|
1046
|
+
if (indexInfo.offset === 0) {
|
|
1047
|
+
const targetName = arrayParamTarget && arraysMatch(arrayTarget, arrayParamTarget)
|
|
1048
|
+
? arrayParamTarget.text
|
|
1049
|
+
: directCallerArr
|
|
1050
|
+
? directCallerArr.text
|
|
1051
|
+
: arrayTarget.text;
|
|
1052
|
+
return {
|
|
1053
|
+
isGuarded: true,
|
|
1054
|
+
evidence: `Index '${indexInfo.baseText}' is callback index parameter for '${directCallerArr ? directCallerArr.text : targetName}.${method}'.`,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Sub-case 2b: Offset > 0 e.g. array[i + 1]! or arr[i + 1]!
|
|
1058
|
+
// Check if inside callback there is an enclosing guard or dominating exit guard
|
|
1059
|
+
let innerNode = node;
|
|
1060
|
+
while (innerNode && innerNode !== callback) {
|
|
1061
|
+
// Form 1: if (i < arr.length - K) or if (i < array.length - K)
|
|
1062
|
+
if (innerNode.parent && ts.isIfStatement(innerNode.parent)) {
|
|
1063
|
+
if (innerNode.parent.thenStatement === innerNode ||
|
|
1064
|
+
(ts.isBlock(innerNode.parent.thenStatement) &&
|
|
1065
|
+
innerNode.parent.thenStatement.statements.includes(innerNode)) ||
|
|
1066
|
+
isNodeDescendantOf(innerNode, innerNode.parent.thenStatement)) {
|
|
1067
|
+
const ifCond = innerNode.parent.expression;
|
|
1068
|
+
const unwrappedCond = unwrapExpression(ifCond, ts);
|
|
1069
|
+
if (ts.isBinaryExpression(unwrappedCond)) {
|
|
1070
|
+
const condLeft = unwrapExpression(unwrappedCond.left, ts);
|
|
1071
|
+
const condRight = unwrapExpression(unwrappedCond.right, ts);
|
|
1072
|
+
const condOp = unwrappedCond.operatorToken.kind;
|
|
1073
|
+
if (ts.isIdentifier(condLeft) && condLeft.text === indexInfo.baseText) {
|
|
1074
|
+
if (condOp === ts.SyntaxKind.LessThanToken) {
|
|
1075
|
+
const lengthMinus = parseArrayLengthOffset(condRight, collectionTargets, ts, checker);
|
|
1076
|
+
if (lengthMinus !== undefined && indexInfo.offset >= 0 && indexInfo.offset <= lengthMinus) {
|
|
1077
|
+
return {
|
|
1078
|
+
isGuarded: true,
|
|
1079
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by '${innerNode.parent.expression.getText()}'.`,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (condOp === ts.SyntaxKind.LessThanEqualsToken) {
|
|
1084
|
+
const lengthMinus = parseArrayLengthOffset(condRight, collectionTargets, ts, checker);
|
|
1085
|
+
if (lengthMinus !== undefined &&
|
|
1086
|
+
lengthMinus >= 1 &&
|
|
1087
|
+
indexInfo.offset >= 0 &&
|
|
1088
|
+
indexInfo.offset <= lengthMinus - 1) {
|
|
1089
|
+
return {
|
|
1090
|
+
isGuarded: true,
|
|
1091
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by '${innerNode.parent.expression.getText()}'.`,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// Form 2: JSX logical AND expression: {i < array.length - K && <Element ... />}
|
|
1100
|
+
if (innerNode.parent &&
|
|
1101
|
+
ts.isBinaryExpression(innerNode.parent) &&
|
|
1102
|
+
innerNode.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1103
|
+
let checkLeft = innerNode;
|
|
1104
|
+
while (checkLeft.parent &&
|
|
1105
|
+
ts.isBinaryExpression(checkLeft.parent) &&
|
|
1106
|
+
checkLeft.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
1107
|
+
checkLeft.parent.right === checkLeft) {
|
|
1108
|
+
const condLeft = unwrapExpression(checkLeft.parent.left, ts);
|
|
1109
|
+
if (ts.isBinaryExpression(condLeft)) {
|
|
1110
|
+
const cLeft = unwrapExpression(condLeft.left, ts);
|
|
1111
|
+
const cRight = unwrapExpression(condLeft.right, ts);
|
|
1112
|
+
const cOp = condLeft.operatorToken.kind;
|
|
1113
|
+
if (ts.isIdentifier(cLeft) && cLeft.text === indexInfo.baseText) {
|
|
1114
|
+
if (cOp === ts.SyntaxKind.LessThanToken) {
|
|
1115
|
+
const lengthMinus = parseArrayLengthOffset(cRight, collectionTargets, ts, checker);
|
|
1116
|
+
if (lengthMinus !== undefined && indexInfo.offset >= 0 && indexInfo.offset <= lengthMinus) {
|
|
1117
|
+
return {
|
|
1118
|
+
isGuarded: true,
|
|
1119
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by '${condLeft.getText()}'.`,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (cOp === ts.SyntaxKind.LessThanEqualsToken) {
|
|
1124
|
+
const lengthMinus = parseArrayLengthOffset(cRight, collectionTargets, ts, checker);
|
|
1125
|
+
if (lengthMinus !== undefined &&
|
|
1126
|
+
lengthMinus >= 1 &&
|
|
1127
|
+
indexInfo.offset >= 0 &&
|
|
1128
|
+
indexInfo.offset <= lengthMinus - 1) {
|
|
1129
|
+
return {
|
|
1130
|
+
isGuarded: true,
|
|
1131
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by '${condLeft.getText()}'.`,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
checkLeft = checkLeft.parent;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
innerNode = innerNode.parent;
|
|
1141
|
+
}
|
|
1142
|
+
// Form 3: Dominating exit guard in statements of callback body
|
|
1143
|
+
if (ts.isBlock(callback.body)) {
|
|
1144
|
+
let stmtNode = node;
|
|
1145
|
+
while (stmtNode.parent && stmtNode.parent !== callback.body) {
|
|
1146
|
+
stmtNode = stmtNode.parent;
|
|
1147
|
+
}
|
|
1148
|
+
const cbStmts = callback.body.statements;
|
|
1149
|
+
const targetIdx = cbStmts.indexOf(stmtNode);
|
|
1150
|
+
if (targetIdx > 0) {
|
|
1151
|
+
for (let s = 0; s < targetIdx; s++) {
|
|
1152
|
+
const prior = cbStmts[s];
|
|
1153
|
+
if (prior && ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
1154
|
+
const unwrappedCond = unwrapExpression(prior.expression, ts);
|
|
1155
|
+
if (ts.isBinaryExpression(unwrappedCond)) {
|
|
1156
|
+
const cLeft = unwrapExpression(unwrappedCond.left, ts);
|
|
1157
|
+
const cRight = unwrapExpression(unwrappedCond.right, ts);
|
|
1158
|
+
const cOp = unwrappedCond.operatorToken.kind;
|
|
1159
|
+
if (ts.isIdentifier(cLeft) && cLeft.text === indexInfo.baseText) {
|
|
1160
|
+
if (cOp === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
1161
|
+
const lengthMinus = parseArrayLengthOffset(cRight, collectionTargets, ts, checker);
|
|
1162
|
+
if (lengthMinus !== undefined && indexInfo.offset >= 0 && indexInfo.offset <= lengthMinus) {
|
|
1163
|
+
return {
|
|
1164
|
+
isGuarded: true,
|
|
1165
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by exit guard '${prior.expression.getText()}'.`,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (cOp === ts.SyntaxKind.GreaterThanToken) {
|
|
1170
|
+
const lengthMinus = parseArrayLengthOffset(cRight, collectionTargets, ts, checker);
|
|
1171
|
+
if (lengthMinus !== undefined &&
|
|
1172
|
+
lengthMinus >= 1 &&
|
|
1173
|
+
indexInfo.offset >= 0 &&
|
|
1174
|
+
indexInfo.offset <= lengthMinus - 1) {
|
|
1175
|
+
return {
|
|
1176
|
+
isGuarded: true,
|
|
1177
|
+
evidence: `Callback index '${indexInfo.baseText}' is guarded non-negative and bounded by exit guard '${prior.expression.getText()}'.`,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// Sub-case 2c: Sliced caller array: arr.slice(0, -K).map((_, i) => arr[i + offset]!)
|
|
1190
|
+
if (ts.isCallExpression(callerExpr) && ts.isPropertyAccessExpression(callerExpr.expression)) {
|
|
1191
|
+
const sliceMethod = callerExpr.expression.name.text;
|
|
1192
|
+
if (sliceMethod === "slice") {
|
|
1193
|
+
const sliceReceiver = normalizeArrayTarget(callerExpr.expression.expression, ts, checker);
|
|
1194
|
+
if (sliceReceiver && arraysMatch(sliceReceiver, arrayTarget)) {
|
|
1195
|
+
const sliceArgs = callerExpr.arguments;
|
|
1196
|
+
let startZero = false;
|
|
1197
|
+
let endMinusK;
|
|
1198
|
+
if (sliceArgs.length === 2) {
|
|
1199
|
+
const arg0 = unwrapExpression(sliceArgs[0], ts);
|
|
1200
|
+
const arg1 = unwrapExpression(sliceArgs[1], ts);
|
|
1201
|
+
if (ts.isNumericLiteral(arg0) && arg0.text === "0") {
|
|
1202
|
+
startZero = true;
|
|
1203
|
+
}
|
|
1204
|
+
if (ts.isPrefixUnaryExpression(arg1) &&
|
|
1205
|
+
arg1.operator === ts.SyntaxKind.MinusToken &&
|
|
1206
|
+
ts.isNumericLiteral(arg1.operand)) {
|
|
1207
|
+
endMinusK = Number(arg1.operand.text);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (startZero && endMinusK !== undefined && endMinusK >= 1) {
|
|
1211
|
+
if (indexInfo.offset >= 0 && indexInfo.offset <= endMinusK) {
|
|
1212
|
+
return {
|
|
1213
|
+
isGuarded: true,
|
|
1214
|
+
evidence: `Index '${indexInfo.baseText} + ${indexInfo.offset}' is bounded by '${sliceReceiver.text}.slice(0, -${endMinusK})' callback index bounds.`,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
curr = curr.parent;
|
|
1227
|
+
}
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Checks if an element access index is the result of array.findIndex() proven non-negative by dominating guard.
|
|
1232
|
+
*/
|
|
1233
|
+
function isFindIndexBoundedIndex(node, operand, ts, checker) {
|
|
1234
|
+
const indexInfo = normalizeIndexExpression(operand.argumentExpression, ts, checker);
|
|
1235
|
+
if (!indexInfo || indexInfo.offset !== 0)
|
|
1236
|
+
return undefined;
|
|
1237
|
+
const arrayTarget = normalizeArrayTarget(operand.expression, ts, checker);
|
|
1238
|
+
if (!arrayTarget)
|
|
1239
|
+
return undefined;
|
|
1240
|
+
const unwrappedIndex = unwrapExpression(operand.argumentExpression, ts);
|
|
1241
|
+
if (!ts.isIdentifier(unwrappedIndex))
|
|
1242
|
+
return undefined;
|
|
1243
|
+
let symbol = checker?.getSymbolAtLocation(unwrappedIndex);
|
|
1244
|
+
if (!symbol)
|
|
1245
|
+
return undefined;
|
|
1246
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1247
|
+
try {
|
|
1248
|
+
symbol = checker?.getAliasedSymbol(symbol);
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
// ignore
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const decls = symbol?.getDeclarations();
|
|
1255
|
+
if (!decls || decls.length === 0)
|
|
1256
|
+
return undefined;
|
|
1257
|
+
const decl = decls[0];
|
|
1258
|
+
if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer)
|
|
1259
|
+
return undefined;
|
|
1260
|
+
const init = unwrapExpression(decl.initializer, ts);
|
|
1261
|
+
if (!ts.isCallExpression(init))
|
|
1262
|
+
return undefined;
|
|
1263
|
+
const callee = unwrapExpression(init.expression, ts);
|
|
1264
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "findIndex")
|
|
1265
|
+
return undefined;
|
|
1266
|
+
const findReceiver = normalizeArrayTarget(callee.expression, ts, checker);
|
|
1267
|
+
if (!findReceiver)
|
|
1268
|
+
return undefined;
|
|
1269
|
+
if (findReceiver.symbol && arrayTarget.symbol) {
|
|
1270
|
+
if (findReceiver.symbol !== arrayTarget.symbol)
|
|
1271
|
+
return undefined;
|
|
1272
|
+
}
|
|
1273
|
+
else if (findReceiver.text !== arrayTarget.text) {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
const targetBaseText = indexInfo.baseText;
|
|
1277
|
+
const targetArrayText = arrayTarget.text;
|
|
1278
|
+
const targetIndexInfo = indexInfo;
|
|
1279
|
+
const isConst = decl.parent && ts.isVariableDeclarationList(decl.parent) && Boolean(decl.parent.flags & ts.NodeFlags.Const);
|
|
1280
|
+
if (!isConst) {
|
|
1281
|
+
let scope = decl;
|
|
1282
|
+
while (scope.parent && !ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent)) {
|
|
1283
|
+
scope = scope.parent;
|
|
1284
|
+
}
|
|
1285
|
+
const container = scope.parent;
|
|
1286
|
+
let reassigned = false;
|
|
1287
|
+
function checkReassign(n) {
|
|
1288
|
+
if (reassigned)
|
|
1289
|
+
return;
|
|
1290
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1291
|
+
const left = unwrapExpression(n.left, ts);
|
|
1292
|
+
if (ts.isIdentifier(left) && left.text === targetBaseText) {
|
|
1293
|
+
reassigned = true;
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
n.forEachChild(checkReassign);
|
|
1298
|
+
}
|
|
1299
|
+
if (container)
|
|
1300
|
+
container.forEachChild(checkReassign);
|
|
1301
|
+
if (reassigned)
|
|
1302
|
+
return undefined;
|
|
1303
|
+
}
|
|
1304
|
+
function isExitNegativeCheck(cond) {
|
|
1305
|
+
const disjuncts = [];
|
|
1306
|
+
function collectOrs(e) {
|
|
1307
|
+
const u = unwrapExpression(e, ts);
|
|
1308
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1309
|
+
collectOrs(u.left);
|
|
1310
|
+
collectOrs(u.right);
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
disjuncts.push(u);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
collectOrs(cond);
|
|
1317
|
+
return disjuncts.some((d) => isLowerBoundNegativeGuard(d, targetIndexInfo, ts));
|
|
1318
|
+
}
|
|
1319
|
+
function isPositiveEntryCheck(cond) {
|
|
1320
|
+
const conjuncts = [];
|
|
1321
|
+
function collectAnds(e) {
|
|
1322
|
+
const u = unwrapExpression(e, ts);
|
|
1323
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1324
|
+
collectAnds(u.left);
|
|
1325
|
+
collectAnds(u.right);
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
conjuncts.push(u);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
collectAnds(cond);
|
|
1332
|
+
return conjuncts.some((c) => {
|
|
1333
|
+
const u = unwrapExpression(c, ts);
|
|
1334
|
+
if (!ts.isBinaryExpression(u))
|
|
1335
|
+
return false;
|
|
1336
|
+
const left = unwrapExpression(u.left, ts);
|
|
1337
|
+
const right = unwrapExpression(u.right, ts);
|
|
1338
|
+
const op = u.operatorToken.kind;
|
|
1339
|
+
if (op === ts.SyntaxKind.ExclamationEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken) {
|
|
1340
|
+
if (ts.isIdentifier(left) && left.text === targetBaseText) {
|
|
1341
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (ts.isIdentifier(right) && right.text === targetBaseText) {
|
|
1346
|
+
if (ts.isPrefixUnaryExpression(left) && left.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(left.operand) && left.operand.text === "1") {
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
1352
|
+
if (ts.isIdentifier(left) && left.text === targetBaseText && ts.isNumericLiteral(right) && right.text === "0") {
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
1357
|
+
if (ts.isIdentifier(left) && left.text === targetBaseText && ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
let isGuarded = false;
|
|
1365
|
+
let guardEvidence = "";
|
|
1366
|
+
let curr = node;
|
|
1367
|
+
while (curr.parent && !ts.isSourceFile(curr.parent)) {
|
|
1368
|
+
if (ts.isIfStatement(curr.parent) && curr.parent.thenStatement === curr) {
|
|
1369
|
+
if (isPositiveEntryCheck(curr.parent.expression)) {
|
|
1370
|
+
isGuarded = true;
|
|
1371
|
+
guardEvidence = `Enclosing guard '${curr.parent.expression.getText()}' establishes non-negative return from findIndex for '${targetArrayText}'.`;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (ts.isBlock(curr.parent) || ts.isSourceFile(curr.parent)) {
|
|
1376
|
+
const stmts = curr.parent.statements;
|
|
1377
|
+
const targetIdx = stmts.indexOf(curr);
|
|
1378
|
+
if (targetIdx > 0) {
|
|
1379
|
+
let guardIdx = -1;
|
|
1380
|
+
let hasPriorMutation = false;
|
|
1381
|
+
function checkMutation(n) {
|
|
1382
|
+
if (hasPriorMutation)
|
|
1383
|
+
return;
|
|
1384
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
1385
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
1386
|
+
if (target && target.text === targetArrayText) {
|
|
1387
|
+
const method = n.expression.name.text;
|
|
1388
|
+
if (method === "splice" || method === "pop" || method === "shift" || method === "unshift" || method === "push") {
|
|
1389
|
+
hasPriorMutation = true;
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1395
|
+
if (ts.isPropertyAccessExpression(n.left) && n.left.name.text === "length") {
|
|
1396
|
+
const target = normalizeArrayTarget(n.left.expression, ts, checker);
|
|
1397
|
+
if (target && target.text === targetArrayText) {
|
|
1398
|
+
hasPriorMutation = true;
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
n.forEachChild(checkMutation);
|
|
1404
|
+
}
|
|
1405
|
+
for (let i = 0; i < targetIdx; i++) {
|
|
1406
|
+
const prior = stmts[i];
|
|
1407
|
+
if (!prior)
|
|
1408
|
+
continue;
|
|
1409
|
+
if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
1410
|
+
if (isExitNegativeCheck(prior.expression)) {
|
|
1411
|
+
guardIdx = i;
|
|
1412
|
+
guardEvidence = `Dominating guard '${prior.expression.getText()}' establishes non-negative return from findIndex for '${targetArrayText}'.`;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (guardIdx !== -1) {
|
|
1418
|
+
for (let i = guardIdx + 1; i < targetIdx; i++) {
|
|
1419
|
+
const between = stmts[i];
|
|
1420
|
+
if (between)
|
|
1421
|
+
checkMutation(between);
|
|
1422
|
+
}
|
|
1423
|
+
if (!hasPriorMutation) {
|
|
1424
|
+
isGuarded = true;
|
|
1425
|
+
break;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (isGuarded)
|
|
1431
|
+
break;
|
|
1432
|
+
curr = curr.parent;
|
|
1433
|
+
}
|
|
1434
|
+
if (isGuarded) {
|
|
1435
|
+
return { isGuarded: true, evidence: guardEvidence };
|
|
1436
|
+
}
|
|
1437
|
+
return undefined;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Checks if a node or its subtrees contain mutating operations on the given array text/symbol.
|
|
1441
|
+
*/
|
|
1442
|
+
function hasArrayMutation(node, arrayText, ts, checker, arraySymbol) {
|
|
1443
|
+
let mutated = false;
|
|
1444
|
+
function check(n) {
|
|
1445
|
+
if (mutated)
|
|
1446
|
+
return;
|
|
1447
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
1448
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
1449
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
1450
|
+
const method = n.expression.name.text;
|
|
1451
|
+
if (method === "splice" ||
|
|
1452
|
+
method === "pop" ||
|
|
1453
|
+
method === "shift" ||
|
|
1454
|
+
method === "unshift" ||
|
|
1455
|
+
method === "push" ||
|
|
1456
|
+
method === "fill" ||
|
|
1457
|
+
method === "reverse" ||
|
|
1458
|
+
method === "sort" ||
|
|
1459
|
+
method === "copyWithin") {
|
|
1460
|
+
mutated = true;
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
// a.length = ... or a = ...
|
|
1466
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1467
|
+
const left = unwrapExpression(n.left, ts);
|
|
1468
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
1469
|
+
const target = normalizeArrayTarget(left.expression, ts, checker);
|
|
1470
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
1471
|
+
mutated = true;
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (ts.isIdentifier(left) && left.text === arrayText) {
|
|
1476
|
+
mutated = true;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (ts.isPropertyAccessExpression(left) && left.getText() === arrayText) {
|
|
1480
|
+
mutated = true;
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
n.forEachChild(check);
|
|
1485
|
+
}
|
|
1486
|
+
check(node);
|
|
1487
|
+
return mutated;
|
|
1488
|
+
}
|
|
1489
|
+
function containsControlFlowExit(body, ts) {
|
|
1490
|
+
let hasExit = false;
|
|
1491
|
+
function check(n) {
|
|
1492
|
+
if (hasExit)
|
|
1493
|
+
return;
|
|
1494
|
+
if (ts.isBreakStatement(n) ||
|
|
1495
|
+
ts.isContinueStatement(n) ||
|
|
1496
|
+
ts.isReturnStatement(n) ||
|
|
1497
|
+
ts.isThrowStatement(n) ||
|
|
1498
|
+
ts.isYieldExpression(n)) {
|
|
1499
|
+
hasExit = true;
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
// Do not recurse into nested function bodies
|
|
1503
|
+
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
n.forEachChild(check);
|
|
1507
|
+
}
|
|
1508
|
+
check(body);
|
|
1509
|
+
return hasExit;
|
|
1510
|
+
}
|
|
1511
|
+
function countUnconditionalPushes(body, arrayText, ts, checker, arraySymbol) {
|
|
1512
|
+
let conditionalPush = false;
|
|
1513
|
+
let pushCalls = 0;
|
|
1514
|
+
function scanNode(n, insideConditional) {
|
|
1515
|
+
if (conditionalPush)
|
|
1516
|
+
return;
|
|
1517
|
+
if (ts.isIfStatement(n) ||
|
|
1518
|
+
ts.isSwitchStatement(n) ||
|
|
1519
|
+
ts.isConditionalExpression(n) ||
|
|
1520
|
+
ts.isTryStatement(n) ||
|
|
1521
|
+
ts.isWhileStatement(n) ||
|
|
1522
|
+
ts.isDoStatement(n) ||
|
|
1523
|
+
ts.isForStatement(n) ||
|
|
1524
|
+
ts.isForOfStatement(n) ||
|
|
1525
|
+
ts.isForInStatement(n)) {
|
|
1526
|
+
n.forEachChild((c) => scanNode(c, true));
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
1530
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
1531
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
1532
|
+
if (n.expression.name.text === "push") {
|
|
1533
|
+
if (insideConditional) {
|
|
1534
|
+
conditionalPush = true;
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
pushCalls += Math.max(1, n.arguments.length);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
n.forEachChild((c) => scanNode(c, insideConditional));
|
|
1542
|
+
}
|
|
1543
|
+
scanNode(body, false);
|
|
1544
|
+
if (conditionalPush)
|
|
1545
|
+
return 0;
|
|
1546
|
+
return pushCalls;
|
|
1547
|
+
}
|
|
1548
|
+
function hasMutationBeforeNode(statementNode, targetNode, arrayText, ts, checker, arraySymbol) {
|
|
1549
|
+
if (statementNode === targetNode)
|
|
1550
|
+
return false;
|
|
1551
|
+
let mutatedBefore = false;
|
|
1552
|
+
function check(n) {
|
|
1553
|
+
if (mutatedBefore)
|
|
1554
|
+
return;
|
|
1555
|
+
if (n.pos >= targetNode.pos)
|
|
1556
|
+
return;
|
|
1557
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
1558
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
1559
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
1560
|
+
const method = n.expression.name.text;
|
|
1561
|
+
if (method === "splice" ||
|
|
1562
|
+
method === "pop" ||
|
|
1563
|
+
method === "shift" ||
|
|
1564
|
+
method === "unshift" ||
|
|
1565
|
+
method === "push" ||
|
|
1566
|
+
method === "fill" ||
|
|
1567
|
+
method === "reverse" ||
|
|
1568
|
+
method === "sort" ||
|
|
1569
|
+
method === "copyWithin") {
|
|
1570
|
+
mutatedBefore = true;
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
// a.length = ... or a = ...
|
|
1576
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1577
|
+
const left = unwrapExpression(n.left, ts);
|
|
1578
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
1579
|
+
const target = normalizeArrayTarget(left.expression, ts, checker);
|
|
1580
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
1581
|
+
mutatedBefore = true;
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (ts.isIdentifier(left) && left.text === arrayText) {
|
|
1586
|
+
mutatedBefore = true;
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (ts.isPropertyAccessExpression(left) && left.getText() === arrayText) {
|
|
1590
|
+
mutatedBefore = true;
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
n.forEachChild(check);
|
|
1595
|
+
}
|
|
1596
|
+
check(statementNode);
|
|
1597
|
+
return mutatedBefore;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Checks if an array is locally constructed with a known initial length and guaranteed loop pushes,
|
|
1601
|
+
* proving that an element access is strictly in-bounds.
|
|
1602
|
+
*/
|
|
1603
|
+
function isConstructedArrayCardinalityBoundedIndex(node, operand, ts, checker) {
|
|
1604
|
+
const arrayTarget = normalizeArrayTarget(operand.expression, ts, checker);
|
|
1605
|
+
if (!arrayTarget)
|
|
1606
|
+
return undefined;
|
|
1607
|
+
const unwrappedArray = unwrapExpression(operand.expression, ts);
|
|
1608
|
+
if (!ts.isIdentifier(unwrappedArray))
|
|
1609
|
+
return undefined;
|
|
1610
|
+
let symbol = checker?.getSymbolAtLocation(unwrappedArray);
|
|
1611
|
+
if (!symbol)
|
|
1612
|
+
return undefined;
|
|
1613
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1614
|
+
try {
|
|
1615
|
+
symbol = checker?.getAliasedSymbol(symbol);
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
// ignore
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const decls = symbol?.getDeclarations();
|
|
1622
|
+
if (!decls || decls.length === 0)
|
|
1623
|
+
return undefined;
|
|
1624
|
+
const decl = decls[0];
|
|
1625
|
+
if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer)
|
|
1626
|
+
return undefined;
|
|
1627
|
+
const init = unwrapExpression(decl.initializer, ts);
|
|
1628
|
+
if (!ts.isArrayLiteralExpression(init))
|
|
1629
|
+
return undefined;
|
|
1630
|
+
const initialLength = init.elements.length;
|
|
1631
|
+
// Find the block or source file containing the declaration
|
|
1632
|
+
let declScope = decl;
|
|
1633
|
+
while (declScope.parent && !ts.isBlock(declScope.parent) && !ts.isSourceFile(declScope.parent)) {
|
|
1634
|
+
declScope = declScope.parent;
|
|
1635
|
+
}
|
|
1636
|
+
const container = declScope.parent;
|
|
1637
|
+
if (!container || (!ts.isBlock(container) && !ts.isSourceFile(container)))
|
|
1638
|
+
return undefined;
|
|
1639
|
+
const stmts = container.statements;
|
|
1640
|
+
const declIdx = stmts.indexOf(declScope);
|
|
1641
|
+
if (declIdx === -1)
|
|
1642
|
+
return undefined;
|
|
1643
|
+
// Find the top-level statement in `container` that contains `node`
|
|
1644
|
+
let nodeScope = node;
|
|
1645
|
+
while (nodeScope.parent && nodeScope.parent !== container) {
|
|
1646
|
+
nodeScope = nodeScope.parent;
|
|
1647
|
+
}
|
|
1648
|
+
const targetIdx = stmts.indexOf(nodeScope);
|
|
1649
|
+
if (targetIdx === -1 || targetIdx < declIdx)
|
|
1650
|
+
return undefined;
|
|
1651
|
+
// Look for an intervening ForStatement that unconditionally pushes to arrayTarget
|
|
1652
|
+
for (let sIdx = declIdx + 1; sIdx <= targetIdx; sIdx++) {
|
|
1653
|
+
const candidateStmt = stmts[sIdx];
|
|
1654
|
+
if (!candidateStmt)
|
|
1655
|
+
continue;
|
|
1656
|
+
if (ts.isForStatement(candidateStmt)) {
|
|
1657
|
+
const forStmt = candidateStmt;
|
|
1658
|
+
// 1. Initializer: let i = 0
|
|
1659
|
+
let initVal = 0;
|
|
1660
|
+
let loopVarName;
|
|
1661
|
+
if (forStmt.initializer && ts.isVariableDeclarationList(forStmt.initializer)) {
|
|
1662
|
+
const d = forStmt.initializer.declarations[0];
|
|
1663
|
+
if (d && ts.isIdentifier(d.name)) {
|
|
1664
|
+
loopVarName = d.name.text;
|
|
1665
|
+
if (d.initializer && ts.isNumericLiteral(d.initializer)) {
|
|
1666
|
+
initVal = Number(d.initializer.text);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (!loopVarName || initVal !== 0)
|
|
1671
|
+
continue;
|
|
1672
|
+
// 2. Condition: i < N or i <= N - 1
|
|
1673
|
+
if (!forStmt.condition)
|
|
1674
|
+
continue;
|
|
1675
|
+
const cond = unwrapExpression(forStmt.condition, ts);
|
|
1676
|
+
if (!ts.isBinaryExpression(cond))
|
|
1677
|
+
continue;
|
|
1678
|
+
const condLeft = unwrapExpression(cond.left, ts);
|
|
1679
|
+
if (!ts.isIdentifier(condLeft) || condLeft.text !== loopVarName)
|
|
1680
|
+
continue;
|
|
1681
|
+
const condOp = cond.operatorToken.kind;
|
|
1682
|
+
const condRight = unwrapExpression(cond.right, ts);
|
|
1683
|
+
let symbolicBoundText;
|
|
1684
|
+
let constantBoundValue;
|
|
1685
|
+
if (condOp === ts.SyntaxKind.LessThanToken) {
|
|
1686
|
+
if (ts.isIdentifier(condRight)) {
|
|
1687
|
+
symbolicBoundText = condRight.text;
|
|
1688
|
+
}
|
|
1689
|
+
else if (ts.isNumericLiteral(condRight)) {
|
|
1690
|
+
constantBoundValue = Number(condRight.text);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
else if (condOp === ts.SyntaxKind.LessThanEqualsToken) {
|
|
1694
|
+
if (ts.isBinaryExpression(condRight) && condRight.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
1695
|
+
const rLeft = unwrapExpression(condRight.left, ts);
|
|
1696
|
+
const rRight = unwrapExpression(condRight.right, ts);
|
|
1697
|
+
if (ts.isNumericLiteral(rRight) && rRight.text === "1") {
|
|
1698
|
+
if (ts.isIdentifier(rLeft)) {
|
|
1699
|
+
symbolicBoundText = rLeft.text;
|
|
1700
|
+
}
|
|
1701
|
+
else if (ts.isNumericLiteral(rLeft)) {
|
|
1702
|
+
constantBoundValue = Number(rLeft.text);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if (!symbolicBoundText && constantBoundValue === undefined)
|
|
1708
|
+
continue;
|
|
1709
|
+
// 3. Loop body invariant: no control flow exits (break, continue, return, throw)
|
|
1710
|
+
if (containsControlFlowExit(forStmt.statement, ts))
|
|
1711
|
+
continue;
|
|
1712
|
+
// 4. Count unconditional pushes to arrayTarget in the loop body
|
|
1713
|
+
const pushCount = countUnconditionalPushes(forStmt.statement, arrayTarget.text, ts, checker, symbol);
|
|
1714
|
+
if (pushCount === 0)
|
|
1715
|
+
continue;
|
|
1716
|
+
// 5. Ensure no mutating operations on arrayTarget occurred between forStmt and nodeScope
|
|
1717
|
+
let hasInterveningMutation = false;
|
|
1718
|
+
for (let m = sIdx + 1; m < targetIdx; m++) {
|
|
1719
|
+
const stmt = stmts[m];
|
|
1720
|
+
if (stmt && hasArrayMutation(stmt, arrayTarget.text, ts, checker, symbol)) {
|
|
1721
|
+
hasInterveningMutation = true;
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (hasInterveningMutation)
|
|
1726
|
+
continue;
|
|
1727
|
+
// Check if inside nodeScope itself (before node) there is a mutation
|
|
1728
|
+
if (hasMutationBeforeNode(nodeScope, node, arrayTarget.text, ts, checker, symbol)) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
const indexArg = unwrapExpression(operand.argumentExpression, ts);
|
|
1732
|
+
// Case A: Symbolic index matching loop bound N (e.g. amplitudes[numLayers]!)
|
|
1733
|
+
// Requires initialLength >= 1 and pushCount >= 1
|
|
1734
|
+
if (symbolicBoundText && ts.isIdentifier(indexArg) && indexArg.text === symbolicBoundText) {
|
|
1735
|
+
if (initialLength >= 1 && pushCount >= 1) {
|
|
1736
|
+
return {
|
|
1737
|
+
isGuarded: true,
|
|
1738
|
+
evidence: `Array '${arrayTarget.text}' is constructed with ${initialLength} initial element(s) and ${symbolicBoundText} guaranteed pushes, proving index '${symbolicBoundText}' is in-bounds.`,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
// Case B: Literal numeric index matching constant loop bound C (e.g. coeffs[0]! where coeffs has 8 rows)
|
|
1743
|
+
if (constantBoundValue !== undefined && ts.isNumericLiteral(indexArg)) {
|
|
1744
|
+
const requestedIndex = Number(indexArg.text);
|
|
1745
|
+
const guaranteedTotalLength = initialLength + constantBoundValue * pushCount;
|
|
1746
|
+
if (Number.isInteger(requestedIndex) && requestedIndex >= 0 && requestedIndex < guaranteedTotalLength) {
|
|
1747
|
+
return {
|
|
1748
|
+
isGuarded: true,
|
|
1749
|
+
evidence: `Array '${arrayTarget.text}' is constructed with guaranteed length ${guaranteedTotalLength}, proving index ${requestedIndex} is in-bounds.`,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return undefined;
|
|
1756
|
+
}
|
|
1757
|
+
function isInitialNegativeOrUndefined(expr, ts) {
|
|
1758
|
+
if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.MinusToken) {
|
|
1759
|
+
if (ts.isNumericLiteral(expr.operand) && expr.operand.text === "1") {
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (expr.kind === ts.SyntaxKind.UndefinedKeyword || expr.kind === ts.SyntaxKind.NullKeyword) {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
if (ts.isIdentifier(expr) && expr.text === "undefined") {
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
function isExitNegativeCheckForIndex(cond, indexName, ts) {
|
|
1772
|
+
const disjuncts = [];
|
|
1773
|
+
function collectOrs(e) {
|
|
1774
|
+
const u = unwrapExpression(e, ts);
|
|
1775
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1776
|
+
collectOrs(u.left);
|
|
1777
|
+
collectOrs(u.right);
|
|
1778
|
+
}
|
|
1779
|
+
else {
|
|
1780
|
+
disjuncts.push(u);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
collectOrs(cond);
|
|
1784
|
+
return disjuncts.some((d) => {
|
|
1785
|
+
const u = unwrapExpression(d, ts);
|
|
1786
|
+
if (!ts.isBinaryExpression(u))
|
|
1787
|
+
return false;
|
|
1788
|
+
const left = unwrapExpression(u.left, ts);
|
|
1789
|
+
const right = unwrapExpression(u.right, ts);
|
|
1790
|
+
const op = u.operatorToken.kind;
|
|
1791
|
+
// index === -1 or -1 === index
|
|
1792
|
+
if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
|
1793
|
+
if (ts.isIdentifier(left) && left.text === indexName) {
|
|
1794
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (ts.isIdentifier(right) && right.text === indexName) {
|
|
1799
|
+
if (ts.isPrefixUnaryExpression(left) && left.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(left.operand) && left.operand.text === "1") {
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
// index < 0 or index <= -1
|
|
1805
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
1806
|
+
if (ts.isIdentifier(left) && left.text === indexName && ts.isNumericLiteral(right) && right.text === "0") {
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
1811
|
+
if (ts.isIdentifier(left) && left.text === indexName) {
|
|
1812
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return false;
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Checks if an index variable originates exclusively from a bounded loop search over a source array,
|
|
1822
|
+
* with a dominating guard proving non-negative return and matching target array.
|
|
1823
|
+
*/
|
|
1824
|
+
function isSearchProvenanceBoundedIndex(node, operand, ts, checker) {
|
|
1825
|
+
const targetArray = normalizeArrayTarget(operand.expression, ts, checker);
|
|
1826
|
+
if (!targetArray)
|
|
1827
|
+
return undefined;
|
|
1828
|
+
const indexArg = unwrapExpression(operand.argumentExpression, ts);
|
|
1829
|
+
if (!ts.isIdentifier(indexArg))
|
|
1830
|
+
return undefined;
|
|
1831
|
+
const indexName = indexArg.text;
|
|
1832
|
+
let symbol = checker?.getSymbolAtLocation(indexArg);
|
|
1833
|
+
if (!symbol)
|
|
1834
|
+
return undefined;
|
|
1835
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
1836
|
+
try {
|
|
1837
|
+
symbol = checker?.getAliasedSymbol(symbol);
|
|
1838
|
+
}
|
|
1839
|
+
catch {
|
|
1840
|
+
// ignore
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const decls = symbol?.getDeclarations();
|
|
1844
|
+
if (!decls || decls.length === 0)
|
|
1845
|
+
return undefined;
|
|
1846
|
+
const rawDecl = decls[0];
|
|
1847
|
+
if (!rawDecl || !ts.isVariableDeclaration(rawDecl))
|
|
1848
|
+
return undefined;
|
|
1849
|
+
const varDecl = rawDecl;
|
|
1850
|
+
// Find the scope where index variable is declared
|
|
1851
|
+
let scope = varDecl;
|
|
1852
|
+
while (scope.parent && !ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent)) {
|
|
1853
|
+
scope = scope.parent;
|
|
1854
|
+
}
|
|
1855
|
+
const container = scope.parent;
|
|
1856
|
+
if (!container || (!ts.isBlock(container) && !ts.isSourceFile(container)))
|
|
1857
|
+
return undefined;
|
|
1858
|
+
// Collect all assignments to indexName in container
|
|
1859
|
+
let sourceArrayText;
|
|
1860
|
+
let sourceArraySymbol;
|
|
1861
|
+
let invalidAssignment = false;
|
|
1862
|
+
let hasValidLoopAssignment = false;
|
|
1863
|
+
function analyzeAssignments(n) {
|
|
1864
|
+
if (invalidAssignment)
|
|
1865
|
+
return;
|
|
1866
|
+
// Check initializer of the declaration: let victimIdx = -1
|
|
1867
|
+
if (n === varDecl) {
|
|
1868
|
+
if (varDecl.initializer) {
|
|
1869
|
+
const initVal = unwrapExpression(varDecl.initializer, ts);
|
|
1870
|
+
if (!isInitialNegativeOrUndefined(initVal, ts)) {
|
|
1871
|
+
invalidAssignment = true;
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
// Check binary assignments: victimIdx = ...
|
|
1878
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
1879
|
+
const left = unwrapExpression(n.left, ts);
|
|
1880
|
+
if (ts.isIdentifier(left) && left.text === indexName) {
|
|
1881
|
+
const right = unwrapExpression(n.right, ts);
|
|
1882
|
+
// Case 1: Reset value: -1, undefined, null
|
|
1883
|
+
if (isInitialNegativeOrUndefined(right, ts)) {
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
// Case 2: Loop variable assignment inside for (let i = 0; i < sourceArr.length; i++)
|
|
1887
|
+
let forNode = n;
|
|
1888
|
+
let enclosingFor;
|
|
1889
|
+
while (forNode.parent && forNode.parent !== container) {
|
|
1890
|
+
if (ts.isForStatement(forNode.parent)) {
|
|
1891
|
+
enclosingFor = forNode.parent;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
forNode = forNode.parent;
|
|
1895
|
+
}
|
|
1896
|
+
if (enclosingFor && enclosingFor.condition && ts.isBinaryExpression(enclosingFor.condition)) {
|
|
1897
|
+
// Check that right is the loop variable
|
|
1898
|
+
let loopVar;
|
|
1899
|
+
if (enclosingFor.initializer && ts.isVariableDeclarationList(enclosingFor.initializer)) {
|
|
1900
|
+
const d = enclosingFor.initializer.declarations[0];
|
|
1901
|
+
if (d && ts.isIdentifier(d.name)) {
|
|
1902
|
+
loopVar = d.name.text;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (loopVar && ts.isIdentifier(right) && right.text === loopVar) {
|
|
1906
|
+
// Check condition: loopVar < sourceArr.length
|
|
1907
|
+
const cond = enclosingFor.condition;
|
|
1908
|
+
const condLeft = unwrapExpression(cond.left, ts);
|
|
1909
|
+
const condRight = unwrapExpression(cond.right, ts);
|
|
1910
|
+
if (ts.isIdentifier(condLeft) &&
|
|
1911
|
+
condLeft.text === loopVar &&
|
|
1912
|
+
cond.operatorToken.kind === ts.SyntaxKind.LessThanToken &&
|
|
1913
|
+
ts.isPropertyAccessExpression(condRight) &&
|
|
1914
|
+
condRight.name.text === "length") {
|
|
1915
|
+
const src = normalizeArrayTarget(condRight.expression, ts, checker);
|
|
1916
|
+
if (src) {
|
|
1917
|
+
if (!sourceArrayText) {
|
|
1918
|
+
sourceArrayText = src.text;
|
|
1919
|
+
sourceArraySymbol = src.symbol;
|
|
1920
|
+
}
|
|
1921
|
+
else if (sourceArrayText !== src.text) {
|
|
1922
|
+
invalidAssignment = true;
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
hasValidLoopAssignment = true;
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
// Any other assignment to indexName is untrusted
|
|
1932
|
+
invalidAssignment = true;
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
n.forEachChild(analyzeAssignments);
|
|
1937
|
+
}
|
|
1938
|
+
analyzeAssignments(container);
|
|
1939
|
+
if (invalidAssignment || !hasValidLoopAssignment || !sourceArrayText) {
|
|
1940
|
+
return undefined;
|
|
1941
|
+
}
|
|
1942
|
+
// Check relationship between targetArray and sourceArray:
|
|
1943
|
+
// Either targetArray === sourceArray, OR targetArray is derived from sourceArray preserving length:
|
|
1944
|
+
// e.g. const copy = prev.map(...) or [...prev] or prev.slice()
|
|
1945
|
+
let arraysMatch = false;
|
|
1946
|
+
if (sourceArraySymbol && targetArray.symbol && sourceArraySymbol === targetArray.symbol) {
|
|
1947
|
+
arraysMatch = true;
|
|
1948
|
+
}
|
|
1949
|
+
else if (sourceArrayText === targetArray.text) {
|
|
1950
|
+
arraysMatch = true;
|
|
1951
|
+
}
|
|
1952
|
+
else if (targetArray.symbol) {
|
|
1953
|
+
const tDecls = targetArray.symbol.getDeclarations();
|
|
1954
|
+
if (tDecls && tDecls.length > 0) {
|
|
1955
|
+
const firstTDecl = tDecls[0];
|
|
1956
|
+
if (firstTDecl && ts.isVariableDeclaration(firstTDecl)) {
|
|
1957
|
+
const tInit = firstTDecl.initializer ? unwrapExpression(firstTDecl.initializer, ts) : undefined;
|
|
1958
|
+
if (tInit) {
|
|
1959
|
+
// Form 1: prev.map(...) or prev.slice()
|
|
1960
|
+
if (ts.isCallExpression(tInit) && ts.isPropertyAccessExpression(tInit.expression)) {
|
|
1961
|
+
const method = tInit.expression.name.text;
|
|
1962
|
+
if (method === "map" || (method === "slice" && tInit.arguments.length === 0)) {
|
|
1963
|
+
const baseArr = normalizeArrayTarget(tInit.expression.expression, ts, checker);
|
|
1964
|
+
if (baseArr && (baseArr.text === sourceArrayText || (sourceArraySymbol && baseArr.symbol === sourceArraySymbol))) {
|
|
1965
|
+
arraysMatch = true;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
// Form 2: [...prev]
|
|
1970
|
+
if (ts.isArrayLiteralExpression(tInit) && tInit.elements.length === 1) {
|
|
1971
|
+
const elem = tInit.elements[0];
|
|
1972
|
+
if (elem && ts.isSpreadElement(elem)) {
|
|
1973
|
+
const baseArr = normalizeArrayTarget(elem.expression, ts, checker);
|
|
1974
|
+
if (baseArr && (baseArr.text === sourceArrayText || (sourceArraySymbol && baseArr.symbol === sourceArraySymbol))) {
|
|
1975
|
+
arraysMatch = true;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
// Form 3: Array.from(prev)
|
|
1980
|
+
if (ts.isCallExpression(tInit) && ts.isPropertyAccessExpression(tInit.expression)) {
|
|
1981
|
+
if (tInit.expression.name.text === "from" && tInit.arguments.length >= 1) {
|
|
1982
|
+
const firstArg = tInit.arguments[0];
|
|
1983
|
+
if (firstArg) {
|
|
1984
|
+
const baseArr = normalizeArrayTarget(firstArg, ts, checker);
|
|
1985
|
+
if (baseArr && (baseArr.text === sourceArrayText || (sourceArraySymbol && baseArr.symbol === sourceArraySymbol))) {
|
|
1986
|
+
arraysMatch = true;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (!arraysMatch)
|
|
1996
|
+
return undefined;
|
|
1997
|
+
// Check dominating exit guard:
|
|
1998
|
+
// Find top statement in container enclosing node
|
|
1999
|
+
let nodeScope = node;
|
|
2000
|
+
while (nodeScope.parent && nodeScope.parent !== container) {
|
|
2001
|
+
nodeScope = nodeScope.parent;
|
|
2002
|
+
}
|
|
2003
|
+
const stmts = container.statements;
|
|
2004
|
+
const targetIdx = stmts.indexOf(nodeScope);
|
|
2005
|
+
if (targetIdx <= 0)
|
|
2006
|
+
return undefined;
|
|
2007
|
+
let guardIdx = -1;
|
|
2008
|
+
let guardEvidence = "";
|
|
2009
|
+
for (let i = 0; i < targetIdx; i++) {
|
|
2010
|
+
const prior = stmts[i];
|
|
2011
|
+
if (!prior)
|
|
2012
|
+
continue;
|
|
2013
|
+
if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
2014
|
+
const cond = prior.expression;
|
|
2015
|
+
if (isExitNegativeCheckForIndex(cond, indexName, ts)) {
|
|
2016
|
+
guardIdx = i;
|
|
2017
|
+
guardEvidence = `Dominating guard '${cond.getText()}' establishes '${indexName}' is a valid search index for '${targetArray.text}'.`;
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (guardIdx === -1)
|
|
2023
|
+
return undefined;
|
|
2024
|
+
// Verify no mutations on sourceArray or targetArray between guard and node
|
|
2025
|
+
for (let i = guardIdx + 1; i < targetIdx; i++) {
|
|
2026
|
+
const s = stmts[i];
|
|
2027
|
+
if (s) {
|
|
2028
|
+
if (hasArrayMutation(s, targetArray.text, ts, checker, targetArray.symbol))
|
|
2029
|
+
return undefined;
|
|
2030
|
+
if (hasArrayMutation(s, sourceArrayText, ts, checker, sourceArraySymbol))
|
|
2031
|
+
return undefined;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return {
|
|
2035
|
+
isGuarded: true,
|
|
2036
|
+
evidence: guardEvidence,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Checks if a React state variable indexing a static const array is provably bounded by verified state transitions.
|
|
2041
|
+
*/
|
|
2042
|
+
function isReactStateBoundedIndex(operand, ts, checker) {
|
|
2043
|
+
if (!checker)
|
|
2044
|
+
return undefined;
|
|
2045
|
+
const staticArray = resolveStaticConstArrayLiteral(operand.expression, ts, checker);
|
|
2046
|
+
if (!staticArray)
|
|
2047
|
+
return undefined;
|
|
2048
|
+
const indexExpr = unwrapExpression(operand.argumentExpression, ts);
|
|
2049
|
+
if (!ts.isIdentifier(indexExpr))
|
|
2050
|
+
return undefined;
|
|
2051
|
+
const stateName = indexExpr.text;
|
|
2052
|
+
const stateSymbol = checker.getSymbolAtLocation(indexExpr);
|
|
2053
|
+
if (!stateSymbol)
|
|
2054
|
+
return undefined;
|
|
2055
|
+
const decls = stateSymbol.getDeclarations();
|
|
2056
|
+
if (!decls || decls.length === 0)
|
|
2057
|
+
return undefined;
|
|
2058
|
+
const stateDecl = decls[0];
|
|
2059
|
+
if (!stateDecl || !ts.isBindingElement(stateDecl))
|
|
2060
|
+
return undefined;
|
|
2061
|
+
if (!stateDecl.parent || !ts.isArrayBindingPattern(stateDecl.parent))
|
|
2062
|
+
return undefined;
|
|
2063
|
+
const bindingPattern = stateDecl.parent;
|
|
2064
|
+
const bindingElements = bindingPattern.elements;
|
|
2065
|
+
if (bindingElements.length < 2)
|
|
2066
|
+
return undefined;
|
|
2067
|
+
// Verify 1st element is this state variable
|
|
2068
|
+
if (bindingElements[0] !== stateDecl)
|
|
2069
|
+
return undefined;
|
|
2070
|
+
// Verify 2nd element is the setter
|
|
2071
|
+
const setterElement = bindingElements[1];
|
|
2072
|
+
if (!setterElement || !ts.isBindingElement(setterElement) || !ts.isIdentifier(setterElement.name)) {
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}
|
|
2075
|
+
const setterName = setterElement.name.text;
|
|
2076
|
+
const setterIdent = setterElement.name;
|
|
2077
|
+
const arrayLength = staticArray.length;
|
|
2078
|
+
const arrayName = staticArray.name;
|
|
2079
|
+
// Verify initialized by useState(initialNumber)
|
|
2080
|
+
const varDecl = bindingPattern.parent;
|
|
2081
|
+
if (!ts.isVariableDeclaration(varDecl) || !varDecl.initializer || !ts.isCallExpression(varDecl.initializer)) {
|
|
2082
|
+
return undefined;
|
|
2083
|
+
}
|
|
2084
|
+
const chk = checker;
|
|
2085
|
+
const initCall = varDecl.initializer;
|
|
2086
|
+
if (!resolveReactHook(initCall, chk, "useState")) {
|
|
2087
|
+
return undefined;
|
|
2088
|
+
}
|
|
2089
|
+
const initArg = initCall.arguments[0];
|
|
2090
|
+
if (!initArg || !ts.isNumericLiteral(initArg))
|
|
2091
|
+
return undefined;
|
|
2092
|
+
const initialVal = Number(initArg.text);
|
|
2093
|
+
if (!Number.isInteger(initialVal) || initialVal < 0 || initialVal >= arrayLength) {
|
|
2094
|
+
return undefined;
|
|
2095
|
+
}
|
|
2096
|
+
// Find enclosing component / hook function
|
|
2097
|
+
let funcNode = varDecl;
|
|
2098
|
+
while (funcNode.parent &&
|
|
2099
|
+
!ts.isFunctionDeclaration(funcNode) &&
|
|
2100
|
+
!ts.isFunctionExpression(funcNode) &&
|
|
2101
|
+
!ts.isArrowFunction(funcNode) &&
|
|
2102
|
+
!ts.isSourceFile(funcNode)) {
|
|
2103
|
+
funcNode = funcNode.parent;
|
|
2104
|
+
}
|
|
2105
|
+
if (ts.isSourceFile(funcNode))
|
|
2106
|
+
return undefined;
|
|
2107
|
+
// Check all references to setterName within the enclosing function
|
|
2108
|
+
let setterEscapes = false;
|
|
2109
|
+
let hasInvalidSetterWrite = false;
|
|
2110
|
+
let setterCallCount = 0;
|
|
2111
|
+
function scanSetterUsages(n) {
|
|
2112
|
+
if (setterEscapes || hasInvalidSetterWrite || !ts)
|
|
2113
|
+
return;
|
|
2114
|
+
if (ts.isIdentifier(n) && n.text === setterName && n !== setterIdent) {
|
|
2115
|
+
const parent = n.parent;
|
|
2116
|
+
if (!parent)
|
|
2117
|
+
return;
|
|
2118
|
+
// Check if it's being called: setter(arg)
|
|
2119
|
+
if (ts.isCallExpression(parent) && parent.expression === n) {
|
|
2120
|
+
setterCallCount++;
|
|
2121
|
+
const arg = parent.arguments[0];
|
|
2122
|
+
if (!arg) {
|
|
2123
|
+
hasInvalidSetterWrite = true;
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
// Sub-case 1: Literal integer: setter(0)
|
|
2127
|
+
if (ts.isNumericLiteral(arg)) {
|
|
2128
|
+
const val = Number(arg.text);
|
|
2129
|
+
if (!Number.isInteger(val) || val < 0 || val >= arrayLength) {
|
|
2130
|
+
hasInvalidSetterWrite = true;
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
// Sub-case 2: Functional updater: setter(s => s + 1) or setter(prev => ...)
|
|
2136
|
+
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
2137
|
+
const updaterParam = arg.parameters[0];
|
|
2138
|
+
const paramText = updaterParam && ts.isIdentifier(updaterParam.name) ? updaterParam.name.text : undefined;
|
|
2139
|
+
let retExpr;
|
|
2140
|
+
if (ts.isBlock(arg.body)) {
|
|
2141
|
+
for (const st of arg.body.statements) {
|
|
2142
|
+
if (ts.isReturnStatement(st) && st.expression) {
|
|
2143
|
+
retExpr = unwrapExpression(st.expression, ts);
|
|
2144
|
+
break;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
else if (ts.isExpression(arg.body)) {
|
|
2149
|
+
retExpr = unwrapExpression(arg.body, ts);
|
|
2150
|
+
}
|
|
2151
|
+
if (!retExpr) {
|
|
2152
|
+
hasInvalidSetterWrite = true;
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
// Bounded literal integer: s => 0
|
|
2156
|
+
if (ts.isNumericLiteral(retExpr)) {
|
|
2157
|
+
const num = Number(retExpr.text);
|
|
2158
|
+
if (Number.isInteger(num) && num >= 0 && num < arrayLength) {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
hasInvalidSetterWrite = true;
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
// Increment: s => s + 1 (requires enclosing upper bound guard)
|
|
2165
|
+
if (ts.isBinaryExpression(retExpr) &&
|
|
2166
|
+
retExpr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
2167
|
+
const l = unwrapExpression(retExpr.left, ts);
|
|
2168
|
+
const r = unwrapExpression(retExpr.right, ts);
|
|
2169
|
+
if (((paramText && ts.isIdentifier(l) && l.text === paramText) || (ts.isIdentifier(l) && l.text === stateName)) &&
|
|
2170
|
+
ts.isNumericLiteral(r)) {
|
|
2171
|
+
const inc = Number(r.text);
|
|
2172
|
+
if (inc >= 1) {
|
|
2173
|
+
let isGuardedInc = false;
|
|
2174
|
+
let curCheck = parent;
|
|
2175
|
+
while (curCheck.parent && curCheck.parent !== funcNode) {
|
|
2176
|
+
if (ts.isIfStatement(curCheck.parent)) {
|
|
2177
|
+
const ifStmt = curCheck.parent;
|
|
2178
|
+
if (ifStmt.thenStatement === curCheck ||
|
|
2179
|
+
(ts.isBlock(ifStmt.thenStatement) && isNodeDescendantOf(curCheck, ifStmt.thenStatement))) {
|
|
2180
|
+
if (isStateUpperBoundGuard(ifStmt.expression, stateName, arrayLength - inc, ts, checker)) {
|
|
2181
|
+
isGuardedInc = true;
|
|
2182
|
+
break;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
curCheck = curCheck.parent;
|
|
2187
|
+
}
|
|
2188
|
+
if (isGuardedInc)
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
hasInvalidSetterWrite = true;
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
// Decrement: s => s - 1 (requires enclosing lower bound guard)
|
|
2196
|
+
if (ts.isBinaryExpression(retExpr) &&
|
|
2197
|
+
retExpr.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
2198
|
+
const l = unwrapExpression(retExpr.left, ts);
|
|
2199
|
+
const r = unwrapExpression(retExpr.right, ts);
|
|
2200
|
+
if (((paramText && ts.isIdentifier(l) && l.text === paramText) || (ts.isIdentifier(l) && l.text === stateName)) &&
|
|
2201
|
+
ts.isNumericLiteral(r)) {
|
|
2202
|
+
const dec = Number(r.text);
|
|
2203
|
+
if (dec >= 1) {
|
|
2204
|
+
let isGuardedDec = false;
|
|
2205
|
+
let curCheck = parent;
|
|
2206
|
+
while (curCheck.parent && curCheck.parent !== funcNode) {
|
|
2207
|
+
if (ts.isIfStatement(curCheck.parent)) {
|
|
2208
|
+
const ifStmt = curCheck.parent;
|
|
2209
|
+
if (ifStmt.thenStatement === curCheck ||
|
|
2210
|
+
(ts.isBlock(ifStmt.thenStatement) && isNodeDescendantOf(curCheck, ifStmt.thenStatement))) {
|
|
2211
|
+
if (isStateLowerBoundGuard(ifStmt.expression, stateName, dec, ts, checker)) {
|
|
2212
|
+
isGuardedDec = true;
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
curCheck = curCheck.parent;
|
|
2218
|
+
}
|
|
2219
|
+
if (isGuardedDec)
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
hasInvalidSetterWrite = true;
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
hasInvalidSetterWrite = true;
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
// Sub-case 3: Identifier target (e.g. setStep(target)): check if target was bounds-checked in enclosing function
|
|
2230
|
+
if (ts.isIdentifier(arg)) {
|
|
2231
|
+
const paramName = arg.text;
|
|
2232
|
+
let enclosingBlock = parent;
|
|
2233
|
+
while (enclosingBlock.parent &&
|
|
2234
|
+
!ts.isBlock(enclosingBlock) &&
|
|
2235
|
+
enclosingBlock !== funcNode) {
|
|
2236
|
+
enclosingBlock = enclosingBlock.parent;
|
|
2237
|
+
}
|
|
2238
|
+
let paramGuarded = false;
|
|
2239
|
+
if (ts.isBlock(enclosingBlock)) {
|
|
2240
|
+
for (const stmt of enclosingBlock.statements) {
|
|
2241
|
+
if (stmt.end <= parent.pos && ts.isIfStatement(stmt)) {
|
|
2242
|
+
if (statementExitsControlFlow(stmt.thenStatement, ts)) {
|
|
2243
|
+
if (isParamBoundedCondition(stmt.expression, paramName, arrayLength, ts, true)) {
|
|
2244
|
+
paramGuarded = true;
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (!paramGuarded) {
|
|
2252
|
+
// Check entry guard enclosing parent
|
|
2253
|
+
let curN = parent;
|
|
2254
|
+
while (curN.parent && curN.parent !== funcNode) {
|
|
2255
|
+
if (ts.isIfStatement(curN.parent)) {
|
|
2256
|
+
const ifStmt = curN.parent;
|
|
2257
|
+
if (ifStmt.thenStatement === curN ||
|
|
2258
|
+
(ts.isBlock(ifStmt.thenStatement) && isNodeDescendantOf(curN, ifStmt.thenStatement))) {
|
|
2259
|
+
if (isParamBoundedCondition(ifStmt.expression, paramName, arrayLength, ts, false)) {
|
|
2260
|
+
paramGuarded = true;
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
curN = curN.parent;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (!paramGuarded) {
|
|
2269
|
+
hasInvalidSetterWrite = true;
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
// Sub-case 4: Binary expression: step + 1 or step - 1 (check enclosing if guard)
|
|
2275
|
+
if (ts.isBinaryExpression(arg)) {
|
|
2276
|
+
const left = unwrapExpression(arg.left, ts);
|
|
2277
|
+
const right = unwrapExpression(arg.right, ts);
|
|
2278
|
+
if (ts.isIdentifier(left) && left.text === stateName && ts.isNumericLiteral(right)) {
|
|
2279
|
+
const offsetNum = Number(right.text);
|
|
2280
|
+
if (arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
2281
|
+
let isGuarded = false;
|
|
2282
|
+
let curN = parent;
|
|
2283
|
+
while (curN.parent && curN.parent !== funcNode) {
|
|
2284
|
+
if (ts.isIfStatement(curN.parent)) {
|
|
2285
|
+
const ifStmt = curN.parent;
|
|
2286
|
+
if (ifStmt.thenStatement === curN ||
|
|
2287
|
+
(ts.isBlock(ifStmt.thenStatement) && isNodeDescendantOf(curN, ifStmt.thenStatement))) {
|
|
2288
|
+
if (isStateUpperBoundGuard(ifStmt.expression, stateName, arrayLength - offsetNum, ts, checker)) {
|
|
2289
|
+
isGuarded = true;
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
curN = curN.parent;
|
|
2295
|
+
}
|
|
2296
|
+
if (isGuarded)
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
else if (arg.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
2300
|
+
let isGuarded = false;
|
|
2301
|
+
let curN = parent;
|
|
2302
|
+
while (curN.parent && curN.parent !== funcNode) {
|
|
2303
|
+
if (ts.isIfStatement(curN.parent)) {
|
|
2304
|
+
const ifStmt = curN.parent;
|
|
2305
|
+
if (ifStmt.thenStatement === curN ||
|
|
2306
|
+
(ts.isBlock(ifStmt.thenStatement) && isNodeDescendantOf(curN, ifStmt.thenStatement))) {
|
|
2307
|
+
if (isStateLowerBoundGuard(ifStmt.expression, stateName, offsetNum, ts, checker)) {
|
|
2308
|
+
isGuarded = true;
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
curN = curN.parent;
|
|
2314
|
+
}
|
|
2315
|
+
if (isGuarded)
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
hasInvalidSetterWrite = true;
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
// Unknown setter argument
|
|
2323
|
+
hasInvalidSetterWrite = true;
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
// If setter appears as a JSX attribute or JSX expression (e.g. <Child onStep={setStep} />) or is returned
|
|
2327
|
+
if (ts.isJsxAttribute(parent) ||
|
|
2328
|
+
ts.isJsxExpression(parent) ||
|
|
2329
|
+
ts.isJsxSpreadAttribute(parent) ||
|
|
2330
|
+
ts.isReturnStatement(parent) ||
|
|
2331
|
+
ts.isPropertyAssignment(parent) ||
|
|
2332
|
+
ts.isVariableDeclaration(parent)) {
|
|
2333
|
+
setterEscapes = true;
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
// Passed as an argument to another function call (not setter itself)
|
|
2337
|
+
if (ts.isCallExpression(parent) && parent.expression !== n) {
|
|
2338
|
+
if (resolveReactHook(parent, chk, "useCallback") ||
|
|
2339
|
+
resolveReactHook(parent, chk, "useMemo") ||
|
|
2340
|
+
resolveReactHook(parent, chk, "useEffect") ||
|
|
2341
|
+
resolveReactHook(parent, chk, "useLayoutEffect")) {
|
|
2342
|
+
// Setter in hook dependency array is fine
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
setterEscapes = true;
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (ts.isArrayLiteralExpression(parent)) {
|
|
2349
|
+
// Check if array literal is a hook dependency array
|
|
2350
|
+
if (parent.parent &&
|
|
2351
|
+
ts.isCallExpression(parent.parent) &&
|
|
2352
|
+
(resolveReactHook(parent.parent, chk, "useCallback") ||
|
|
2353
|
+
resolveReactHook(parent.parent, chk, "useMemo") ||
|
|
2354
|
+
resolveReactHook(parent.parent, chk, "useEffect") ||
|
|
2355
|
+
resolveReactHook(parent.parent, chk, "useLayoutEffect"))) {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
setterEscapes = true;
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
// Any other unmodeled usage is treated as escape
|
|
2362
|
+
setterEscapes = true;
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
n.forEachChild(scanSetterUsages);
|
|
2366
|
+
}
|
|
2367
|
+
scanSetterUsages(funcNode);
|
|
2368
|
+
if (setterEscapes || hasInvalidSetterWrite) {
|
|
2369
|
+
return undefined;
|
|
2370
|
+
}
|
|
2371
|
+
return {
|
|
2372
|
+
isGuarded: true,
|
|
2373
|
+
evidence: `React state '${stateName}' is initialized to ${initialVal} and all state transitions are proven within [0, ${arrayLength - 1}] for static array '${arrayName}'.`,
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Checks if a non-null assertion operand is guarded against null/undefined or out-of-bounds access.
|
|
2378
|
+
*/
|
|
2379
|
+
export function isGuardedNonNullAssertion(node, ts, checker) {
|
|
2380
|
+
const operand = unwrapExpression(node.expression, ts);
|
|
2381
|
+
// Case 1: Indexed access non-null assertion: `array[index]!`
|
|
2382
|
+
if (ts.isElementAccessExpression(operand)) {
|
|
2383
|
+
// 1a. Check static array literal bounded index: e.g. tiers[0]! or colW[2]!
|
|
2384
|
+
const staticLiteralCheck = isStaticArrayLiteralBoundedIndex(operand, ts, checker);
|
|
2385
|
+
if (staticLiteralCheck?.isGuarded) {
|
|
2386
|
+
return staticLiteralCheck;
|
|
2387
|
+
}
|
|
2388
|
+
// 1b. Check fixed-length equality or comparison guards: e.g. if (sliceArgs.length === 2) sliceArgs[0]!
|
|
2389
|
+
const fixedLengthCheck = isFixedLengthGuardedIndex(node, operand, ts, checker);
|
|
2390
|
+
if (fixedLengthCheck?.isGuarded) {
|
|
2391
|
+
return fixedLengthCheck;
|
|
2392
|
+
}
|
|
2393
|
+
// 1c. Check constructed array cardinality: e.g. amplitudes[numLayers]! or coeffs[0]!
|
|
2394
|
+
const cardinalityCheck = isConstructedArrayCardinalityBoundedIndex(node, operand, ts, checker);
|
|
2395
|
+
if (cardinalityCheck?.isGuarded) {
|
|
2396
|
+
return cardinalityCheck;
|
|
2397
|
+
}
|
|
2398
|
+
// 1d. Check loop or callback bounds: e.g. for (let i = 0; i < arr.length; i++) arr[i]!
|
|
2399
|
+
const loopCheck = isLoopOrCallbackBoundedIndex(node, operand, ts, checker);
|
|
2400
|
+
if (loopCheck?.isGuarded) {
|
|
2401
|
+
return loopCheck;
|
|
2402
|
+
}
|
|
2403
|
+
// 1d. Check React state bounds: e.g. STEPS[step]! with constrained setStep transitions
|
|
2404
|
+
const reactStateCheck = isReactStateBoundedIndex(operand, ts, checker);
|
|
2405
|
+
if (reactStateCheck?.isGuarded) {
|
|
2406
|
+
return reactStateCheck;
|
|
2407
|
+
}
|
|
2408
|
+
// 1e. Check search provenance: e.g. copy[victimIdx]!
|
|
2409
|
+
const provenanceCheck = isSearchProvenanceBoundedIndex(node, operand, ts, checker);
|
|
2410
|
+
if (provenanceCheck?.isGuarded) {
|
|
2411
|
+
return provenanceCheck;
|
|
2412
|
+
}
|
|
2413
|
+
// 1f. Check findIndex contract: e.g. const i = arr.findIndex(...); if (i !== -1) arr[i]!
|
|
2414
|
+
const findIndexCheck = isFindIndexBoundedIndex(node, operand, ts, checker);
|
|
2415
|
+
if (findIndexCheck?.isGuarded) {
|
|
2416
|
+
return findIndexCheck;
|
|
2417
|
+
}
|
|
2418
|
+
// 1g. Dominating statement-level bounds exit check: if (index < 0 || index >= array.length) return;
|
|
2419
|
+
const arrayInfo = normalizeArrayTarget(operand.expression, ts, checker);
|
|
2420
|
+
const indexInfo = normalizeIndexExpression(operand.argumentExpression, ts, checker);
|
|
2421
|
+
if (arrayInfo && indexInfo) {
|
|
2422
|
+
let walk = node;
|
|
2423
|
+
while (walk.parent &&
|
|
2424
|
+
!ts.isFunctionDeclaration(walk.parent) &&
|
|
2425
|
+
!ts.isFunctionExpression(walk.parent) &&
|
|
2426
|
+
!ts.isArrowFunction(walk.parent) &&
|
|
2427
|
+
!ts.isSourceFile(walk.parent)) {
|
|
2428
|
+
if (ts.isIfStatement(walk.parent)) {
|
|
2429
|
+
if (walk.parent.thenStatement === walk ||
|
|
2430
|
+
(ts.isBlock(walk.parent.thenStatement) &&
|
|
2431
|
+
walk.parent.thenStatement.statements.includes(walk))) {
|
|
2432
|
+
if (isPositiveBoundsEntryGuard(walk.parent.expression, indexInfo, arrayInfo, ts)) {
|
|
2433
|
+
return {
|
|
2434
|
+
isGuarded: true,
|
|
2435
|
+
evidence: `Enclosing guard '${walk.parent.expression.getText()}' proves index '${indexInfo.baseText}${indexInfo.offset ? ` + ${indexInfo.offset}` : ""}' is strictly in-bounds for '${arrayInfo.text}'.`,
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
walk = walk.parent;
|
|
2441
|
+
}
|
|
2442
|
+
let stmt = node;
|
|
2443
|
+
while (stmt.parent && !ts.isBlock(stmt.parent) && !ts.isSourceFile(stmt.parent)) {
|
|
2444
|
+
stmt = stmt.parent;
|
|
2445
|
+
}
|
|
2446
|
+
if (stmt.parent && (ts.isBlock(stmt.parent) || ts.isSourceFile(stmt.parent))) {
|
|
2447
|
+
const statements = stmt.parent.statements;
|
|
2448
|
+
const targetIndex = statements.indexOf(stmt);
|
|
2449
|
+
if (targetIndex > 0) {
|
|
2450
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
2451
|
+
const prior = statements[i];
|
|
2452
|
+
if (!prior)
|
|
2453
|
+
continue;
|
|
2454
|
+
if (ts.isIfStatement(prior)) {
|
|
2455
|
+
if (statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
2456
|
+
if (isCompleteBoundsExitGuard(prior.expression, indexInfo, arrayInfo, ts)) {
|
|
2457
|
+
return {
|
|
2458
|
+
isGuarded: true,
|
|
2459
|
+
evidence: `Dominating guard '${prior.expression.getText()}' proves index '${indexInfo.baseText}${indexInfo.offset ? ` + ${indexInfo.offset}` : ""}' is strictly in-bounds for '${arrayInfo.text}'.`,
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
// Case 2: Direct variable / identifier non-null assertion: `target!`
|
|
2470
|
+
if (ts.isIdentifier(operand)) {
|
|
2471
|
+
const targetName = operand.text;
|
|
2472
|
+
const targetSymbol = checker?.getSymbolAtLocation(operand);
|
|
2473
|
+
let resolvedTargetSymbol = targetSymbol;
|
|
2474
|
+
if (resolvedTargetSymbol && (resolvedTargetSymbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
2475
|
+
try {
|
|
2476
|
+
resolvedTargetSymbol = checker?.getAliasedSymbol(resolvedTargetSymbol);
|
|
2477
|
+
}
|
|
2478
|
+
catch {
|
|
2479
|
+
// ignore
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
let stmt = node;
|
|
2483
|
+
while (stmt.parent && !ts.isBlock(stmt.parent) && !ts.isSourceFile(stmt.parent)) {
|
|
2484
|
+
stmt = stmt.parent;
|
|
2485
|
+
}
|
|
2486
|
+
if (stmt.parent && (ts.isBlock(stmt.parent) || ts.isSourceFile(stmt.parent))) {
|
|
2487
|
+
const statements = ts.isBlock(stmt.parent) ? stmt.parent.statements : stmt.parent.statements;
|
|
2488
|
+
const targetIndex = statements.indexOf(stmt);
|
|
2489
|
+
if (targetIndex > 0) {
|
|
2490
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
2491
|
+
const prior = statements[i];
|
|
2492
|
+
if (!prior)
|
|
2493
|
+
continue;
|
|
2494
|
+
if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
2495
|
+
const cond = unwrapExpression(prior.expression, ts);
|
|
2496
|
+
let isGuardMatched = false;
|
|
2497
|
+
let evidenceStr = "";
|
|
2498
|
+
// 1. if (!target) return;
|
|
2499
|
+
if (ts.isPrefixUnaryExpression(cond) && cond.operator === ts.SyntaxKind.ExclamationToken) {
|
|
2500
|
+
const inner = unwrapExpression(cond.operand, ts);
|
|
2501
|
+
if (ts.isIdentifier(inner)) {
|
|
2502
|
+
const innerSym = checker?.getSymbolAtLocation(inner);
|
|
2503
|
+
let resolvedInnerSym = innerSym;
|
|
2504
|
+
if (resolvedInnerSym && (resolvedInnerSym.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
2505
|
+
try {
|
|
2506
|
+
resolvedInnerSym = checker?.getAliasedSymbol(resolvedInnerSym);
|
|
2507
|
+
}
|
|
2508
|
+
catch {
|
|
2509
|
+
// ignore
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const matchesSymbol = resolvedTargetSymbol && resolvedInnerSym
|
|
2513
|
+
? resolvedInnerSym === resolvedTargetSymbol
|
|
2514
|
+
: inner.text === targetName;
|
|
2515
|
+
if (matchesSymbol) {
|
|
2516
|
+
isGuardMatched = true;
|
|
2517
|
+
evidenceStr = `Dominating guard '!${targetName}' proves operand is non-null.`;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
// 2. if (target === null || target === undefined || target == null) return;
|
|
2522
|
+
if (!isGuardMatched && ts.isBinaryExpression(cond)) {
|
|
2523
|
+
const left = unwrapExpression(cond.left, ts);
|
|
2524
|
+
const right = unwrapExpression(cond.right, ts);
|
|
2525
|
+
const op = cond.operatorToken.kind;
|
|
2526
|
+
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
2527
|
+
op === ts.SyntaxKind.EqualsEqualsToken) {
|
|
2528
|
+
let targetSide;
|
|
2529
|
+
let nullSide;
|
|
2530
|
+
if (ts.isIdentifier(left)) {
|
|
2531
|
+
targetSide = left;
|
|
2532
|
+
nullSide = right;
|
|
2533
|
+
}
|
|
2534
|
+
else if (ts.isIdentifier(right)) {
|
|
2535
|
+
targetSide = right;
|
|
2536
|
+
nullSide = left;
|
|
2537
|
+
}
|
|
2538
|
+
if (targetSide && nullSide) {
|
|
2539
|
+
const innerSym = checker?.getSymbolAtLocation(targetSide);
|
|
2540
|
+
let resolvedInnerSym = innerSym;
|
|
2541
|
+
if (resolvedInnerSym && (resolvedInnerSym.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
2542
|
+
try {
|
|
2543
|
+
resolvedInnerSym = checker?.getAliasedSymbol(resolvedInnerSym);
|
|
2544
|
+
}
|
|
2545
|
+
catch {
|
|
2546
|
+
// ignore
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
const matchesSymbol = resolvedTargetSymbol && resolvedInnerSym
|
|
2550
|
+
? resolvedInnerSym === resolvedTargetSymbol
|
|
2551
|
+
: targetSide.text === targetName;
|
|
2552
|
+
if (matchesSymbol &&
|
|
2553
|
+
(nullSide.kind === ts.SyntaxKind.NullKeyword ||
|
|
2554
|
+
nullSide.kind === ts.SyntaxKind.UndefinedKeyword ||
|
|
2555
|
+
(ts.isIdentifier(nullSide) && nullSide.text === "undefined"))) {
|
|
2556
|
+
isGuardMatched = true;
|
|
2557
|
+
evidenceStr = `Dominating null check proves '${targetName}' is non-null.`;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
if (isGuardMatched) {
|
|
2563
|
+
// Invalidation check: verify target was NOT reassigned between guard and assertion!
|
|
2564
|
+
let isReassigned = false;
|
|
2565
|
+
for (let j = i + 1; j < targetIndex; j++) {
|
|
2566
|
+
const s = statements[j];
|
|
2567
|
+
if (s && hasVariableReassignment(s, targetName, resolvedTargetSymbol, ts, checker)) {
|
|
2568
|
+
isReassigned = true;
|
|
2569
|
+
break;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
if (!isReassigned) {
|
|
2573
|
+
const currentStmt = statements[targetIndex];
|
|
2574
|
+
if (currentStmt && hasVariableReassignment(currentStmt, targetName, resolvedTargetSymbol, ts, checker, node.pos)) {
|
|
2575
|
+
isReassigned = true;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
if (!isReassigned) {
|
|
2579
|
+
return {
|
|
2580
|
+
isGuarded: true,
|
|
2581
|
+
evidence: evidenceStr,
|
|
2582
|
+
};
|
|
269
2583
|
}
|
|
270
2584
|
}
|
|
271
2585
|
}
|