@clear-capabilities/agentic-security-scanner 0.151.2 → 0.152.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/CHANGELOG.md +184 -0
- package/dist/1905.index.js +30 -2
- package/dist/3499.index.js +20 -1
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -4
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +842 -17
- package/src/dataflow/engine.js +865 -90
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/discovery/THREAT_MODEL.md +122 -0
- package/src/discovery/disprove.js +10 -1
- package/src/discovery/lenses.js +20 -1
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +119 -7
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +527 -24
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
|
@@ -240,11 +240,38 @@ function evalUnary(node, scope) {
|
|
|
240
240
|
return value;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// SARD_80_F1 W5.27 — Juliet's own "Control flow: if(privateReturnsTrue())
|
|
244
|
+
// and if(privateReturnsFalse())" idiom (Flow Variant 08/11, confirmed via
|
|
245
|
+
// the public Java mirror's own `CWE601_Open_Redirect__Servlet_connect_tcp_08
|
|
246
|
+
// .java`) branches on a CALL to a same-class, zero-arg, single-`return
|
|
247
|
+
// true;`/`return false;`-bodied private helper, not a literal/field the
|
|
248
|
+
// existing constant evaluator already resolves. A bare method-call suffix
|
|
249
|
+
// used to unconditionally fail evaluation (the comment below explains why
|
|
250
|
+
// that's still correct for anything with arguments or a non-trivial body) —
|
|
251
|
+
// this narrowly recognizes the ONE resolvable shape (a bare, zero-arg call
|
|
252
|
+
// whose target method was pre-folded into `scope` by
|
|
253
|
+
// `_collectClassMethodConstants`, namespaced so a call `f()` can never be
|
|
254
|
+
// confused with a plain variable reference `f`) and falls through to the
|
|
255
|
+
// existing UNKNOWN behavior for everything else — an unresolvable callee,
|
|
256
|
+
// an argument list, or a chained `.field`/`.method()`/`[idx]` continuation.
|
|
243
257
|
function evalPrimary(node, scope) {
|
|
244
258
|
if (!node) return UNKNOWN;
|
|
245
259
|
const ch = node.children || {};
|
|
246
260
|
const prefix = ch.primaryPrefix?.[0];
|
|
247
261
|
if (!prefix) return UNKNOWN;
|
|
262
|
+
if (ch.primarySuffix && ch.primarySuffix.length === 1) {
|
|
263
|
+
const invocation = ch.primarySuffix[0].children?.methodInvocationSuffix?.[0];
|
|
264
|
+
if (invocation && !invocation.children?.argumentList) {
|
|
265
|
+
const fr = prefix.children?.fqnOrRefType?.[0];
|
|
266
|
+
const parts = fr?.children?.fqnOrRefTypePartFirst || [];
|
|
267
|
+
if (parts.length === 1) {
|
|
268
|
+
const ident = parts[0].children?.fqnOrRefTypePartCommon?.[0]?.children?.Identifier?.[0]?.image;
|
|
269
|
+
const key = ident && `()${ident}`;
|
|
270
|
+
if (key && scope.has(key)) return scope.get(key);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return UNKNOWN;
|
|
274
|
+
}
|
|
248
275
|
const value = evalPrimaryPrefix(prefix, scope);
|
|
249
276
|
// Suffix chains (.field, .method(), [idx]) make evaluation fail.
|
|
250
277
|
if (ch.primarySuffix && ch.primarySuffix.length > 0) {
|
|
@@ -363,7 +390,25 @@ function walkStatement(stmtNode, scope, out) {
|
|
|
363
390
|
return;
|
|
364
391
|
}
|
|
365
392
|
|
|
366
|
-
// Loop bodies, try, etc. — descend but don't bind constants.
|
|
393
|
+
// Loop bodies, try, etc. — descend but don't bind constants. Any variable
|
|
394
|
+
// REASSIGNED somewhere inside (a try body's `data = readerBuffered.
|
|
395
|
+
// readLine();` is the canonical case — Juliet's own dominant idiom:
|
|
396
|
+
// `T x = null; try { x = source(); } catch (...) {} if (x != null) {
|
|
397
|
+
// sink(x); }`) must be invalidated in `scope` first, or a later `if (x !=
|
|
398
|
+
// null)` after the block still sees the STALE pre-block value (`null`)
|
|
399
|
+
// and gets constant-folded to a permanently-dead branch — silently
|
|
400
|
+
// deleting the taint finding inside it. Confirmed via SARD_80_F1's CWE-83
|
|
401
|
+
// URLConnection variants: every one of them uses exactly this shape with
|
|
402
|
+
// a `null` initializer (File-sourced variants happen to initialize with
|
|
403
|
+
// `""` instead, which sidesteps the bug by accident — `"" != null` folds
|
|
404
|
+
// to constant-TRUE, marking the absent else-branch dead instead of the
|
|
405
|
+
// sink-bearing then-branch). Deliberately conservative in the OPPOSITE
|
|
406
|
+
// direction from the rest of this file's folding: when this walker
|
|
407
|
+
// cannot prove what happens to a variable, it must NOT keep trusting a
|
|
408
|
+
// value that may already be stale, since silently marking a live branch
|
|
409
|
+
// dead is a false negative — far worse than the lost folding opportunity
|
|
410
|
+
// of a still-cleared-but-actually-unreachable branch.
|
|
411
|
+
for (const name of _collectAssignedNames(stmtNode)) scope.delete(name);
|
|
367
412
|
for (const key of Object.keys(ch)) {
|
|
368
413
|
const arr = ch[key];
|
|
369
414
|
if (Array.isArray(arr)) for (const child of arr) {
|
|
@@ -372,6 +417,44 @@ function walkStatement(stmtNode, scope, out) {
|
|
|
372
417
|
}
|
|
373
418
|
}
|
|
374
419
|
|
|
420
|
+
/** Bare local-variable name of a `unaryExpression` node used as an
|
|
421
|
+
* assignment target, or null when it's not a simple identifier (a member
|
|
422
|
+
* write / array-index write can't alias a tracked local, so there is
|
|
423
|
+
* nothing in `scope` for it to invalidate). Mirrors evalPrimaryPrefix's
|
|
424
|
+
* own bare-identifier extraction but for a WRITE position rather than a
|
|
425
|
+
* read, so it does not require the name to already be in scope. */
|
|
426
|
+
function _bareAssignTargetName(unaryNode) {
|
|
427
|
+
const primary = unaryNode?.children?.primary?.[0];
|
|
428
|
+
const prefix = primary?.children?.primaryPrefix?.[0];
|
|
429
|
+
if (!prefix || (primary.children?.primarySuffix?.length || 0) > 0) return null;
|
|
430
|
+
const fr = prefix.children?.fqnOrRefType?.[0];
|
|
431
|
+
const parts = fr?.children?.fqnOrRefTypePartFirst || [];
|
|
432
|
+
if (parts.length !== 1) return null;
|
|
433
|
+
return parts[0].children?.fqnOrRefTypePartCommon?.[0]?.children?.Identifier?.[0]?.image || null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Recursively collect every local-variable name assigned (`x = …`, `x +=
|
|
437
|
+
* …`, `x++`/`x--`/`++x`/`--x`) anywhere in `node`'s subtree. Grammar shapes
|
|
438
|
+
* confirmed directly against java-parser's own CST: a `binaryExpression`
|
|
439
|
+
* with an `AssignmentOperator` child is `=`/`+=`/`-=`/etc. (LHS in its
|
|
440
|
+
* `unaryExpression` child); a `unaryExpression` with a `UnarySuffixOperator`
|
|
441
|
+
* or `UnaryPrefixOperator` child is `x++`/`x--`/`++x`/`--x`. */
|
|
442
|
+
function _collectAssignedNames(node, out) {
|
|
443
|
+
out = out || new Set();
|
|
444
|
+
if (!node || typeof node !== 'object') return out;
|
|
445
|
+
if (node.name === 'binaryExpression' && node.children?.AssignmentOperator) {
|
|
446
|
+
const name = _bareAssignTargetName(node.children.unaryExpression?.[0]);
|
|
447
|
+
if (name) out.add(name);
|
|
448
|
+
} else if (node.name === 'unaryExpression'
|
|
449
|
+
&& (node.children?.UnarySuffixOperator || node.children?.UnaryPrefixOperator)) {
|
|
450
|
+
const name = _bareAssignTargetName(node);
|
|
451
|
+
if (name) out.add(name);
|
|
452
|
+
}
|
|
453
|
+
const ch = node.children;
|
|
454
|
+
if (ch) for (const key of Object.keys(ch)) for (const child of ch[key]) _collectAssignedNames(child, out);
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
|
|
375
458
|
function captureLocalDecl(lvdNode, scope) {
|
|
376
459
|
if (!lvdNode) return;
|
|
377
460
|
const ch = lvdNode.children || {};
|
|
@@ -424,6 +507,7 @@ function walkSwitch(switchNode, scope, out) {
|
|
|
424
507
|
// For each switchBlockStatementGroup, check if its case label matches `v`.
|
|
425
508
|
const groups = blockNode.children?.switchBlockStatementGroup || [];
|
|
426
509
|
let matchedAny = false;
|
|
510
|
+
const defaultGroups = [];
|
|
427
511
|
for (const g of groups) {
|
|
428
512
|
const labels = g.children?.switchLabel || [];
|
|
429
513
|
let groupMatches = false;
|
|
@@ -433,7 +517,24 @@ function walkSwitch(switchNode, scope, out) {
|
|
|
433
517
|
// default — matches if nothing else does (decided after)
|
|
434
518
|
continue;
|
|
435
519
|
}
|
|
436
|
-
|
|
520
|
+
// PRD W2 (SARD_80_F1_EXECUTION_PRD.md) — `caseConstant`'s own CST
|
|
521
|
+
// child is `conditionalExpression`, not `expression` (confirmed via
|
|
522
|
+
// direct java-parser CST inspection: `caseConstant.children` is
|
|
523
|
+
// exactly `{conditionalExpression: [...]}`). The stale `expression`
|
|
524
|
+
// key made `labelExpr` undefined for EVERY colon-form case label,
|
|
525
|
+
// which made `groupMatches` false for every real case — including
|
|
526
|
+
// ones that DO match the scrutinee — so `walkSwitch` marked the
|
|
527
|
+
// LIVE, matching case as "unreachable" and left the genuinely-dead
|
|
528
|
+
// `default` untouched: exactly backwards. Found investigating why a
|
|
529
|
+
// real, verified taint chain reaching a sink AFTER a
|
|
530
|
+
// `switch(6){case 6:<tainted>;break;default:safe;break;}` (Juliet's
|
|
531
|
+
// own constant-switch idiom) produced zero findings — the tainted
|
|
532
|
+
// assignment's own line was being reported as "dead code" and
|
|
533
|
+
// suppressed via `applyJavaBenchSuppressions`'s source-line check.
|
|
534
|
+
// `evalExpr` already handles a bare `conditionalExpression` node
|
|
535
|
+
// directly (see its own dispatch above), so no other change is
|
|
536
|
+
// needed once the correct child key is read.
|
|
537
|
+
const labelExpr = lblCh.caseConstant?.[0]?.children?.conditionalExpression?.[0]
|
|
437
538
|
|| lblCh.caseLabelElement?.[0]?.children?.expression?.[0];
|
|
438
539
|
if (!labelExpr) continue;
|
|
439
540
|
const lv = evalExpr(labelExpr, scope);
|
|
@@ -442,23 +543,179 @@ function walkSwitch(switchNode, scope, out) {
|
|
|
442
543
|
if (groupMatches) matchedAny = true;
|
|
443
544
|
else {
|
|
444
545
|
// Group does NOT match scrutinee → its block is dead (unless default and nothing else matched).
|
|
445
|
-
//
|
|
546
|
+
// Resolved in a second pass below, once every group's own match state
|
|
547
|
+
// is known (a "default"-only group's own dead-ness depends on whether
|
|
548
|
+
// ANY OTHER group in the switch matched — information not available
|
|
549
|
+
// until the whole switch has been scanned once).
|
|
446
550
|
const onlyDefault = labels.every(l => l.children?.Default);
|
|
447
|
-
if (onlyDefault) continue;
|
|
551
|
+
if (onlyDefault) { defaultGroups.push(g); continue; }
|
|
448
552
|
const r = rangeOf(g);
|
|
449
553
|
if (r) out.push({ startLine: r.startLine, endLine: r.endLine, reason: 'unreachable case (switch on constant)' });
|
|
450
554
|
}
|
|
451
555
|
}
|
|
556
|
+
// SARD_80_F1 W5.35 — `matchedAny` was computed above but never actually
|
|
557
|
+
// consulted: the comment here said "we'll resolve default later," but
|
|
558
|
+
// nothing ever did, leaving `default:` permanently exempt from dead-code
|
|
559
|
+
// marking regardless of whether some OTHER case explicitly matched the
|
|
560
|
+
// scrutinee. Confirmed via a real-corpus fp (Juliet's own Flow Variant 15,
|
|
561
|
+
// `switch(6){case 6: data="foo"; default: data=null;}` — `case 6` matches
|
|
562
|
+
// 6, so `default` is provably dead, but the pre-fix code never marked it,
|
|
563
|
+
// leaving `_trailingIdentIsLiteral`'s backward scan seeing the DEAD
|
|
564
|
+
// `default: data=null;` as the "textually nearest" assignment instead of
|
|
565
|
+
// the genuinely-live `case 6: data="foo";`). Now that every group's own
|
|
566
|
+
// match state is known, a `default`-only group is dead exactly when some
|
|
567
|
+
// OTHER group explicitly matched — the one case this function can prove
|
|
568
|
+
// unreachable with total confidence, since Java guarantees at most one
|
|
569
|
+
// group runs per switch execution.
|
|
570
|
+
if (matchedAny) {
|
|
571
|
+
for (const g of defaultGroups) {
|
|
572
|
+
const r = rangeOf(g);
|
|
573
|
+
if (r) out.push({ startLine: r.startLine, endLine: r.endLine, reason: 'unreachable default (switch on constant, another case matched)' });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
// SARD_80_F1 W4.J34 — `final` FIELD constant propagation (this file's own
|
|
580
|
+
// header comment already scoped "non-final field" as out-of-scope,
|
|
581
|
+
// implying a same-class FINAL field was always intended to be in scope,
|
|
582
|
+
// just not yet implemented). Juliet's own "Flow Variant 04: Control flow:
|
|
583
|
+
// if(PRIVATE_STATIC_FINAL_TRUE) and if(PRIVATE_STATIC_FINAL_FALSE)" (used
|
|
584
|
+
// across dozens of CWEs, confirmed via the public Java Juliet mirror's own
|
|
585
|
+
// `CWE601_Open_Redirect__Servlet_connect_tcp_04.java`) is IDENTICAL to the
|
|
586
|
+
// already-modeled literal `if(true)/if(false)` idiom (Flow Variant 02),
|
|
587
|
+
// except the condition is a reference to a `private static final boolean`
|
|
588
|
+
// FIELD rather than the literal keyword — every method body previously
|
|
589
|
+
// started dead-branch analysis with a fresh EMPTY scope (`new Map()`),
|
|
590
|
+
// so `evalPrimaryPrefix`'s own `scope.has(ident)` check could never
|
|
591
|
+
// resolve the field reference, and the entire Flow-Variant-04 family was
|
|
592
|
+
// invisible to this constant-folding pass, leaving a genuinely dead
|
|
593
|
+
// `else { data = null; }` branch counted as live and (via the SAME
|
|
594
|
+
// mechanism already fixed for Variant 02 at W4.J33) defeating any
|
|
595
|
+
// downstream "nearest assignment" precision heuristic built on top of it.
|
|
596
|
+
// Deliberately scoped to `final` fields ONLY (checked via the field's own
|
|
597
|
+
// `fieldModifier` CST children, never inferred from naming convention) —
|
|
598
|
+
// a NON-final field could legitimately be reassigned elsewhere (a setter,
|
|
599
|
+
// another method), and treating its declared initializer as an eternal
|
|
600
|
+
// constant would risk marking a genuinely LIVE branch as dead, a false
|
|
601
|
+
// NEGATIVE this session has been careful never to introduce. `static` is
|
|
602
|
+
// not required (an instance-level `private final boolean x = true;` is
|
|
603
|
+
// equally a real compile-time constant once the object exists, and Juliet
|
|
604
|
+
// itself only ever uses `static final` for this idiom, so the distinction
|
|
605
|
+
// is moot in practice but the fix is correct either way).
|
|
606
|
+
// SARD_80_F1 W4.J36 — Juliet's OWN variant family for this idiom goes well
|
|
607
|
+
// beyond the literal (`02`) and `static final` (`04`) forms already
|
|
608
|
+
// modeled: Flow Variant 05 ("if(privateTrue) and if(privateFalse)") uses a
|
|
609
|
+
// plain, NON-static, NON-final `private boolean privateTrue = true;`
|
|
610
|
+
// instance field, and Variant 07 ("if(privateFive==5) and
|
|
611
|
+
// if(privateFive!=5)") compares a non-final `private int` field against a
|
|
612
|
+
// literal with `==`/`!=` (confirmed via the public Java Juliet mirror's own
|
|
613
|
+
// `CWE601_Open_Redirect__Servlet_connect_tcp_05.java`/`_07.java`) — both
|
|
614
|
+
// already fall out of `evalBinary`'s existing equality-operator support
|
|
615
|
+
// for free, once the field itself resolves to a value at all. A bare
|
|
616
|
+
// non-final field is NOT a compile-time constant in general (W4.J34's own
|
|
617
|
+
// header comment on why `final` is required), but Java's real "effectively
|
|
618
|
+
// final" concept — a variable assigned exactly once, wherever that
|
|
619
|
+
// assignment happens to sit — covers this shape soundly: if `privateTrue`
|
|
620
|
+
// is written to nowhere else in the WHOLE FILE besides its own declaration,
|
|
621
|
+
// it can never actually change, so folding it is exactly as safe as
|
|
622
|
+
// folding a real `final` field. Checked with a deliberately simple,
|
|
623
|
+
// conservative TEXT scan (`_isEffectivelyFinal`) rather than a full CST
|
|
624
|
+
// call-graph/alias analysis: counting `\bname\s*=(?!=)` occurrences across
|
|
625
|
+
// the ENTIRE source (not just this class) can only ever OVER-count (an
|
|
626
|
+
// unrelated same-named field/local elsewhere in the file, or in another
|
|
627
|
+
// class in the same file) — which only means the optimization is missed,
|
|
628
|
+
// never that a genuinely-reassigned field gets folded. That failure mode
|
|
629
|
+
// is the same "safe in the conservative direction" shape every dead-branch
|
|
630
|
+
// check in this file already uses.
|
|
631
|
+
function _isEffectivelyFinal(source, fieldName) {
|
|
632
|
+
if (!source || !fieldName) return false;
|
|
633
|
+
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
634
|
+
const assignRe = new RegExp(`\\b${escaped}\\s*=(?!=)`, 'g');
|
|
635
|
+
const matches = source.match(assignRe) || [];
|
|
636
|
+
return matches.length <= 1;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Unwraps a `blockStatement` CST node down to a `returnStatement`, following
|
|
640
|
+
// the same `statement` / `statementWithoutTrailingSubstatement` nesting
|
|
641
|
+
// `walkStatement` already unwraps elsewhere in this file — or returns null
|
|
642
|
+
// for anything else (an ambiguous body is left alone, not guessed at).
|
|
643
|
+
function _asReturnStatement(blockStatement) {
|
|
644
|
+
let node = blockStatement?.children?.statement?.[0]
|
|
645
|
+
|| blockStatement?.children?.statementWithoutTrailingSubstatement?.[0];
|
|
646
|
+
while (node) {
|
|
647
|
+
if (node.children?.returnStatement) return node.children.returnStatement[0];
|
|
648
|
+
node = node.children?.statement?.[0] || node.children?.statementWithoutTrailingSubstatement?.[0];
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
452
651
|
}
|
|
453
652
|
|
|
653
|
+
// SARD_80_F1 W5.27 — see `evalPrimary`'s own header comment for the full
|
|
654
|
+
// incident writeup. Folds a same-class, zero-parameter method whose ENTIRE
|
|
655
|
+
// body is EXACTLY one `return <expr>;` statement into the SAME class-scoped
|
|
656
|
+
// constants map `_collectClassConstants` already builds for `final`/
|
|
657
|
+
// effectively-final fields — namespaced under `()<name>` so a method call
|
|
658
|
+
// `f()` can never collide with, or be confused with, a plain field/local
|
|
659
|
+
// reference `f`. Deliberately conservative in every dimension already
|
|
660
|
+
// established by this file's sibling folders: a method with ANY parameter
|
|
661
|
+
// is skipped (a caller-supplied argument could change the outcome, breaking
|
|
662
|
+
// "always returns the same value"); a body with more than one statement is
|
|
663
|
+
// ambiguous and skipped (mirrors `_resolveCalleeReturnIsLiteral`'s identical
|
|
664
|
+
// "exactly one return" requirement in `java-bench-extras.js`, the same
|
|
665
|
+
// interprocedural-constant precedent applied here to conditions instead of
|
|
666
|
+
// assignments); the return expression is evaluated through the ALREADY-
|
|
667
|
+
// collected field-constants map, so `return SOME_FINAL_FIELD;` also resolves
|
|
668
|
+
// correctly, but a return of anything else (a call, a computed expression)
|
|
669
|
+
// correctly stays `UNKNOWN` and is not folded.
|
|
670
|
+
function _collectClassMethodConstants(classBody, consts) {
|
|
671
|
+
const decls = classBody?.children?.classBodyDeclaration || [];
|
|
672
|
+
for (const bd of decls) {
|
|
673
|
+
const method = bd.children?.classMemberDeclaration?.[0]?.children?.methodDeclaration?.[0];
|
|
674
|
+
if (!method) continue;
|
|
675
|
+
const declarator = method.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0];
|
|
676
|
+
if (!declarator || declarator.children?.formalParameterList) continue;
|
|
677
|
+
const name = declarator.children?.Identifier?.[0]?.image;
|
|
678
|
+
if (!name) continue;
|
|
679
|
+
const block = method.children?.methodBody?.[0]?.children?.block?.[0];
|
|
680
|
+
const stmts = block?.children?.blockStatements?.[0]?.children?.blockStatement || [];
|
|
681
|
+
if (stmts.length !== 1) continue;
|
|
682
|
+
const returnStmt = _asReturnStatement(stmts[0]);
|
|
683
|
+
const exprNode = returnStmt?.children?.expression?.[0];
|
|
684
|
+
if (!exprNode) continue;
|
|
685
|
+
const value = evalExpr(exprNode, consts);
|
|
686
|
+
if (value !== UNKNOWN) consts.set(`()${name}`, value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function _collectClassConstants(classBody, source) {
|
|
691
|
+
const consts = new Map();
|
|
692
|
+
const decls = classBody?.children?.classBodyDeclaration || [];
|
|
693
|
+
for (const bd of decls) {
|
|
694
|
+
const fd = bd.children?.classMemberDeclaration?.[0]?.children?.fieldDeclaration?.[0];
|
|
695
|
+
if (!fd) continue;
|
|
696
|
+
const isFinal = (fd.children?.fieldModifier || []).some(m => m.children?.Final);
|
|
697
|
+
const declarators = fd.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator || [];
|
|
698
|
+
for (const d of declarators) {
|
|
699
|
+
const name = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
700
|
+
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0];
|
|
701
|
+
if (!name || !initExpr) continue;
|
|
702
|
+
if (!isFinal && !_isEffectivelyFinal(source, name)) continue;
|
|
703
|
+
const value = evalExpr(initExpr, consts);
|
|
704
|
+
if (value !== UNKNOWN) consts.set(name, value);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return consts;
|
|
708
|
+
}
|
|
454
709
|
|
|
455
710
|
// ─── Public API ───────────────────────────────────────────────────────────
|
|
456
711
|
|
|
457
712
|
/** Walk every method body anywhere in the type hierarchy (including inner
|
|
458
713
|
* classes, interfaces, anonymous classes). Recursive. */
|
|
459
|
-
function walkClassBody(classBody, out) {
|
|
714
|
+
function walkClassBody(classBody, out, source) {
|
|
460
715
|
if (!classBody) return;
|
|
461
716
|
const decls = classBody.children?.classBodyDeclaration || [];
|
|
717
|
+
const classConsts = _collectClassConstants(classBody, source);
|
|
718
|
+
_collectClassMethodConstants(classBody, classConsts);
|
|
462
719
|
for (const bd of decls) {
|
|
463
720
|
const member = bd.children?.classMemberDeclaration?.[0];
|
|
464
721
|
if (!member) continue;
|
|
@@ -468,20 +725,20 @@ function walkClassBody(classBody, out) {
|
|
|
468
725
|
if (memCh.methodDeclaration) {
|
|
469
726
|
const method = memCh.methodDeclaration[0];
|
|
470
727
|
const block = method.children?.methodBody?.[0]?.children?.block?.[0];
|
|
471
|
-
if (block) walkBlock(block, new Map(), out);
|
|
728
|
+
if (block) walkBlock(block, new Map(classConsts), out);
|
|
472
729
|
}
|
|
473
730
|
|
|
474
731
|
// Constructors
|
|
475
732
|
if (memCh.constructorDeclaration) {
|
|
476
733
|
const ctor = memCh.constructorDeclaration[0];
|
|
477
734
|
const block = ctor.children?.constructorBody?.[0];
|
|
478
|
-
if (block) walkBlock(block, new Map(), out);
|
|
735
|
+
if (block) walkBlock(block, new Map(classConsts), out);
|
|
479
736
|
}
|
|
480
737
|
|
|
481
738
|
// Nested class / interface
|
|
482
739
|
if (memCh.classDeclaration) {
|
|
483
740
|
const cd = memCh.classDeclaration[0];
|
|
484
|
-
walkClassDeclaration(cd, out);
|
|
741
|
+
walkClassDeclaration(cd, out, source);
|
|
485
742
|
}
|
|
486
743
|
if (memCh.interfaceDeclaration) {
|
|
487
744
|
const id = memCh.interfaceDeclaration[0];
|
|
@@ -490,12 +747,12 @@ function walkClassBody(classBody, out) {
|
|
|
490
747
|
}
|
|
491
748
|
}
|
|
492
749
|
|
|
493
|
-
function walkClassDeclaration(cd, out) {
|
|
750
|
+
function walkClassDeclaration(cd, out, source) {
|
|
494
751
|
if (!cd) return;
|
|
495
752
|
const ncd = cd.children?.normalClassDeclaration?.[0];
|
|
496
753
|
if (ncd) {
|
|
497
754
|
const body = ncd.children?.classBody?.[0];
|
|
498
|
-
walkClassBody(body, out);
|
|
755
|
+
walkClassBody(body, out, source);
|
|
499
756
|
}
|
|
500
757
|
// Enums and records also have bodies that may contain methods.
|
|
501
758
|
const ed = cd.children?.enumDeclaration?.[0];
|
|
@@ -547,7 +804,7 @@ export function deadBranchRanges(source) {
|
|
|
547
804
|
const tds = cu?.children?.typeDeclaration || [];
|
|
548
805
|
for (const td of tds) {
|
|
549
806
|
const cd = td.children?.classDeclaration?.[0];
|
|
550
|
-
if (cd) walkClassDeclaration(cd, out);
|
|
807
|
+
if (cd) walkClassDeclaration(cd, out, source);
|
|
551
808
|
const id = td.children?.interfaceDeclaration?.[0];
|
|
552
809
|
if (id) walkInterfaceDeclaration(id, out);
|
|
553
810
|
}
|