circle-ir 4.16.1 → 4.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/dist/analysis/advisories.d.ts +186 -0
  2. package/dist/analysis/advisories.d.ts.map +1 -0
  3. package/dist/analysis/advisories.js +391 -0
  4. package/dist/analysis/advisories.js.map +1 -0
  5. package/dist/analysis/comment-lines.d.ts +14 -0
  6. package/dist/analysis/comment-lines.d.ts.map +1 -0
  7. package/dist/analysis/comment-lines.js +34 -0
  8. package/dist/analysis/comment-lines.js.map +1 -0
  9. package/dist/analysis/config-loader.d.ts.map +1 -1
  10. package/dist/analysis/config-loader.js +59 -5
  11. package/dist/analysis/config-loader.js.map +1 -1
  12. package/dist/analysis/constant-caller-gate.d.ts +2 -2
  13. package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
  14. package/dist/analysis/constant-caller-gate.js +205 -29
  15. package/dist/analysis/constant-caller-gate.js.map +1 -1
  16. package/dist/analysis/constant-propagation/index.d.ts +2 -0
  17. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/index.js +10 -0
  19. package/dist/analysis/constant-propagation/index.js.map +1 -1
  20. package/dist/analysis/constant-propagation/propagator.d.ts +15 -0
  21. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  22. package/dist/analysis/constant-propagation/propagator.js +224 -20
  23. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  24. package/dist/analysis/constant-propagation/types.d.ts +2 -0
  25. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  26. package/dist/analysis/constant-propagation.d.ts +1 -1
  27. package/dist/analysis/constant-propagation.d.ts.map +1 -1
  28. package/dist/analysis/constant-propagation.js +1 -1
  29. package/dist/analysis/constant-propagation.js.map +1 -1
  30. package/dist/analysis/entry-point-detection.d.ts.map +1 -1
  31. package/dist/analysis/entry-point-detection.js +72 -0
  32. package/dist/analysis/entry-point-detection.js.map +1 -1
  33. package/dist/analysis/interprocedural.d.ts.map +1 -1
  34. package/dist/analysis/interprocedural.js +75 -8
  35. package/dist/analysis/interprocedural.js.map +1 -1
  36. package/dist/analysis/maven-declared.d.ts +72 -0
  37. package/dist/analysis/maven-declared.d.ts.map +1 -0
  38. package/dist/analysis/maven-declared.js +216 -0
  39. package/dist/analysis/maven-declared.js.map +1 -0
  40. package/dist/analysis/maven-version.d.ts +28 -0
  41. package/dist/analysis/maven-version.d.ts.map +1 -0
  42. package/dist/analysis/maven-version.js +172 -0
  43. package/dist/analysis/maven-version.js.map +1 -0
  44. package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
  45. package/dist/analysis/passes/constant-propagation-pass.js +29 -1
  46. package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
  47. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  48. package/dist/analysis/passes/cross-file-pass.js +84 -2
  49. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  50. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
  51. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  52. package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
  53. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  54. package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
  55. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
  56. package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
  57. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
  58. package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
  59. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/interprocedural-pass.js +562 -0
  61. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  62. package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
  63. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  64. package/dist/analysis/passes/language-sources-pass.js +1079 -14
  65. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  66. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  67. package/dist/analysis/passes/param-source-flow-gate-pass.js +82 -3
  68. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  69. package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
  70. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  71. package/dist/analysis/passes/sink-filter-pass.js +690 -12
  72. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  73. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  74. package/dist/analysis/passes/taint-propagation-pass.js +298 -19
  75. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  76. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
  77. package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
  78. package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
  79. package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
  80. package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
  81. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  82. package/dist/analysis/passes/weak-random-pass.js +29 -0
  83. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  84. package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
  85. package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
  86. package/dist/analysis/promote-cross-file-flows.js +32 -0
  87. package/dist/analysis/promote-cross-file-flows.js.map +1 -0
  88. package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
  89. package/dist/analysis/rust-literal-guard.js +100 -1
  90. package/dist/analysis/rust-literal-guard.js.map +1 -1
  91. package/dist/analysis/sbom.d.ts +100 -0
  92. package/dist/analysis/sbom.d.ts.map +1 -1
  93. package/dist/analysis/sbom.js +514 -3
  94. package/dist/analysis/sbom.js.map +1 -1
  95. package/dist/analysis/semver-version.d.ts +30 -0
  96. package/dist/analysis/semver-version.d.ts.map +1 -0
  97. package/dist/analysis/semver-version.js +76 -0
  98. package/dist/analysis/semver-version.js.map +1 -0
  99. package/dist/analysis/taint-matcher.d.ts +5 -0
  100. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  101. package/dist/analysis/taint-matcher.js +226 -4
  102. package/dist/analysis/taint-matcher.js.map +1 -1
  103. package/dist/analyzer.d.ts +57 -0
  104. package/dist/analyzer.d.ts.map +1 -1
  105. package/dist/analyzer.js +69 -6
  106. package/dist/analyzer.js.map +1 -1
  107. package/dist/browser/circle-ir.js +3360 -314
  108. package/dist/core/circle-ir-core.cjs +783 -220
  109. package/dist/core/circle-ir-core.js +783 -220
  110. package/dist/core/extractors/dfg.js +205 -2
  111. package/dist/core/extractors/dfg.js.map +1 -1
  112. package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
  113. package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
  114. package/dist/core/extractors/ts-declaration-detail.js +266 -0
  115. package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
  116. package/dist/core/extractors/types.js +53 -9
  117. package/dist/core/extractors/types.js.map +1 -1
  118. package/dist/graph/analysis-pass.d.ts +4 -1
  119. package/dist/graph/analysis-pass.d.ts.map +1 -1
  120. package/dist/graph/analysis-pass.js +4 -1
  121. package/dist/graph/analysis-pass.js.map +1 -1
  122. package/dist/graph-facts/dead-code.d.ts +62 -0
  123. package/dist/graph-facts/dead-code.d.ts.map +1 -0
  124. package/dist/graph-facts/dead-code.js +90 -0
  125. package/dist/graph-facts/dead-code.js.map +1 -0
  126. package/dist/graph-facts/dependency-calls.d.ts +86 -0
  127. package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
  128. package/dist/graph-facts/dependency-calls.js +93 -0
  129. package/dist/graph-facts/dependency-calls.js.map +1 -0
  130. package/dist/graph-facts/index.d.ts +10 -0
  131. package/dist/graph-facts/index.d.ts.map +1 -0
  132. package/dist/graph-facts/index.js +10 -0
  133. package/dist/graph-facts/index.js.map +1 -0
  134. package/dist/graph-facts/maven-packages.d.ts +42 -0
  135. package/dist/graph-facts/maven-packages.d.ts.map +1 -0
  136. package/dist/graph-facts/maven-packages.js +133 -0
  137. package/dist/graph-facts/maven-packages.js.map +1 -0
  138. package/dist/graph-facts/reach.d.ts +203 -0
  139. package/dist/graph-facts/reach.d.ts.map +1 -0
  140. package/dist/graph-facts/reach.js +219 -0
  141. package/dist/graph-facts/reach.js.map +1 -0
  142. package/dist/graph-facts/removal-reach.d.ts +73 -0
  143. package/dist/graph-facts/removal-reach.d.ts.map +1 -0
  144. package/dist/graph-facts/removal-reach.js +64 -0
  145. package/dist/graph-facts/removal-reach.js.map +1 -0
  146. package/dist/index.d.ts +21 -6
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.js +20 -3
  149. package/dist/index.js.map +1 -1
  150. package/dist/navigation/go-graph.d.ts +68 -0
  151. package/dist/navigation/go-graph.d.ts.map +1 -0
  152. package/dist/navigation/go-graph.js +753 -0
  153. package/dist/navigation/go-graph.js.map +1 -0
  154. package/dist/navigation/index.d.ts +7 -1
  155. package/dist/navigation/index.d.ts.map +1 -1
  156. package/dist/navigation/index.js +6 -0
  157. package/dist/navigation/index.js.map +1 -1
  158. package/dist/navigation/java-entry-tables.d.ts +43 -0
  159. package/dist/navigation/java-entry-tables.d.ts.map +1 -0
  160. package/dist/navigation/java-entry-tables.js +56 -0
  161. package/dist/navigation/java-entry-tables.js.map +1 -0
  162. package/dist/navigation/java-graph.d.ts +64 -0
  163. package/dist/navigation/java-graph.d.ts.map +1 -0
  164. package/dist/navigation/java-graph.js +466 -0
  165. package/dist/navigation/java-graph.js.map +1 -0
  166. package/dist/navigation/navigation-index.d.ts +91 -2
  167. package/dist/navigation/navigation-index.d.ts.map +1 -1
  168. package/dist/navigation/navigation-index.js +372 -45
  169. package/dist/navigation/navigation-index.js.map +1 -1
  170. package/dist/navigation/python-graph.d.ts +73 -0
  171. package/dist/navigation/python-graph.d.ts.map +1 -0
  172. package/dist/navigation/python-graph.js +867 -0
  173. package/dist/navigation/python-graph.js.map +1 -0
  174. package/dist/navigation/ts-call-edges.d.ts +193 -0
  175. package/dist/navigation/ts-call-edges.d.ts.map +1 -0
  176. package/dist/navigation/ts-call-edges.js +2089 -0
  177. package/dist/navigation/ts-call-edges.js.map +1 -0
  178. package/dist/navigation/ts-entry-tables.d.ts +88 -0
  179. package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
  180. package/dist/navigation/ts-entry-tables.js +50 -0
  181. package/dist/navigation/ts-entry-tables.js.map +1 -0
  182. package/dist/path-set/branches.d.ts +52 -0
  183. package/dist/path-set/branches.d.ts.map +1 -0
  184. package/dist/path-set/branches.js +124 -0
  185. package/dist/path-set/branches.js.map +1 -0
  186. package/dist/path-set/finding-diff.d.ts +63 -0
  187. package/dist/path-set/finding-diff.d.ts.map +1 -0
  188. package/dist/path-set/finding-diff.js +150 -0
  189. package/dist/path-set/finding-diff.js.map +1 -0
  190. package/dist/path-set/index.d.ts +18 -0
  191. package/dist/path-set/index.d.ts.map +1 -0
  192. package/dist/path-set/index.js +12 -0
  193. package/dist/path-set/index.js.map +1 -0
  194. package/dist/path-set/line-diff.d.ts +19 -0
  195. package/dist/path-set/line-diff.d.ts.map +1 -0
  196. package/dist/path-set/line-diff.js +98 -0
  197. package/dist/path-set/line-diff.js.map +1 -0
  198. package/dist/path-set/path-set.d.ts +88 -0
  199. package/dist/path-set/path-set.d.ts.map +1 -0
  200. package/dist/path-set/path-set.js +716 -0
  201. package/dist/path-set/path-set.js.map +1 -0
  202. package/dist/path-set/templates.d.ts +42 -0
  203. package/dist/path-set/templates.d.ts.map +1 -0
  204. package/dist/path-set/templates.js +119 -0
  205. package/dist/path-set/templates.js.map +1 -0
  206. package/dist/path-set/types.d.ts +195 -0
  207. package/dist/path-set/types.d.ts.map +1 -0
  208. package/dist/path-set/types.js +8 -0
  209. package/dist/path-set/types.js.map +1 -0
  210. package/dist/remedy-class/index.d.ts +12 -0
  211. package/dist/remedy-class/index.d.ts.map +1 -0
  212. package/dist/remedy-class/index.js +9 -0
  213. package/dist/remedy-class/index.js.map +1 -0
  214. package/dist/remedy-class/match.d.ts +165 -0
  215. package/dist/remedy-class/match.d.ts.map +1 -0
  216. package/dist/remedy-class/match.js +705 -0
  217. package/dist/remedy-class/match.js.map +1 -0
  218. package/dist/remedy-class/output-context.d.ts +25 -0
  219. package/dist/remedy-class/output-context.d.ts.map +1 -0
  220. package/dist/remedy-class/output-context.js +150 -0
  221. package/dist/remedy-class/output-context.js.map +1 -0
  222. package/dist/remedy-class/sha256.d.ts +18 -0
  223. package/dist/remedy-class/sha256.d.ts.map +1 -0
  224. package/dist/remedy-class/sha256.js +89 -0
  225. package/dist/remedy-class/sha256.js.map +1 -0
  226. package/dist/remedy-class/table.d.ts +130 -0
  227. package/dist/remedy-class/table.d.ts.map +1 -0
  228. package/dist/remedy-class/table.js +457 -0
  229. package/dist/remedy-class/table.js.map +1 -0
  230. package/dist/resolution/cross-file.d.ts +18 -0
  231. package/dist/resolution/cross-file.d.ts.map +1 -1
  232. package/dist/resolution/cross-file.js +194 -5
  233. package/dist/resolution/cross-file.js.map +1 -1
  234. package/dist/stable-ids/builder.d.ts +20 -0
  235. package/dist/stable-ids/builder.d.ts.map +1 -0
  236. package/dist/stable-ids/builder.js +65 -0
  237. package/dist/stable-ids/builder.js.map +1 -0
  238. package/dist/stable-ids/diff.d.ts +18 -0
  239. package/dist/stable-ids/diff.d.ts.map +1 -0
  240. package/dist/stable-ids/diff.js +37 -0
  241. package/dist/stable-ids/diff.js.map +1 -0
  242. package/dist/stable-ids/go.d.ts +28 -0
  243. package/dist/stable-ids/go.d.ts.map +1 -0
  244. package/dist/stable-ids/go.js +160 -0
  245. package/dist/stable-ids/go.js.map +1 -0
  246. package/dist/stable-ids/hash.d.ts +22 -0
  247. package/dist/stable-ids/hash.d.ts.map +1 -0
  248. package/dist/stable-ids/hash.js +167 -0
  249. package/dist/stable-ids/hash.js.map +1 -0
  250. package/dist/stable-ids/index.d.ts +30 -0
  251. package/dist/stable-ids/index.d.ts.map +1 -0
  252. package/dist/stable-ids/index.js +59 -0
  253. package/dist/stable-ids/index.js.map +1 -0
  254. package/dist/stable-ids/java.d.ts +28 -0
  255. package/dist/stable-ids/java.d.ts.map +1 -0
  256. package/dist/stable-ids/java.js +224 -0
  257. package/dist/stable-ids/java.js.map +1 -0
  258. package/dist/stable-ids/moves.d.ts +87 -0
  259. package/dist/stable-ids/moves.d.ts.map +1 -0
  260. package/dist/stable-ids/moves.js +222 -0
  261. package/dist/stable-ids/moves.js.map +1 -0
  262. package/dist/stable-ids/nodes.d.ts +32 -0
  263. package/dist/stable-ids/nodes.d.ts.map +1 -0
  264. package/dist/stable-ids/nodes.js +92 -0
  265. package/dist/stable-ids/nodes.js.map +1 -0
  266. package/dist/stable-ids/normalize.d.ts +35 -0
  267. package/dist/stable-ids/normalize.d.ts.map +1 -0
  268. package/dist/stable-ids/normalize.js +174 -0
  269. package/dist/stable-ids/normalize.js.map +1 -0
  270. package/dist/stable-ids/parse-errors.d.ts +24 -0
  271. package/dist/stable-ids/parse-errors.d.ts.map +1 -0
  272. package/dist/stable-ids/parse-errors.js +46 -0
  273. package/dist/stable-ids/parse-errors.js.map +1 -0
  274. package/dist/stable-ids/python.d.ts +27 -0
  275. package/dist/stable-ids/python.d.ts.map +1 -0
  276. package/dist/stable-ids/python.js +212 -0
  277. package/dist/stable-ids/python.js.map +1 -0
  278. package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
  279. package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
  280. package/dist/stable-ids/ts-parse-errors.js +75 -0
  281. package/dist/stable-ids/ts-parse-errors.js.map +1 -0
  282. package/dist/stable-ids/types.d.ts +106 -0
  283. package/dist/stable-ids/types.d.ts.map +1 -0
  284. package/dist/stable-ids/types.js +16 -0
  285. package/dist/stable-ids/types.js.map +1 -0
  286. package/dist/stable-ids/typescript.d.ts +30 -0
  287. package/dist/stable-ids/typescript.d.ts.map +1 -0
  288. package/dist/stable-ids/typescript.js +362 -0
  289. package/dist/stable-ids/typescript.js.map +1 -0
  290. package/dist/types/config.d.ts +9 -0
  291. package/dist/types/config.d.ts.map +1 -1
  292. package/dist/types/index.d.ts +53 -0
  293. package/dist/types/index.d.ts.map +1 -1
  294. package/dist/types/index.js.map +1 -1
  295. package/package.json +3 -3
