circle-ir 4.14.2 → 4.16.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 (168) hide show
  1. package/configs/sinks/golang.json +4 -4
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +11 -7
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/const-table-lookup.d.ts +29 -0
  6. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  7. package/dist/analysis/const-table-lookup.js +145 -0
  8. package/dist/analysis/const-table-lookup.js.map +1 -0
  9. package/dist/analysis/constant-caller-gate.d.ts +39 -0
  10. package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
  11. package/dist/analysis/constant-caller-gate.js +298 -0
  12. package/dist/analysis/constant-caller-gate.js.map +1 -0
  13. package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
  14. package/dist/analysis/constant-propagation/evaluator.js +3 -1
  15. package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
  16. package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
  17. package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/patterns.js +33 -0
  19. package/dist/analysis/constant-propagation/patterns.js.map +1 -1
  20. package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
  21. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  22. package/dist/analysis/constant-propagation/propagator.js +396 -49
  23. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  24. package/dist/analysis/constant-propagation/types.d.ts +9 -0
  25. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  26. package/dist/analysis/findings.d.ts.map +1 -1
  27. package/dist/analysis/findings.js +149 -4
  28. package/dist/analysis/findings.js.map +1 -1
  29. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  30. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  31. package/dist/analysis/go-strings-replacer.js +145 -0
  32. package/dist/analysis/go-strings-replacer.js.map +1 -0
  33. package/dist/analysis/interprocedural.d.ts.map +1 -1
  34. package/dist/analysis/interprocedural.js +148 -0
  35. package/dist/analysis/interprocedural.js.map +1 -1
  36. package/dist/analysis/java-array-element-keys.d.ts +14 -0
  37. package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
  38. package/dist/analysis/java-array-element-keys.js +36 -0
  39. package/dist/analysis/java-array-element-keys.js.map +1 -0
  40. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  41. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  42. package/dist/analysis/java-canonical-prefix.js +145 -0
  43. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  44. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  45. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  46. package/dist/analysis/java-crlf-strip.js +195 -0
  47. package/dist/analysis/java-crlf-strip.js.map +1 -0
  48. package/dist/analysis/js-crlf-strip.d.ts +26 -0
  49. package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
  50. package/dist/analysis/js-crlf-strip.js +244 -0
  51. package/dist/analysis/js-crlf-strip.js.map +1 -0
  52. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  53. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  54. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  55. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  56. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/cross-file-pass.js +43 -0
  58. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  59. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
  60. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  61. package/dist/analysis/passes/csharp-sanitizer-credit.js +127 -12
  62. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  63. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  64. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  65. package/dist/analysis/passes/interprocedural-pass.js +20 -1
  66. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  67. package/dist/analysis/passes/language-sources-pass.d.ts +76 -2
  68. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  69. package/dist/analysis/passes/language-sources-pass.js +2045 -156
  70. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  71. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  72. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  73. package/dist/analysis/passes/param-source-flow-gate-pass.js +214 -6
  74. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  75. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
  76. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
  77. package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
  78. package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
  79. package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
  80. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  81. package/dist/analysis/passes/scan-secrets-pass.js +7 -1
  82. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  83. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  84. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  85. package/dist/analysis/passes/sink-filter-pass.js +509 -27
  86. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  87. package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
  88. package/dist/analysis/passes/taint-matcher-pass.js +2 -2
  89. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
  90. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  91. package/dist/analysis/passes/taint-propagation-pass.js +1606 -60
  92. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  93. package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
  94. package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
  95. package/dist/analysis/passes/weak-hash-pass.js +191 -4
  96. package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
  97. package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
  98. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  99. package/dist/analysis/passes/weak-random-pass.js +30 -0
  100. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  101. package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
  102. package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
  103. package/dist/analysis/project-callee-escape-gate.js +243 -0
  104. package/dist/analysis/project-callee-escape-gate.js.map +1 -0
  105. package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
  106. package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
  107. package/dist/analysis/rust-literal-guard-rebind.js +197 -0
  108. package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
  109. package/dist/analysis/rust-literal-guard.d.ts +33 -0
  110. package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
  111. package/dist/analysis/rust-literal-guard.js +422 -0
  112. package/dist/analysis/rust-literal-guard.js.map +1 -0
  113. package/dist/analysis/scalar-derivation.d.ts +33 -0
  114. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  115. package/dist/analysis/scalar-derivation.js +368 -0
  116. package/dist/analysis/scalar-derivation.js.map +1 -0
  117. package/dist/analysis/taint-matcher.d.ts +2 -2
  118. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  119. package/dist/analysis/taint-matcher.js +695 -34
  120. package/dist/analysis/taint-matcher.js.map +1 -1
  121. package/dist/analysis/taint-propagation.d.ts +26 -1
  122. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  123. package/dist/analysis/taint-propagation.js +375 -24
  124. package/dist/analysis/taint-propagation.js.map +1 -1
  125. package/dist/analyzer.d.ts +16 -2
  126. package/dist/analyzer.d.ts.map +1 -1
  127. package/dist/analyzer.js +19 -2
  128. package/dist/analyzer.js.map +1 -1
  129. package/dist/browser/circle-ir.js +25539 -19200
  130. package/dist/core/circle-ir-core.cjs +3945 -2126
  131. package/dist/core/circle-ir-core.js +3945 -2126
  132. package/dist/core/extractors/calls.d.ts +12 -1
  133. package/dist/core/extractors/calls.d.ts.map +1 -1
  134. package/dist/core/extractors/calls.js +332 -22
  135. package/dist/core/extractors/calls.js.map +1 -1
  136. package/dist/core/extractors/dfg.js +336 -10
  137. package/dist/core/extractors/dfg.js.map +1 -1
  138. package/dist/core/extractors/types.d.ts +5 -1
  139. package/dist/core/extractors/types.d.ts.map +1 -1
  140. package/dist/core/extractors/types.js +62 -0
  141. package/dist/core/extractors/types.js.map +1 -1
  142. package/dist/graph/analysis-pass.d.ts +6 -1
  143. package/dist/graph/analysis-pass.d.ts.map +1 -1
  144. package/dist/graph/analysis-pass.js +2 -1
  145. package/dist/graph/analysis-pass.js.map +1 -1
  146. package/dist/graph/code-graph.d.ts +9 -0
  147. package/dist/graph/code-graph.d.ts.map +1 -1
  148. package/dist/graph/code-graph.js +11 -0
  149. package/dist/graph/code-graph.js.map +1 -1
  150. package/dist/navigation/navigation-index.d.ts +134 -14
  151. package/dist/navigation/navigation-index.d.ts.map +1 -1
  152. package/dist/navigation/navigation-index.js +1158 -104
  153. package/dist/navigation/navigation-index.js.map +1 -1
  154. package/dist/navigation/types.d.ts +59 -2
  155. package/dist/navigation/types.d.ts.map +1 -1
  156. package/dist/resolution/cross-file.d.ts +16 -0
  157. package/dist/resolution/cross-file.d.ts.map +1 -1
  158. package/dist/resolution/cross-file.js +110 -42
  159. package/dist/resolution/cross-file.js.map +1 -1
  160. package/dist/resolution/type-hierarchy.d.ts +3 -0
  161. package/dist/resolution/type-hierarchy.d.ts.map +1 -1
  162. package/dist/resolution/type-hierarchy.js +45 -1
  163. package/dist/resolution/type-hierarchy.js.map +1 -1
  164. package/dist/types/index.d.ts +33 -0
  165. package/dist/types/index.d.ts.map +1 -1
  166. package/dist/types/index.js.map +1 -1
  167. package/dist/wasm/web-tree-sitter.wasm +0 -0
  168. package/package.json +3 -3
