sourceverity 1.0.2 → 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 (67) 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 +501 -45
  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 +84 -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 +52 -8
  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 +2 -1
  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/engine/runner.d.ts +6 -1
  26. package/dist/engine/runner.d.ts.map +1 -1
  27. package/dist/engine/runner.js +37 -20
  28. package/dist/engine/runner.js.map +1 -1
  29. package/dist/engine/symbols.d.ts +17 -4
  30. package/dist/engine/symbols.d.ts.map +1 -1
  31. package/dist/engine/symbols.js +267 -34
  32. package/dist/engine/symbols.js.map +1 -1
  33. package/dist/reporters/sarif.d.ts +2 -0
  34. package/dist/reporters/sarif.d.ts.map +1 -1
  35. package/dist/reporters/sarif.js +36 -7
  36. package/dist/reporters/sarif.js.map +1 -1
  37. package/dist/rules/async/floatingPromise.d.ts.map +1 -1
  38. package/dist/rules/async/floatingPromise.js +4 -2
  39. package/dist/rules/async/floatingPromise.js.map +1 -1
  40. package/dist/rules/browser/eventListenerCleanup.d.ts.map +1 -1
  41. package/dist/rules/browser/eventListenerCleanup.js +2 -9
  42. package/dist/rules/browser/eventListenerCleanup.js.map +1 -1
  43. package/dist/rules/browser/observerCleanup.d.ts.map +1 -1
  44. package/dist/rules/browser/observerCleanup.js +2 -9
  45. package/dist/rules/browser/observerCleanup.js.map +1 -1
  46. package/dist/rules/browser/timerCleanup.d.ts.map +1 -1
  47. package/dist/rules/browser/timerCleanup.js +2 -9
  48. package/dist/rules/browser/timerCleanup.js.map +1 -1
  49. package/dist/rules/network/fetchStatusUnchecked.d.ts.map +1 -1
  50. package/dist/rules/network/fetchStatusUnchecked.js +245 -46
  51. package/dist/rules/network/fetchStatusUnchecked.js.map +1 -1
  52. package/dist/rules/react/asyncEffectCallback.d.ts.map +1 -1
  53. package/dist/rules/react/asyncEffectCallback.js +2 -9
  54. package/dist/rules/react/asyncEffectCallback.js.map +1 -1
  55. package/dist/rules/react/derivedStateEffect.d.ts.map +1 -1
  56. package/dist/rules/react/derivedStateEffect.js +2 -9
  57. package/dist/rules/react/derivedStateEffect.js.map +1 -1
  58. package/dist/rules/react/missingEffectCleanup.d.ts.map +1 -1
  59. package/dist/rules/react/missingEffectCleanup.js +102 -10
  60. package/dist/rules/react/missingEffectCleanup.js.map +1 -1
  61. package/dist/rules/typescript/uncheckedIndexAccess.d.ts.map +1 -1
  62. package/dist/rules/typescript/uncheckedIndexAccess.js +55 -0
  63. package/dist/rules/typescript/uncheckedIndexAccess.js.map +1 -1
  64. package/dist/rules/typescript/unsafeUnvalidatedAssertion.d.ts.map +1 -1
  65. package/dist/rules/typescript/unsafeUnvalidatedAssertion.js +9 -4
  66. package/dist/rules/typescript/unsafeUnvalidatedAssertion.js.map +1 -1
  67. package/package.json +1 -1
@@ -3,6 +3,11 @@ export interface GuardCheckResult {
3
3
  readonly isGuarded: boolean;
4
4
  readonly evidence?: string | undefined;
5
5
  }
6
+ /**
7
+ * Checks if a statement definitely exits control flow (return, throw, break, continue).
8
+ */
9
+ export declare function statementExitsControlFlow(stmt: tsType.Statement, ts: typeof tsType): boolean;
10
+ export declare function hasVariableReassignment(node: tsType.Node, targetName: string, targetSymbol: tsType.Symbol | undefined, ts: typeof tsType, checker?: tsType.TypeChecker | undefined, beforePos?: number, afterPos?: number): boolean;
6
11
  /**
7
12
  * Checks if a non-null assertion operand is guarded against null/undefined or out-of-bounds access.
8
13
  */