@@ -0,0 +1,716 @@
1
+ /**
2
+ * Path sets of one weakness class, and their difference between two versions
3
+ * of a tree.
4
+ *
5
+ * `pathSetOf` collects every source-to-sink path whose sink carries the given
6
+ * CWE from an already analysed project (per-file `taint.flows` and project
7
+ * `taint_paths`) and gives each a **pin-independent key**: no line number
8
+ * takes part, so an edit that only shifts lines keeps every key. The key is
9
+ * built from the declaration that holds each end (a stable id where the
10
+ * caller supplies `ids@0.1` for the file, else a qualified name built from the
11
+ * IR), the call shape of the end, and its ordinal among same-shape ends in
12
+ * that declaration.
13
+ *
14
+ * `diffPathSets` compares two sets as a set operation and counts, per version,
15
+ * the unresolved edges and the analysis budgets hit, so an empty set on a
16
+ * partly analysed tree is never read as clean. Neither function judges a
17
+ * change: they report which paths exist where.
18
+ *
19
+ * Pure: IR in, values out. Browser and Node compatible.
20
+ */
21
+ import { DEFAULT_SINKS } from '../analysis/config-loader.js';
22
+ import { DominatorGraph } from '../graph/dominator-graph.js';
23
+ import { digest128 } from '../stable-ids/hash.js';
24
+ import { sideOf } from './branches.js';
25
+ import { PATH_KEY_SCHEME, } from './types.js';
26
+ /** `CWE-089`, `cwe-89` and `89` all normalise to `CWE-89`. */
27
+ export function normalizeCwe(cwe) {
28
+ const m = /^(?:cwe-)?0*(\d+)$/i.exec(String(cwe).trim());
29
+ if (!m)
30
+ throw new Error(`path-set: not a CWE id: ${JSON.stringify(cwe)}`);
31
+ return `CWE-${m[1]}`;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Declarations
35
+ // ---------------------------------------------------------------------------
36
+ /** `package.Type$Inner` — the qualified@0.1 name of a type. */
37
+ export function typePath(t) {
38
+ const name = t.enclosing_type ? `${t.enclosing_type}$${t.name}` : t.name;
39
+ return t.package ? `${t.package}.${name}` : name;
40
+ }
41
+ /** `package.Type$Inner.method(T1,T2)` — the qualified@0.1 name of a method. */
42
+ export function qualifiedMethodName(type, method) {
43
+ return `${typePath(type)}.${method.name}(${method.parameters.map(p => p.type ?? '?').join(',')})`;
44
+ }
45
+ function span(start, end) {
46
+ return end - start;
47
+ }
48
+ /** The declaration that holds `line` in `file`. */
49
+ export function declAt(file, ir, line, ids) {
50
+ if (ids) {
51
+ let best;
52
+ for (const d of ids.decls) {
53
+ if (d.kind !== 'function' && d.kind !== 'method')
54
+ continue;
55
+ if (line < d.start_line || line > d.end_line)
56
+ continue;
57
+ if (!best || span(d.start_line, d.end_line) < span(best.start_line, best.end_line))
58
+ best = d;
59
+ }
60
+ best ??= ids.decls[0];
61
+ if (best)
62
+ return { scheme: 'ids@0.1', id: best.id, name: best.qualified_name };
63
+ }
64
+ let type;
65
+ for (const t of ir.types) {
66
+ if (line < t.start_line || line > t.end_line)
67
+ continue;
68
+ if (!type || span(t.start_line, t.end_line) < span(type.start_line, type.end_line))
69
+ type = t;
70
+ }
71
+ let qualified;
72
+ if (type) {
73
+ const method = type.methods
74
+ .filter(m => line >= m.start_line && line <= m.end_line)
75
+ .sort((a, b) => span(a.start_line, a.end_line) - span(b.start_line, b.end_line))[0];
76
+ qualified = method ? qualifiedMethodName(type, method) : `${typePath(type)}.<init>`;
77
+ }
78
+ else {
79
+ // No type holds the line (top-level function in a language whose IR keeps
80
+ // functions outside types): fall back to the enclosing method name the
81
+ // call extractor recorded, else the module.
82
+ const inMethod = ir.calls.find(c => c.location.line === line && c.in_method)?.in_method;
83
+ qualified = inMethod ? `<module>.${inMethod}` : '<module>';
84
+ }
85
+ const id = `${file}#${qualified}`;
86
+ return { scheme: 'qualified@0.1', id, name: qualified };
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Anchors
90
+ // ---------------------------------------------------------------------------
91
+ /** `req.getParameter() in find` → `getParameter`. */
92
+ function callNameFromLocation(location) {
93
+ const head = location.split(' in ')[0].trim().replace(/\(\)$/, '');
94
+ const dot = head.lastIndexOf('.');
95
+ return dot >= 0 ? head.slice(dot + 1) : head;
96
+ }
97
+ function sourceShape(s) {
98
+ const what = s.method ?? (s.annotation ? `@${s.annotation}` : callNameFromLocation(s.location));
99
+ return `${s.type}:${what}`;
100
+ }
101
+ function sinkShape(s) {
102
+ return `${s.type}:${s.class ?? '?'}.${s.method ?? '?'}(${(s.argPositions ?? []).join(',')})`;
103
+ }
104
+ /** Ordinal of `item` among items of the same shape in the same declaration, by position. */
105
+ function ordinalOf(item, all, shape, declId, column) {
106
+ const myShape = shape(item);
107
+ const myDecl = declId(item);
108
+ const peers = all
109
+ .filter(x => shape(x) === myShape && declId(x) === myDecl)
110
+ .sort((a, b) => a.line - b.line || column(a) - column(b));
111
+ return peers.indexOf(item);
112
+ }
113
+ function sourceEnd(ctx, line, type) {
114
+ const src = ctx.ir.taint.sources.find(s => s.line === line && s.type === type);
115
+ if (!src)
116
+ return undefined;
117
+ const decl = declAt(ctx.file, ctx.ir, line, ctx.ids);
118
+ const declIdOf = (s) => declAt(ctx.file, ctx.ir, s.line, ctx.ids).id;
119
+ const ordinal = ordinalOf(src, ctx.ir.taint.sources, sourceShape, declIdOf, () => 0);
120
+ const end = {
121
+ anchor: `${decl.scheme}:${decl.id}|${sourceShape(src)}|#${ordinal}`,
122
+ decl,
123
+ file: ctx.file,
124
+ line,
125
+ type: src.type,
126
+ };
127
+ if (src.code !== undefined)
128
+ end.code = src.code;
129
+ return end;
130
+ }
131
+ /** Simple-name class of a call's receiver, or of a static receiver written as a type. */
132
+ function callClass(c) {
133
+ if (c.receiver_type)
134
+ return c.receiver_type.slice(c.receiver_type.lastIndexOf('.') + 1);
135
+ const r = c.receiver;
136
+ if (!r || r.includes('('))
137
+ return null;
138
+ const last = r.slice(r.lastIndexOf('.') + 1);
139
+ return /^[A-Z]/.test(last) ? last : null;
140
+ }
141
+ /**
142
+ * Ordinal of a sink call among the calls of the same shape (class, method) in
143
+ * its declaration, by position. Counted over calls, not over the engine's
144
+ * sink records, so a call the engine stops flagging at one version does not
145
+ * shift the ordinals of the others.
146
+ */
147
+ function callOrdinal(ctx, declId, cls, method, line, column) {
148
+ if (!method)
149
+ return null;
150
+ const same = (c) => c.method_name === method && (cls === undefined || callClass(c) === cls);
151
+ const peers = ctx.ir.calls
152
+ .filter(c => same(c) && declAt(ctx.file, ctx.ir, c.location.line, ctx.ids).id === declId)
153
+ .sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column);
154
+ const onLine = peers.filter(c => c.location.line === line);
155
+ if (onLine.length === 0)
156
+ return null;
157
+ const mine = column === undefined
158
+ ? onLine[0]
159
+ : onLine.reduce((best, c) => (Math.abs(c.location.column - column) < Math.abs(best.location.column - column) ? c : best));
160
+ return peers.indexOf(mine);
161
+ }
162
+ function sinkEnd(ctx, line, type) {
163
+ const candidates = ctx.ir.taint.sinks
164
+ .filter(s => s.line === line && s.type === type)
165
+ .sort((a, b) => (a.callColumn ?? 0) - (b.callColumn ?? 0));
166
+ const sink = candidates[0];
167
+ if (!sink)
168
+ return undefined;
169
+ const decl = declAt(ctx.file, ctx.ir, line, ctx.ids);
170
+ const declIdOf = (s) => declAt(ctx.file, ctx.ir, s.line, ctx.ids).id;
171
+ const ordinal = callOrdinal(ctx, decl.id, sink.class, sink.method, line, sink.callColumn)
172
+ ?? ordinalOf(sink, ctx.ir.taint.sinks, sinkShape, declIdOf, s => s.callColumn ?? 0);
173
+ const call = {};
174
+ if (sink.class !== undefined)
175
+ call.class = sink.class;
176
+ if (sink.method !== undefined)
177
+ call.method = sink.method;
178
+ if (sink.argPositions !== undefined)
179
+ call.argPositions = sink.argPositions;
180
+ const end = {
181
+ anchor: `${decl.scheme}:${decl.id}|${sinkShape(sink)}|#${ordinal}`,
182
+ decl,
183
+ file: ctx.file,
184
+ line,
185
+ type: sink.type,
186
+ cwe: normalizeCwe(sink.cwe),
187
+ call,
188
+ };
189
+ if (sink.code !== undefined)
190
+ end.code = sink.code;
191
+ return end;
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // Endpoints: a sink whose result feeds another sink of the class
195
+ // ---------------------------------------------------------------------------
196
+ const IDENT = /[A-Za-z_$][\w$]*/g;
197
+ const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
198
+ function identifiers(expr) {
199
+ return new Set((expr ?? '').replace(STRING_LITERAL, '""').match(IDENT) ?? []);
200
+ }
201
+ function operands(c) {
202
+ return [c.receiver ?? '', ...c.arguments.map(a => a.expression)].join(' ');
203
+ }
204
+ /** The engine's own registered sink this call has the shape of, for the CWE. */
205
+ function registeredSink(c, cwe, language) {
206
+ const cls = callClass(c);
207
+ return DEFAULT_SINKS.find(p => normalizeCwe(p.cwe) === cwe &&
208
+ p.method === c.method_name &&
209
+ (p.languages === undefined || p.languages.includes(language)) &&
210
+ !p.exclude_languages?.includes(language) &&
211
+ (p.class === undefined || p.class === cls || (cls === null && p.allow_unresolved_receiver === true)));
212
+ }
213
+ function sinkCallOf(ctx, sink) {
214
+ return ctx.ir.calls
215
+ .filter(c => c.location.line === sink.line && (!sink.call.method || c.method_name === sink.call.method))
216
+ .sort((a, b) => a.location.column - b.location.column)[0];
217
+ }
218
+ /** Def ids reached (via chains) from the defs written on the call's line. */
219
+ function reachedFrom(ctx, line) {
220
+ const next = new Map();
221
+ for (const ch of ctx.ir.dfg.chains ?? []) {
222
+ const l = next.get(ch.from_def);
223
+ if (l)
224
+ l.push(ch.to_def);
225
+ else
226
+ next.set(ch.from_def, [ch.to_def]);
227
+ }
228
+ const seen = new Set();
229
+ const stack = ctx.ir.dfg.defs.filter(d => d.line === line && d.kind !== 'param').map(d => d.id);
230
+ while (stack.length) {
231
+ const d = stack.pop();
232
+ if (seen.has(d))
233
+ continue;
234
+ seen.add(d);
235
+ stack.push(...(next.get(d) ?? []));
236
+ }
237
+ return seen;
238
+ }
239
+ /**
240
+ * Follow a flagged sink to the endpoint its result feeds: an enclosing call on
241
+ * the same line, or a later call in the same declaration that uses a value
242
+ * derived from it, when that call is a registered sink of the class
243
+ * (`Path.resolve(name)` → `Files.readAllBytes(real)`). Returns the sink as is
244
+ * when nothing further is reached.
245
+ */
246
+ function endpointOf(ctx, sink) {
247
+ const language = ctx.ir.meta.language;
248
+ let cur = sinkCallOf(ctx, sink);
249
+ if (!cur)
250
+ return sink;
251
+ const first = cur;
252
+ for (let hop = 0; hop < 5; hop++) {
253
+ const from = cur;
254
+ const declId = declAt(ctx.file, ctx.ir, from.location.line, ctx.ids).id;
255
+ const onLine = ctx.ir.calls.filter(c => c.location.line === from.location.line).filter(c => c !== from && registeredSink(c, sink.cwe, language));
256
+ const enclosing = onLine.find(c => operands(c).includes(`${from.method_name}(`));
257
+ let next = enclosing;
258
+ if (!next) {
259
+ const reached = reachedFrom(ctx, from.location.line);
260
+ const names = new Set(ctx.ir.dfg.defs.filter(d => reached.has(d.id)).map(d => d.variable));
261
+ next = ctx.ir.calls
262
+ .filter(c => c.location.line > from.location.line && registeredSink(c, sink.cwe, language))
263
+ .filter(c => declAt(ctx.file, ctx.ir, c.location.line, ctx.ids).id === declId)
264
+ .filter(c => {
265
+ const ids = identifiers(operands(c));
266
+ return ctx.ir.dfg.uses.some(u => u.line === c.location.line && u.def_id !== null && reached.has(u.def_id) && ids.has(u.variable))
267
+ || [...names].some(n => ids.has(n) && ctx.ir.dfg.uses.some(u => u.line === c.location.line && u.variable === n));
268
+ })
269
+ .sort((a, b) => a.location.line - b.location.line || a.location.column - b.location.column)[0];
270
+ }
271
+ if (!next)
272
+ break;
273
+ cur = next;
274
+ }
275
+ if (cur === first)
276
+ return sink;
277
+ const reg = registeredSink(cur, sink.cwe, language);
278
+ /* c8 ignore next */
279
+ if (!reg)
280
+ return sink;
281
+ const decl = declAt(ctx.file, ctx.ir, cur.location.line, ctx.ids);
282
+ const cls = reg.class ?? callClass(cur) ?? undefined;
283
+ const ordinal = callOrdinal(ctx, decl.id, reg.class, cur.method_name, cur.location.line, cur.location.column) ?? 0;
284
+ const shape = `${reg.type}:${cls ?? '?'}.${cur.method_name}(${reg.arg_positions.join(',')})`;
285
+ const endpoint = {
286
+ anchor: `${decl.scheme}:${decl.id}|${shape}|#${ordinal}`,
287
+ decl,
288
+ file: ctx.file,
289
+ line: cur.location.line,
290
+ type: reg.type,
291
+ cwe: sink.cwe,
292
+ call: { ...(cls !== undefined ? { class: cls } : {}), method: cur.method_name, argPositions: reg.arg_positions },
293
+ intermediate: { line: sink.line, call: sink.call, ...(sink.code !== undefined ? { code: sink.code } : {}) },
294
+ };
295
+ return endpoint;
296
+ }
297
+ /** Names a sink call hands on as string literals (`addAttribute("q", q)` → `q`). */
298
+ export function handedNames(c) {
299
+ if (!c)
300
+ return [];
301
+ return c.arguments.map(a => a.literal).filter((x) => typeof x === 'string' && /^[A-Za-z_][\w]*$/.test(x));
302
+ }
303
+ function templateUsesOf(ctx, sink, facts) {
304
+ if (facts.length === 0)
305
+ return [];
306
+ const names = handedNames(sinkCallOf(ctx, sink));
307
+ if (names.length === 0)
308
+ return [];
309
+ return facts
310
+ .filter(f => f.names.some(n => names.includes(n)))
311
+ .map(f => {
312
+ const u = { file: f.file, line: f.line, engine: f.engine, construct: f.construct, attributes: f.attributes, names: f.names.filter(n => names.includes(n)) };
313
+ if (f.element !== undefined)
314
+ u.element = f.element;
315
+ if (f.inAttribute !== undefined)
316
+ u.inAttribute = f.inAttribute;
317
+ return u;
318
+ })
319
+ .sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.construct.localeCompare(b.construct)));
320
+ }
321
+ // ---------------------------------------------------------------------------
322
+ // Guards: the endpoint runs on one side of a check of the path's own data
323
+ // ---------------------------------------------------------------------------
324
+ function methodRange(ir, line) {
325
+ let best;
326
+ for (const t of ir.types) {
327
+ for (const m of t.methods) {
328
+ if (line < m.start_line || line > m.end_line)
329
+ continue;
330
+ if (!best || m.end_line - m.start_line < best.end - best.start)
331
+ best = { start: m.start_line, end: m.end_line };
332
+ }
333
+ }
334
+ return best;
335
+ }
336
+ /** Does the CFG block holding line `a` dominate the block holding line `b` (same method)? */
337
+ export function lineDominates(ir, a, b) {
338
+ const range = methodRange(ir, b);
339
+ const blocks = ir.cfg.blocks.filter(x => !range || (x.start_line >= range.start && x.end_line <= range.end));
340
+ const holding = (line) => blocks.find(x => x.start_line <= line && line <= x.end_line);
341
+ const ba = holding(a);
342
+ const bb = holding(b);
343
+ if (!ba || !bb)
344
+ return false;
345
+ const entry = blocks.find(x => x.type === 'entry') ?? blocks.slice().sort((x, y) => x.id - y.id)[0];
346
+ return new DominatorGraph(ir.cfg, entry.id).dominates(ba.id, bb.id);
347
+ }
348
+ function guardOf(ctx, sink, pathVars) {
349
+ if (!ctx.branches)
350
+ return undefined;
351
+ const call = sinkCallOf(ctx, sink);
352
+ const vars = new Set([...pathVars, ...identifiers(call ? operands(call) : sink.code)]);
353
+ const declId = sink.decl.id;
354
+ for (const fact of ctx.branches) {
355
+ if (fact.line >= sink.line)
356
+ continue;
357
+ if (declAt(ctx.file, ctx.ir, fact.line, ctx.ids).id !== declId)
358
+ continue;
359
+ const side = sideOf(fact, sink.line);
360
+ if (side === 'both')
361
+ continue;
362
+ for (const bc of fact.calls) {
363
+ const ir = ctx.ir.calls.find(c => c.location.line === bc.line && c.method_name === bc.method);
364
+ if (!ir || ![...identifiers(operands(ir))].some(n => vars.has(n)))
365
+ continue;
366
+ if (!lineDominates(ctx.ir, fact.line, sink.line))
367
+ continue;
368
+ return { file: ctx.file, line: fact.line, method: bc.method, negated: bc.negated, sinkSide: side, callValue: side !== bc.negated };
369
+ }
370
+ }
371
+ return undefined;
372
+ }
373
+ /** The pin-independent key of a path. */
374
+ export function pathKey(cwe, sourceAnchor, sinkAnchor) {
375
+ return digest128([PATH_KEY_SCHEME, cwe, sourceAnchor, sinkAnchor]);
376
+ }
377
+ // ---------------------------------------------------------------------------
378
+ // Unresolved edges on a path
379
+ // ---------------------------------------------------------------------------
380
+ function methodSpan(ir, line) {
381
+ let best;
382
+ for (const t of ir.types) {
383
+ for (const m of t.methods) {
384
+ if (line < m.start_line || line > m.end_line)
385
+ continue;
386
+ if (!best || m.end_line - m.start_line < best.end - best.start)
387
+ best = { start: m.start_line, end: m.end_line };
388
+ }
389
+ }
390
+ return best;
391
+ }
392
+ /**
393
+ * The lines a path occupies, per file: source, sink (and intermediate sink)
394
+ * and hop lines, plus the lines in the source's and sink's declarations that
395
+ * use one of the path's variables.
396
+ */
397
+ function pathLines(ctxByFile, v) {
398
+ const out = new Map();
399
+ const add = (file, line) => {
400
+ let s = out.get(file);
401
+ if (!s)
402
+ out.set(file, (s = new Set()));
403
+ s.add(line);
404
+ };
405
+ add(v.source.file, v.source.line);
406
+ add(v.sink.file, v.sink.line);
407
+ if (v.sink.intermediate)
408
+ add(v.sink.file, v.sink.intermediate.line);
409
+ for (const h of v.hops)
410
+ add(h.file, h.line);
411
+ const vars = new Set(v.hops.map(h => h.variable).filter((x) => !!x));
412
+ for (const end of [v.source, v.sink]) {
413
+ const ctx = ctxByFile.get(end.file);
414
+ if (!ctx || vars.size === 0)
415
+ continue;
416
+ const span = methodSpan(ctx.ir, end.line);
417
+ if (!span)
418
+ continue;
419
+ for (const u of ctx.ir.dfg.uses) {
420
+ if (u.line >= span.start && u.line <= span.end && vars.has(u.variable))
421
+ add(end.file, u.line);
422
+ }
423
+ }
424
+ return out;
425
+ }
426
+ function unresolvedOn(ctxByFile, projectTypes, crossFile, lines, sink) {
427
+ const sites = [];
428
+ const inProject = (cands) => (cands ?? []).some(c => projectTypes.has(c.split('.').slice(0, -1).pop() ?? c));
429
+ for (const [file, set] of lines) {
430
+ const ctx = ctxByFile.get(file);
431
+ if (!ctx)
432
+ continue;
433
+ for (const u of ctx.ir.unresolved) {
434
+ if (!set.has(u.context.line))
435
+ continue;
436
+ // the unresolved call is the sink call itself: same line, and its candidate (or its text) names the sink's method
437
+ const methods = (u.context.candidates ?? []).map(c => c.split('.').pop() ?? c);
438
+ const atSink = file === sink.file && u.context.line === sink.line && !!sink.call.method &&
439
+ (methods.length > 0 ? methods.includes(sink.call.method) : u.context.code.includes(`${sink.call.method}(`));
440
+ sites.push({ file, line: u.context.line, type: u.type, reason: u.reason, code: u.context.code, atSink, inProject: inProject(u.context.candidates) });
441
+ }
442
+ }
443
+ for (const c of crossFile ?? []) {
444
+ if (c.resolved || !lines.get(c.from.file)?.has(c.from.line))
445
+ continue;
446
+ sites.push({
447
+ file: c.from.file, line: c.from.line, type: 'cross_file_call', reason: 'unresolved_cross_file_call',
448
+ code: `${c.from.method} -> ${c.to.file}#${c.to.method}`, atSink: false, inProject: true,
449
+ });
450
+ }
451
+ sites.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.type.localeCompare(b.type) || a.code.localeCompare(b.code)));
452
+ return { count: sites.length, sites };
453
+ }
454
+ const variantId = (v) => `${v.source.file}:${v.source.line}->${v.sink.file}:${v.sink.line}`;
455
+ /** Every path of class `cwe` in one analysed tree, keyed and sorted by key. */
456
+ export function pathSetOf(project, options) {
457
+ const cwe = normalizeCwe(options.cwe);
458
+ const ctxByFile = new Map();
459
+ for (const f of project.files) {
460
+ const ctx = { file: f.file, ir: f.analysis };
461
+ const ids = options.stableIds?.[f.file];
462
+ if (ids)
463
+ ctx.ids = ids;
464
+ const br = options.branches?.[f.file];
465
+ if (br)
466
+ ctx.branches = br;
467
+ ctxByFile.set(f.file, ctx);
468
+ }
469
+ const variants = [];
470
+ for (const ctx of ctxByFile.values()) {
471
+ for (const flow of ctx.ir.taint.flows ?? []) {
472
+ const sink = sinkEnd(ctx, flow.sink_line, flow.sink_type);
473
+ if (!sink || sink.cwe !== cwe)
474
+ continue;
475
+ const source = sourceEnd(ctx, flow.source_line, flow.source_type);
476
+ if (!source)
477
+ continue;
478
+ variants.push({
479
+ source,
480
+ sink,
481
+ sanitized: flow.sanitized,
482
+ hops: flow.path.map(s => ({ file: ctx.file, line: s.line, variable: s.variable })),
483
+ confidence: flow.confidence,
484
+ });
485
+ }
486
+ }
487
+ for (const p of project.taint_paths ?? []) {
488
+ if (!p.path_exists || normalizeCwe(p.sink.cwe) !== cwe)
489
+ continue;
490
+ const sctx = ctxByFile.get(p.source.file);
491
+ const kctx = ctxByFile.get(p.sink.file);
492
+ if (!sctx || !kctx)
493
+ continue;
494
+ const source = sourceEnd(sctx, p.source.line, p.source.type);
495
+ const sink = sinkEnd(kctx, p.sink.line, p.sink.type);
496
+ if (!source || !sink)
497
+ continue;
498
+ variants.push({
499
+ source,
500
+ sink,
501
+ sanitized: p.sanitizers_in_path.length > 0,
502
+ hops: p.hops.map(h => {
503
+ const hop = { file: h.file, line: h.line };
504
+ if (h.method)
505
+ hop.method = h.method;
506
+ if (h.variable)
507
+ hop.variable = h.variable;
508
+ return hop;
509
+ }),
510
+ confidence: p.confidence,
511
+ });
512
+ }
513
+ // Endpoints and guards (see endpointOf / guardOf), and template hand-offs.
514
+ const templateFacts = Object.values(options.templates ?? {}).flat();
515
+ for (const v of variants) {
516
+ const kctx = ctxByFile.get(v.sink.file);
517
+ v.sink = endpointOf(kctx, v.sink);
518
+ const uses = templateUsesOf(kctx, v.sink, templateFacts);
519
+ if (uses.length > 0)
520
+ v.sink = { ...v.sink, templates: uses };
521
+ if (v.sanitized)
522
+ continue;
523
+ const pathVars = new Set(v.hops.map(h => h.variable).filter((x) => !!x));
524
+ const g = guardOf(kctx, v.sink, pathVars);
525
+ if (g)
526
+ v.guard = g;
527
+ }
528
+ // Group by key; a variant seen both per-file and cross-file counts once.
529
+ const byKey = new Map();
530
+ for (const v of variants) {
531
+ const key = pathKey(cwe, v.source.anchor, v.sink.anchor);
532
+ let group = byKey.get(key);
533
+ if (!group)
534
+ byKey.set(key, (group = new Map()));
535
+ const id = variantId(v);
536
+ const list = group.get(id);
537
+ if (list)
538
+ list.push(v);
539
+ else
540
+ group.set(id, [v]);
541
+ }
542
+ const projectTypes = new Set(project.files.flatMap(f => f.analysis.types.map(t => t.name)));
543
+ const paths = [];
544
+ const files = new Set();
545
+ const schemes = new Set();
546
+ for (const [key, group] of byKey) {
547
+ const ids = [...group.keys()].sort();
548
+ const all = ids.flatMap(id => group.get(id));
549
+ const first = group.get(ids[0])[0];
550
+ // a variant is open if any of its observations is neither sanitized nor guarded
551
+ const open = ids.some(id => group.get(id).some(v => !v.sanitized && !v.guard));
552
+ const guard = all.find(v => !v.sanitized && v.guard)?.guard;
553
+ const state = open ? 'present' : guard ? 'guarded' : 'sanitized';
554
+ const entry = {
555
+ key,
556
+ cwe,
557
+ source: first.source,
558
+ sink: first.sink,
559
+ state,
560
+ variants: ids.length,
561
+ hops: first.hops,
562
+ confidence: Math.max(...all.map(v => v.confidence)),
563
+ unresolved: { count: 0, sites: [] },
564
+ };
565
+ if (state === 'guarded' && guard)
566
+ entry.guard = guard;
567
+ // unresolved edges over the lines of every variant of this path
568
+ const lines = new Map();
569
+ for (const v of all) {
570
+ for (const [f, set] of pathLines(ctxByFile, v)) {
571
+ const acc = lines.get(f);
572
+ if (acc)
573
+ for (const l of set)
574
+ acc.add(l);
575
+ else
576
+ lines.set(f, new Set(set));
577
+ }
578
+ }
579
+ entry.unresolved = unresolvedOn(ctxByFile, projectTypes, project.cross_file_calls, lines, first.sink);
580
+ paths.push(entry);
581
+ for (const v of all) {
582
+ files.add(v.source.file);
583
+ files.add(v.sink.file);
584
+ for (const h of v.hops)
585
+ files.add(h.file);
586
+ schemes.add(v.source.decl.scheme);
587
+ schemes.add(v.sink.decl.scheme);
588
+ }
589
+ }
590
+ paths.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
591
+ return {
592
+ scheme: PATH_KEY_SCHEME,
593
+ cwe,
594
+ paths,
595
+ declKeySchemes: [...schemes].sort(),
596
+ files: [...files].sort(),
597
+ };
598
+ }
599
+ // ---------------------------------------------------------------------------
600
+ // Two trees
601
+ // ---------------------------------------------------------------------------
602
+ /** The set change between two states. */
603
+ export function changeOf(before, after) {
604
+ if (before === 'present') {
605
+ if (after === 'absent')
606
+ return 'removed';
607
+ if (after === 'sanitized')
608
+ return 'sanitized';
609
+ if (after === 'guarded')
610
+ return 'guarded';
611
+ return 'unchanged';
612
+ }
613
+ return after === 'present' ? 'added' : 'other';
614
+ }
615
+ /** Unresolved edges, budget flags and parse health of one analysed tree. */
616
+ export function pinFacts(project, pathFiles, pathCount) {
617
+ const byType = {
618
+ virtual_dispatch: 0,
619
+ taint_propagation: 0,
620
+ reflection: 0,
621
+ dynamic_call: 0,
622
+ };
623
+ let total = 0;
624
+ let onPathFiles = 0;
625
+ const flags = [];
626
+ for (const f of project.files) {
627
+ for (const u of f.analysis.unresolved) {
628
+ total++;
629
+ byType[u.type]++;
630
+ if (pathFiles.has(f.file))
631
+ onPathFiles++;
632
+ }
633
+ const ps = f.analysis.parse_status;
634
+ if (ps && !ps.success && pathFiles.has(f.file))
635
+ flags.push(`parse-error:${f.file}`);
636
+ }
637
+ const crossFileCallsUnresolved = project.cross_file_calls.filter(c => !c.resolved && (pathFiles.has(c.from.file) || pathFiles.has(c.to.file))).length;
638
+ if (project.cross_file_budget_exceeded)
639
+ flags.push('cross_file_budget_exceeded');
640
+ if (project.per_file_budget_exceeded)
641
+ flags.push('per_file_budget_exceeded');
642
+ if (project.project_size_budget_exceeded)
643
+ flags.push('project_size_budget_exceeded');
644
+ flags.sort();
645
+ return {
646
+ pathCount,
647
+ unresolved: { total, byType, onPathFiles },
648
+ crossFileCallsUnresolved,
649
+ complete: flags.length === 0,
650
+ flags,
651
+ };
652
+ }
653
+ /** Compare two path sets of the same class. */
654
+ export function diffPathSets(before, after) {
655
+ if (before.cwe !== after.cwe)
656
+ throw new Error(`path-set: cannot diff ${before.cwe} against ${after.cwe}`);
657
+ const b = new Map(before.paths.map(p => [p.key, p]));
658
+ const a = new Map(after.paths.map(p => [p.key, p]));
659
+ const keys = [...new Set([...b.keys(), ...a.keys()])].sort();
660
+ const counts = { removed: 0, sanitized: 0, guarded: 0, unchanged: 0, added: 0, other: 0 };
661
+ const entries = keys.map(key => {
662
+ const pb = b.get(key);
663
+ const pa = a.get(key);
664
+ const state = { before: pb?.state ?? 'absent', after: pa?.state ?? 'absent' };
665
+ const change = changeOf(state.before, state.after);
666
+ counts[change]++;
667
+ const entry = { key, state, change };
668
+ if (pb)
669
+ entry.before = pb;
670
+ if (pa)
671
+ entry.after = pa;
672
+ // the view this path hands to rendered it differently at the two versions
673
+ if (pb?.sink.templates || pa?.sink.templates) {
674
+ const shape = (p) => JSON.stringify((p?.sink.templates ?? []).map(t => [t.file, t.engine, t.construct, t.attributes, t.inAttribute ?? null]));
675
+ if (shape(pb) !== shape(pa))
676
+ entry.templateChanged = true;
677
+ }
678
+ return entry;
679
+ });
680
+ return { entries, counts };
681
+ }
682
+ /**
683
+ * Every path of class `cwe` at two versions of a tree, the change per key,
684
+ * and per version the unresolved edges and budget flags. Path files are the
685
+ * union over both versions, so both pins are counted over the same region.
686
+ */
687
+ export function pathSetDiff(before, after, options) {
688
+ const at = (pin) => {
689
+ const o = { cwe: options.cwe };
690
+ const ids = options.stableIds?.[pin];
691
+ if (ids)
692
+ o.stableIds = ids;
693
+ const br = options.branches?.[pin];
694
+ if (br)
695
+ o.branches = br;
696
+ const tp = options.templates?.[pin];
697
+ if (tp)
698
+ o.templates = tp;
699
+ return o;
700
+ };
701
+ const setB = pathSetOf(before, at('before'));
702
+ const setA = pathSetOf(after, at('after'));
703
+ const files = new Set([...setB.files, ...setA.files]);
704
+ const { entries, counts } = diffPathSets(setB, setA);
705
+ return {
706
+ scheme: PATH_KEY_SCHEME,
707
+ cwe: setB.cwe,
708
+ entries,
709
+ counts,
710
+ pins: {
711
+ before: pinFacts(before, files, setB.paths.length),
712
+ after: pinFacts(after, files, setA.paths.length),
713
+ },
714
+ };
715
+ }
716
+ //# sourceMappingURL=path-set.js.map