@@ -168,6 +168,7 @@ function buildCSharpDFG(tree, cache) {
168
168
  * Build DFG for Java code.
169
169
  */
170
170
  function buildJavaDFG(tree, cache) {
171
+ multiLineValueLinks = [];
171
172
  const defs = [];
172
173
  const uses = [];
173
174
  let defIdCounter = 1;
@@ -219,6 +220,7 @@ function buildJavaDFG(tree, cache) {
219
220
  }
220
221
  // Compute def-use chains
221
222
  const chains = computeChains(defs, uses);
223
+ linkMultiLineValues(chains, uses);
222
224
  return { defs, uses, chains };
223
225
  }
224
226
  /**
@@ -352,6 +354,51 @@ function processBlock(block, startDefId, startUseId, scopeStack, isJavaScript) {
352
354
  }
353
355
  return { defs, uses, nextDefId: defId, nextUseId: useId };
354
356
  }
357
+ /**
358
+ * Java: a local whose initializer / assigned value wraps over several lines
359
+ * (`label = new String(\n Base64.decode(\n picked.getBytes()));`) holds uses on
360
+ * lines after the definition's own line, and `computeChains` links a use to a
361
+ * definition only on the same line. Each such definition is recorded here with
362
+ * the uses of its value, and `buildJavaDFG` links them once every use has its
363
+ * final reaching definition (cognium-dev#597).
364
+ */
365
+ let multiLineValueLinks = [];
366
+ function recordMultiLineValue(value, defId, valueUses, isJavaScript) {
367
+ if (isJavaScript || value.endPosition.row <= value.startPosition.row)
368
+ return;
369
+ // A lambda, anonymous class or switch expression inside the value has its own
370
+ // statements; their uses do not feed the value itself. Leave those alone.
371
+ if (findNodes(value, 'lambda_expression').length > 0
372
+ || findNodes(value, 'class_body').length > 0
373
+ || findNodes(value, 'switch_expression').length > 0)
374
+ return;
375
+ const useIds = valueUses.filter(u => u.line > value.startPosition.row + 1).map(u => u.id);
376
+ if (useIds.length > 0)
377
+ multiLineValueLinks.push({ defId, useIds });
378
+ }
379
+ /** Chain each use inside a wrapped value to the definition it feeds (#597). */
380
+ function linkMultiLineValues(chains, uses) {
381
+ if (multiLineValueLinks.length === 0)
382
+ return;
383
+ const useById = new Map();
384
+ for (const u of uses)
385
+ useById.set(u.id, u);
386
+ const seen = new Set(chains.map(c => `${c.from_def}|${c.to_def}|${c.via}`));
387
+ for (const { defId, useIds } of multiLineValueLinks) {
388
+ for (const id of useIds) {
389
+ const u = useById.get(id);
390
+ if (!u || u.def_id === null || u.def_id === defId)
391
+ continue;
392
+ const key = `${u.def_id}|${defId}|${u.variable}`;
393
+ if (seen.has(key))
394
+ continue;
395
+ seen.add(key);
396
+ chains.push({ from_def: u.def_id, to_def: defId, via: u.variable });
397
+ }
398
+ }
399
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
400
+ multiLineValueLinks = [];
401
+ }
355
402
  /**
356
403
  * Process a single statement.
357
404
  */
@@ -368,6 +415,7 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
368
415
  const declarators = findNodes(stmt, 'variable_declarator');
369
416
  for (const decl of declarators) {
370
417
  const nameNode = decl.childForFieldName('name');
418
+ let declaredDefId = null;
371
419
  if (nameNode) {
372
420
  // Handle simple identifiers
373
421
  if (nameNode.type === 'identifier') {
@@ -380,6 +428,8 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
380
428
  };
381
429
  defs.push(def);
382
430
  currentScope(scopeStack).set(name, def.id);
431
+ markDeclared(currentScope(scopeStack), name);
432
+ declaredDefId = def.id;
383
433
  }
384
434
  else if (isJavaScript) {
385
435
  // Handle destructuring patterns in JavaScript
@@ -396,6 +446,8 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
396
446
  const initUses = extractUses(init, useId, scopeStack, isJavaScript);
397
447
  uses.push(...initUses.uses);
398
448
  useId = initUses.nextId;
449
+ if (declaredDefId !== null)
450
+ recordMultiLineValue(init, declaredDefId, initUses.uses, isJavaScript);
399
451
  }
400
452
  }
401
453
  }