@@ -1 +1 @@
1
- {"version":3,"file":"controlFlow.d.ts","sourceRoot":"","sources":["../../src/analysis/controlFlow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,MAAM,YAAY,CAAC;AAG1C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAixED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,CAAC,iBAAiB,EAC9B,EAAE,EAAE,OAAO,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,CAAC,WAAW,GAAG,SAAS,GACvC,gBAAgB,CAqKlB"}
1
+ {"version":3,"file":"controlFlow.d.ts","sourceRoot":"","sources":["../../src/analysis/controlFlow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,MAAM,YAAY,CAAC;AAI1C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,EAAE,EAAE,OAAO,MAAM,GAAG,OAAO,CAkB5F;AAuBD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,CAAC,IAAI,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,CAAC,MAAM,GAAG,SAAS,EACvC,EAAE,EAAE,OAAO,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,CAAC,WAAW,GAAG,SAAS,EACxC,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAoFT;AA2jFD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,CAAC,iBAAiB,EAC9B,EAAE,EAAE,OAAO,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,CAAC,WAAW,GAAG,SAAS,GACvC,gBAAgB,CAiPlB"}
@@ -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)) {
@@ -1715,7 +1959,7 @@ function isSearchProvenanceBoundedIndex(node, operand, ts, checker) {
1715
1959
  // Form 1: prev.map(...) or prev.slice()
1716
1960
  if (ts.isCallExpression(tInit) && ts.isPropertyAccessExpression(tInit.expression)) {
1717
1961
  const method = tInit.expression.name.text;
1718
- if (method === "map" || method === "slice") {
1962
+ if (method === "map" || (method === "slice" && tInit.arguments.length === 0)) {
1719
1963
  const baseArr = normalizeArrayTarget(tInit.expression.expression, ts, checker);
1720
1964
  if (baseArr && (baseArr.text === sourceArrayText || (sourceArraySymbol && baseArr.symbol === sourceArraySymbol))) {
1721
1965
  arraysMatch = true;
@@ -1837,17 +2081,11 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1837
2081
  if (!ts.isVariableDeclaration(varDecl) || !varDecl.initializer || !ts.isCallExpression(varDecl.initializer)) {
1838
2082
  return undefined;
1839
2083
  }
2084
+ const chk = checker;
1840
2085
  const initCall = varDecl.initializer;
1841
- const hookCallee = unwrapExpression(initCall.expression, ts);
1842
- let isUseState = false;
1843
- if (ts.isIdentifier(hookCallee) && hookCallee.text === "useState") {
1844
- isUseState = true;
1845
- }
1846
- else if (ts.isPropertyAccessExpression(hookCallee) && hookCallee.name.text === "useState") {
1847
- isUseState = true;
1848
- }
1849
- if (!isUseState)
2086
+ if (!resolveReactHook(initCall, chk, "useState")) {
1850
2087
  return undefined;
2088
+ }
1851
2089
  const initArg = initCall.arguments[0];
1852
2090
  if (!initArg || !ts.isNumericLiteral(initArg))
1853
2091
  return undefined;
@@ -1894,16 +2132,103 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1894
2132
  }
1895
2133
  return;
1896
2134
  }
1897
- // Sub-case 2: Functional updater: setter(s => s + 1) or setter(prev => Math.min(prev + 1, L - 1))
2135
+ // Sub-case 2: Functional updater: setter(s => s + 1) or setter(prev => ...)
1898
2136
  if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
1899
- // Verify functional updater returns values strictly in [0, L - 1]
1900
- // Simple safe pattern: s => s + 1 (guarded) or s => Math.min(s + 1, L - 1)
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;
1901
2227
  return;
1902
2228
  }
1903
2229
  // Sub-case 3: Identifier target (e.g. setStep(target)): check if target was bounds-checked in enclosing function
1904
2230
  if (ts.isIdentifier(arg)) {
1905
2231
  const paramName = arg.text;
1906
- // Look in enclosing block for early guard on paramName: if (target < 0 || target > STEPS.length - 1) return;
1907
2232
  let enclosingBlock = parent;
1908
2233
  while (enclosingBlock.parent &&
1909
2234
  !ts.isBlock(enclosingBlock) &&
@@ -1915,10 +2240,7 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1915
2240
  for (const stmt of enclosingBlock.statements) {
1916
2241
  if (stmt.end <= parent.pos && ts.isIfStatement(stmt)) {
1917
2242
  if (statementExitsControlFlow(stmt.thenStatement, ts)) {
1918
- const condText = stmt.expression.getText();
1919
- if (condText.includes(paramName) &&
1920
- (condText.includes("0") || condText.includes("< 0") || condText.includes("<= 0")) &&
1921
- (condText.includes("length") || condText.includes(String(arrayLength)))) {
2243
+ if (isParamBoundedCondition(stmt.expression, paramName, arrayLength, ts, true)) {
1922
2244
  paramGuarded = true;
1923
2245
  break;
1924
2246
  }
@@ -1926,6 +2248,23 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1926
2248
  }
1927
2249
  }
1928
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
+ }
1929
2268
  if (!paramGuarded) {
1930
2269
  hasInvalidSetterWrite = true;
1931
2270
  return;
@@ -1934,6 +2273,50 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1934
2273
  }
1935
2274
  // Sub-case 4: Binary expression: step + 1 or step - 1 (check enclosing if guard)
1936
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;
1937
2320
  return;
1938
2321
  }
1939
2322
  // Unknown setter argument
@@ -1952,9 +2335,10 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1952
2335
  }
1953
2336
  // Passed as an argument to another function call (not setter itself)
1954
2337
  if (ts.isCallExpression(parent) && parent.expression !== n) {
1955
- const callee = unwrapExpression(parent.expression, ts);
1956
- if (ts.isIdentifier(callee) &&
1957
- (callee.text === "useCallback" || callee.text === "useMemo" || callee.text === "useEffect")) {
2338
+ if (resolveReactHook(parent, chk, "useCallback") ||
2339
+ resolveReactHook(parent, chk, "useMemo") ||
2340
+ resolveReactHook(parent, chk, "useEffect") ||
2341
+ resolveReactHook(parent, chk, "useLayoutEffect")) {
1958
2342
  // Setter in hook dependency array is fine
1959
2343
  return;
1960
2344
  }
@@ -1965,11 +2349,11 @@ function isReactStateBoundedIndex(operand, ts, checker) {
1965
2349
  // Check if array literal is a hook dependency array
1966
2350
  if (parent.parent &&
1967
2351
  ts.isCallExpression(parent.parent) &&
1968
- ts.isIdentifier(unwrapExpression(parent.parent.expression, ts))) {
1969
- const hookName = unwrapExpression(parent.parent.expression, ts).getText();
1970
- if (hookName === "useCallback" || hookName === "useMemo" || hookName === "useEffect") {
1971
- return;
1972
- }
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;
1973
2357
  }
1974
2358
  setterEscapes = true;
1975
2359
  return;
@@ -2085,6 +2469,16 @@ export function isGuardedNonNullAssertion(node, ts, checker) {
2085
2469
  // Case 2: Direct variable / identifier non-null assertion: `target!`
2086
2470
  if (ts.isIdentifier(operand)) {
2087
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
+ }
2088
2482
  let stmt = node;
2089
2483
  while (stmt.parent && !ts.isBlock(stmt.parent) && !ts.isSourceFile(stmt.parent)) {
2090
2484
  stmt = stmt.parent;
@@ -2099,33 +2493,95 @@ export function isGuardedNonNullAssertion(node, ts, checker) {
2099
2493
  continue;
2100
2494
  if (ts.isIfStatement(prior) && statementExitsControlFlow(prior.thenStatement, ts)) {
2101
2495
  const cond = unwrapExpression(prior.expression, ts);
2102
- // if (!target) return;
2496
+ let isGuardMatched = false;
2497
+ let evidenceStr = "";
2498
+ // 1. if (!target) return;
2103
2499
  if (ts.isPrefixUnaryExpression(cond) && cond.operator === ts.SyntaxKind.ExclamationToken) {
2104
2500
  const inner = unwrapExpression(cond.operand, ts);
2105
- if (ts.isIdentifier(inner) && inner.text === targetName) {
2106
- return {
2107
- isGuarded: true,
2108
- evidence: `Dominating guard '!${targetName}' proves operand is non-null.`,
2109
- };
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
+ }
2110
2519
  }
2111
2520
  }
2112
- // if (target === null || target === undefined || target == null) return;
2113
- if (ts.isBinaryExpression(cond)) {
2521
+ // 2. if (target === null || target === undefined || target == null) return;
2522
+ if (!isGuardMatched && ts.isBinaryExpression(cond)) {
2114
2523
  const left = unwrapExpression(cond.left, ts);
2115
2524
  const right = unwrapExpression(cond.right, ts);
2116
- if ((ts.isIdentifier(left) && left.text === targetName) ||
2117
- (ts.isIdentifier(right) && right.text === targetName)) {
2118
- const other = ts.isIdentifier(left) && left.text === targetName ? right : left;
2119
- if (other.kind === ts.SyntaxKind.NullKeyword ||
2120
- other.kind === ts.SyntaxKind.UndefinedKeyword ||
2121
- (ts.isIdentifier(other) && other.text === "undefined")) {
2122
- return {
2123
- isGuarded: true,
2124
- evidence: `Dominating null check proves '${targetName}' is non-null.`,
2125
- };
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
+ }
2126
2559
  }
2127
2560
  }
2128
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
+ };
2583
+ }
2584
+ }
2129
2585
  }
2130
2586
  }
2131
2587
  }