@memberjunction/react-test-harness 2.120.0 → 2.122.0
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/lib/component-linter.d.ts +5 -0
- package/dist/lib/component-linter.d.ts.map +1 -1
- package/dist/lib/component-linter.js +2554 -2451
- package/dist/lib/component-linter.js.map +1 -1
- package/dist/lib/control-flow-analyzer.d.ts +184 -0
- package/dist/lib/control-flow-analyzer.d.ts.map +1 -0
- package/dist/lib/control-flow-analyzer.js +825 -0
- package/dist/lib/control-flow-analyzer.js.map +1 -0
- package/dist/lib/type-context.d.ts +182 -0
- package/dist/lib/type-context.d.ts.map +1 -0
- package/dist/lib/type-context.js +394 -0
- package/dist/lib/type-context.js.map +1 -0
- package/dist/lib/type-inference-engine.d.ts +148 -0
- package/dist/lib/type-inference-engine.d.ts.map +1 -0
- package/dist/lib/type-inference-engine.js +826 -0
- package/dist/lib/type-inference-engine.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Control Flow Analyzer
|
|
4
|
+
*
|
|
5
|
+
* Tracks how types and values narrow through JavaScript code based on runtime checks.
|
|
6
|
+
* Similar to TypeScript's control flow analysis for type narrowing.
|
|
7
|
+
*
|
|
8
|
+
* This eliminates false positives in linting rules by understanding patterns like:
|
|
9
|
+
* - if (x != null) { x.method() } // x is non-null here
|
|
10
|
+
* - if (typeof x === 'number') { x + 1 } // x is number here
|
|
11
|
+
* - if (arr.length > 0) { arr[0] } // arr has elements here
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const cfa = new ControlFlowAnalyzer(ast, componentSpec);
|
|
15
|
+
* if (cfa.isDefinitelyNonNull(node, path)) {
|
|
16
|
+
* // Safe to access property - no violation
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.ControlFlowAnalyzer = void 0;
|
|
44
|
+
const t = __importStar(require("@babel/types"));
|
|
45
|
+
const type_inference_engine_1 = require("./type-inference-engine");
|
|
46
|
+
class ControlFlowAnalyzer {
|
|
47
|
+
constructor(ast, componentSpec) {
|
|
48
|
+
this.ast = ast;
|
|
49
|
+
this.componentSpec = componentSpec;
|
|
50
|
+
this.typeEngine = new type_inference_engine_1.TypeInferenceEngine(componentSpec);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a node is protected by a ternary/conditional guard
|
|
54
|
+
* Useful for checking expressions inside template literals within ternaries
|
|
55
|
+
*
|
|
56
|
+
* @param node - The node to check (e.g., member expression)
|
|
57
|
+
* @param path - Current path in the AST (should be close to the node's location)
|
|
58
|
+
* @returns true if protected by a ternary guard, false otherwise
|
|
59
|
+
*/
|
|
60
|
+
isProtectedByTernary(node, path) {
|
|
61
|
+
const varName = this.extractVariableName(node);
|
|
62
|
+
if (!varName)
|
|
63
|
+
return false;
|
|
64
|
+
// DEBUG: Log what we're looking for
|
|
65
|
+
const debugEnabled = false; // Set to true to enable logging
|
|
66
|
+
if (debugEnabled) {
|
|
67
|
+
console.log('[CFA] isProtectedByTernary checking:', varName);
|
|
68
|
+
console.log('[CFA] Starting from path type:', path.node.type);
|
|
69
|
+
}
|
|
70
|
+
// Walk up from the path to find ConditionalExpression parents
|
|
71
|
+
let currentPath = path;
|
|
72
|
+
let depth = 0;
|
|
73
|
+
while (currentPath) {
|
|
74
|
+
const parentPath = currentPath.parentPath;
|
|
75
|
+
if (debugEnabled) {
|
|
76
|
+
console.log(`[CFA] Depth ${depth}: current=${currentPath.node.type}, parent=${parentPath?.node.type || 'null'}`);
|
|
77
|
+
}
|
|
78
|
+
if (parentPath && t.isConditionalExpression(parentPath.node)) {
|
|
79
|
+
const test = parentPath.node.test;
|
|
80
|
+
if (debugEnabled) {
|
|
81
|
+
console.log('[CFA] Found ConditionalExpression!');
|
|
82
|
+
console.log('[CFA] Test type:', test.type);
|
|
83
|
+
console.log('[CFA] Consequent type:', parentPath.node.consequent.type);
|
|
84
|
+
}
|
|
85
|
+
if (this.detectNullGuard(test, varName) || this.detectTruthinessGuard(test, varName)) {
|
|
86
|
+
if (debugEnabled) {
|
|
87
|
+
console.log('[CFA] Guard detected for', varName);
|
|
88
|
+
}
|
|
89
|
+
// Check if current node is the consequent or is inside it
|
|
90
|
+
// The consequent is the "true" branch of the ternary
|
|
91
|
+
if (currentPath.node === parentPath.node.consequent) {
|
|
92
|
+
if (debugEnabled) {
|
|
93
|
+
console.log('[CFA] Current node IS the consequent - PROTECTED');
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// Check if path is inside the consequent by walking up
|
|
98
|
+
let checkPath = path;
|
|
99
|
+
while (checkPath && checkPath !== parentPath) {
|
|
100
|
+
if (checkPath.node === parentPath.node.consequent) {
|
|
101
|
+
if (debugEnabled) {
|
|
102
|
+
console.log('[CFA] Path is inside consequent - PROTECTED');
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
checkPath = checkPath.parentPath;
|
|
107
|
+
}
|
|
108
|
+
if (debugEnabled) {
|
|
109
|
+
console.log('[CFA] Not in consequent - checking failed');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (debugEnabled) {
|
|
113
|
+
console.log('[CFA] No guard detected for', varName);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
currentPath = parentPath;
|
|
117
|
+
depth++;
|
|
118
|
+
}
|
|
119
|
+
if (debugEnabled) {
|
|
120
|
+
console.log('[CFA] No protection found after', depth, 'levels');
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check if a variable/property is definitely non-null at this location
|
|
126
|
+
*
|
|
127
|
+
* Detects patterns like:
|
|
128
|
+
* - if (x != null) { x.method() } // x is non-null here
|
|
129
|
+
* - if (x !== undefined) { x.prop }
|
|
130
|
+
* - if (x) { x.prop } // truthiness check
|
|
131
|
+
* - x && x.prop // short-circuit &&
|
|
132
|
+
* - x ? x.prop : default // ternary check
|
|
133
|
+
*
|
|
134
|
+
* @param node - The node being accessed (identifier or member expression)
|
|
135
|
+
* @param path - Current path in the AST
|
|
136
|
+
* @returns true if guaranteed non-null, false otherwise
|
|
137
|
+
*/
|
|
138
|
+
isDefinitelyNonNull(node, path) {
|
|
139
|
+
const varName = this.extractVariableName(node);
|
|
140
|
+
if (!varName)
|
|
141
|
+
return false;
|
|
142
|
+
// For member expressions like products.length, also check if just the object (products) is guarded
|
|
143
|
+
// This handles cases like: !products || ... || products.length
|
|
144
|
+
const objectName = (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
|
|
145
|
+
t.isIdentifier(node.object)
|
|
146
|
+
? node.object.name
|
|
147
|
+
: null;
|
|
148
|
+
let currentPath = path.parentPath;
|
|
149
|
+
while (currentPath) {
|
|
150
|
+
const node = currentPath.node;
|
|
151
|
+
// Check if we're inside an if statement with a guard
|
|
152
|
+
if (t.isIfStatement(node)) {
|
|
153
|
+
const test = node.test;
|
|
154
|
+
// Check for null guard on full path or object
|
|
155
|
+
if (this.detectNullGuard(test, varName) || (objectName && this.detectNullGuard(test, objectName))) {
|
|
156
|
+
// Make sure we're in the consequent (then block)
|
|
157
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Check for truthiness guard on full path or object
|
|
162
|
+
if (this.detectTruthinessGuard(test, varName) || (objectName && this.detectTruthinessGuard(test, objectName))) {
|
|
163
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Check if we're inside a logical && expression
|
|
169
|
+
if (t.isLogicalExpression(node) && node.operator === '&&') {
|
|
170
|
+
// Check if left side is a guard for our variable or its object
|
|
171
|
+
if (this.detectNullGuard(node.left, varName) ||
|
|
172
|
+
this.detectTruthinessGuard(node.left, varName) ||
|
|
173
|
+
(objectName && this.detectNullGuard(node.left, objectName)) ||
|
|
174
|
+
(objectName && this.detectTruthinessGuard(node.left, objectName))) {
|
|
175
|
+
// Make sure we're in the right side
|
|
176
|
+
if (this.isInRightSide(path, currentPath)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Check if we're inside a logical || expression with negated guard
|
|
182
|
+
// Pattern: !x || !y || x.prop - if we evaluate x.prop, !x must be false
|
|
183
|
+
if (t.isLogicalExpression(node) && node.operator === '||') {
|
|
184
|
+
// Check if left side contains a negated guard for our variable or object
|
|
185
|
+
if (this.detectNegatedCheck(node.left, varName) ||
|
|
186
|
+
(objectName && this.detectNegatedCheck(node.left, objectName))) {
|
|
187
|
+
// Make sure we're in the right side
|
|
188
|
+
if (this.isInRightSide(path, currentPath)) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Check if we're inside a ternary with guard
|
|
194
|
+
if (t.isConditionalExpression(node)) {
|
|
195
|
+
// Check if we're in the consequent (true branch) with a guard on full path or object
|
|
196
|
+
if (this.detectNullGuard(node.test, varName) ||
|
|
197
|
+
this.detectTruthinessGuard(node.test, varName) ||
|
|
198
|
+
(objectName && this.detectNullGuard(node.test, objectName)) ||
|
|
199
|
+
(objectName && this.detectTruthinessGuard(node.test, objectName))) {
|
|
200
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Check if we're in the alternate (else branch) with a negated guard
|
|
205
|
+
// Pattern: !x || x.length === 0 ? ... : <here>
|
|
206
|
+
// In the else branch, we know !x is false, so x is truthy
|
|
207
|
+
if (this.isInAlternate(path, currentPath)) {
|
|
208
|
+
if (this.detectNegatedNullGuard(node.test, varName) ||
|
|
209
|
+
(objectName && this.detectNegatedNullGuard(node.test, objectName))) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
currentPath = currentPath.parentPath;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check if a variable is narrowed to a specific type at this location
|
|
220
|
+
*
|
|
221
|
+
* Detects patterns like:
|
|
222
|
+
* - if (typeof x === 'number') { x + 1 } // x is number
|
|
223
|
+
* - if (x instanceof Date) { x.getTime() } // x is Date
|
|
224
|
+
* - if (Array.isArray(x)) { x.push() } // x is array
|
|
225
|
+
*
|
|
226
|
+
* @param node - The node being checked
|
|
227
|
+
* @param path - Current path in the AST
|
|
228
|
+
* @param expectedType - The type to check for ('number', 'string', 'Date', etc.)
|
|
229
|
+
* @returns true if narrowed to that type, false otherwise
|
|
230
|
+
*/
|
|
231
|
+
isNarrowedToType(node, path, expectedType) {
|
|
232
|
+
const varName = this.extractVariableName(node);
|
|
233
|
+
if (!varName)
|
|
234
|
+
return false;
|
|
235
|
+
let currentPath = path.parentPath;
|
|
236
|
+
while (currentPath) {
|
|
237
|
+
const node = currentPath.node;
|
|
238
|
+
// Check if we're inside an if statement with a typeof guard
|
|
239
|
+
if (t.isIfStatement(node)) {
|
|
240
|
+
// First check the test directly
|
|
241
|
+
const detectedType = this.detectTypeofGuard(node.test, varName);
|
|
242
|
+
if (detectedType === expectedType) {
|
|
243
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Also check recursively if the test is a && expression
|
|
248
|
+
if (t.isLogicalExpression(node.test) && node.test.operator === '&&') {
|
|
249
|
+
const recursiveType = this.detectTypeofGuardRecursive(node.test, varName);
|
|
250
|
+
if (recursiveType === expectedType) {
|
|
251
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Check if we're inside a logical && expression with typeof guard
|
|
258
|
+
if (t.isLogicalExpression(node) && node.operator === '&&') {
|
|
259
|
+
const detectedType = this.detectTypeofGuard(node.left, varName);
|
|
260
|
+
if (detectedType === expectedType) {
|
|
261
|
+
if (this.isInRightSide(path, currentPath)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Also check nested && expressions recursively
|
|
266
|
+
if (t.isLogicalExpression(node.left) && node.left.operator === '&&') {
|
|
267
|
+
const nestedType = this.detectTypeofGuardRecursive(node.left, varName);
|
|
268
|
+
if (nestedType === expectedType) {
|
|
269
|
+
if (this.isInRightSide(path, currentPath)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
currentPath = currentPath.parentPath;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Recursively check for typeof guards in nested && expressions
|
|
281
|
+
*/
|
|
282
|
+
detectTypeofGuardRecursive(expr, varName) {
|
|
283
|
+
if (t.isLogicalExpression(expr) && expr.operator === '&&') {
|
|
284
|
+
// Check left side
|
|
285
|
+
const leftType = this.detectTypeofGuard(expr.left, varName);
|
|
286
|
+
if (leftType)
|
|
287
|
+
return leftType;
|
|
288
|
+
// Check right side recursively
|
|
289
|
+
const rightType = this.detectTypeofGuardRecursive(expr.right, varName);
|
|
290
|
+
if (rightType)
|
|
291
|
+
return rightType;
|
|
292
|
+
}
|
|
293
|
+
// Check this expression directly
|
|
294
|
+
return this.detectTypeofGuard(expr, varName);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Detect typeof guard pattern: typeof x === 'type'
|
|
298
|
+
*/
|
|
299
|
+
detectTypeofGuard(test, varName) {
|
|
300
|
+
if (!t.isBinaryExpression(test))
|
|
301
|
+
return null;
|
|
302
|
+
// typeof x === 'type'
|
|
303
|
+
if (test.operator === '===' || test.operator === '==') {
|
|
304
|
+
if (t.isUnaryExpression(test.left) &&
|
|
305
|
+
test.left.operator === 'typeof' &&
|
|
306
|
+
t.isIdentifier(test.left.argument) &&
|
|
307
|
+
test.left.argument.name === varName &&
|
|
308
|
+
t.isStringLiteral(test.right)) {
|
|
309
|
+
return test.right.value; // Return the type
|
|
310
|
+
}
|
|
311
|
+
// Reversed: 'type' === typeof x
|
|
312
|
+
if (t.isStringLiteral(test.left) &&
|
|
313
|
+
t.isUnaryExpression(test.right) &&
|
|
314
|
+
test.right.operator === 'typeof' &&
|
|
315
|
+
t.isIdentifier(test.right.argument) &&
|
|
316
|
+
test.right.argument.name === varName) {
|
|
317
|
+
return test.left.value;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Detect null/undefined guard pattern: x != null, x !== null, x !== undefined
|
|
324
|
+
* Also detects short-circuit OR patterns: !x || x.prop (x.prop is safe due to short-circuit)
|
|
325
|
+
*/
|
|
326
|
+
detectNullGuard(test, varName) {
|
|
327
|
+
// Pattern 1: Short-circuit OR with truthiness check
|
|
328
|
+
// !x || x.prop - if x is null/undefined, !x is true and second operand never evaluates
|
|
329
|
+
if (t.isLogicalExpression(test) && test.operator === '||') {
|
|
330
|
+
// Check immediate left side first
|
|
331
|
+
const left = test.left;
|
|
332
|
+
// Check if left side is !varName (negation/falsiness check)
|
|
333
|
+
if (t.isUnaryExpression(left) && left.operator === '!' &&
|
|
334
|
+
t.isIdentifier(left.argument) && left.argument.name === varName) {
|
|
335
|
+
// The right side is protected by short-circuit evaluation
|
|
336
|
+
// If varName is null/undefined, left is true and right never evaluates
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
// Also check for: !x.prop || x.prop.method
|
|
340
|
+
// Handles cases like: !results || results.length === 0
|
|
341
|
+
if (t.isUnaryExpression(left) && left.operator === '!' &&
|
|
342
|
+
t.isMemberExpression(left.argument) &&
|
|
343
|
+
t.isIdentifier(left.argument.object) &&
|
|
344
|
+
left.argument.object.name === varName) {
|
|
345
|
+
// This proves varName itself is checked for truthiness
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
// Handle OR chains: A || B || C || products.length
|
|
349
|
+
// Recursively check the left side of the OR chain
|
|
350
|
+
if (this.detectNullGuard(left, varName)) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Pattern 2: Standard binary null checks
|
|
355
|
+
if (!t.isBinaryExpression(test))
|
|
356
|
+
return false;
|
|
357
|
+
// x != null OR x !== null OR x !== undefined
|
|
358
|
+
if (test.operator === '!=' || test.operator === '!==') {
|
|
359
|
+
const left = test.left;
|
|
360
|
+
const right = test.right;
|
|
361
|
+
// Check if left is our variable
|
|
362
|
+
// For member expressions, serialize the full path for comparison
|
|
363
|
+
let leftVarName = null;
|
|
364
|
+
if (t.isIdentifier(left)) {
|
|
365
|
+
leftVarName = left.name;
|
|
366
|
+
}
|
|
367
|
+
else if (t.isMemberExpression(left) || t.isOptionalMemberExpression(left)) {
|
|
368
|
+
leftVarName = this.serializeMemberExpression(left);
|
|
369
|
+
}
|
|
370
|
+
// Check if right is null or undefined
|
|
371
|
+
const isNullish = (t.isNullLiteral(right) ||
|
|
372
|
+
(t.isIdentifier(right) && right.name === 'undefined'));
|
|
373
|
+
return leftVarName === varName && isNullish;
|
|
374
|
+
}
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Detect truthiness guard pattern: if (x), x && expr, x ? ... : ...
|
|
379
|
+
*
|
|
380
|
+
* Handles:
|
|
381
|
+
* - Simple identifier: if (x)
|
|
382
|
+
* - Member expression: if (obj.prop)
|
|
383
|
+
* - Full path matching: if (item.TotalCost) protects item.TotalCost.toFixed()
|
|
384
|
+
*/
|
|
385
|
+
detectTruthinessGuard(test, varName) {
|
|
386
|
+
// Direct identifier: if (x)
|
|
387
|
+
if (t.isIdentifier(test) && test.name === varName) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
// Member expression or optional member expression: if (obj.prop) or if (obj?.prop)
|
|
391
|
+
// Extract the full path and compare with varName
|
|
392
|
+
if (t.isMemberExpression(test) || t.isOptionalMemberExpression(test)) {
|
|
393
|
+
const testPath = this.serializeMemberExpression(test);
|
|
394
|
+
if (testPath === varName) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Logical AND expression: if (x && y), both x and y are truthy in consequent
|
|
399
|
+
// Check if varName appears on the left side of the && chain
|
|
400
|
+
if (t.isLogicalExpression(test) && test.operator === '&&') {
|
|
401
|
+
// Check left side first
|
|
402
|
+
if (this.detectTruthinessGuard(test.left, varName)) {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
// For varName on the right side, it's only guaranteed truthy if left is also truthy
|
|
406
|
+
// So we recursively check both sides
|
|
407
|
+
if (this.detectTruthinessGuard(test.right, varName)) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if expression contains a negated check (!x) for the variable
|
|
415
|
+
* Used for detecting guards within OR chains
|
|
416
|
+
*
|
|
417
|
+
* @param expr - Expression to check
|
|
418
|
+
* @param varName - Variable name to look for
|
|
419
|
+
* @returns true if !varName exists in the expression
|
|
420
|
+
*/
|
|
421
|
+
detectNegatedCheck(expr, varName) {
|
|
422
|
+
// Direct negation: !x
|
|
423
|
+
if (t.isUnaryExpression(expr) && expr.operator === '!') {
|
|
424
|
+
if (t.isIdentifier(expr.argument) && expr.argument.name === varName) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
// Also check !x.prop pattern (guards x)
|
|
428
|
+
if (t.isMemberExpression(expr.argument) &&
|
|
429
|
+
t.isIdentifier(expr.argument.object) &&
|
|
430
|
+
expr.argument.object.name === varName) {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Recursively check OR chains: A || B || C
|
|
435
|
+
if (t.isLogicalExpression(expr) && expr.operator === '||') {
|
|
436
|
+
return this.detectNegatedCheck(expr.left, varName) ||
|
|
437
|
+
this.detectNegatedCheck(expr.right, varName);
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Detect negated null guard pattern for alternate branches
|
|
443
|
+
*
|
|
444
|
+
* In the alternate (else) branch of these patterns, the variable is proven truthy:
|
|
445
|
+
* - !x || ... ? ... : <here> (if !x is false, x is truthy)
|
|
446
|
+
* - !x.prop || ... ? ... : <here> (if !x.prop is false, x is truthy)
|
|
447
|
+
*
|
|
448
|
+
* @param test - The conditional test expression
|
|
449
|
+
* @param varName - Variable name or full member path to check
|
|
450
|
+
* @returns true if the pattern proves varName is truthy in the alternate branch
|
|
451
|
+
*/
|
|
452
|
+
detectNegatedNullGuard(test, varName) {
|
|
453
|
+
// Pattern: !x || x.length === 0 ? ... : <else>
|
|
454
|
+
// In else branch, both !x and x.length === 0 are false
|
|
455
|
+
if (t.isLogicalExpression(test) && test.operator === '||') {
|
|
456
|
+
const left = test.left;
|
|
457
|
+
// Check if left side is !varName (negation check)
|
|
458
|
+
if (t.isUnaryExpression(left) && left.operator === '!') {
|
|
459
|
+
// Check if the negated expression is our variable
|
|
460
|
+
if (t.isIdentifier(left.argument) && left.argument.name === varName) {
|
|
461
|
+
// In the alternate branch, !varName is false, so varName is truthy
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
// Also handle !x.prop pattern
|
|
465
|
+
if (t.isMemberExpression(left.argument) &&
|
|
466
|
+
t.isIdentifier(left.argument.object) &&
|
|
467
|
+
left.argument.object.name === varName) {
|
|
468
|
+
// In the alternate branch, !x.prop is false, so x is truthy
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Extract variable name or full member expression path from node
|
|
477
|
+
*
|
|
478
|
+
* Examples:
|
|
479
|
+
* - `arr` → "arr"
|
|
480
|
+
* - `obj.prop` → "obj.prop"
|
|
481
|
+
* - `result.Results` → "result.Results"
|
|
482
|
+
* - `accountsResult.Results` → "accountsResult.Results"
|
|
483
|
+
*
|
|
484
|
+
* This allows CFA to match guards on nested properties correctly.
|
|
485
|
+
* For example, guard `accountsResult.Results?.length > 0` protects `accountsResult.Results[0]`
|
|
486
|
+
*/
|
|
487
|
+
extractVariableName(node) {
|
|
488
|
+
// Simple identifier: arr
|
|
489
|
+
if (t.isIdentifier(node)) {
|
|
490
|
+
return node.name;
|
|
491
|
+
}
|
|
492
|
+
// Member expression: serialize the full path
|
|
493
|
+
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
|
494
|
+
return this.serializeMemberExpression(node);
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Serialize a member expression to its full path string
|
|
500
|
+
* Handles both regular and optional member expressions
|
|
501
|
+
*
|
|
502
|
+
* Examples:
|
|
503
|
+
* - obj.prop → "obj.prop"
|
|
504
|
+
* - obj?.prop → "obj.prop" (normalized, ignores optional chaining syntax)
|
|
505
|
+
* - obj.prop.nested → "obj.prop.nested"
|
|
506
|
+
*/
|
|
507
|
+
serializeMemberExpression(node) {
|
|
508
|
+
const parts = [];
|
|
509
|
+
let current = node;
|
|
510
|
+
// Walk up the member expression chain
|
|
511
|
+
while (t.isMemberExpression(current) || t.isOptionalMemberExpression(current)) {
|
|
512
|
+
// Get the property name
|
|
513
|
+
if (t.isIdentifier(current.property)) {
|
|
514
|
+
parts.unshift(current.property.name);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// Computed property or private name - can't serialize
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
// Move to the object
|
|
521
|
+
current = current.object;
|
|
522
|
+
}
|
|
523
|
+
// Base should be an identifier
|
|
524
|
+
if (t.isIdentifier(current)) {
|
|
525
|
+
parts.unshift(current.name);
|
|
526
|
+
return parts.join('.');
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Check if path is inside the consequent (then block) of an if/ternary
|
|
532
|
+
*/
|
|
533
|
+
isInConsequent(targetPath, ifPath) {
|
|
534
|
+
const ifNode = ifPath.node;
|
|
535
|
+
if (t.isIfStatement(ifNode)) {
|
|
536
|
+
// Walk up from target to see if we hit the consequent
|
|
537
|
+
let current = targetPath;
|
|
538
|
+
while (current && current !== ifPath) {
|
|
539
|
+
if (current.node === ifNode.consequent) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
current = current.parentPath;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (t.isConditionalExpression(ifNode)) {
|
|
546
|
+
// Check if we're in the consequent branch
|
|
547
|
+
let current = targetPath;
|
|
548
|
+
while (current && current !== ifPath) {
|
|
549
|
+
if (current.node === ifNode.consequent) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
current = current.parentPath;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Check if path is in the alternate (else) branch of an if or ternary
|
|
559
|
+
*/
|
|
560
|
+
isInAlternate(targetPath, ifPath) {
|
|
561
|
+
const ifNode = ifPath.node;
|
|
562
|
+
if (t.isIfStatement(ifNode)) {
|
|
563
|
+
// Walk up from target to see if we hit the alternate
|
|
564
|
+
let current = targetPath;
|
|
565
|
+
while (current && current !== ifPath) {
|
|
566
|
+
if (current.node === ifNode.alternate) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
current = current.parentPath;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (t.isConditionalExpression(ifNode)) {
|
|
573
|
+
// Check if we're in the alternate branch
|
|
574
|
+
let current = targetPath;
|
|
575
|
+
while (current && current !== ifPath) {
|
|
576
|
+
if (current.node === ifNode.alternate) {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
current = current.parentPath;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Check if path is on the right side of a logical && expression
|
|
586
|
+
*/
|
|
587
|
+
isInRightSide(targetPath, logicalPath) {
|
|
588
|
+
const logicalNode = logicalPath.node;
|
|
589
|
+
if (!t.isLogicalExpression(logicalNode)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
// Walk up from target to see if we hit the right side
|
|
593
|
+
let current = targetPath;
|
|
594
|
+
while (current && current !== logicalPath) {
|
|
595
|
+
if (current.node === logicalNode.right) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
current = current.parentPath;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Check if an array access is safe due to bounds checking guards
|
|
604
|
+
*
|
|
605
|
+
* Detects patterns like:
|
|
606
|
+
* - if (arr.length > 0) { arr[0] } // index 0 is safe
|
|
607
|
+
* - if (arr.length > 2) { arr[2] } // index 2 is safe
|
|
608
|
+
* - if (arr.length === 0) return; arr[0] // early return pattern
|
|
609
|
+
* - arr.length > 0 && arr[0] // inline guard
|
|
610
|
+
* - arr ? arr[0] : default // ternary guard
|
|
611
|
+
*
|
|
612
|
+
* @param arrayNode - The array being accessed (identifier or member expression)
|
|
613
|
+
* @param accessIndex - The index being accessed (e.g., 0 for arr[0])
|
|
614
|
+
* @param path - Current path in the AST
|
|
615
|
+
* @returns true if access is guaranteed safe, false otherwise
|
|
616
|
+
*/
|
|
617
|
+
isArrayAccessSafe(arrayNode, accessIndex, path) {
|
|
618
|
+
const arrayName = this.extractVariableName(arrayNode);
|
|
619
|
+
if (!arrayName)
|
|
620
|
+
return false;
|
|
621
|
+
// Pattern 1: Ternary with truthiness or length check
|
|
622
|
+
// arr ? arr[0] : default OR arr.length > 0 ? arr[0] : default
|
|
623
|
+
// Also handles nested cases: arr ? `${arr[0]}` : default
|
|
624
|
+
let currentPath = path.parentPath;
|
|
625
|
+
while (currentPath) {
|
|
626
|
+
if (t.isConditionalExpression(currentPath.node)) {
|
|
627
|
+
const test = currentPath.node.test;
|
|
628
|
+
// Check if we're in the consequent (true branch)
|
|
629
|
+
// Walk up from our path to see if we're inside the consequent
|
|
630
|
+
let inConsequent = false;
|
|
631
|
+
let checkPath = path;
|
|
632
|
+
while (checkPath && checkPath !== currentPath) {
|
|
633
|
+
if (checkPath.node === currentPath.node.consequent) {
|
|
634
|
+
inConsequent = true;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
// Also check if we're inside the consequent
|
|
638
|
+
let parent = checkPath.parentPath;
|
|
639
|
+
while (parent && parent !== currentPath) {
|
|
640
|
+
if (parent.node === currentPath.node.consequent) {
|
|
641
|
+
inConsequent = true;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
parent = parent.parentPath;
|
|
645
|
+
}
|
|
646
|
+
if (inConsequent)
|
|
647
|
+
break;
|
|
648
|
+
checkPath = checkPath.parentPath;
|
|
649
|
+
}
|
|
650
|
+
if (inConsequent) {
|
|
651
|
+
// Simple truthiness: arr ? arr[0] : default
|
|
652
|
+
if (t.isIdentifier(test) && test.name === arrayName) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
// Length check in test
|
|
656
|
+
const maxSafeIndex = this.getMaxSafeIndexFromLengthCheck(test, arrayName);
|
|
657
|
+
if (maxSafeIndex >= accessIndex) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
currentPath = currentPath.parentPath;
|
|
663
|
+
}
|
|
664
|
+
// Pattern 2: Inline && guard
|
|
665
|
+
// arr && arr[0] OR arr.length > 0 && arr[0]
|
|
666
|
+
// Walk up to find any LogicalExpression ancestor
|
|
667
|
+
currentPath = path.parentPath;
|
|
668
|
+
while (currentPath) {
|
|
669
|
+
if (t.isLogicalExpression(currentPath.node) && currentPath.node.operator === '&&') {
|
|
670
|
+
const left = currentPath.node.left;
|
|
671
|
+
// Check if we're on the right side
|
|
672
|
+
if (this.isInRightSide(path, currentPath)) {
|
|
673
|
+
// Truthiness check
|
|
674
|
+
if (t.isIdentifier(left) && left.name === arrayName) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
// Length check
|
|
678
|
+
const maxSafeIndex = this.getMaxSafeIndexFromLengthCheck(left, arrayName);
|
|
679
|
+
if (maxSafeIndex >= accessIndex) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
currentPath = currentPath.parentPath;
|
|
685
|
+
}
|
|
686
|
+
// Pattern 3: if statement with guard
|
|
687
|
+
currentPath = path.parentPath;
|
|
688
|
+
while (currentPath) {
|
|
689
|
+
if (t.isIfStatement(currentPath.node)) {
|
|
690
|
+
const test = currentPath.node.test;
|
|
691
|
+
const maxSafeIndex = this.getMaxSafeIndexFromLengthCheck(test, arrayName);
|
|
692
|
+
if (maxSafeIndex >= accessIndex) {
|
|
693
|
+
// Check if we're in the consequent block
|
|
694
|
+
if (this.isInConsequent(path, currentPath)) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
// Check for early return pattern
|
|
698
|
+
const consequent = currentPath.node.consequent;
|
|
699
|
+
if (this.hasEarlyReturn(consequent)) {
|
|
700
|
+
// Code after early return is safe
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Also check for truthiness guard with early return
|
|
705
|
+
if (t.isUnaryExpression(test) && test.operator === '!' &&
|
|
706
|
+
t.isIdentifier(test.argument) && test.argument.name === arrayName) {
|
|
707
|
+
// if (!arr) return; pattern makes arr[0] safe after
|
|
708
|
+
if (this.hasEarlyReturn(currentPath.node.consequent)) {
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
currentPath = currentPath.parentPath;
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Extract the maximum safe array index from a length check expression
|
|
719
|
+
*
|
|
720
|
+
* Examples:
|
|
721
|
+
* - arr.length > 0 → returns 0 (index 0 is safe)
|
|
722
|
+
* - arr?.length > 0 → returns 0 (optional chaining proves non-null)
|
|
723
|
+
* - arr.length > 2 → returns 2 (indices 0-2 are safe)
|
|
724
|
+
* - arr.length >= 3 → returns 2 (indices 0-2 are safe)
|
|
725
|
+
* - arr.length !== 0 → returns 0 (index 0 is safe)
|
|
726
|
+
*
|
|
727
|
+
* @param test - The test expression to analyze
|
|
728
|
+
* @param arrayName - The array variable name to look for
|
|
729
|
+
* @returns Maximum safe index, or -1 if no length check found
|
|
730
|
+
*/
|
|
731
|
+
getMaxSafeIndexFromLengthCheck(test, arrayName) {
|
|
732
|
+
if (t.isBinaryExpression(test)) {
|
|
733
|
+
const { left, right, operator } = test;
|
|
734
|
+
// Check for arr.length > N or arr.length >= N (including optional chaining)
|
|
735
|
+
if (this.isLengthAccess(left, arrayName) && t.isNumericLiteral(right)) {
|
|
736
|
+
const checkValue = right.value;
|
|
737
|
+
// arr.length > N means indices 0 to N are safe
|
|
738
|
+
if (operator === '>') {
|
|
739
|
+
return checkValue;
|
|
740
|
+
}
|
|
741
|
+
// arr.length >= N means indices 0 to N-1 are safe
|
|
742
|
+
if (operator === '>=') {
|
|
743
|
+
return checkValue - 1;
|
|
744
|
+
}
|
|
745
|
+
// arr.length !== 0 or arr.length != 0 means index 0 is safe
|
|
746
|
+
if ((operator === '!==' || operator === '!=') && checkValue === 0) {
|
|
747
|
+
return 0;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// Check reverse: N < arr.length or N <= arr.length
|
|
751
|
+
if (this.isLengthAccess(right, arrayName) && t.isNumericLiteral(left)) {
|
|
752
|
+
const checkValue = left.value;
|
|
753
|
+
// N < arr.length means indices 0 to N are safe
|
|
754
|
+
if (operator === '<') {
|
|
755
|
+
return checkValue;
|
|
756
|
+
}
|
|
757
|
+
// N <= arr.length means indices 0 to N-1 are safe
|
|
758
|
+
if (operator === '<=') {
|
|
759
|
+
return checkValue - 1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
// Check logical expressions (recursively)
|
|
764
|
+
if (t.isLogicalExpression(test)) {
|
|
765
|
+
const leftMax = this.getMaxSafeIndexFromLengthCheck(test.left, arrayName);
|
|
766
|
+
const rightMax = this.getMaxSafeIndexFromLengthCheck(test.right, arrayName);
|
|
767
|
+
// For && operator, both sides must be true, so take minimum
|
|
768
|
+
if (test.operator === '&&') {
|
|
769
|
+
if (leftMax >= 0 && rightMax >= 0) {
|
|
770
|
+
return Math.min(leftMax, rightMax);
|
|
771
|
+
}
|
|
772
|
+
return Math.max(leftMax, rightMax);
|
|
773
|
+
}
|
|
774
|
+
// For || operator, either side can be true, so take maximum
|
|
775
|
+
if (test.operator === '||') {
|
|
776
|
+
return Math.max(leftMax, rightMax);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return -1;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Check if an expression is accessing the length property of an array
|
|
783
|
+
* Handles both regular and optional chaining: arr.length and arr?.length
|
|
784
|
+
* Also handles nested paths: obj.arr.length and obj.arr?.length
|
|
785
|
+
*/
|
|
786
|
+
isLengthAccess(expr, arrayName) {
|
|
787
|
+
// Check if it's a member expression accessing 'length'
|
|
788
|
+
if (!t.isMemberExpression(expr) && !t.isOptionalMemberExpression(expr)) {
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
// Property must be 'length'
|
|
792
|
+
if (!t.isIdentifier(expr.property) || expr.property.name !== 'length') {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
// Get the object being accessed (e.g., 'arr' in arr.length or 'obj.arr' in obj.arr.length)
|
|
796
|
+
const objectPath = this.serializeMemberExpression(expr.object);
|
|
797
|
+
// Simple case: arr.length where arrayName is 'arr'
|
|
798
|
+
if (t.isIdentifier(expr.object) && expr.object.name === arrayName) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
// Nested case: obj.arr.length where arrayName is 'obj.arr'
|
|
802
|
+
if (objectPath === arrayName) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Check if a statement or block contains an early return
|
|
809
|
+
*/
|
|
810
|
+
hasEarlyReturn(statement) {
|
|
811
|
+
if (t.isReturnStatement(statement)) {
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
if (t.isBlockStatement(statement)) {
|
|
815
|
+
for (const stmt of statement.body) {
|
|
816
|
+
if (t.isReturnStatement(stmt)) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
exports.ControlFlowAnalyzer = ControlFlowAnalyzer;
|
|
825
|
+
//# sourceMappingURL=control-flow-analyzer.js.map
|