sourceverity 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -3
- package/dist/analysis/arrayBounds.d.ts +4 -0
- package/dist/analysis/arrayBounds.d.ts.map +1 -0
- package/dist/analysis/arrayBounds.js +4 -0
- package/dist/analysis/arrayBounds.js.map +1 -0
- package/dist/analysis/arrayBoundsCore.d.ts +69 -0
- package/dist/analysis/arrayBoundsCore.d.ts.map +1 -0
- package/dist/analysis/arrayBoundsCore.js +957 -0
- package/dist/analysis/arrayBoundsCore.js.map +1 -0
- package/dist/analysis/arrayConstructionBounds.d.ts +14 -0
- package/dist/analysis/arrayConstructionBounds.d.ts.map +1 -0
- package/dist/analysis/arrayConstructionBounds.js +445 -0
- package/dist/analysis/arrayConstructionBounds.js.map +1 -0
- package/dist/analysis/arrayIterationBounds.d.ts +12 -0
- package/dist/analysis/arrayIterationBounds.d.ts.map +1 -0
- package/dist/analysis/arrayIterationBounds.js +578 -0
- package/dist/analysis/arrayIterationBounds.js.map +1 -0
- package/dist/analysis/{controlFlow.d.ts → controlFlowGuards.d.ts} +1 -9
- package/dist/analysis/controlFlowGuards.d.ts.map +1 -0
- package/dist/analysis/controlFlowGuards.js +119 -0
- package/dist/analysis/controlFlowGuards.js.map +1 -0
- package/dist/analysis/controlFlowTypes.d.ts +5 -0
- package/dist/analysis/controlFlowTypes.d.ts.map +1 -0
- package/dist/analysis/controlFlowTypes.js +2 -0
- package/dist/analysis/controlFlowTypes.js.map +1 -0
- package/dist/analysis/frameworks.d.ts.map +1 -1
- package/dist/analysis/frameworks.js +11 -23
- package/dist/analysis/frameworks.js.map +1 -1
- package/dist/analysis/functions.d.ts.map +1 -1
- package/dist/analysis/functions.js +70 -30
- package/dist/analysis/functions.js.map +1 -1
- package/dist/analysis/nonNullGuards.d.ts +7 -0
- package/dist/analysis/nonNullGuards.d.ts.map +1 -0
- package/dist/analysis/nonNullGuards.js +409 -0
- package/dist/analysis/nonNullGuards.js.map +1 -0
- package/dist/analysis/promiseFlow.d.ts +5 -0
- package/dist/analysis/promiseFlow.d.ts.map +1 -1
- package/dist/analysis/promiseFlow.js +331 -1
- package/dist/analysis/promiseFlow.js.map +1 -1
- package/dist/analysis/reactStateBounds.d.ts +8 -0
- package/dist/analysis/reactStateBounds.d.ts.map +1 -0
- package/dist/analysis/reactStateBounds.js +390 -0
- package/dist/analysis/reactStateBounds.js.map +1 -0
- package/dist/analysis/values.d.ts.map +1 -1
- package/dist/analysis/values.js +3 -0
- package/dist/analysis/values.js.map +1 -1
- package/dist/baseline/manager.d.ts +2 -0
- package/dist/baseline/manager.d.ts.map +1 -1
- package/dist/baseline/manager.js +45 -18
- package/dist/baseline/manager.js.map +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +22 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/engine/runner.js +1 -1
- package/dist/engine/runner.js.map +1 -1
- package/dist/engine/symbols.d.ts +0 -1
- package/dist/engine/symbols.d.ts.map +1 -1
- package/dist/engine/symbols.js +2 -12
- package/dist/engine/symbols.js.map +1 -1
- package/dist/repository/detector.d.ts.map +1 -1
- package/dist/repository/detector.js +31 -11
- package/dist/repository/detector.js.map +1 -1
- package/dist/rules/async/floatingPromise.d.ts.map +1 -1
- package/dist/rules/async/floatingPromise.js +23 -3
- package/dist/rules/async/floatingPromise.js.map +1 -1
- package/dist/rules/network/fetchStatusUnchecked.js +1 -1
- package/dist/rules/network/fetchStatusUnchecked.js.map +1 -1
- package/dist/rules/react/derivedStateEffect.d.ts.map +1 -1
- package/dist/rules/react/derivedStateEffect.js +60 -5
- package/dist/rules/react/derivedStateEffect.js.map +1 -1
- package/dist/rules/typescript/nonNullAssertionRisk.js +1 -1
- package/dist/rules/typescript/nonNullAssertionRisk.js.map +1 -1
- package/package.json +1 -1
- package/dist/analysis/controlFlow.d.ts.map +0 -1
- package/dist/analysis/controlFlow.js +0 -2592
- package/dist/analysis/controlFlow.js.map +0 -1
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
import { statementExitsControlFlow } from "./controlFlowGuards.js";
|
|
2
|
+
import { unwrapExpression } from "./values.js";
|
|
3
|
+
function isParamBoundedCondition(cond, paramName, arrayLength, ts, isExitGuard) {
|
|
4
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
5
|
+
if (isExitGuard) {
|
|
6
|
+
// Exit guard disjunction: param < 0 || param >= arrayLength (or param > arrayLength - 1)
|
|
7
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
8
|
+
let hasLowerExit = false;
|
|
9
|
+
let hasUpperExit = false;
|
|
10
|
+
const disjuncts = [];
|
|
11
|
+
function collectOrs(e) {
|
|
12
|
+
const u = unwrapExpression(e, ts);
|
|
13
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
14
|
+
collectOrs(u.left);
|
|
15
|
+
collectOrs(u.right);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
disjuncts.push(u);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
collectOrs(unwrapped);
|
|
22
|
+
for (const d of disjuncts) {
|
|
23
|
+
if (ts.isBinaryExpression(d)) {
|
|
24
|
+
const op = d.operatorToken.kind;
|
|
25
|
+
const left = unwrapExpression(d.left, ts);
|
|
26
|
+
const right = unwrapExpression(d.right, ts);
|
|
27
|
+
if (ts.isIdentifier(left) && left.text === paramName) {
|
|
28
|
+
if (ts.isNumericLiteral(right)) {
|
|
29
|
+
const n = Number(right.text);
|
|
30
|
+
if ((op === ts.SyntaxKind.LessThanToken && n === 0) || (op === ts.SyntaxKind.LessThanEqualsToken && n === -1)) {
|
|
31
|
+
hasLowerExit = true;
|
|
32
|
+
}
|
|
33
|
+
if ((op === ts.SyntaxKind.GreaterThanEqualsToken && n >= arrayLength) || (op === ts.SyntaxKind.GreaterThanToken && n >= arrayLength - 1)) {
|
|
34
|
+
hasUpperExit = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (ts.isPropertyAccessExpression(right) && right.name.text === "length") {
|
|
38
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken || op === ts.SyntaxKind.GreaterThanToken) {
|
|
39
|
+
hasUpperExit = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (ts.isBinaryExpression(right) && right.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
43
|
+
const rLeft = unwrapExpression(right.left, ts);
|
|
44
|
+
if (ts.isPropertyAccessExpression(rLeft) && rLeft.name.text === "length") {
|
|
45
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
46
|
+
hasUpperExit = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return hasLowerExit && hasUpperExit;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Entry guard conjunction: param >= 0 && param < arrayLength
|
|
58
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
59
|
+
let hasLowerEntry = false;
|
|
60
|
+
let hasUpperEntry = false;
|
|
61
|
+
const conjuncts = [];
|
|
62
|
+
function collectAnds(e) {
|
|
63
|
+
const u = unwrapExpression(e, ts);
|
|
64
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
65
|
+
collectAnds(u.left);
|
|
66
|
+
collectAnds(u.right);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
conjuncts.push(u);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
collectAnds(unwrapped);
|
|
73
|
+
for (const c of conjuncts) {
|
|
74
|
+
if (ts.isBinaryExpression(c)) {
|
|
75
|
+
const op = c.operatorToken.kind;
|
|
76
|
+
const left = unwrapExpression(c.left, ts);
|
|
77
|
+
const right = unwrapExpression(c.right, ts);
|
|
78
|
+
if (ts.isIdentifier(left) && left.text === paramName) {
|
|
79
|
+
if (ts.isNumericLiteral(right)) {
|
|
80
|
+
const n = Number(right.text);
|
|
81
|
+
if ((op === ts.SyntaxKind.GreaterThanEqualsToken && n === 0) || (op === ts.SyntaxKind.GreaterThanToken && n === -1)) {
|
|
82
|
+
hasLowerEntry = true;
|
|
83
|
+
}
|
|
84
|
+
if ((op === ts.SyntaxKind.LessThanToken && n <= arrayLength) || (op === ts.SyntaxKind.LessThanEqualsToken && n <= arrayLength - 1)) {
|
|
85
|
+
hasUpperEntry = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (ts.isPropertyAccessExpression(right) && right.name.text === "length") {
|
|
89
|
+
if (op === ts.SyntaxKind.LessThanToken || op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
90
|
+
hasUpperEntry = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return hasLowerEntry && hasUpperEntry;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
function normalizeIndexExpression(expr, ts, checker) {
|
|
102
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
103
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
104
|
+
const sym = checker?.getSymbolAtLocation(unwrapped);
|
|
105
|
+
return { baseText: unwrapped.text, baseSymbol: sym, offset: 0 };
|
|
106
|
+
}
|
|
107
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
108
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
109
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
110
|
+
if (ts.isIdentifier(left) && ts.isNumericLiteral(right)) {
|
|
111
|
+
const num = Number(right.text);
|
|
112
|
+
if (!Number.isNaN(num)) {
|
|
113
|
+
const sym = checker?.getSymbolAtLocation(left);
|
|
114
|
+
if (unwrapped.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
115
|
+
return { baseText: left.text, baseSymbol: sym, offset: num };
|
|
116
|
+
}
|
|
117
|
+
if (unwrapped.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
118
|
+
return { baseText: left.text, baseSymbol: sym, offset: -num };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function arraysMatch(a, b) {
|
|
126
|
+
if (!a || !b)
|
|
127
|
+
return false;
|
|
128
|
+
if (a.symbol && b.symbol && a.symbol === b.symbol)
|
|
129
|
+
return true;
|
|
130
|
+
return a.text === b.text;
|
|
131
|
+
}
|
|
132
|
+
function isNodeDescendantOf(child, parent) {
|
|
133
|
+
let cur = child;
|
|
134
|
+
while (cur) {
|
|
135
|
+
if (cur === parent)
|
|
136
|
+
return true;
|
|
137
|
+
cur = cur.parent;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function normalizeArrayTarget(expr, ts, checker) {
|
|
142
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
143
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
144
|
+
const sym = checker?.getSymbolAtLocation(unwrapped);
|
|
145
|
+
return { text: unwrapped.text, symbol: sym };
|
|
146
|
+
}
|
|
147
|
+
if (ts.isPropertyAccessExpression(unwrapped)) {
|
|
148
|
+
const sym = checker?.getSymbolAtLocation(unwrapped);
|
|
149
|
+
return { text: unwrapped.getText(), symbol: sym };
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Checks if a binary condition expression guards an index lower bound (e.g. index === -1 or index < 0).
|
|
155
|
+
*/
|
|
156
|
+
function isLowerBoundNegativeGuard(cond, indexInfo, ts, checker) {
|
|
157
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
158
|
+
if (!ts.isBinaryExpression(unwrapped))
|
|
159
|
+
return false;
|
|
160
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
161
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
162
|
+
const op = unwrapped.operatorToken.kind;
|
|
163
|
+
function matchesIndex(ident) {
|
|
164
|
+
if (!ts.isIdentifier(ident))
|
|
165
|
+
return false;
|
|
166
|
+
if (ident.text !== indexInfo.baseText)
|
|
167
|
+
return false;
|
|
168
|
+
if (checker && indexInfo.baseSymbol) {
|
|
169
|
+
let sym = checker.getSymbolAtLocation(ident);
|
|
170
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
171
|
+
try {
|
|
172
|
+
sym = checker.getAliasedSymbol(sym);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// ignore
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (sym && sym !== indexInfo.baseSymbol)
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
// index === -1 or -1 === index
|
|
184
|
+
if (op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsEqualsToken) {
|
|
185
|
+
if (matchesIndex(left)) {
|
|
186
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (matchesIndex(right)) {
|
|
191
|
+
if (ts.isPrefixUnaryExpression(left) && left.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(left.operand) && left.operand.text === "1") {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// index < 0
|
|
197
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
198
|
+
if (matchesIndex(left) && ts.isNumericLiteral(right) && right.text === "0") {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// 0 > index
|
|
203
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
204
|
+
if (ts.isNumericLiteral(left) && left.text === "0" && matchesIndex(right)) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Checks if an expression represents `array.length` or `array.length - K`.
|
|
212
|
+
*/
|
|
213
|
+
function parseArrayLengthOffset(expr, arrayInfo, ts, checker) {
|
|
214
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
215
|
+
const targets = Array.isArray(arrayInfo) ? arrayInfo : [arrayInfo];
|
|
216
|
+
// array.length
|
|
217
|
+
if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "length") {
|
|
218
|
+
const objTarget = normalizeArrayTarget(unwrapped.expression, ts, checker);
|
|
219
|
+
if (objTarget && targets.some((t) => arraysMatch(objTarget, t))) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// array.length - K
|
|
224
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
225
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
226
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
227
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
228
|
+
const objTarget = normalizeArrayTarget(left.expression, ts, checker);
|
|
229
|
+
if (objTarget && targets.some((t) => arraysMatch(objTarget, t)) && ts.isNumericLiteral(right)) {
|
|
230
|
+
const k = Number(right.text);
|
|
231
|
+
if (!Number.isNaN(k))
|
|
232
|
+
return k;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
function parseLengthPropertyTarget(expr, ts, checker) {
|
|
239
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
240
|
+
if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "length") {
|
|
241
|
+
return normalizeArrayTarget(unwrapped.expression, ts, checker);
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
function parseGuardedLengthCondition(expr, arrayTarget, ts, checker) {
|
|
246
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
247
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
248
|
+
const op = unwrapped.operatorToken.kind;
|
|
249
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
250
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
251
|
+
// Conjunction: A && B
|
|
252
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
253
|
+
const leftFact = parseGuardedLengthCondition(unwrapped.left, arrayTarget, ts, checker);
|
|
254
|
+
const rightFact = parseGuardedLengthCondition(unwrapped.right, arrayTarget, ts, checker);
|
|
255
|
+
if (leftFact && rightFact) {
|
|
256
|
+
return {
|
|
257
|
+
minLength: Math.max(leftFact.minLength, rightFact.minLength),
|
|
258
|
+
exactLength: leftFact.exactLength ?? rightFact.exactLength,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return leftFact ?? rightFact;
|
|
262
|
+
}
|
|
263
|
+
const leftTarget = parseLengthPropertyTarget(left, ts, checker);
|
|
264
|
+
const rightTarget = parseLengthPropertyTarget(right, ts, checker);
|
|
265
|
+
// Left is arr.length, right is number literal
|
|
266
|
+
if (arraysMatch(leftTarget, arrayTarget) && ts.isNumericLiteral(right)) {
|
|
267
|
+
const num = Number(right.text);
|
|
268
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
269
|
+
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) {
|
|
270
|
+
return { minLength: num, exactLength: num };
|
|
271
|
+
}
|
|
272
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
273
|
+
return { minLength: num };
|
|
274
|
+
}
|
|
275
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
276
|
+
return { minLength: num + 1 };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Right is arr.length, left is number literal
|
|
281
|
+
if (arraysMatch(rightTarget, arrayTarget) && ts.isNumericLiteral(left)) {
|
|
282
|
+
const num = Number(left.text);
|
|
283
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
284
|
+
if (op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) {
|
|
285
|
+
return { minLength: num, exactLength: num };
|
|
286
|
+
}
|
|
287
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
288
|
+
return { minLength: num };
|
|
289
|
+
}
|
|
290
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
291
|
+
return { minLength: num + 1 };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
function parseExitGuardedLengthCondition(expr, arrayTarget, ts, checker) {
|
|
299
|
+
const unwrapped = unwrapExpression(expr, ts);
|
|
300
|
+
// Disjunction: A || B
|
|
301
|
+
if (ts.isBinaryExpression(unwrapped) && unwrapped.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
302
|
+
const leftFact = parseExitGuardedLengthCondition(unwrapped.left, arrayTarget, ts, checker);
|
|
303
|
+
const rightFact = parseExitGuardedLengthCondition(unwrapped.right, arrayTarget, ts, checker);
|
|
304
|
+
if (leftFact && rightFact) {
|
|
305
|
+
return {
|
|
306
|
+
minLength: Math.max(leftFact.minLength, rightFact.minLength),
|
|
307
|
+
exactLength: leftFact.exactLength ?? rightFact.exactLength,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return leftFact ?? rightFact;
|
|
311
|
+
}
|
|
312
|
+
// !arr.length or !arr
|
|
313
|
+
if (ts.isPrefixUnaryExpression(unwrapped) && unwrapped.operator === ts.SyntaxKind.ExclamationToken) {
|
|
314
|
+
const inner = unwrapExpression(unwrapped.operand, ts);
|
|
315
|
+
const target = parseLengthPropertyTarget(inner, ts, checker);
|
|
316
|
+
if (arraysMatch(target, arrayTarget)) {
|
|
317
|
+
return { minLength: 1 };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (ts.isBinaryExpression(unwrapped)) {
|
|
321
|
+
const op = unwrapped.operatorToken.kind;
|
|
322
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
323
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
324
|
+
const leftTarget = parseLengthPropertyTarget(left, ts, checker);
|
|
325
|
+
const rightTarget = parseLengthPropertyTarget(right, ts, checker);
|
|
326
|
+
// Left is arr.length, right is number
|
|
327
|
+
if (arraysMatch(leftTarget, arrayTarget) && ts.isNumericLiteral(right)) {
|
|
328
|
+
const num = Number(right.text);
|
|
329
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
330
|
+
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
331
|
+
return { minLength: num, exactLength: num };
|
|
332
|
+
}
|
|
333
|
+
if (op === ts.SyntaxKind.LessThanToken) {
|
|
334
|
+
return { minLength: num };
|
|
335
|
+
}
|
|
336
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken) {
|
|
337
|
+
return { minLength: num + 1 };
|
|
338
|
+
}
|
|
339
|
+
if ((op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) && num === 0) {
|
|
340
|
+
return { minLength: 1 };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Right is arr.length, left is number
|
|
345
|
+
if (arraysMatch(rightTarget, arrayTarget) && ts.isNumericLiteral(left)) {
|
|
346
|
+
const num = Number(left.text);
|
|
347
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
348
|
+
if (op === ts.SyntaxKind.ExclamationEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken) {
|
|
349
|
+
return { minLength: num, exactLength: num };
|
|
350
|
+
}
|
|
351
|
+
if (op === ts.SyntaxKind.GreaterThanToken) {
|
|
352
|
+
return { minLength: num };
|
|
353
|
+
}
|
|
354
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken) {
|
|
355
|
+
return { minLength: num + 1 };
|
|
356
|
+
}
|
|
357
|
+
if ((op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.EqualsEqualsToken) && num === 0) {
|
|
358
|
+
return { minLength: 1 };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Checks if a binary condition expression guards an upper bound against array.length.
|
|
367
|
+
* e.g. `index >= array.length` or `index >= array.length - 1` or `index > array.length - 1`.
|
|
368
|
+
*/
|
|
369
|
+
function isUpperBoundLengthGuard(cond, indexInfo, arrayInfo, ts, checker) {
|
|
370
|
+
const unwrapped = unwrapExpression(cond, ts);
|
|
371
|
+
if (!ts.isBinaryExpression(unwrapped))
|
|
372
|
+
return { isGuard: false, maxAllowedOffset: 0 };
|
|
373
|
+
const left = unwrapExpression(unwrapped.left, ts);
|
|
374
|
+
const right = unwrapExpression(unwrapped.right, ts);
|
|
375
|
+
const op = unwrapped.operatorToken.kind;
|
|
376
|
+
function matchesIndex(ident) {
|
|
377
|
+
if (!ts.isIdentifier(ident))
|
|
378
|
+
return false;
|
|
379
|
+
if (ident.text !== indexInfo.baseText)
|
|
380
|
+
return false;
|
|
381
|
+
if (checker && indexInfo.baseSymbol) {
|
|
382
|
+
let sym = checker.getSymbolAtLocation(ident);
|
|
383
|
+
if (sym && (sym.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
384
|
+
try {
|
|
385
|
+
sym = checker.getAliasedSymbol(sym);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// ignore
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (sym && sym !== indexInfo.baseSymbol)
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
// Pattern: `index >= [length expression]`
|
|
397
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken && matchesIndex(left)) {
|
|
398
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts, checker);
|
|
399
|
+
if (lengthMinus !== undefined) {
|
|
400
|
+
return { isGuard: true, maxAllowedOffset: lengthMinus };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Pattern: `[length expression] <= index`
|
|
404
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && matchesIndex(right)) {
|
|
405
|
+
const lengthMinus = parseArrayLengthOffset(left, arrayInfo, ts, checker);
|
|
406
|
+
if (lengthMinus !== undefined) {
|
|
407
|
+
return { isGuard: true, maxAllowedOffset: lengthMinus };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// Pattern: `index > [length expression]`
|
|
411
|
+
if (op === ts.SyntaxKind.GreaterThanToken && matchesIndex(left)) {
|
|
412
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts, checker);
|
|
413
|
+
if (lengthMinus !== undefined) {
|
|
414
|
+
return { isGuard: true, maxAllowedOffset: lengthMinus - 1 };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Pattern: `[length expression] < index`
|
|
418
|
+
if (op === ts.SyntaxKind.LessThanToken && matchesIndex(right)) {
|
|
419
|
+
const lengthMinus = parseArrayLengthOffset(left, arrayInfo, ts, checker);
|
|
420
|
+
if (lengthMinus !== undefined) {
|
|
421
|
+
return { isGuard: true, maxAllowedOffset: lengthMinus - 1 };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { isGuard: false, maxAllowedOffset: 0 };
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Checks if an exit condition contains both a lower-bound check and an upper-bound check.
|
|
428
|
+
*/
|
|
429
|
+
function isCompleteBoundsExitGuard(cond, indexInfo, arrayInfo, ts, checker) {
|
|
430
|
+
// Collect all OR disjuncts: A || B || C
|
|
431
|
+
const disjuncts = [];
|
|
432
|
+
function collectOrs(e) {
|
|
433
|
+
const u = unwrapExpression(e, ts);
|
|
434
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
435
|
+
collectOrs(u.left);
|
|
436
|
+
collectOrs(u.right);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
disjuncts.push(u);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
collectOrs(cond);
|
|
443
|
+
let hasLower = false;
|
|
444
|
+
let maxUpperOffset = -1;
|
|
445
|
+
for (const d of disjuncts) {
|
|
446
|
+
if (isLowerBoundNegativeGuard(d, indexInfo, ts, checker)) {
|
|
447
|
+
hasLower = true;
|
|
448
|
+
}
|
|
449
|
+
const upper = isUpperBoundLengthGuard(d, indexInfo, arrayInfo, ts, checker);
|
|
450
|
+
if (upper.isGuard) {
|
|
451
|
+
maxUpperOffset = Math.max(maxUpperOffset, upper.maxAllowedOffset);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (hasLower && maxUpperOffset >= indexInfo.offset) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Checks if an entry condition contains both a lower-bound non-negative check and an upper-bound check against array.length.
|
|
461
|
+
* e.g. `if (i >= 0 && i < array.length - 1)`
|
|
462
|
+
*/
|
|
463
|
+
function isPositiveBoundsEntryGuard(cond, indexInfo, arrayInfo, ts) {
|
|
464
|
+
const conjuncts = [];
|
|
465
|
+
function collectAnds(e) {
|
|
466
|
+
const u = unwrapExpression(e, ts);
|
|
467
|
+
if (ts.isBinaryExpression(u) && u.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
468
|
+
collectAnds(u.left);
|
|
469
|
+
collectAnds(u.right);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
conjuncts.push(u);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
collectAnds(cond);
|
|
476
|
+
let hasLower = false;
|
|
477
|
+
let maxUpperOffset = -1;
|
|
478
|
+
for (const c of conjuncts) {
|
|
479
|
+
const u = unwrapExpression(c, ts);
|
|
480
|
+
if (!ts.isBinaryExpression(u))
|
|
481
|
+
continue;
|
|
482
|
+
const left = unwrapExpression(u.left, ts);
|
|
483
|
+
const right = unwrapExpression(u.right, ts);
|
|
484
|
+
const op = u.operatorToken.kind;
|
|
485
|
+
if (op === ts.SyntaxKind.GreaterThanEqualsToken && ts.isIdentifier(left) && left.text === indexInfo.baseText && ts.isNumericLiteral(right) && right.text === "0") {
|
|
486
|
+
hasLower = true;
|
|
487
|
+
}
|
|
488
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && ts.isNumericLiteral(left) && left.text === "0" && ts.isIdentifier(right) && right.text === indexInfo.baseText) {
|
|
489
|
+
hasLower = true;
|
|
490
|
+
}
|
|
491
|
+
if (op === ts.SyntaxKind.GreaterThanToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
492
|
+
if (ts.isPrefixUnaryExpression(right) && right.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(right.operand) && right.operand.text === "1") {
|
|
493
|
+
hasLower = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (op === ts.SyntaxKind.LessThanToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
497
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts);
|
|
498
|
+
if (lengthMinus !== undefined) {
|
|
499
|
+
maxUpperOffset = Math.max(maxUpperOffset, lengthMinus);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (op === ts.SyntaxKind.LessThanEqualsToken && ts.isIdentifier(left) && left.text === indexInfo.baseText) {
|
|
503
|
+
const lengthMinus = parseArrayLengthOffset(right, arrayInfo, ts);
|
|
504
|
+
if (lengthMinus !== undefined && lengthMinus >= 1) {
|
|
505
|
+
maxUpperOffset = Math.max(maxUpperOffset, lengthMinus - 1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (hasLower && indexInfo.offset >= 0 && indexInfo.offset <= maxUpperOffset) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Checks if an array target is a static, non-empty const array initialized with an array literal.
|
|
516
|
+
*/
|
|
517
|
+
function resolveStaticConstArrayLiteral(arrayExpr, ts, checker) {
|
|
518
|
+
if (!checker)
|
|
519
|
+
return undefined;
|
|
520
|
+
const unwrapped = unwrapExpression(arrayExpr, ts);
|
|
521
|
+
let symbol = checker.getSymbolAtLocation(unwrapped);
|
|
522
|
+
if (!symbol)
|
|
523
|
+
return undefined;
|
|
524
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
525
|
+
try {
|
|
526
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// ignore
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const decls = symbol.getDeclarations();
|
|
533
|
+
if (!decls || decls.length === 0)
|
|
534
|
+
return undefined;
|
|
535
|
+
for (const decl of decls) {
|
|
536
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
537
|
+
const init = unwrapExpression(decl.initializer, ts);
|
|
538
|
+
if (ts.isArrayLiteralExpression(init)) {
|
|
539
|
+
const isConst = decl.parent &&
|
|
540
|
+
ts.isVariableDeclarationList(decl.parent) &&
|
|
541
|
+
Boolean(decl.parent.flags & ts.NodeFlags.Const);
|
|
542
|
+
if (isConst) {
|
|
543
|
+
const length = init.elements.length;
|
|
544
|
+
if (length > 0) {
|
|
545
|
+
return {
|
|
546
|
+
elements: init.elements,
|
|
547
|
+
length,
|
|
548
|
+
name: ts.isIdentifier(decl.name) ? decl.name.text : unwrapped.getText(),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Checks if a static array access has a literal integer index guaranteed to be in-bounds.
|
|
559
|
+
*/
|
|
560
|
+
function isStaticArrayLiteralBoundedIndex(operand, ts, checker) {
|
|
561
|
+
const staticArray = resolveStaticConstArrayLiteral(operand.expression, ts, checker);
|
|
562
|
+
if (!staticArray)
|
|
563
|
+
return undefined;
|
|
564
|
+
const indexExpr = unwrapExpression(operand.argumentExpression, ts);
|
|
565
|
+
// Case A: Literal numeric integer index e.g. tiers[0]! or colW[2]!
|
|
566
|
+
if (ts.isNumericLiteral(indexExpr)) {
|
|
567
|
+
const k = Number(indexExpr.text);
|
|
568
|
+
if (Number.isInteger(k) && k >= 0 && k < staticArray.length) {
|
|
569
|
+
return {
|
|
570
|
+
isGuarded: true,
|
|
571
|
+
evidence: `Literal index ${k} is statically in-bounds for constant array '${staticArray.name}' of length ${staticArray.length}.`,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Case B: array[array.length - 1]!
|
|
576
|
+
if (ts.isBinaryExpression(indexExpr) && indexExpr.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
577
|
+
const left = unwrapExpression(indexExpr.left, ts);
|
|
578
|
+
const right = unwrapExpression(indexExpr.right, ts);
|
|
579
|
+
if (ts.isPropertyAccessExpression(left) &&
|
|
580
|
+
left.name.text === "length" &&
|
|
581
|
+
ts.isNumericLiteral(right) &&
|
|
582
|
+
right.text === "1") {
|
|
583
|
+
return {
|
|
584
|
+
isGuarded: true,
|
|
585
|
+
evidence: `Index 'length - 1' is statically in-bounds for non-empty constant array '${staticArray.name}' of length ${staticArray.length}.`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Checks if a literal index access is proven in-bounds by a dominating fixed-length equality or comparison guard.
|
|
593
|
+
* e.g. `if (arr.length === 2) arr[0]!` or `if (arr.length >= 2) arr[1]!`
|
|
594
|
+
*/
|
|
595
|
+
function isFixedLengthGuardedIndex(node, operand, ts, checker) {
|
|
596
|
+
const arrayTarget = normalizeArrayTarget(operand.expression, ts, checker);
|
|
597
|
+
if (!arrayTarget)
|
|
598
|
+
return undefined;
|
|
599
|
+
const indexExpr = unwrapExpression(operand.argumentExpression, ts);
|
|
600
|
+
let k;
|
|
601
|
+
let lastElementOffset;
|
|
602
|
+
if (ts.isNumericLiteral(indexExpr)) {
|
|
603
|
+
const num = Number(indexExpr.text);
|
|
604
|
+
if (Number.isInteger(num) && num >= 0) {
|
|
605
|
+
k = num;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (ts.isBinaryExpression(indexExpr) && indexExpr.operatorToken.kind === ts.SyntaxKind.MinusToken) {
|
|
609
|
+
const left = unwrapExpression(indexExpr.left, ts);
|
|
610
|
+
const right = unwrapExpression(indexExpr.right, ts);
|
|
611
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
612
|
+
const lengthArrayTarget = normalizeArrayTarget(left.expression, ts, checker);
|
|
613
|
+
if (lengthArrayTarget && arraysMatch(lengthArrayTarget, arrayTarget)) {
|
|
614
|
+
if (ts.isNumericLiteral(right)) {
|
|
615
|
+
const offsetVal = Number(right.text);
|
|
616
|
+
if (Number.isInteger(offsetVal) && offsetVal >= 1) {
|
|
617
|
+
lastElementOffset = offsetVal;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (k === undefined && lastElementOffset === undefined)
|
|
624
|
+
return undefined;
|
|
625
|
+
function checkFactSatisfiesBound(fact) {
|
|
626
|
+
if (lastElementOffset !== undefined) {
|
|
627
|
+
return fact.minLength >= lastElementOffset && (fact.exactLength === undefined || fact.exactLength >= lastElementOffset);
|
|
628
|
+
}
|
|
629
|
+
if (k !== undefined) {
|
|
630
|
+
const maxAllowed = fact.exactLength ?? fact.minLength;
|
|
631
|
+
return k < maxAllowed;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
const targetText = arrayTarget.text;
|
|
636
|
+
function formatEvidence(prefix, exprText, fact) {
|
|
637
|
+
if (lastElementOffset !== undefined) {
|
|
638
|
+
return `${prefix} '${exprText}' proves '${targetText}' has length >= ${fact.minLength}, establishing index '${targetText}.length - ${lastElementOffset}' is strictly in-bounds.`;
|
|
639
|
+
}
|
|
640
|
+
return `${prefix} '${exprText}' proves '${targetText}' has length ${fact.exactLength !== undefined ? `=== ${fact.exactLength}` : `>= ${fact.minLength}`}, establishing index ${k} is in-bounds.`;
|
|
641
|
+
}
|
|
642
|
+
// 1. Walk up enclosing AST nodes (if statements, && expressions, ternary expressions)
|
|
643
|
+
let curr = node;
|
|
644
|
+
while (curr.parent &&
|
|
645
|
+
!ts.isFunctionDeclaration(curr.parent) &&
|
|
646
|
+
!ts.isFunctionExpression(curr.parent) &&
|
|
647
|
+
!ts.isArrowFunction(curr.parent) &&
|
|
648
|
+
!ts.isSourceFile(curr.parent)) {
|
|
649
|
+
// Case A: Enclosing IfStatement
|
|
650
|
+
if (ts.isIfStatement(curr.parent)) {
|
|
651
|
+
const ifStmt = curr.parent;
|
|
652
|
+
if (ifStmt.thenStatement === curr ||
|
|
653
|
+
(ts.isBlock(ifStmt.thenStatement) && ifStmt.thenStatement.statements.includes(curr)) ||
|
|
654
|
+
isNodeDescendantOf(curr, ifStmt.thenStatement)) {
|
|
655
|
+
const fact = parseGuardedLengthCondition(ifStmt.expression, arrayTarget, ts, checker);
|
|
656
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
657
|
+
if (!hasMutationBeforeNode(ifStmt.thenStatement, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
658
|
+
return {
|
|
659
|
+
isGuarded: true,
|
|
660
|
+
evidence: formatEvidence("Enclosing guard", ifStmt.expression.getText(), fact),
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else if (ifStmt.elseStatement &&
|
|
666
|
+
(ifStmt.elseStatement === curr ||
|
|
667
|
+
(ts.isBlock(ifStmt.elseStatement) && ifStmt.elseStatement.statements.includes(curr)) ||
|
|
668
|
+
isNodeDescendantOf(curr, ifStmt.elseStatement))) {
|
|
669
|
+
const fact = parseExitGuardedLengthCondition(ifStmt.expression, arrayTarget, ts, checker);
|
|
670
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
671
|
+
if (!hasMutationBeforeNode(ifStmt.elseStatement, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
672
|
+
return {
|
|
673
|
+
isGuarded: true,
|
|
674
|
+
evidence: formatEvidence("Else branch of guard", ifStmt.expression.getText(), fact),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Case B: Logical AND expression: arr.length === 2 && arr[0]!
|
|
681
|
+
if (ts.isBinaryExpression(curr.parent) &&
|
|
682
|
+
curr.parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
|
|
683
|
+
(curr.parent.right === curr || isNodeDescendantOf(curr, curr.parent.right))) {
|
|
684
|
+
const fact = parseGuardedLengthCondition(curr.parent.left, arrayTarget, ts, checker);
|
|
685
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
686
|
+
if (!hasMutationBeforeNode(curr.parent.right, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
687
|
+
return {
|
|
688
|
+
isGuarded: true,
|
|
689
|
+
evidence: formatEvidence("Logical AND guard", curr.parent.left.getText(), fact),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Case C: ConditionalExpression: arr.length === 2 ? arr[0]! : fallback
|
|
695
|
+
if (ts.isConditionalExpression(curr.parent)) {
|
|
696
|
+
const condExpr = curr.parent;
|
|
697
|
+
if (condExpr.whenTrue === curr || isNodeDescendantOf(curr, condExpr.whenTrue)) {
|
|
698
|
+
const fact = parseGuardedLengthCondition(condExpr.condition, arrayTarget, ts, checker);
|
|
699
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
700
|
+
if (!hasMutationBeforeNode(condExpr.whenTrue, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
701
|
+
return {
|
|
702
|
+
isGuarded: true,
|
|
703
|
+
evidence: formatEvidence("Ternary guard", condExpr.condition.getText(), fact),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
else if (condExpr.whenFalse === curr || isNodeDescendantOf(curr, condExpr.whenFalse)) {
|
|
709
|
+
const fact = parseExitGuardedLengthCondition(condExpr.condition, arrayTarget, ts, checker);
|
|
710
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
711
|
+
if (!hasMutationBeforeNode(condExpr.whenFalse, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
712
|
+
return {
|
|
713
|
+
isGuarded: true,
|
|
714
|
+
evidence: formatEvidence("Ternary false branch of", condExpr.condition.getText(), fact),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
curr = curr.parent;
|
|
721
|
+
}
|
|
722
|
+
// 2. Check dominating early-exit guards in prior sibling statements and ancestor blocks
|
|
723
|
+
let childBlockNode = node;
|
|
724
|
+
while (childBlockNode.parent) {
|
|
725
|
+
const container = childBlockNode.parent;
|
|
726
|
+
if (ts.isFunctionDeclaration(container) ||
|
|
727
|
+
ts.isFunctionExpression(container) ||
|
|
728
|
+
ts.isArrowFunction(container) ||
|
|
729
|
+
ts.isMethodDeclaration(container) ||
|
|
730
|
+
ts.isConstructorDeclaration(container) ||
|
|
731
|
+
ts.isGetAccessor(container) ||
|
|
732
|
+
ts.isSetAccessor(container)) {
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
if (ts.isBlock(container) ||
|
|
736
|
+
ts.isSourceFile(container) ||
|
|
737
|
+
ts.isCaseClause(container) ||
|
|
738
|
+
ts.isDefaultClause(container)) {
|
|
739
|
+
let stmtInContainer = childBlockNode;
|
|
740
|
+
while (stmtInContainer.parent && stmtInContainer.parent !== container) {
|
|
741
|
+
stmtInContainer = stmtInContainer.parent;
|
|
742
|
+
}
|
|
743
|
+
const statements = container.statements;
|
|
744
|
+
const targetIndex = statements.indexOf(stmtInContainer);
|
|
745
|
+
if (targetIndex > 0) {
|
|
746
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
747
|
+
const prior = statements[i];
|
|
748
|
+
if (!prior)
|
|
749
|
+
continue;
|
|
750
|
+
if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
|
|
751
|
+
const fact = parseExitGuardedLengthCondition(prior.expression, arrayTarget, ts, checker);
|
|
752
|
+
if (fact && checkFactSatisfiesBound(fact)) {
|
|
753
|
+
let hasInterveningMutation = false;
|
|
754
|
+
for (let m = i + 1; m < targetIndex; m++) {
|
|
755
|
+
const s = statements[m];
|
|
756
|
+
if (s && hasArrayMutation(s, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
757
|
+
hasInterveningMutation = true;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (!hasInterveningMutation &&
|
|
762
|
+
!hasMutationBeforeNode(stmtInContainer, node, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
763
|
+
// Loop check: if node is inside a loop under container, verify loop body has no mutations
|
|
764
|
+
let loopMutated = false;
|
|
765
|
+
let loopCheck = node;
|
|
766
|
+
while (loopCheck.parent && loopCheck.parent !== container) {
|
|
767
|
+
const lp = loopCheck.parent;
|
|
768
|
+
if (ts.isForStatement(lp) ||
|
|
769
|
+
ts.isForInStatement(lp) ||
|
|
770
|
+
ts.isForOfStatement(lp) ||
|
|
771
|
+
ts.isWhileStatement(lp) ||
|
|
772
|
+
ts.isDoStatement(lp)) {
|
|
773
|
+
if (hasArrayMutation(lp.statement, arrayTarget.text, ts, checker, arrayTarget.symbol)) {
|
|
774
|
+
loopMutated = true;
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
loopCheck = lp;
|
|
779
|
+
}
|
|
780
|
+
if (!loopMutated) {
|
|
781
|
+
return {
|
|
782
|
+
isGuarded: true,
|
|
783
|
+
evidence: formatEvidence("Dominating guard", prior.expression.getText(), fact),
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
childBlockNode = container;
|
|
793
|
+
}
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
export { containsControlFlowExit, countUnconditionalPushes, hasArrayMutation, hasMutationBeforeNode, isLowerBoundNegativeGuard, arraysMatch, isCompleteBoundsExitGuard, isFixedLengthGuardedIndex, isNodeDescendantOf, isParamBoundedCondition, isPositiveBoundsEntryGuard, isStaticArrayLiteralBoundedIndex, normalizeArrayTarget, normalizeIndexExpression, resolveStaticConstArrayLiteral, parseArrayLengthOffset, };
|
|
797
|
+
/**
|
|
798
|
+
* Checks if a node or its subtrees contain mutating operations on the given array text/symbol.
|
|
799
|
+
*/
|
|
800
|
+
function hasArrayMutation(node, arrayText, ts, checker, arraySymbol) {
|
|
801
|
+
let mutated = false;
|
|
802
|
+
function check(n) {
|
|
803
|
+
if (mutated)
|
|
804
|
+
return;
|
|
805
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
806
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
807
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
808
|
+
const method = n.expression.name.text;
|
|
809
|
+
if (method === "splice" ||
|
|
810
|
+
method === "pop" ||
|
|
811
|
+
method === "shift" ||
|
|
812
|
+
method === "unshift" ||
|
|
813
|
+
method === "push" ||
|
|
814
|
+
method === "fill" ||
|
|
815
|
+
method === "reverse" ||
|
|
816
|
+
method === "sort" ||
|
|
817
|
+
method === "copyWithin") {
|
|
818
|
+
mutated = true;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// a.length = ... or a = ...
|
|
824
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
825
|
+
const left = unwrapExpression(n.left, ts);
|
|
826
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
827
|
+
const target = normalizeArrayTarget(left.expression, ts, checker);
|
|
828
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
829
|
+
mutated = true;
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (ts.isIdentifier(left) && left.text === arrayText) {
|
|
834
|
+
mutated = true;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (ts.isPropertyAccessExpression(left) && left.getText() === arrayText) {
|
|
838
|
+
mutated = true;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
n.forEachChild(check);
|
|
843
|
+
}
|
|
844
|
+
check(node);
|
|
845
|
+
return mutated;
|
|
846
|
+
}
|
|
847
|
+
function containsControlFlowExit(body, ts) {
|
|
848
|
+
let hasExit = false;
|
|
849
|
+
function check(n) {
|
|
850
|
+
if (hasExit)
|
|
851
|
+
return;
|
|
852
|
+
if (ts.isBreakStatement(n) ||
|
|
853
|
+
ts.isContinueStatement(n) ||
|
|
854
|
+
ts.isReturnStatement(n) ||
|
|
855
|
+
ts.isThrowStatement(n) ||
|
|
856
|
+
ts.isYieldExpression(n)) {
|
|
857
|
+
hasExit = true;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// Do not recurse into nested function bodies
|
|
861
|
+
if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
n.forEachChild(check);
|
|
865
|
+
}
|
|
866
|
+
check(body);
|
|
867
|
+
return hasExit;
|
|
868
|
+
}
|
|
869
|
+
function countUnconditionalPushes(body, arrayText, ts, checker, arraySymbol) {
|
|
870
|
+
let conditionalPush = false;
|
|
871
|
+
let pushCalls = 0;
|
|
872
|
+
function scanNode(n, insideConditional) {
|
|
873
|
+
if (conditionalPush)
|
|
874
|
+
return;
|
|
875
|
+
if (ts.isIfStatement(n) ||
|
|
876
|
+
ts.isSwitchStatement(n) ||
|
|
877
|
+
ts.isConditionalExpression(n) ||
|
|
878
|
+
ts.isTryStatement(n) ||
|
|
879
|
+
ts.isWhileStatement(n) ||
|
|
880
|
+
ts.isDoStatement(n) ||
|
|
881
|
+
ts.isForStatement(n) ||
|
|
882
|
+
ts.isForOfStatement(n) ||
|
|
883
|
+
ts.isForInStatement(n)) {
|
|
884
|
+
n.forEachChild((c) => scanNode(c, true));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
888
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
889
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
890
|
+
if (n.expression.name.text === "push") {
|
|
891
|
+
if (insideConditional) {
|
|
892
|
+
conditionalPush = true;
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
pushCalls += Math.max(1, n.arguments.length);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
n.forEachChild((c) => scanNode(c, insideConditional));
|
|
900
|
+
}
|
|
901
|
+
scanNode(body, false);
|
|
902
|
+
if (conditionalPush)
|
|
903
|
+
return 0;
|
|
904
|
+
return pushCalls;
|
|
905
|
+
}
|
|
906
|
+
function hasMutationBeforeNode(statementNode, targetNode, arrayText, ts, checker, arraySymbol) {
|
|
907
|
+
if (statementNode === targetNode)
|
|
908
|
+
return false;
|
|
909
|
+
let mutatedBefore = false;
|
|
910
|
+
function check(n) {
|
|
911
|
+
if (mutatedBefore)
|
|
912
|
+
return;
|
|
913
|
+
if (n.pos >= targetNode.pos)
|
|
914
|
+
return;
|
|
915
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
916
|
+
const target = normalizeArrayTarget(n.expression.expression, ts, checker);
|
|
917
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
918
|
+
const method = n.expression.name.text;
|
|
919
|
+
if (method === "splice" ||
|
|
920
|
+
method === "pop" ||
|
|
921
|
+
method === "shift" ||
|
|
922
|
+
method === "unshift" ||
|
|
923
|
+
method === "push" ||
|
|
924
|
+
method === "fill" ||
|
|
925
|
+
method === "reverse" ||
|
|
926
|
+
method === "sort" ||
|
|
927
|
+
method === "copyWithin") {
|
|
928
|
+
mutatedBefore = true;
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// a.length = ... or a = ...
|
|
934
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
935
|
+
const left = unwrapExpression(n.left, ts);
|
|
936
|
+
if (ts.isPropertyAccessExpression(left) && left.name.text === "length") {
|
|
937
|
+
const target = normalizeArrayTarget(left.expression, ts, checker);
|
|
938
|
+
if (target && (target.text === arrayText || (arraySymbol && target.symbol === arraySymbol))) {
|
|
939
|
+
mutatedBefore = true;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (ts.isIdentifier(left) && left.text === arrayText) {
|
|
944
|
+
mutatedBefore = true;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (ts.isPropertyAccessExpression(left) && left.getText() === arrayText) {
|
|
948
|
+
mutatedBefore = true;
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
n.forEachChild(check);
|
|
953
|
+
}
|
|
954
|
+
check(statementNode);
|
|
955
|
+
return mutatedBefore;
|
|
956
|
+
}
|
|
957
|
+
//# sourceMappingURL=arrayBoundsCore.js.map
|