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.
Files changed (82) hide show
  1. package/dist/analysis/controlFlow.d.ts +5 -0
  2. package/dist/analysis/controlFlow.d.ts.map +1 -1
  3. package/dist/analysis/controlFlow.js +2388 -74
  4. package/dist/analysis/controlFlow.js.map +1 -1
  5. package/dist/analysis/frameworks.d.ts.map +1 -1
  6. package/dist/analysis/frameworks.js +136 -12
  7. package/dist/analysis/frameworks.js.map +1 -1
  8. package/dist/analysis/functions.d.ts.map +1 -1
  9. package/dist/analysis/functions.js +95 -19
  10. package/dist/analysis/functions.js.map +1 -1
  11. package/dist/analysis/promiseFlow.d.ts.map +1 -1
  12. package/dist/analysis/promiseFlow.js +85 -11
  13. package/dist/analysis/promiseFlow.js.map +1 -1
  14. package/dist/baseline/manager.js +1 -1
  15. package/dist/baseline/manager.js.map +1 -1
  16. package/dist/baseline/types.d.ts +1 -1
  17. package/dist/baseline/types.d.ts.map +1 -1
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +4 -2
  20. package/dist/cli/main.js.map +1 -1
  21. package/dist/core/paths.d.ts +0 -4
  22. package/dist/core/paths.d.ts.map +1 -1
  23. package/dist/core/paths.js +0 -6
  24. package/dist/core/paths.js.map +1 -1
  25. package/dist/core/version.d.ts +9 -0
  26. package/dist/core/version.d.ts.map +1 -0
  27. package/dist/core/version.js +51 -0
  28. package/dist/core/version.js.map +1 -0
  29. package/dist/engine/runner.d.ts +6 -1
  30. package/dist/engine/runner.d.ts.map +1 -1
  31. package/dist/engine/runner.js +37 -20
  32. package/dist/engine/runner.js.map +1 -1
  33. package/dist/engine/symbols.d.ts +17 -4
  34. package/dist/engine/symbols.d.ts.map +1 -1
  35. package/dist/engine/symbols.js +267 -34
  36. package/dist/engine/symbols.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/reporters/agent.d.ts +1 -1
  42. package/dist/reporters/agent.d.ts.map +1 -1
  43. package/dist/reporters/agent.js +2 -1
  44. package/dist/reporters/agent.js.map +1 -1
  45. package/dist/reporters/sarif.d.ts +2 -0
  46. package/dist/reporters/sarif.d.ts.map +1 -1
  47. package/dist/reporters/sarif.js +38 -8
  48. package/dist/reporters/sarif.js.map +1 -1
  49. package/dist/reporters/terminal.d.ts.map +1 -1
  50. package/dist/reporters/terminal.js +2 -1
  51. package/dist/reporters/terminal.js.map +1 -1
  52. package/dist/rules/async/floatingPromise.d.ts.map +1 -1
  53. package/dist/rules/async/floatingPromise.js +4 -2
  54. package/dist/rules/async/floatingPromise.js.map +1 -1
  55. package/dist/rules/browser/eventListenerCleanup.d.ts.map +1 -1
  56. package/dist/rules/browser/eventListenerCleanup.js +2 -9
  57. package/dist/rules/browser/eventListenerCleanup.js.map +1 -1
  58. package/dist/rules/browser/observerCleanup.d.ts.map +1 -1
  59. package/dist/rules/browser/observerCleanup.js +2 -9
  60. package/dist/rules/browser/observerCleanup.js.map +1 -1
  61. package/dist/rules/browser/timerCleanup.d.ts.map +1 -1
  62. package/dist/rules/browser/timerCleanup.js +194 -27
  63. package/dist/rules/browser/timerCleanup.js.map +1 -1
  64. package/dist/rules/network/fetchStatusUnchecked.d.ts.map +1 -1
  65. package/dist/rules/network/fetchStatusUnchecked.js +245 -46
  66. package/dist/rules/network/fetchStatusUnchecked.js.map +1 -1
  67. package/dist/rules/react/asyncEffectCallback.d.ts.map +1 -1
  68. package/dist/rules/react/asyncEffectCallback.js +2 -9
  69. package/dist/rules/react/asyncEffectCallback.js.map +1 -1
  70. package/dist/rules/react/derivedStateEffect.d.ts.map +1 -1
  71. package/dist/rules/react/derivedStateEffect.js +2 -9
  72. package/dist/rules/react/derivedStateEffect.js.map +1 -1
  73. package/dist/rules/react/missingEffectCleanup.d.ts.map +1 -1
  74. package/dist/rules/react/missingEffectCleanup.js +102 -10
  75. package/dist/rules/react/missingEffectCleanup.js.map +1 -1
  76. package/dist/rules/typescript/uncheckedIndexAccess.d.ts.map +1 -1
  77. package/dist/rules/typescript/uncheckedIndexAccess.js +55 -0
  78. package/dist/rules/typescript/uncheckedIndexAccess.js.map +1 -1
  79. package/dist/rules/typescript/unsafeUnvalidatedAssertion.d.ts.map +1 -1
  80. package/dist/rules/typescript/unsafeUnvalidatedAssertion.js +58 -21
  81. package/dist/rules/typescript/unsafeUnvalidatedAssertion.js.map +1 -1
  82. 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 obj = unwrapExpression(unwrapped.expression, ts);
