circle-ir 3.8.3 → 3.9.7

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 (159) hide show
  1. package/README.md +82 -5
  2. package/dist/analysis/config-loader.js +1 -1
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/dfg-verifier.d.ts +3 -14
  5. package/dist/analysis/dfg-verifier.js +43 -74
  6. package/dist/analysis/dfg-verifier.js.map +1 -1
  7. package/dist/analysis/interprocedural.d.ts +5 -1
  8. package/dist/analysis/interprocedural.js +62 -60
  9. package/dist/analysis/interprocedural.js.map +1 -1
  10. package/dist/analysis/metrics/index.d.ts +2 -0
  11. package/dist/analysis/metrics/index.js +2 -0
  12. package/dist/analysis/metrics/index.js.map +1 -0
  13. package/dist/analysis/metrics/metric-pass.d.ts +27 -0
  14. package/dist/analysis/metrics/metric-pass.js +2 -0
  15. package/dist/analysis/metrics/metric-pass.js.map +1 -0
  16. package/dist/analysis/metrics/metric-runner.d.ts +21 -0
  17. package/dist/analysis/metrics/metric-runner.js +47 -0
  18. package/dist/analysis/metrics/metric-runner.js.map +1 -0
  19. package/dist/analysis/metrics/passes/cohesion-metrics-pass.d.ts +21 -0
  20. package/dist/analysis/metrics/passes/cohesion-metrics-pass.js +100 -0
  21. package/dist/analysis/metrics/passes/cohesion-metrics-pass.js.map +1 -0
  22. package/dist/analysis/metrics/passes/complexity-metrics-pass.d.ts +15 -0
  23. package/dist/analysis/metrics/passes/complexity-metrics-pass.js +76 -0
  24. package/dist/analysis/metrics/passes/complexity-metrics-pass.js.map +1 -0
  25. package/dist/analysis/metrics/passes/composite-metrics-pass.d.ts +17 -0
  26. package/dist/analysis/metrics/passes/composite-metrics-pass.js +77 -0
  27. package/dist/analysis/metrics/passes/composite-metrics-pass.js.map +1 -0
  28. package/dist/analysis/metrics/passes/coupling-metrics-pass.d.ts +19 -0
  29. package/dist/analysis/metrics/passes/coupling-metrics-pass.js +94 -0
  30. package/dist/analysis/metrics/passes/coupling-metrics-pass.js.map +1 -0
  31. package/dist/analysis/metrics/passes/data-flow-metrics-pass.d.ts +14 -0
  32. package/dist/analysis/metrics/passes/data-flow-metrics-pass.js +25 -0
  33. package/dist/analysis/metrics/passes/data-flow-metrics-pass.js.map +1 -0
  34. package/dist/analysis/metrics/passes/documentation-metrics-pass.d.ts +15 -0
  35. package/dist/analysis/metrics/passes/documentation-metrics-pass.js +64 -0
  36. package/dist/analysis/metrics/passes/documentation-metrics-pass.js.map +1 -0
  37. package/dist/analysis/metrics/passes/halstead-metrics-pass.d.ts +16 -0
  38. package/dist/analysis/metrics/passes/halstead-metrics-pass.js +95 -0
  39. package/dist/analysis/metrics/passes/halstead-metrics-pass.js.map +1 -0
  40. package/dist/analysis/metrics/passes/inheritance-metrics-pass.d.ts +18 -0
  41. package/dist/analysis/metrics/passes/inheritance-metrics-pass.js +73 -0
  42. package/dist/analysis/metrics/passes/inheritance-metrics-pass.js.map +1 -0
  43. package/dist/analysis/metrics/passes/size-metrics-pass.d.ts +11 -0
  44. package/dist/analysis/metrics/passes/size-metrics-pass.js +64 -0
  45. package/dist/analysis/metrics/passes/size-metrics-pass.js.map +1 -0
  46. package/dist/analysis/passes/circular-dependency-pass.d.ts +18 -0
  47. package/dist/analysis/passes/circular-dependency-pass.js +39 -0
  48. package/dist/analysis/passes/circular-dependency-pass.js.map +1 -0
  49. package/dist/analysis/passes/constant-propagation-pass.d.ts +22 -0
  50. package/dist/analysis/passes/constant-propagation-pass.js +44 -0
  51. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -0
  52. package/dist/analysis/passes/cross-file-pass.d.ts +27 -0
  53. package/dist/analysis/passes/cross-file-pass.js +102 -0
  54. package/dist/analysis/passes/cross-file-pass.js.map +1 -0
  55. package/dist/analysis/passes/dead-code-pass.d.ts +25 -0
  56. package/dist/analysis/passes/dead-code-pass.js +117 -0
  57. package/dist/analysis/passes/dead-code-pass.js.map +1 -0
  58. package/dist/analysis/passes/dependency-fan-out-pass.d.ts +19 -0
  59. package/dist/analysis/passes/dependency-fan-out-pass.js +35 -0
  60. package/dist/analysis/passes/dependency-fan-out-pass.js.map +1 -0
  61. package/dist/analysis/passes/interprocedural-pass.d.ts +29 -0
  62. package/dist/analysis/passes/interprocedural-pass.js +169 -0
  63. package/dist/analysis/passes/interprocedural-pass.js.map +1 -0
  64. package/dist/analysis/passes/language-sources-pass.d.ts +76 -0
  65. package/dist/analysis/passes/language-sources-pass.js +491 -0
  66. package/dist/analysis/passes/language-sources-pass.js.map +1 -0
  67. package/dist/analysis/passes/leaked-global-pass.d.ts +34 -0
  68. package/dist/analysis/passes/leaked-global-pass.js +108 -0
  69. package/dist/analysis/passes/leaked-global-pass.js.map +1 -0
  70. package/dist/analysis/passes/missing-await-pass.d.ts +29 -0
  71. package/dist/analysis/passes/missing-await-pass.js +90 -0
  72. package/dist/analysis/passes/missing-await-pass.js.map +1 -0
  73. package/dist/analysis/passes/missing-public-doc-pass.d.ts +35 -0
  74. package/dist/analysis/passes/missing-public-doc-pass.js +148 -0
  75. package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -0
  76. package/dist/analysis/passes/n-plus-one-pass.d.ts +29 -0
  77. package/dist/analysis/passes/n-plus-one-pass.js +100 -0
  78. package/dist/analysis/passes/n-plus-one-pass.js.map +1 -0
  79. package/dist/analysis/passes/null-deref-pass.d.ts +32 -0
  80. package/dist/analysis/passes/null-deref-pass.js +130 -0
  81. package/dist/analysis/passes/null-deref-pass.js.map +1 -0
  82. package/dist/analysis/passes/orphan-module-pass.d.ts +21 -0
  83. package/dist/analysis/passes/orphan-module-pass.js +38 -0
  84. package/dist/analysis/passes/orphan-module-pass.js.map +1 -0
  85. package/dist/analysis/passes/resource-leak-pass.d.ts +43 -0
  86. package/dist/analysis/passes/resource-leak-pass.js +156 -0
  87. package/dist/analysis/passes/resource-leak-pass.js.map +1 -0
  88. package/dist/analysis/passes/sink-filter-pass.d.ts +39 -0
  89. package/dist/analysis/passes/sink-filter-pass.js +231 -0
  90. package/dist/analysis/passes/sink-filter-pass.js.map +1 -0
  91. package/dist/analysis/passes/stale-doc-ref-pass.d.ts +21 -0
  92. package/dist/analysis/passes/stale-doc-ref-pass.js +96 -0
  93. package/dist/analysis/passes/stale-doc-ref-pass.js.map +1 -0
  94. package/dist/analysis/passes/string-concat-loop-pass.d.ts +26 -0
  95. package/dist/analysis/passes/string-concat-loop-pass.js +87 -0
  96. package/dist/analysis/passes/string-concat-loop-pass.js.map +1 -0
  97. package/dist/analysis/passes/sync-io-async-pass.d.ts +28 -0
  98. package/dist/analysis/passes/sync-io-async-pass.js +80 -0
  99. package/dist/analysis/passes/sync-io-async-pass.js.map +1 -0
  100. package/dist/analysis/passes/taint-matcher-pass.d.ts +24 -0
  101. package/dist/analysis/passes/taint-matcher-pass.js +71 -0
  102. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -0
  103. package/dist/analysis/passes/taint-propagation-pass.d.ts +22 -0
  104. package/dist/analysis/passes/taint-propagation-pass.js +266 -0
  105. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -0
  106. package/dist/analysis/passes/todo-in-prod-pass.d.ts +28 -0
  107. package/dist/analysis/passes/todo-in-prod-pass.js +71 -0
  108. package/dist/analysis/passes/todo-in-prod-pass.js.map +1 -0
  109. package/dist/analysis/passes/unchecked-return-pass.d.ts +34 -0
  110. package/dist/analysis/passes/unchecked-return-pass.js +106 -0
  111. package/dist/analysis/passes/unchecked-return-pass.js.map +1 -0
  112. package/dist/analysis/passes/unused-variable-pass.d.ts +36 -0
  113. package/dist/analysis/passes/unused-variable-pass.js +150 -0
  114. package/dist/analysis/passes/unused-variable-pass.js.map +1 -0
  115. package/dist/analysis/passes/variable-shadowing-pass.d.ts +41 -0
  116. package/dist/analysis/passes/variable-shadowing-pass.js +211 -0
  117. package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -0
  118. package/dist/analysis/path-finder.d.ts +3 -13
  119. package/dist/analysis/path-finder.js +48 -63
  120. package/dist/analysis/path-finder.js.map +1 -1
  121. package/dist/analysis/taint-matcher.js +8 -1
  122. package/dist/analysis/taint-matcher.js.map +1 -1
  123. package/dist/analysis/taint-propagation.d.ts +5 -1
  124. package/dist/analysis/taint-propagation.js +44 -41
  125. package/dist/analysis/taint-propagation.js.map +1 -1
  126. package/dist/analyzer.d.ts +42 -1
  127. package/dist/analyzer.js +234 -1476
  128. package/dist/analyzer.js.map +1 -1
  129. package/dist/browser/circle-ir.js +3414 -1272
  130. package/dist/core/circle-ir-core.cjs +361 -107
  131. package/dist/core/circle-ir-core.js +361 -107
  132. package/dist/core/extractors/imports.js +18 -0
  133. package/dist/core/extractors/imports.js.map +1 -1
  134. package/dist/graph/analysis-pass.d.ts +68 -0
  135. package/dist/graph/analysis-pass.js +51 -0
  136. package/dist/graph/analysis-pass.js.map +1 -0
  137. package/dist/graph/code-graph.d.ts +92 -0
  138. package/dist/graph/code-graph.js +262 -0
  139. package/dist/graph/code-graph.js.map +1 -0
  140. package/dist/graph/import-graph.d.ts +33 -0
  141. package/dist/graph/import-graph.js +170 -0
  142. package/dist/graph/import-graph.js.map +1 -0
  143. package/dist/graph/index.d.ts +4 -0
  144. package/dist/graph/index.js +5 -0
  145. package/dist/graph/index.js.map +1 -0
  146. package/dist/graph/project-graph.d.ts +43 -0
  147. package/dist/graph/project-graph.js +80 -0
  148. package/dist/graph/project-graph.js.map +1 -0
  149. package/dist/graph/scope-graph.d.ts +63 -0
  150. package/dist/graph/scope-graph.js +89 -0
  151. package/dist/graph/scope-graph.js.map +1 -0
  152. package/dist/index.d.ts +2 -2
  153. package/dist/index.js +1 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/resolution/cross-file.js +52 -19
  156. package/dist/resolution/cross-file.js.map +1 -1
  157. package/dist/types/index.d.ts +151 -0
  158. package/docs/SPEC.md +10 -6
  159. package/package.json +3 -2