@@ -433,21 +485,37 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
433
485
  }
434
486
  // Process branches
435
487
  const consequence = stmt.childForFieldName('consequence');
488
+ const alternative = stmt.childForFieldName('alternative');
489
+ const topAlts = alternateDefs.get(currentScope(scopeStack));
490
+ const before = new Map();
491
+ if (topAlts)
492
+ for (const [name, ids] of topAlts)
493
+ before.set(name, [...ids]);
494
+ let armA = null;
495
+ let armB = null;
436
496
  if (consequence) {
437
- const result = processStatement(consequence, defId, useId, scopeStack, isJavaScript);
497
+ const result = processArm(consequence, defId, useId, scopeStack, isJavaScript);
498
+ armA = result.popped;
438
499
  defs.push(...result.defs);
439
500
  uses.push(...result.uses);
440
501
  defId = result.nextDefId;
441
502
  useId = result.nextUseId;
442
503
  }
443
- const alternative = stmt.childForFieldName('alternative');
444
504
  if (alternative) {
445
- const result = processStatement(alternative, defId, useId, scopeStack, isJavaScript);
505
+ const result = processArm(alternative, defId, useId, scopeStack, isJavaScript);
506
+ armB = result.popped;
446
507
  defs.push(...result.defs);
447
508
  uses.push(...result.uses);
448
509
  defId = result.nextDefId;
449
510
  useId = result.nextUseId;
450
511
  }
