@memberjunction/react-test-harness 2.121.0 → 2.122.1

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.
@@ -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