@clear-capabilities/agentic-security-scanner 0.151.3 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/agentic-security.mjs +80 -80
  3. package/dist/agentic-security.mjs.sha256 +1 -1
  4. package/package.json +5 -5
  5. package/src/dataflow/CLAUDE.md +1 -1
  6. package/src/dataflow/catalog.js +802 -13
  7. package/src/dataflow/engine.js +852 -89
  8. package/src/dataflow/incremental.js +13 -0
  9. package/src/dataflow/index.js +18 -5
  10. package/src/dataflow/path-feasibility.js +86 -9
  11. package/src/dataflow/string-domain.js +132 -0
  12. package/src/dataflow/summaries.js +69 -8
  13. package/src/engine.js +108 -3
  14. package/src/ir/CLAUDE.md +2 -1
  15. package/src/ir/balanced-call.js +33 -1
  16. package/src/ir/callgraph.js +26 -2
  17. package/src/ir/class-hierarchy.js +101 -6
  18. package/src/ir/csharp-ir.js +97 -8
  19. package/src/ir/index.js +21 -1
  20. package/src/ir/parser-cs.js +706 -24
  21. package/src/ir/parser-java.js +498 -17
  22. package/src/ir/parser-js.js +22 -0
  23. package/src/ir/parser-php.js +446 -43
  24. package/src/ir/parser-rust.js +1338 -0
  25. package/src/ir/php-include-merge.js +190 -0
  26. package/src/lineage/sink-registry.js +18 -0
  27. package/src/lineage/source-registry.js +23 -0
  28. package/src/posture/csharp-analysis.js +341 -43
  29. package/src/posture/finding-defaults.js +5 -1
  30. package/src/posture/fix-coverage.js +1 -0
  31. package/src/posture/proof-coverage.js +1 -0
  32. package/src/sast/CLAUDE.md +1 -1
  33. package/src/sast/authz.js +144 -1
  34. package/src/sast/crypto-protocol.js +65 -7
  35. package/src/sast/csharp-cross-file.js +125 -0
  36. package/src/sast/csharp-structural.js +78 -2
  37. package/src/sast/csharp.js +574 -42
  38. package/src/sast/java-ast-folding.js +268 -11
  39. package/src/sast/java-bench-extras.js +530 -2
  40. package/src/sast/java-deserialization.js +31 -2
  41. package/src/sast/java-structural-cross-file.js +146 -0
  42. package/src/sast/java-structural.js +499 -6
  43. package/src/sast/ldap-injection.js +133 -3
  44. package/src/sast/rust.js +57 -0
  45. 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
- const labelExpr = lblCh.caseConstant?.[0]?.children?.expression?.[0]
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
- // We'll resolve default later. For now, conservatively skip "default-only" groups.
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
  }