512
+ // An `else if` arm is merged too: its own merge has already put the chain's
513
+ // final def on the arm's scope only when every arm of the chain assigns, so
514
+ // a chain with no final `else` (or one arm that skips the name) keeps the
515
+ // reaching def.
516
+ if (armA && armB) {
517
+ mergeIfElseDefs(armA, armB, before, scopeStack);
518
+ }
451
519
  break;
452
520
  }
453
521
  case 'for_statement':
@@ -525,7 +593,7 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
525
593
  // Process body
526
594
  const body = stmt.childForFieldName('body');
527
595
  if (body) {
528
- const result = processStatement(body, defId, useId, scopeStack, isJavaScript);
596
+ const result = processArm(body, defId, useId, scopeStack, isJavaScript);
529
597
  defs.push(...result.defs);
530
598
  uses.push(...result.uses);
531
599
  defId = result.nextDefId;
@@ -555,13 +623,22 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
555
623
  const group = body.child(i);
556
624
  if (!group)
557
625
  continue;
626
+ // JS (#582): each `case` / `default` gets a scope of its own and its
627
+ // defs are parked beside the others, so a constant in `default` does
628
+ // not replace a tainted assignment in an earlier `case`.
629
+ if (isJavaScript)
630
+ scopeStack.push(new Map());
558
631
  const result = processBlock(group, defId, useId, scopeStack, isJavaScript);
559
632
  defs.push(...result.defs);
560
633
  uses.push(...result.uses);
561
634
  defId = result.nextDefId;
562
635
  useId = result.nextUseId;
636
+ if (isJavaScript)
637
+ carryBranchDefsOut(scopeStack.pop(), scopeStack, false);
563
638
  }
564
- scopeStack.pop();
639
+ // Alternates parked on the switch scope go on to the enclosing scope.
640
+ const poppedSwitch = scopeStack.pop();
641
+ carryBranchDefsOut(poppedSwitch, scopeStack, false, true);
565
642
  }
566
643
  break;
567
644
  }
