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.
- package/dist/analysis/controlFlow.d.ts +5 -0
- package/dist/analysis/controlFlow.d.ts.map +1 -1
- package/dist/analysis/controlFlow.js +501 -45
- package/dist/analysis/controlFlow.js.map +1 -1
- package/dist/analysis/frameworks.d.ts.map +1 -1
- package/dist/analysis/frameworks.js +84 -12
- package/dist/analysis/frameworks.js.map +1 -1
- package/dist/analysis/functions.d.ts.map +1 -1
- package/dist/analysis/functions.js +52 -8
- package/dist/analysis/functions.js.map +1 -1
- package/dist/analysis/promiseFlow.d.ts.map +1 -1
- package/dist/analysis/promiseFlow.js +85 -11
- package/dist/analysis/promiseFlow.js.map +1 -1
- package/dist/baseline/manager.js +1 -1
- package/dist/baseline/manager.js.map +1 -1
- package/dist/baseline/types.d.ts +1 -1
- package/dist/baseline/types.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/core/paths.d.ts +0 -4
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +0 -6
- package/dist/core/paths.js.map +1 -1
- package/dist/engine/runner.d.ts +6 -1
- package/dist/engine/runner.d.ts.map +1 -1
- package/dist/engine/runner.js +37 -20
- package/dist/engine/runner.js.map +1 -1
- package/dist/engine/symbols.d.ts +17 -4
- package/dist/engine/symbols.d.ts.map +1 -1
- package/dist/engine/symbols.js +267 -34
- package/dist/engine/symbols.js.map +1 -1
- package/dist/reporters/sarif.d.ts +2 -0
- package/dist/reporters/sarif.d.ts.map +1 -1
- package/dist/reporters/sarif.js +36 -7
- package/dist/reporters/sarif.js.map +1 -1
- package/dist/rules/async/floatingPromise.d.ts.map +1 -1
- package/dist/rules/async/floatingPromise.js +4 -2
- package/dist/rules/async/floatingPromise.js.map +1 -1
- package/dist/rules/browser/eventListenerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/eventListenerCleanup.js +2 -9
- package/dist/rules/browser/eventListenerCleanup.js.map +1 -1
- package/dist/rules/browser/observerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/observerCleanup.js +2 -9
- package/dist/rules/browser/observerCleanup.js.map +1 -1
- package/dist/rules/browser/timerCleanup.d.ts.map +1 -1
- package/dist/rules/browser/timerCleanup.js +2 -9
- package/dist/rules/browser/timerCleanup.js.map +1 -1
- package/dist/rules/network/fetchStatusUnchecked.d.ts.map +1 -1
- package/dist/rules/network/fetchStatusUnchecked.js +245 -46
- package/dist/rules/network/fetchStatusUnchecked.js.map +1 -1
- package/dist/rules/react/asyncEffectCallback.d.ts.map +1 -1
- package/dist/rules/react/asyncEffectCallback.js +2 -9
- package/dist/rules/react/asyncEffectCallback.js.map +1 -1
- package/dist/rules/react/derivedStateEffect.d.ts.map +1 -1
- package/dist/rules/react/derivedStateEffect.js +2 -9
- package/dist/rules/react/derivedStateEffect.js.map +1 -1
- package/dist/rules/react/missingEffectCleanup.d.ts.map +1 -1
- package/dist/rules/react/missingEffectCleanup.js +102 -10
- package/dist/rules/react/missingEffectCleanup.js.map +1 -1
- package/dist/rules/typescript/uncheckedIndexAccess.d.ts.map +1 -1
- package/dist/rules/typescript/uncheckedIndexAccess.js +55 -0
- package/dist/rules/typescript/uncheckedIndexAccess.js.map +1 -1
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.d.ts.map +1 -1
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.js +9 -4
- package/dist/rules/typescript/unsafeUnvalidatedAssertion.js.map +1 -1
- 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;
|
|
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
|
-
|
|
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 =>
|
|
2135
|
+
// Sub-case 2: Functional updater: setter(s => s + 1) or setter(prev => ...)
|
|
1898
2136
|
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
(
|
|
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
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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
|
-
|
|
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)
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
}
|