102
- if (ts.isIdentifier(obj) && obj.text === arrayInfo.text) {
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 obj = unwrapExpression(left.expression, ts);
112
- if (ts.isIdentifier(obj) && obj.text === arrayInfo.text && ts.isNumericLiteral(right)) {
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-null assertion operand is guarded against null/undefined or out-of-bounds access.
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
- export function isGuardedNonNullAssertion(node, ts, checker) {
191
- const operand = unwrapExpression(node.expression, ts);
192
- // Case 1: Indexed access non-null assertion: `array[index]!`
193
- if (ts.isElementAccessExpression(operand)) {
194
- const arrayInfo = normalizeArrayTarget(operand.expression, ts, checker);
195
- const indexInfo = normalizeIndexExpression(operand.argumentExpression, ts, checker);
196
- if (arrayInfo && indexInfo) {
197
- // Find enclosing block / function containing the statement of this node
198
- let stmt = node;
199
- while (stmt.parent && !ts.isBlock(stmt.parent) && !ts.isSourceFile(stmt.parent)) {
200
- stmt = stmt.parent;
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
- if (stmt.parent && (ts.isBlock(stmt.parent) || ts.isSourceFile(stmt.parent))) {
203
- const statements = ts.isBlock(stmt.parent) ? stmt.parent.statements : stmt.parent.statements;
204
- const targetIndex = statements.indexOf(stmt);
205
- if (targetIndex > 0) {
206
- // Look at prior sibling statements
207
- for (let i = 0; i < targetIndex; i++) {
208
- const prior = statements[i];
209
- if (!prior)
210
- continue;
211
- // Look for early-exit if guard: if (index === -1 || index >= array.length - 1) return null;
212
- if (ts.isIfStatement(prior)) {
213
- if (statementExitsControlFlow(prior.thenStatement, ts)) {
214
- if (isCompleteBoundsExitGuard(prior.expression, indexInfo, arrayInfo, ts)) {
215
- return {
216
- isGuarded: true,
217
- evidence: `Dominating guard '${prior.expression.getText()}' proves index '${indexInfo.baseText}${indexInfo.offset ? ` + ${indexInfo.offset}` : ""}' is strictly in-bounds for '${arrayInfo.text}'.`,
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
- // Case 2: Direct variable / identifier non-null assertion: `target!`
228
- if (ts.isIdentifier(operand)) {
229
- const targetName = operand.text;
230
- let stmt = node;
231
- while (stmt.parent && !ts.isBlock(stmt.parent) && !ts.isSourceFile(stmt.parent)) {
232
- stmt = stmt.parent;
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
- if (stmt.parent && (ts.isBlock(stmt.parent) || ts.isSourceFile(stmt.parent))) {
235
- const statements = ts.isBlock(stmt.parent) ? stmt.parent.statements : stmt.parent.statements;
236
- const targetIndex = statements.indexOf(stmt);
237
- if (targetIndex > 0) {
238
- for (let i = 0; i < targetIndex; i++) {
239
- const prior = statements[i];
240
- if (!prior)
241
- continue;
242
- if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
243
- const cond = unwrapExpression(prior.expression, ts);
244
- // if (!target) return;
245
- if (ts.isPrefixUnaryExpression(cond) && cond.operator === ts.SyntaxKind.ExclamationToken) {
246
- const inner = unwrapExpression(cond.operand, ts);
247
- if (ts.isIdentifier(inner) && inner.text === targetName) {
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 '!${targetName}' proves operand is non-null.`,
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
- // if (target === null || target === undefined || target == null) return;
255
- if (ts.isBinaryExpression(cond)) {
256
- const left = unwrapExpression(cond.left, ts);
257
- const right = unwrapExpression(cond.right, ts);
258
- if ((ts.isIdentifier(left) && left.text === targetName) ||
259
- (ts.isIdentifier(right) && right.text === targetName)) {
260
- const other = ts.isIdentifier(left) && left.text === targetName ? right : left;
261
- if (other.kind === ts.SyntaxKind.NullKeyword ||
262
- other.kind === ts.SyntaxKind.UndefinedKeyword ||
263
- (ts.isIdentifier(other) && other.text === "undefined")) {
264
- return {
265
- isGuarded: true,
266
- evidence: `Dominating null check proves '${targetName}' is non-null.`,
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
  }