@@ -574,7 +651,33 @@ function processStatement(stmt, startDefId, startUseId, scopeStack, isJavaScript
574
651
  uses.push(...result.uses);
575
652
  defId = result.nextDefId;
576
653
  useId = result.nextUseId;
577
- scopeStack.pop();
654
+ const popped = scopeStack.pop();
655
+ // A bare `{ … }` (or `synchronized (x) { … }`) always runs, so its defs
656
+ // replace the enclosing scope's; an if/loop/try/switch/lambda body may
657
+ // not, so its defs are kept beside them.
658
+ // JS: a bare `{ … }` inside a statement_block likewise always runs; the
659
+ // block of an `if` / `else` / loop / `try` may not, so (#582) its defs are
660
+ // carried out as alternates, as for Java (#692).
661
+ const alwaysRuns = stmt.parent?.type === 'block' || stmt.parent?.type === 'synchronized_statement' ||
662
+ (isJavaScript && stmt.parent?.type === 'statement_block');
663
+ lastPoppedBlock = popped;
664
+ carryBranchDefsOut(popped, scopeStack, alwaysRuns);
665
+ break;
666
+ }
667
+ case 'else_clause': {
668
+ // JavaScript: `else { … }` / `else if (…) { … }` hold their arm one level
669
+ // down; without this the arm fell to `default` and recorded no defs, so an
670
+ // assignment in an `else` never reached a use after the `if` (#582).
671
+ for (let i = 0; i < stmt.childCount; i++) {
672
+ const arm = stmt.child(i);
673
+ if (!arm || arm.type === 'else')
674
+ continue;
675
+ const result = processStatement(arm, defId, useId, scopeStack, isJavaScript);
676
+ defs.push(...result.defs);
677
+ uses.push(...result.uses);
678
+ defId = result.nextDefId;
679
+ useId = result.nextUseId;
680
+ }
578
681
  break;
579
682
  }
580
683
  default: {
@@ -600,6 +703,7 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
600
703
  // 2. Then create a def for the left side
601
704
  const left = expr.childForFieldName('left');
602
705
  const right = expr.childForFieldName('right');
706
+ let assignedValueUses = [];
603
707
  // Process right side first - may contain nested assignments
604
708
  if (right) {
605
709
  if (right.type === 'assignment_expression') {
@@ -615,6 +719,7 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
615
719
  const rightUses = extractUses(right, useId, scopeStack, isJavaScript);
616
720
  uses.push(...rightUses.uses);
617
721
  useId = rightUses.nextId;
722
+ assignedValueUses = rightUses.uses;
618
723
  }
619
724
  }
620
725
  // Now process left side as a definition
@@ -628,6 +733,10 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
628
733
  };
629
734
  defs.push(def);
630
735
  currentScope(scopeStack).set(name, def.id);
736
+ if (right && assignedValueUses.length > 0)
737
+ recordMultiLineValue(right, def.id, assignedValueUses, isJavaScript);
738
+ // A straight-line reassignment replaces any branch defs carried out (#596).
739
+ alternateDefs.get(currentScope(scopeStack))?.delete(name);
631
740
  }
632
741
  else if (left) {
633
742
  // Field access or array access - extract uses
@@ -740,6 +849,12 @@ function extractUses(node, startId, scopeStack, isJavaScript) {
740
849
  line: n.startPosition.row + 1,
741
850
  def_id: reachingDef,
742
851
  });
852
+ // A def made inside a nested block may also reach this use (#596).
853
+ for (const alt of findAlternateDefs(name, scopeStack)) {
854
+ if (alt === reachingDef)
855
+ continue;
856
+ uses.push({ id: id++, variable: name, line: n.startPosition.row + 1, def_id: alt });
857
+ }
743
858
  }
744
859
  });
745
860
  return { uses, nextId: id };
@@ -825,7 +940,7 @@ function extractJSParameterDefs(params, startId) {
825
940
  addPatternDefs(nameNode, line);
826
941
  return;
827
942
  }
