sourceverity 1.0.2 → 1.0.4

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