@@ -6800,6 +6800,20 @@ function extractJavaScriptImports(tree) {
6800
6800
  const importInfos = extractJSImportInfo(importStmt);
6801
6801
  imports.push(...importInfos);
6802
6802
  }
6803
+ const exportStatements = findNodes(tree.rootNode, "export_statement");
6804
+ for (const exportStmt of exportStatements) {
6805
+ const sourceNode = exportStmt.childForFieldName("source");
6806
+ if (!sourceNode) continue;
6807
+ const fromPackage = getNodeText(sourceNode).replace(/['"]/g, "");
6808
+ if (!fromPackage) continue;
6809
+ imports.push({
6810
+ imported_name: "*",
6811
+ from_package: fromPackage,
6812
+ alias: null,
6813
+ is_wildcard: true,
6814
+ line_number: exportStmt.startPosition.row + 1
6815
+ });
6816
+ }
6803
6817
  const requireCalls = findRequireCalls(tree);
6804
6818
  imports.push(...requireCalls);
6805
6819
  return imports;
@@ -9566,7 +9580,7 @@ var DEFAULT_SINKS = [
9566
9580
  // Jenkins/CI Pipeline execution
9567
9581
  { method: "executeScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9568
9582
  { method: "runScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9569
- { method: "evaluate", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9583
+ { method: "evaluate", class: "ScriptEngine", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
9570
9584
  { method: "execute", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
9571
9585
  { method: "run", class: "Script", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [] },
9572
9586
  { method: "checkout", class: "SCM", type: "code_injection", cwe: "CWE-94", severity: "high", arg_positions: [0] },
@@ -10526,7 +10540,10 @@ function matchesSourcePattern(call, pattern) {
10526
10540
  return false;
10527
10541
  }
10528
10542
  if (pattern.class && pattern.class !== "constructor") {
10529
- if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
10543
+ if (!call.receiver) {
10544
+ return false;
10545
+ }
10546
+ if (!receiverMightBeClass(call.receiver, pattern.class)) {
10530
10547
  return false;
10531
10548
  }
10532
10549
  }
@@ -10885,38 +10902,296 @@ function formatSanitizerMethod(call) {
10885
10902
  return `${call.method_name}()`;
10886
10903
  }
10887
10904
 
10888
- // src/analysis/taint-propagation.ts
10889
- function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10890
- const taintedVars = [];
10891
- const flows = [];
10892
- const reachableSinks = /* @__PURE__ */ new Map();
10893
- const defById = /* @__PURE__ */ new Map();
10894
- const defsByLine = /* @__PURE__ */ new Map();
10895
- const usesByLine = /* @__PURE__ */ new Map();
10896
- const callsByLine = /* @__PURE__ */ new Map();
10897
- const sanitizersByLine = /* @__PURE__ */ new Map();
10898
- for (const def of dfg.defs) {
10899
- defById.set(def.id, def);
10900
- const existing = defsByLine.get(def.line) ?? [];
10901
- existing.push(def);
10902
- defsByLine.set(def.line, existing);
10905
+ // src/graph/code-graph.ts
10906
+ var CodeGraph = class {
10907
+ ir;
10908
+ constructor(ir) {
10909
+ this.ir = ir;
10910
+ }
10911
+ // ---------------------------------------------------------------------------
10912
+ // DFG indexes
10913
+ // ---------------------------------------------------------------------------
10914
+ _defById = null;
10915
+ get defById() {
10916
+ if (!this._defById) {
10917
+ this._defById = /* @__PURE__ */ new Map();
10918
+ for (const def of this.ir.dfg.defs) {
10919
+ this._defById.set(def.id, def);
10920
+ }
10921
+ }
10922
+ return this._defById;
10923
+ }
10924
+ _defsByLine = null;
10925
+ get defsByLine() {
10926
+ if (!this._defsByLine) {
10927
+ this._defsByLine = /* @__PURE__ */ new Map();
10928
+ for (const def of this.ir.dfg.defs) {
10929
+ const arr = this._defsByLine.get(def.line) ?? [];
10930
+ arr.push(def);
10931
+ this._defsByLine.set(def.line, arr);
10932
+ }
10933
+ }
10934
+ return this._defsByLine;
10935
+ }
10936
+ _defsByVar = null;
10937
+ get defsByVar() {
10938
+ if (!this._defsByVar) {
10939
+ this._defsByVar = /* @__PURE__ */ new Map();
10940
+ for (const def of this.ir.dfg.defs) {
10941
+ const arr = this._defsByVar.get(def.variable) ?? [];
10942
+ arr.push(def);
10943
+ this._defsByVar.set(def.variable, arr);
10944
+ }
10945
+ }
10946
+ return this._defsByVar;
10947
+ }
10948
+ _usesByLine = null;
10949
+ get usesByLine() {
10950
+ if (!this._usesByLine) {
10951
+ this._usesByLine = /* @__PURE__ */ new Map();
10952
+ for (const use of this.ir.dfg.uses) {
10953
+ const arr = this._usesByLine.get(use.line) ?? [];
10954
+ arr.push(use);
10955
+ this._usesByLine.set(use.line, arr);
10956
+ }
10957
+ }
10958
+ return this._usesByLine;
10959
+ }
10960
+ _usesByDefId = null;
10961
+ get usesByDefId() {
10962
+ if (!this._usesByDefId) {
10963
+ this._usesByDefId = /* @__PURE__ */ new Map();
10964
+ for (const use of this.ir.dfg.uses) {
10965
+ if (use.def_id !== null) {
10966
+ const arr = this._usesByDefId.get(use.def_id) ?? [];
10967
+ arr.push(use);
10968
+ this._usesByDefId.set(use.def_id, arr);
10969
+ }
10970
+ }
10971
+ }
10972
+ return this._usesByDefId;
10973
+ }
10974
+ _chainsByFromDef = null;
10975
+ get chainsByFromDef() {
10976
+ if (!this._chainsByFromDef) {
10977
+ this._chainsByFromDef = /* @__PURE__ */ new Map();
10978
+ for (const chain of this.ir.dfg.chains ?? []) {
10979
+ const arr = this._chainsByFromDef.get(chain.from_def) ?? [];
10980
+ arr.push(chain);
10981
+ this._chainsByFromDef.set(chain.from_def, arr);
10982
+ }
10983
+ }
10984
+ return this._chainsByFromDef;
10985
+ }
10986
+ // ---------------------------------------------------------------------------
10987
+ // Call indexes
10988
+ // ---------------------------------------------------------------------------
10989
+ _callsByLine = null;
10990
+ get callsByLine() {
10991
+ if (!this._callsByLine) {
10992
+ this._callsByLine = /* @__PURE__ */ new Map();
10993
+ for (const call of this.ir.calls) {
10994
+ const arr = this._callsByLine.get(call.location.line) ?? [];
10995
+ arr.push(call);
10996
+ this._callsByLine.set(call.location.line, arr);
10997
+ }
10998
+ }
10999
+ return this._callsByLine;
11000
+ }
11001
+ _callsByMethod = null;
11002
+ get callsByMethod() {
11003
+ if (!this._callsByMethod) {
11004
+ this._callsByMethod = /* @__PURE__ */ new Map();
11005
+ for (const call of this.ir.calls) {
11006
+ const arr = this._callsByMethod.get(call.method_name) ?? [];
11007
+ arr.push(call);
11008
+ this._callsByMethod.set(call.method_name, arr);
11009
+ }
11010
+ }
11011
+ return this._callsByMethod;
10903
11012
  }
10904
- for (const use of dfg.uses) {
10905
- const existing = usesByLine.get(use.line) ?? [];
10906
- existing.push(use);
10907
- usesByLine.set(use.line, existing);
11013
+ // ---------------------------------------------------------------------------
11014
+ // Type / method indexes
11015
+ // ---------------------------------------------------------------------------
11016
+ _methodsByName = null;
11017
+ get methodsByName() {
11018
+ if (!this._methodsByName) {
11019
+ this._methodsByName = /* @__PURE__ */ new Map();
11020
+ for (const type of this.ir.types) {
11021
+ for (const method of type.methods) {
11022
+ const arr = this._methodsByName.get(method.name) ?? [];
11023
+ arr.push({ type, method });
11024
+ this._methodsByName.set(method.name, arr);
11025
+ }
11026
+ }
11027
+ }
11028
+ return this._methodsByName;
10908
11029
  }
10909
- for (const call of calls) {
10910
- const existing = callsByLine.get(call.location.line) ?? [];
10911
- existing.push(call);
10912
- callsByLine.set(call.location.line, existing);
11030
+ /**
11031
+ * Returns the TypeInfo + MethodInfo whose line range contains `line`, or null.
11032
+ * Used by passes that need the enclosing method context for a given line.
11033
+ */
11034
+ methodAtLine(line) {
11035
+ for (const type of this.ir.types) {
11036
+ for (const method of type.methods) {
11037
+ if (line >= method.start_line && line <= method.end_line) {
11038
+ return { type, method };
11039
+ }
11040
+ }
11041
+ }
11042
+ return null;
11043
+ }
11044
+ // ---------------------------------------------------------------------------
11045
+ // Taint indexes
11046
+ // ---------------------------------------------------------------------------
11047
+ _sanitizersByLine = null;
11048
+ get sanitizersByLine() {
11049
+ if (!this._sanitizersByLine) {
11050
+ this._sanitizersByLine = /* @__PURE__ */ new Map();
11051
+ for (const san of this.ir.taint.sanitizers ?? []) {
11052
+ const arr = this._sanitizersByLine.get(san.line) ?? [];
11053
+ arr.push(san);
11054
+ this._sanitizersByLine.set(san.line, arr);
11055
+ }
11056
+ }
11057
+ return this._sanitizersByLine;
11058
+ }
11059
+ // ---------------------------------------------------------------------------
11060
+ // Query primitives
11061
+ // ---------------------------------------------------------------------------
11062
+ /** All DFGDefs at a given line. Returns [] if none. */
11063
+ defsAtLine(line) {
11064
+ return this.defsByLine.get(line) ?? [];
11065
+ }
11066
+ /** All DFGUses at a given line. Returns [] if none. */
11067
+ usesAtLine(line) {
11068
+ return this.usesByLine.get(line) ?? [];
11069
+ }
11070
+ /** All DFGUses that reach a specific definition ID. Returns [] if none. */
11071
+ usesOfDef(defId) {
11072
+ return this.usesByDefId.get(defId) ?? [];
10913
11073
  }
10914
- for (const san of sanitizers) {
10915
- const existing = sanitizersByLine.get(san.line) ?? [];
10916
- existing.push(san);
10917
- sanitizersByLine.set(san.line, existing);
11074
+ /** All CallInfos at a given line. Returns [] if none. */
11075
+ callsAtLine(line) {
11076
+ return this.callsByLine.get(line) ?? [];
11077
+ }
11078
+ /** DFGChains outgoing from a definition ID. Returns [] if none. */
11079
+ chainsFrom(defId) {
11080
+ return this.chainsByFromDef.get(defId) ?? [];
11081
+ }
11082
+ /**
11083
+ * All definitions of `variable` that appear strictly after `afterLine`
11084
+ * and at or before `upToLine`. Used to detect whether a variable is
11085
+ * redefined between a taint source and a sink.
11086
+ */
11087
+ laterDefsOfVar(variable, afterLine, upToLine) {
11088
+ return (this.defsByVar.get(variable) ?? []).filter(
11089
+ (d) => d.line > afterLine && d.line <= upToLine
11090
+ );
10918
11091
  }
10919
- const rawInitialTaint = findInitialTaint(sources, dfg, callsByLine, defsByLine);
11092
+ // ---------------------------------------------------------------------------
11093
+ // CFG indexes
11094
+ // ---------------------------------------------------------------------------
11095
+ _blockById = null;
11096
+ get blockById() {
11097
+ if (!this._blockById) {
11098
+ this._blockById = /* @__PURE__ */ new Map();
11099
+ for (const block of this.ir.cfg.blocks) {
11100
+ this._blockById.set(block.id, block);
11101
+ }
11102
+ }
11103
+ return this._blockById;
11104
+ }
11105
+ /**
11106
+ * Returns the line range of each detected loop body in the file.
11107
+ *
11108
+ * A loop is identified by CFG back-edges (type = "back"). For each back edge
11109
+ * `A → B`, B is the loop header and A is the last block before the back-jump.
11110
+ * The loop body spans from `header.start_line` to `A.end_line` (inclusive).
11111
+ *
11112
+ * Returns `{ start_line, end_line }` — one entry per back-edge.
11113
+ * Overlapping ranges are returned separately; callers can merge as needed.
11114
+ *
11115
+ * Usage: check whether line L is inside any loop with
11116
+ * `graph.loopBodies().some(r => L >= r.start_line && L <= r.end_line)`
11117
+ */
11118
+ loopBodies() {
11119
+ const loops = [];
11120
+ for (const edge of this.ir.cfg.edges) {
11121
+ if (edge.type !== "back") continue;
11122
+ const header = this.blockById.get(edge.to);
11123
+ const tail = this.blockById.get(edge.from);
11124
+ if (header && tail) {
11125
+ loops.push({ start_line: header.start_line, end_line: tail.end_line });
11126
+ }
11127
+ }
11128
+ return loops;
11129
+ }
11130
+ /**
11131
+ * Propagate a set of tainted definition IDs through DFGChains to a fixpoint.
11132
+ *
11133
+ * Returns a new Set (does not mutate the input). Each chain edge
11134
+ * `from_def → to_def` spreads taint: if `from_def` is tainted, `to_def`
11135
+ * becomes tainted. Iterates until no new IDs are added.
11136
+ */
11137
+ propagateTaintedDefIds(seed) {
11138
+ const result = new Set(seed);
11139
+ let changed = true;
11140
+ while (changed) {
11141
+ changed = false;
11142
+ for (const [fromDef, chains] of this.chainsByFromDef) {
11143
+ if (!result.has(fromDef)) continue;
11144
+ for (const chain of chains) {
11145
+ if (!result.has(chain.to_def)) {
11146
+ result.add(chain.to_def);
11147
+ changed = true;
11148
+ }
11149
+ }
11150
+ }
11151
+ }
11152
+ return result;
11153
+ }
11154
+ };
11155
+
11156
+ // src/analysis/taint-propagation.ts
11157
+ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
11158
+ let graph;
11159
+ let sources;
11160
+ let sinks;
11161
+ let sanitizers;
11162
+ if (graphOrDfg instanceof CodeGraph) {
11163
+ graph = graphOrDfg;
11164
+ sources = callsOrSources;
11165
+ sinks = sourcesOrSinks;
11166
+ sanitizers = sinksOrSanitizers;
11167
+ } else {
11168
+ const dfg = graphOrDfg;
11169
+ const calls = callsOrSources;
11170
+ sources = sourcesOrSinks;
11171
+ sinks = sinksOrSanitizers;
11172
+ sanitizers = sanitizersArg ?? [];
11173
+ graph = new CodeGraph({
11174
+ meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
11175
+ types: [],
11176
+ calls,
11177
+ cfg: { blocks: [], edges: [] },
11178
+ dfg,
11179
+ taint: { sources: [], sinks: [], sanitizers },
11180
+ imports: [],
11181
+ exports: [],
11182
+ unresolved: [],
11183
+ enriched: {}
11184
+ });
11185
+ }
11186
+ const taintedVars = [];
11187
+ const flows = [];
11188
+ const reachableSinks = /* @__PURE__ */ new Map();
11189
+ const defsByLine = graph.defsByLine;
11190
+ const usesByLine = graph.usesByLine;
11191
+ const callsByLine = graph.callsByLine;
11192
+ const sanitizersByLine = graph.sanitizersByLine;
11193
+ const defById = graph.defById;
11194
+ const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
10920
11195
  const initialTaint = rawInitialTaint.filter((tv) => {
10921
11196
  if (tv.line === tv.sourceLine) return true;
10922
11197
  const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
@@ -10925,7 +11200,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10925
11200
  taintedVars.push(...initialTaint);
10926
11201
  const propagatedTaint = propagateThroughChains(
10927
11202
  initialTaint,
10928
- dfg.chains ?? [],
11203
+ graph.chainsByFromDef,
10929
11204
  defById,
10930
11205
  sanitizersByLine
10931
11206
  );
@@ -10959,9 +11234,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10959
11234
  const flow = buildTaintFlow(
10960
11235
  source,
10961
11236
  sink,
10962
- taintInfo,
10963
- dfg,
10964
- defById
11237
+ taintInfo
10965
11238
  );
10966
11239
  flows.push(flow);
10967
11240
  const existingSources = reachableSinks.get(sink) ?? [];
@@ -10981,7 +11254,7 @@ function propagateTaint(dfg, calls, sources, sinks, sanitizers) {
10981
11254
  }
10982
11255
  return { taintedVars, flows, reachableSinks };
10983
11256
  }
10984
- function findInitialTaint(sources, dfg, callsByLine, defsByLine) {
11257
+ function findInitialTaint(sources, callsByLine, defsByLine) {
10985
11258
  const tainted = [];
10986
11259
  for (const source of sources) {
10987
11260
  const defsOnLine = defsByLine.get(source.line) ?? [];
@@ -11013,19 +11286,13 @@ function findInitialTaint(sources, dfg, callsByLine, defsByLine) {
11013
11286
  }
11014
11287
  return tainted;
11015
11288
  }
11016
- function propagateThroughChains(initialTaint, chains, defById, sanitizersByLine) {
11289
+ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
11017
11290
  const propagated = [];
11018
11291
  const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
11019
11292
  const taintInfoByDefId = /* @__PURE__ */ new Map();
11020
11293
  for (const t of initialTaint) {
11021
11294
  taintInfoByDefId.set(t.defId, t);
11022
11295
  }
11023
- const chainsByFromDef = /* @__PURE__ */ new Map();
11024
- for (const chain of chains) {
11025
- const existing = chainsByFromDef.get(chain.from_def) ?? [];
11026
- existing.push(chain);
11027
- chainsByFromDef.set(chain.from_def, existing);
11028
- }
11029
11296
  const queue = [...initialTaint.map((t) => t.defId)];
11030
11297
  const visited = new Set(queue);
11031
11298
  while (queue.length > 0) {
@@ -11095,7 +11362,7 @@ function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
11095
11362
  }
11096
11363
  return { sanitized: false };
11097
11364
  }
11098
- function buildTaintFlow(source, sink, taintInfo, dfg, defById) {
11365
+ function buildTaintFlow(source, sink, taintInfo) {
11099
11366
  const path = [];
11100
11367
  path.push({
11101
11368
  variable: taintInfo.variable,
@@ -13472,65 +13739,54 @@ function normalizeCondition(cond) {
13472
13739
 
13473
13740
  // src/analysis/path-finder.ts
13474
13741
  var PathFinder = class {
13475
- dfg;
13476
- calls;
13742
+ graph;
13477
13743
  sources;
13478
13744
  sinks;
13479
13745
  sanitizers;
13480
13746
  config;
13481
- // Lookup maps
13482
- defById = /* @__PURE__ */ new Map();
13483
- defsByLine = /* @__PURE__ */ new Map();
13484
- defsByVar = /* @__PURE__ */ new Map();
13485
- usesByLine = /* @__PURE__ */ new Map();
13486
- usesByDefId = /* @__PURE__ */ new Map();
13487
- callsByLine = /* @__PURE__ */ new Map();
13488
- sanitizerLines = /* @__PURE__ */ new Set();
13489
- constructor(dfg, calls, sources, sinks, sanitizers, config = {}) {
13490
- this.dfg = dfg;
13491
- this.calls = calls;
13492
- this.sources = sources;
13493
- this.sinks = sinks;
13494
- this.sanitizers = sanitizers;
13495
- this.config = {
13496
- maxPathLength: config.maxPathLength ?? 50,
13497
- maxPathsPerSink: config.maxPathsPerSink ?? 10,
13498
- includeCode: config.includeCode ?? false,
13499
- sourceLines: config.sourceLines ?? []
13500
- };
13501
- this.buildLookupMaps();
13502
- }
13503
- /**
13504
- * Build all lookup maps for efficient querying
13505
- */
13506
- buildLookupMaps() {
13507
- for (const def of this.dfg.defs) {
13508
- this.defById.set(def.id, def);
13509
- const byLine = this.defsByLine.get(def.line) ?? [];
13510
- byLine.push(def);
13511
- this.defsByLine.set(def.line, byLine);
13512
- const byVar = this.defsByVar.get(def.variable) ?? [];
13513
- byVar.push(def);
13514
- this.defsByVar.set(def.variable, byVar);
13515
- }
13516
- for (const use of this.dfg.uses) {
13517
- const byLine = this.usesByLine.get(use.line) ?? [];
13518
- byLine.push(use);
13519
- this.usesByLine.set(use.line, byLine);
13520
- if (use.def_id !== null) {
13521
- const byDefId = this.usesByDefId.get(use.def_id) ?? [];
13522
- byDefId.push(use);
13523
- this.usesByDefId.set(use.def_id, byDefId);
13524
- }
13525
- }
13526
- for (const call of this.calls) {
13527
- const byLine = this.callsByLine.get(call.location.line) ?? [];
13528
- byLine.push(call);
13529
- this.callsByLine.set(call.location.line, byLine);
13530
- }
13531
- for (const sanitizer of this.sanitizers) {
13532
- this.sanitizerLines.add(sanitizer.line);
13747
+ sanitizerLines;
13748
+ constructor(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersOrConfig, config = {}) {
13749
+ if (graphOrDfg instanceof CodeGraph) {
13750
+ this.graph = graphOrDfg;
13751
+ this.sources = callsOrSources;
13752
+ this.sinks = sourcesOrSinks;
13753
+ this.sanitizers = sinksOrSanitizers;
13754
+ const cfg = sanitizersOrConfig;
13755
+ this.config = {
13756
+ maxPathLength: cfg?.maxPathLength ?? 50,
13757
+ maxPathsPerSink: cfg?.maxPathsPerSink ?? 10,
13758
+ includeCode: cfg?.includeCode ?? false,
13759
+ sourceLines: cfg?.sourceLines ?? []
13760
+ };
13761
+ } else {
13762
+ const dfg = graphOrDfg;
13763
+ const calls = callsOrSources;
13764
+ const sources = sourcesOrSinks;
13765
+ const sinks = sinksOrSanitizers;
13766
+ const sanitizers = sanitizersOrConfig ?? [];
13767
+ this.graph = new CodeGraph({
13768
+ meta: { circle_ir: "3.0", file: "", language: "java", loc: 0, hash: "" },
13769
+ types: [],
13770
+ calls,
13771
+ cfg: { blocks: [], edges: [] },
13772
+ dfg,
13773
+ taint: { sources: [], sinks: [], sanitizers },
13774
+ imports: [],
13775
+ exports: [],
13776
+ unresolved: [],
13777
+ enriched: {}
13778
+ });
13779
+ this.sources = sources;
13780
+ this.sinks = sinks;
13781
+ this.sanitizers = sanitizers;
13782
+ this.config = {
13783
+ maxPathLength: config.maxPathLength ?? 50,
13784
+ maxPathsPerSink: config.maxPathsPerSink ?? 10,
13785
+ includeCode: config.includeCode ?? false,
13786
+ sourceLines: config.sourceLines ?? []
13787
+ };
13533
13788
  }
13789
+ this.sanitizerLines = new Set(this.sanitizers.map((s) => s.line));
13534
13790
  }
13535
13791
  /**
13536
13792
  * Find all taint paths from sources to sinks
@@ -13539,7 +13795,7 @@ var PathFinder = class {
13539
13795
  const paths = [];
13540
13796
  let pathId = 1;
13541
13797
  for (const source of this.sources) {
13542
- const sourceDefs = this.defsByLine.get(source.line) ?? [];
13798
+ const sourceDefs = this.graph.defsAtLine(source.line);
13543
13799
  for (const sourceDef of sourceDefs) {
13544
13800
  const pathsFromSource = this.findPathsFromSource(source, sourceDef, pathId);
13545
13801
  paths.push(...pathsFromSource);
@@ -13595,7 +13851,7 @@ var PathFinder = class {
13595
13851
  description: `Flows into ${sink.type} sink`,
13596
13852
  code: this.getCodeAtLine(sink.line)
13597
13853
  };
13598
- const call = this.callsByLine.get(sink.line)?.[0];
13854
+ const call = this.graph.callsAtLine(sink.line)[0];
13599
13855
  paths.push({
13600
13856
  id: `path-${startPathId + paths.length}`,
13601
13857
  source: {
@@ -13619,7 +13875,7 @@ var PathFinder = class {
13619
13875
  pathsPerSink.set(sink.line, sinkCount + 1);
13620
13876
  }
13621
13877
  }
13622
- const uses = this.usesByDefId.get(state.currentDef.id) ?? [];
13878
+ const uses = this.graph.usesOfDef(state.currentDef.id);
13623
13879
  for (const use of uses) {
13624
13880
  let sanitizer = state.sanitizer;
13625
13881
  if (this.sanitizerLines.has(use.line) && !sanitizer) {
@@ -13628,7 +13884,7 @@ var PathFinder = class {
13628
13884
  sanitizer = { line: san.line, method: san.method };
13629
13885
  }
13630
13886
  }
13631
- const nextDefs = this.defsByLine.get(use.line) ?? [];
13887
+ const nextDefs = this.graph.defsAtLine(use.line);
13632
13888
  for (const nextDef of nextDefs) {
13633
13889
  if (state.visited.has(nextDef.id)) continue;
13634
13890
  const hop = this.createHop(state.currentDef, nextDef, use);
@@ -13641,7 +13897,7 @@ var PathFinder = class {
13641
13897
  sanitizer
13642
13898
  });
13643
13899
  }
13644
- const laterDefs = (this.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
13900
+ const laterDefs = (this.graph.defsByVar.get(use.variable) ?? []).filter((d) => d.line > use.line && !state.visited.has(d.id));
13645
13901
  for (const laterDef of laterDefs.slice(0, 3)) {
13646
13902
  const hop = {
13647
13903
  line: laterDef.line,
@@ -13667,14 +13923,12 @@ var PathFinder = class {
13667
13923
  * Check if a definition reaches a sink
13668
13924
  */
13669
13925
  reachesSink(def, sink) {
13670
- const uses = this.usesByLine.get(sink.line) ?? [];
13671
- for (const use of uses) {
13926
+ for (const use of this.graph.usesAtLine(sink.line)) {
13672
13927
  if (use.variable === def.variable || use.def_id === def.id) {
13673
13928
  return true;
13674
13929
  }
13675
13930
  }
13676
- const calls = this.callsByLine.get(sink.line) ?? [];
13677
- for (const call of calls) {
13931
+ for (const call of this.graph.callsAtLine(sink.line)) {
13678
13932
  for (const arg of call.arguments) {
13679
13933
  if (arg.variable === def.variable) {
13680
13934
  return true;
@@ -13687,7 +13941,7 @@ var PathFinder = class {
13687
13941
  * Create a hop description between two definitions
13688
13942
  */
13689
13943
  createHop(fromDef, toDef, use) {
13690
- const call = this.callsByLine.get(toDef.line)?.[0];
13944
+ const call = this.graph.callsAtLine(toDef.line)[0];
13691
13945
  let operation = "assign";
13692
13946
  let description = `Assigned to ${toDef.variable}`;
13693
13947
  if (call) {
@@ -80,6 +80,24 @@ function extractJavaScriptImports(tree) {
80
80
  const importInfos = extractJSImportInfo(importStmt);
81
81
  imports.push(...importInfos);
82
82
  }
83
+ // Find re-export statements: `export { X } from './file'`
84
+ // These create an implicit import dependency that must be tracked by ImportGraph.
85
+ const exportStatements = findNodes(tree.rootNode, 'export_statement');
86
+ for (const exportStmt of exportStatements) {
87
+ const sourceNode = exportStmt.childForFieldName('source');
88
+ if (!sourceNode)
89
+ continue; // no `from '...'` clause — not a re-export
90
+ const fromPackage = getNodeText(sourceNode).replace(/['"]/g, '');
91
+ if (!fromPackage)
92
+ continue;
93
+ imports.push({
94
+ imported_name: '*',
95
+ from_package: fromPackage,
96
+ alias: null,
97
+ is_wildcard: true,
98
+ line_number: exportStmt.startPosition.row + 1,
99
+ });
100
+ }
83
101
  // Find CommonJS require calls
84
102
  const requireCalls = findRequireCalls(tree);
85
103
  imports.push(...requireCalls);