828
- if (patternNode.type === 'assignment_pattern') {
943
+ if (isDestructuringDefaultPattern(patternNode.type)) {
829
944
  const leftNode = patternNode.childForFieldName('left');
830
945
  if (leftNode)
831
946
  addPatternDefs(leftNode, line);
@@ -869,7 +984,7 @@ function extractJSParameterDefs(params, startId) {
869
984
  }
870
985
  }
871
986
  // Assignment pattern (default value): name = defaultValue
872
- else if (param.type === 'assignment_pattern') {
987
+ else if (isDestructuringDefaultPattern(param.type)) {
873
988
  const leftNode = param.childForFieldName('left');
874
989
  if (leftNode && leftNode.type === 'identifier') {
875
990
  defs.push({
@@ -932,6 +1047,10 @@ function extractJSFieldDefs(body, startId) {
932
1047
  }
933
1048
  return defs;
934
1049
  }
1050
+ /** Tree-sitter-javascript object destructuring default (`{ sort = "name" }`). */
1051
+ function isDestructuringDefaultPattern(type) {
1052
+ return type === 'assignment_pattern' || type === 'object_assignment_pattern';
1053
+ }
935
1054
  /**
936
1055
  * Extract definitions from destructuring patterns.
937
1056
  */
@@ -982,7 +1101,7 @@ function extractDestructuringDefs(node, startId) {
982
1101
  });
983
1102
  }
984
1103
  }
985
- else if (child.type === 'assignment_pattern') {
1104
+ else if (isDestructuringDefaultPattern(child.type)) {
986
1105
  // { foo = defaultValue }
987
1106
  const leftNode = child.childForFieldName('left');
988
1107
  if (leftNode && leftNode.type === 'shorthand_property_identifier_pattern') {
@@ -1028,7 +1147,7 @@ function extractDestructuringDefs(node, startId) {
1028
1147
  });
1029
1148
  }
1030
1149
  }
1031
- else if (child.type === 'assignment_pattern') {
1150
+ else if (isDestructuringDefaultPattern(child.type)) {
1032
1151
  // [x = defaultValue]
1033
1152
  const leftNode = child.childForFieldName('left');
1034
1153
  if (leftNode && leftNode.type === 'identifier') {
@@ -1056,6 +1175,213 @@ function extractDestructuringDefs(node, startId) {
1056
1175
  function currentScope(scopeStack) {
1057
1176
  return scopeStack[scopeStack.length - 1];
1058
1177
  }
1178
+ /**
1179
+ * Java: an assignment inside a braced block to a variable declared outside it
1180
+ * was bound in the block's own scope, so it was dropped when the block ended
1181
+ * and later uses saw only the outer def (cognium-dev#596 / #597:
1182
+ * `String t = ""; if (a) { if (b) t = tainted; } exec(c + t)` had no flow).
1183
+ * The block def may or may not run, so it is kept as an *alternate* reaching
1184
+ * def of the enclosing scope, next to the outer one, not in place of it.
1185
+ */
1186
+ /** The scope the most recent `block` / `statement_block` case popped (read right after processing a block arm). */
1187
+ let lastPoppedBlock = null;
1188
+ /**
1189
+ * True when the `if` / loop that owns `arm` sits directly in a bare `{ … }` or
1190
+ * `synchronized` block. Only there is a braceless body given a scope of its own:
1191
+ * at method level a braceless `if (!ALLOWED.contains(c)) c = "name";` overwrites
1192
+ * the def on purpose (the allowlist idiom).
1193
+ */
1194
+ function inAlwaysRunBlock(arm) {
1195
+ let n = arm.parent;
1196
+ while (n && (n.type === 'if_statement' || n.type === 'for_statement' || n.type === 'enhanced_for_statement' ||
1197
+ n.type === 'while_statement' || n.type === 'do_statement'))
1198
+ n = n.parent;
1199
+ return !!n && n.type === 'block' && !!n.parent &&
1200
+ (n.parent.type === 'block' || n.parent.type === 'synchronized_statement');
1201
+ }
1202
+ /**
1203
+ * True when the `if` / loop that owns the braceless `arm` sits in a braced
1204
+ * block that is not a method body (a braced `if` / `else` / loop / `try` arm, or
1205
+ * a bare / `synchronized` block). That block shares one scope slot per name, so
1206
+ * a braceless body there must not assign into it: the write may not run, and it
1207
+ * would hide an earlier write in the same block.
1208
+ */
1209
+ /** Braceless `if` / `else` arm at statement level (parent has an alternative). */
1210
+ function isBracelessIfElseArm(arm) {
1211
+ const parent = arm.parent;
1212
+ if (!parent || parent.type !== 'if_statement')
1213
+ return false;
1214
+ return parent.childForFieldName('alternative') != null;
1215
+ }
1216
+ function inNestedBlock(arm) {
1217
+ let n = arm.parent;
1218
+ while (n && (n.type === 'if_statement' || n.type === 'for_statement' || n.type === 'enhanced_for_statement' ||
1219
+ n.type === 'while_statement' || n.type === 'do_statement'))
1220
+ n = n.parent;
1221
+ if (!n || n.type !== 'block' || !n.parent)
1222
+ return false;
1223
+ switch (n.parent.type) {
1224
+ case 'block':
1225
+ case 'synchronized_statement':
1226
+ case 'if_statement':
1227
+ case 'for_statement':
1228
+ case 'enhanced_for_statement':
1229
+ case 'while_statement':
1230
+ case 'do_statement':
1231
+ case 'try_statement':
1232
+ case 'try_with_resources_statement':
1233
+ case 'catch_clause':
1234
+ case 'finally_clause':
1235
+ case 'labeled_statement':
1236
+ return true;
1237
+ default:
1238
+ return false;
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Process the body of an `if` / `else` / loop. A braced body is a `block` and
1243
+ * pops its own scope. A braceless body (`if (c) t = x;`) is a single statement
1244
+ * that would otherwise assign in the enclosing scope as if it always ran, so for
1245
+ * Java inside an always-run block it gets a scope of its own and its defs are
1246
+ * carried out as alternates. Returns the scope the body assigned in (null
1247
+ * elsewhere), for `if` / `else` merging.
1248
+ */
1249
+ function processArm(node, defId, useId, scopeStack, isJavaScript) {
1250
+ const isBlock = node.type === 'block' || node.type === 'statement_block';
1251
+ const bracelessTopIfElse = !isBlock && isBracelessIfElseArm(node);
1252
+ if (isJavaScript || (isBlock ? !inAlwaysRunBlock(node) : !inNestedBlock(node) && !bracelessTopIfElse)) {
1253
+ lastPoppedBlock = null;
1254
+ const result = processStatement(node, defId, useId, scopeStack, isJavaScript);
1255
+ return { ...result, popped: null };
1256
+ }
1257
+ if (isBlock) {
1258
+ lastPoppedBlock = null;
1259
+ const result = processStatement(node, defId, useId, scopeStack, isJavaScript);
1260
+ return { ...result, popped: lastPoppedBlock };
1261
+ }
1262
+ scopeStack.push(new Map());
1263
+ const result = processStatement(node, defId, useId, scopeStack, isJavaScript);
1264
+ const popped = scopeStack.pop();
1265
+ carryBranchDefsOut(popped, scopeStack, false);
1266
+ return { ...result, popped };
1267
+ }
1268
+ /**
1269
+ * After an `if` / `else` whose two arms both assign `name` directly, every path
1270
+ * writes it: the arm defs replace the reaching def (and the alternates that
1271
+ * existed before the `if`) instead of sitting beside them.
1272
+ */
1273
+ function mergeIfElseDefs(armA, armB, before, scopeStack) {
1274
+ const top = scopeStack[scopeStack.length - 1];
1275
+ if (!top)
1276
+ return;
1277
+ const alts = alternateDefs.get(top);
1278
+ for (const [name, idA] of armA) {
1279
+ const idB = armB.get(name);
1280
+ if (idB === undefined)
1281
+ continue;
1282
+ if (declaredNames.get(armA)?.has(name) || declaredNames.get(armB)?.has(name))
1283
+ continue;
1284
+ if (!scopeStack.some((scope) => scope.has(name)))
1285
+ continue;
1286
+ const prior = new Set(before.get(name) ?? []);
1287
+ const arms = (alts?.get(name) ?? []).filter((id) => !prior.has(id) && id !== idB);
1288
+ if (!arms.includes(idA))
1289
+ arms.push(idA);
1290
+ top.set(name, idB);
1291
+ let map = alts;
1292
+ if (!map) {
1293
+ map = new Map();
1294
+ alternateDefs.set(top, map);
1295
+ }
1296
+ map.set(name, arms);
1297
+ }
1298
+ }
1299
+ const declaredNames = new WeakMap();
1300
+ const alternateDefs = new WeakMap();
1301
+ function markDeclared(scope, name) {
1302
+ let set = declaredNames.get(scope);
1303
+ if (!set) {
1304
+ set = new Set();
1305
+ declaredNames.set(scope, set);
1306
+ }
1307
+ set.add(name);
1308
+ alternateDefs.get(scope)?.delete(name);
1309
+ }
1310
+ function carryBranchDefsOut(popped, scopeStack, alwaysRuns = false, altsOnly = false) {
1311
+ const declared = declaredNames.get(popped);
1312
+ const poppedAlts = alternateDefs.get(popped);
1313
+ const top = scopeStack[scopeStack.length - 1];
1314
+ if (!top)
1315
+ return;
1316
+ if (alwaysRuns) {
1317
+ // Straight-line block: its defs run whenever it does, so they are as if
1318
+ // made directly in the enclosing scope. They become that scope's reaching
1319
+ // def and drop the alternates for the name; defs the block itself held as
1320
+ // alternates stay alternates. (A def under a braceless `if` / loop body
1321
+ // never lands in `popped`: that body has its own scope, see `processArm`.)
1322
+ for (const [name, id] of popped) {
1323
+ if (declared?.has(name))
1324
+ continue;
1325
+ if (!scopeStack.some((scope) => scope.has(name)))
1326
+ continue;
1327
+ top.set(name, id);
1328
+ let alts = alternateDefs.get(top);
1329
+ if (!alts) {
1330
+ alts = new Map();
1331
+ alternateDefs.set(top, alts);
1332
+ }
1333
+ alts.delete(name);
1334
+ const kept = poppedAlts?.get(name);
1335
+ if (kept && kept.length > 0)
1336
+ alts.set(name, [...kept]);
1337
+ }
1338
+ // Alternates for a name the block did not assign itself stay alternates.
1339
+ if (poppedAlts)
1340
+ for (const [name, ids] of poppedAlts)
1341
+ if (!popped.has(name))
1342
+ carry(name, ids);
1343
+ return;
1344
+ }
1345
+ // The alternate is parked on the immediate parent whenever some outer scope
1346
+ // declares the name, so a later assignment in that parent (or an enclosing
1347
+ // straight-line block) can still kill it; the parent hands it on when it ends.
1348
+ function carry(name, ids) {
1349
+ if (declared?.has(name))
1350
+ return;
1351
+ if (!scopeStack.some((scope) => scope.has(name)))
1352
+ return;
1353
+ let alts = alternateDefs.get(top);
1354
+ if (!alts) {
1355
+ alts = new Map();
1356
+ alternateDefs.set(top, alts);
1357
+ }
1358
+ const list = alts.get(name) ?? [];
1359
+ for (const id of ids)
1360
+ if (!list.includes(id))
1361
+ list.push(id);
1362
+ alts.set(name, list);
1363
+ }
1364
+ if (!altsOnly)
1365
+ for (const [name, id] of popped)
1366
+ carry(name, [id]);
1367
+ if (poppedAlts)
1368
+ for (const [name, ids] of poppedAlts)
1369
+ carry(name, ids);
1370
+ }
1371
+ /** Alternate reaching defs of `name`: those parked on every scope down to the one that binds it. */
1372
+ function findAlternateDefs(name, scopeStack) {
1373
+ const out = [];
1374
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
1375
+ const alts = alternateDefs.get(scopeStack[i])?.get(name);
1376
+ if (alts)
1377
+ for (const id of alts)
1378
+ if (!out.includes(id))
1379
+ out.push(id);
1380
+ if (scopeStack[i].has(name))
1381
+ break;
1382
+ }
1383
+ return out;
1384
+ }
1059
1385
  /**
1060
1386
  * Find the reaching definition for a variable.
1061
1387
  */