@clear-capabilities/agentic-security-scanner 0.74.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 (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,413 @@
1
+ // Unified IR — JS/TS frontend.
2
+ //
3
+ // Walks the Babel AST of one file and emits a structured per-function
4
+ // representation that the dataflow engine consumes. The shape is deliberately
5
+ // minimal: every statement is a node with a kind, every function has a CFG
6
+ // of those nodes, and every call/assignment is exposed as a first-class fact
7
+ // rather than buried in AST shape.
8
+ //
9
+ // Output:
10
+ // {
11
+ // file: '<rel-path>',
12
+ // functions: [{
13
+ // qid: '<file>::<scope>::<name>',
14
+ // name: '<name>',
15
+ // line: <decl line>,
16
+ // params: [<name>...],
17
+ // cfg: { entry: <nodeId>, exit: <nodeId>, nodes: Map<nodeId, Node> },
18
+ // returns:[<nodeId>...],
19
+ // calls: [{site: <nodeId>, callee: '<name>', args: [<exprId>...], line}],
20
+ // reads: Map<varname, [<nodeId>...]>,
21
+ // writes: Map<varname, [{node, source: <exprId>}]>,
22
+ // }],
23
+ // topLevel: <fn-id for module-scope code>,
24
+ // }
25
+ //
26
+ // Node kinds:
27
+ // 'assign' { target: '<lhs-path>', source: <exprDesc> }
28
+ // 'call' { callee: '<callee-path>', args: [<exprDesc>...] }
29
+ // 'return' { value: <exprDesc> | null }
30
+ // 'if' { cond: <exprDesc>, then: <nodeId>, else: <nodeId> | null }
31
+ // 'noop' (used as join points)
32
+ //
33
+ // exprDesc is a small JSON value:
34
+ // { kind: 'ident', name }
35
+ // { kind: 'member', object: <exprDesc>, prop }
36
+ // { kind: 'literal', value }
37
+ // { kind: 'call', callee: <exprDesc>, args: [<exprDesc>...] }
38
+ // { kind: 'binary', op, left, right }
39
+ // { kind: 'logical', op, left, right }
40
+ // { kind: 'tpl' } // template literal — treated as a string concat
41
+ // { kind: 'unknown' }
42
+
43
+ import { transformSync as babelTransformSync } from '@babel/core';
44
+ import presetReact from '@babel/preset-react';
45
+ import presetTypescript from '@babel/preset-typescript';
46
+
47
+ let _nodeIdSeq = 0;
48
+ function nextNodeId() { return 'n' + (++_nodeIdSeq); }
49
+
50
+ // Compact a Babel AST node into our exprDesc.
51
+ function exprOf(n) {
52
+ if (!n) return { kind: 'unknown' };
53
+ switch (n.type) {
54
+ case 'Identifier': return { kind: 'ident', name: n.name };
55
+ case 'NumericLiteral':
56
+ case 'StringLiteral':
57
+ case 'BooleanLiteral':
58
+ case 'NullLiteral': return { kind: 'literal', value: n.value !== undefined ? n.value : null };
59
+ case 'TemplateLiteral': return { kind: 'tpl', parts: (n.expressions || []).map(exprOf) };
60
+ case 'MemberExpression': return {
61
+ kind: 'member',
62
+ object: exprOf(n.object),
63
+ prop: n.computed ? (n.property?.value != null ? String(n.property.value) : '*') : (n.property?.name || '*'),
64
+ };
65
+ case 'CallExpression':
66
+ case 'OptionalCallExpression':
67
+ case 'NewExpression': return {
68
+ kind: 'call',
69
+ callee: exprOf(n.callee),
70
+ args: (n.arguments || []).map(exprOf),
71
+ };
72
+ case 'BinaryExpression': return { kind: 'binary', op: n.operator, left: exprOf(n.left), right: exprOf(n.right) };
73
+ case 'LogicalExpression': return { kind: 'logical', op: n.operator, left: exprOf(n.left), right: exprOf(n.right) };
74
+ case 'AssignmentExpression': return { kind: 'assign-expr', target: lhsPath(n.left), source: exprOf(n.right) };
75
+ case 'AwaitExpression': return exprOf(n.argument);
76
+ case 'YieldExpression': return exprOf(n.argument);
77
+ case 'ConditionalExpression':
78
+ // Treat as union of consequent + alternate — both may be tainted.
79
+ return { kind: 'union', branches: [exprOf(n.consequent), exprOf(n.alternate)] };
80
+ case 'ObjectExpression': return {
81
+ kind: 'object',
82
+ props: (n.properties || []).filter(p => p.type === 'ObjectProperty' && p.key).map(p => ({
83
+ key: p.key.name || (p.key.value != null ? String(p.key.value) : '*'),
84
+ value: exprOf(p.value),
85
+ })),
86
+ };
87
+ case 'ArrayExpression': return { kind: 'array', elements: (n.elements || []).map(exprOf) };
88
+ case 'SpreadElement': return exprOf(n.argument);
89
+ default: return { kind: 'unknown' };
90
+ }
91
+ }
92
+
93
+ // Reduce a Babel LHS node to a string path used as a dataflow variable key.
94
+ function lhsPath(n) {
95
+ if (!n) return null;
96
+ if (n.type === 'Identifier') return n.name;
97
+ if (n.type === 'MemberExpression') {
98
+ const base = lhsPath(n.object);
99
+ const prop = n.computed ? '*' : (n.property?.name || '*');
100
+ if (!base) return null;
101
+ return base + '.' + prop;
102
+ }
103
+ if (n.type === 'ObjectPattern') {
104
+ // Destructured: return an array of (key, alias) pairs the caller can iterate.
105
+ return { kind: 'object-pattern', props: (n.properties || []).map(p => ({
106
+ key: p.key?.name || (p.key?.value != null ? String(p.key.value) : '*'),
107
+ alias: lhsPath(p.value),
108
+ }))};
109
+ }
110
+ if (n.type === 'ArrayPattern') {
111
+ return { kind: 'array-pattern', elements: (n.elements || []).map(lhsPath) };
112
+ }
113
+ if (n.type === 'AssignmentPattern') return lhsPath(n.left);
114
+ if (n.type === 'RestElement') return lhsPath(n.argument);
115
+ return null;
116
+ }
117
+
118
+ function fnQid(file, scopeName, name, line) {
119
+ // The qualified ID is stable across re-runs of the parser as long as the file
120
+ // path + function scope/name + line don't change.
121
+ return `${file}::${scopeName || 'top'}::${name || 'anon'}@${line}`;
122
+ }
123
+
124
+ // ── Main entry ──────────────────────────────────────────────────────────────
125
+ export function parseJsFile(file, code) {
126
+ if (!/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(file)) return null;
127
+ if (!code || code.length > 500_000) return null;
128
+ const functions = [];
129
+
130
+ // Scope stack: each entry is a function being built.
131
+ const stack = [];
132
+ const enterFn = (name, scopeName, node, params) => {
133
+ const line = node.loc?.start?.line || 1;
134
+ const qid = fnQid(file, scopeName, name, line);
135
+ const entryId = nextNodeId();
136
+ const exitId = nextNodeId();
137
+ const fn = {
138
+ qid, name: name || 'anon', line,
139
+ params: (params || []).map(p => {
140
+ if (!p) return null;
141
+ if (p.type === 'Identifier') return { name: p.name, kind: 'ident' };
142
+ if (p.type === 'ObjectPattern') return { name: '<obj>', kind: 'object-pattern',
143
+ props: p.properties.map(pp => ({
144
+ key: pp.key?.name || (pp.key?.value != null ? String(pp.key.value) : '*'),
145
+ alias: lhsPath(pp.value),
146
+ })) };
147
+ if (p.type === 'AssignmentPattern' && p.left?.type === 'Identifier') return { name: p.left.name, kind: 'ident' };
148
+ if (p.type === 'RestElement' && p.argument?.type === 'Identifier') return { name: p.argument.name, kind: 'rest' };
149
+ return null;
150
+ }).filter(Boolean),
151
+ cfg: { entry: entryId, exit: exitId, nodes: new Map() },
152
+ returns: [],
153
+ calls: [],
154
+ reads: new Map(),
155
+ writes: new Map(),
156
+ file,
157
+ _cursor: entryId, // current node ID — next addNode() links from here
158
+ };
159
+ fn.cfg.nodes.set(entryId, { id: entryId, kind: 'entry', succ: [], pred: [], line });
160
+ fn.cfg.nodes.set(exitId, { id: exitId, kind: 'exit', succ: [], pred: [], line });
161
+ stack.push(fn);
162
+ return fn;
163
+ };
164
+ const exitFn = () => {
165
+ const fn = stack.pop();
166
+ if (!fn) return null;
167
+ // Connect cursor → exit.
168
+ linkCfg(fn, fn._cursor, fn.cfg.exit);
169
+ delete fn._cursor;
170
+ functions.push(fn);
171
+ return fn;
172
+ };
173
+
174
+ const currentFn = () => stack[stack.length - 1];
175
+ const linkCfg = (fn, from, to) => {
176
+ if (!from || !to || from === to) return;
177
+ const f = fn.cfg.nodes.get(from); const t = fn.cfg.nodes.get(to);
178
+ if (!f || !t) return;
179
+ if (!f.succ.includes(to)) f.succ.push(to);
180
+ if (!t.pred.includes(from)) t.pred.push(from);
181
+ };
182
+ const addNode = (fn, node) => {
183
+ if (!fn) return null;
184
+ fn.cfg.nodes.set(node.id, node);
185
+ linkCfg(fn, fn._cursor, node.id);
186
+ fn._cursor = node.id;
187
+ return node.id;
188
+ };
189
+
190
+ const recordWrite = (fn, target, source, nodeId) => {
191
+ if (!target || typeof target !== 'string') return;
192
+ if (!fn.writes.has(target)) fn.writes.set(target, []);
193
+ fn.writes.get(target).push({ node: nodeId, source });
194
+ };
195
+ const recordRead = (fn, name, nodeId) => {
196
+ if (!name) return;
197
+ if (!fn.reads.has(name)) fn.reads.set(name, []);
198
+ fn.reads.get(name).push(nodeId);
199
+ };
200
+
201
+ // Visitor — Babel plugin shape.
202
+ const plugin = function () {
203
+ return {
204
+ visitor: {
205
+ Program: {
206
+ enter(path) { enterFn('<module>', '', path.node, []); },
207
+ exit() { exitFn(); },
208
+ },
209
+ FunctionDeclaration: {
210
+ enter(path) {
211
+ const parentName = stack[stack.length - 1]?.name || '';
212
+ enterFn(path.node.id?.name || 'anon', parentName, path.node, path.node.params || []);
213
+ },
214
+ exit() { exitFn(); },
215
+ },
216
+ FunctionExpression: {
217
+ enter(path) {
218
+ const parent = path.parent;
219
+ let name = path.node.id?.name;
220
+ if (!name && parent?.type === 'VariableDeclarator' && parent.id?.type === 'Identifier') name = parent.id.name;
221
+ if (!name && parent?.type === 'AssignmentExpression' && parent.left?.type === 'MemberExpression') {
222
+ name = parent.left.property?.name;
223
+ }
224
+ if (!name && parent?.type === 'ObjectProperty' && parent.key) name = parent.key.name || String(parent.key.value);
225
+ const parentName = stack[stack.length - 1]?.name || '';
226
+ enterFn(name || 'anon', parentName, path.node, path.node.params || []);
227
+ },
228
+ exit() { exitFn(); },
229
+ },
230
+ ArrowFunctionExpression: {
231
+ enter(path) {
232
+ const parent = path.parent;
233
+ let name = null;
234
+ if (parent?.type === 'VariableDeclarator' && parent.id?.type === 'Identifier') name = parent.id.name;
235
+ if (!name && parent?.type === 'AssignmentExpression' && parent.left?.type === 'MemberExpression') {
236
+ name = parent.left.property?.name;
237
+ }
238
+ if (!name && parent?.type === 'ObjectProperty' && parent.key) name = parent.key.name || String(parent.key.value);
239
+ const parentName = stack[stack.length - 1]?.name || '';
240
+ enterFn(name || 'anon', parentName, path.node, path.node.params || []);
241
+ },
242
+ exit() { exitFn(); },
243
+ },
244
+ ClassMethod: {
245
+ enter(path) {
246
+ const cls = path.findParent(p => p.isClassDeclaration() || p.isClassExpression())?.node;
247
+ const className = cls?.id?.name || 'anon';
248
+ const methodName = path.node.key?.name || 'anon';
249
+ enterFn(methodName, className, path.node, path.node.params || []);
250
+ },
251
+ exit() { exitFn(); },
252
+ },
253
+ ObjectMethod: {
254
+ enter(path) {
255
+ const methodName = path.node.key?.name || 'anon';
256
+ const parentName = stack[stack.length - 1]?.name || '';
257
+ enterFn(methodName, parentName, path.node, path.node.params || []);
258
+ },
259
+ exit() { exitFn(); },
260
+ },
261
+
262
+ VariableDeclarator(path) {
263
+ const fn = currentFn(); if (!fn) return;
264
+ const id = lhsPath(path.node.id);
265
+ if (!id) return;
266
+ const initExpr = exprOf(path.node.init);
267
+ const nodeId = nextNodeId();
268
+ const line = path.node.loc?.start?.line || 0;
269
+ addNode(fn, { id: nodeId, kind: 'assign', target: id, source: initExpr, line, succ: [], pred: [] });
270
+ if (typeof id === 'string') recordWrite(fn, id, initExpr, nodeId);
271
+ if (id && typeof id === 'object' && id.kind === 'object-pattern') {
272
+ // x = { foo: a, bar: b } — emit one write per property.
273
+ for (const p of id.props) {
274
+ const alias = typeof p.alias === 'string' ? p.alias : null;
275
+ if (!alias) continue;
276
+ recordWrite(fn, alias, { kind: 'member', object: initExpr, prop: p.key }, nodeId);
277
+ }
278
+ }
279
+ },
280
+
281
+ AssignmentExpression(path) {
282
+ const fn = currentFn(); if (!fn) return;
283
+ const id = lhsPath(path.node.left);
284
+ if (!id) return;
285
+ const rhsExpr = exprOf(path.node.right);
286
+ const nodeId = nextNodeId();
287
+ const line = path.node.loc?.start?.line || 0;
288
+ addNode(fn, { id: nodeId, kind: 'assign', target: id, source: rhsExpr, line, succ: [], pred: [] });
289
+ if (typeof id === 'string') recordWrite(fn, id, rhsExpr, nodeId);
290
+ },
291
+
292
+ CallExpression(path) {
293
+ const fn = currentFn(); if (!fn) return;
294
+ // Skip if this call is itself the RHS of an assignment we just emitted
295
+ // (the assignment node already references it via its source.kind=='call').
296
+ const parent = path.parent;
297
+ if (parent && (parent.type === 'VariableDeclarator' || parent.type === 'AssignmentExpression')) return;
298
+ const calleeExpr = exprOf(path.node.callee);
299
+ const args = (path.node.arguments || []).map(exprOf);
300
+ const line = path.node.loc?.start?.line || 0;
301
+ const nodeId = nextNodeId();
302
+ addNode(fn, { id: nodeId, kind: 'call', callee: calleeExpr, args, line, succ: [], pred: [] });
303
+ // Resolve a flat callee name from the expression — used by the cross-file
304
+ // call graph join later.
305
+ const calleeName =
306
+ (calleeExpr.kind === 'ident' && calleeExpr.name) ||
307
+ (calleeExpr.kind === 'member' && calleeExpr.prop && (calleeExpr.object.kind === 'ident' ? `${calleeExpr.object.name}.${calleeExpr.prop}` : calleeExpr.prop)) ||
308
+ null;
309
+ fn.calls.push({ site: nodeId, callee: calleeName, args, line });
310
+ },
311
+
312
+ ReturnStatement(path) {
313
+ const fn = currentFn(); if (!fn) return;
314
+ const expr = path.node.argument ? exprOf(path.node.argument) : null;
315
+ const nodeId = nextNodeId();
316
+ const line = path.node.loc?.start?.line || 0;
317
+ addNode(fn, { id: nodeId, kind: 'return', value: expr, line, succ: [], pred: [] });
318
+ fn.returns.push(nodeId);
319
+ // Link to exit; subsequent code is unreachable from this branch.
320
+ linkCfg(fn, nodeId, fn.cfg.exit);
321
+ },
322
+
323
+ IfStatement: {
324
+ enter(path) {
325
+ // We model branches by inserting a noop "join" after the if; both
326
+ // branches link to it. Without this, the linear cursor model would
327
+ // miss that statements after the if are reachable from either branch.
328
+ const fn = currentFn(); if (!fn) return;
329
+ const condNodeId = nextNodeId();
330
+ const joinId = nextNodeId();
331
+ const line = path.node.loc?.start?.line || 0;
332
+ addNode(fn, { id: condNodeId, kind: 'if', cond: exprOf(path.node.test), line, succ: [], pred: [] });
333
+ fn.cfg.nodes.set(joinId, { id: joinId, kind: 'noop', succ: [], pred: [], line });
334
+ path.node._asJoin = joinId;
335
+ path.node._asCond = condNodeId;
336
+ path.node._asBranchSavedCursor = fn._cursor; // == condNodeId
337
+ },
338
+ exit(path) {
339
+ const fn = currentFn(); if (!fn) return;
340
+ const joinId = path.node._asJoin;
341
+ const condId = path.node._asCond;
342
+ if (!joinId || !condId) return;
343
+ // The visitor visited the body of the if — Babel's body visit ran
344
+ // *after* the enter(), so fn._cursor now points to the tail of the
345
+ // consequent. Connect it to the join, then if no else branch
346
+ // existed, connect the cond directly to the join (representing
347
+ // the "false" edge).
348
+ linkCfg(fn, fn._cursor, joinId);
349
+ if (!path.node.alternate) linkCfg(fn, condId, joinId);
350
+ fn._cursor = joinId;
351
+ },
352
+ },
353
+
354
+ // We don't deeply model loops; treat the body as a sequence and link
355
+ // its tail back to the loop header. For taint, this gives "any
356
+ // iteration could taint X" which is the conservative answer we want.
357
+ 'WhileStatement|ForStatement|DoWhileStatement|ForInStatement|ForOfStatement': {
358
+ enter(path) {
359
+ const fn = currentFn(); if (!fn) return;
360
+ const headerId = nextNodeId();
361
+ const exitId = nextNodeId();
362
+ const line = path.node.loc?.start?.line || 0;
363
+ addNode(fn, { id: headerId, kind: 'loop-header', line, succ: [], pred: [] });
364
+ fn.cfg.nodes.set(exitId, { id: exitId, kind: 'noop', succ: [], pred: [], line });
365
+ path.node._loopHeader = headerId;
366
+ path.node._loopExit = exitId;
367
+ },
368
+ exit(path) {
369
+ const fn = currentFn(); if (!fn) return;
370
+ const headerId = path.node._loopHeader;
371
+ const exitId = path.node._loopExit;
372
+ if (!headerId || !exitId) return;
373
+ linkCfg(fn, fn._cursor, headerId); // back-edge
374
+ linkCfg(fn, headerId, exitId); // exit edge
375
+ fn._cursor = exitId;
376
+ },
377
+ },
378
+
379
+ TryStatement: {
380
+ enter() { /* approximate try/catch as sequential — taint flows through both */ },
381
+ },
382
+
383
+ ThrowStatement(path) {
384
+ const fn = currentFn(); if (!fn) return;
385
+ const expr = exprOf(path.node.argument);
386
+ const nodeId = nextNodeId();
387
+ const line = path.node.loc?.start?.line || 0;
388
+ addNode(fn, { id: nodeId, kind: 'throw', value: expr, line, succ: [], pred: [] });
389
+ linkCfg(fn, nodeId, fn.cfg.exit);
390
+ },
391
+ },
392
+ };
393
+ };
394
+
395
+ try {
396
+ babelTransformSync(code, {
397
+ filename: file,
398
+ presets: [presetReact, [presetTypescript, { isTSX: true, allExtensions: true }]],
399
+ plugins: [plugin],
400
+ ast: false, code: false, babelrc: false, configFile: false,
401
+ });
402
+ } catch {
403
+ return null;
404
+ }
405
+
406
+ // Convert reads/writes Maps to plain objects for downstream JSON serializability.
407
+ for (const fn of functions) {
408
+ fn.reads = Object.fromEntries(fn.reads);
409
+ fn.writes = Object.fromEntries(fn.writes);
410
+ fn.cfg.nodes = Object.fromEntries(fn.cfg.nodes);
411
+ }
412
+ return { file, functions, topLevel: functions.find(f => f.name === '<module>')?.qid || null };
413
+ }
@@ -0,0 +1,258 @@
1
+ // Kotlin IR frontend (v0.66).
2
+ //
3
+ // Regex-based, pragmatic, focused on Spring / Ktor / Exposed / java.io
4
+ // surface area. Parallel approach to parser-cs.js (C#).
5
+ //
6
+ // What we model:
7
+ // - top-level functions: `fun name(params): RetType { body }`
8
+ // - member functions: `fun Class.name(params) { body }` (extension fns)
9
+ // - assignments: `val x = …` `var x = …` `x = …`
10
+ // - calls (statement-form): `obj.method(args)` / `method(args)`
11
+ // - return: `return expr`
12
+ //
13
+ // What we do NOT model:
14
+ // - lambdas (collapsed to opaque expression)
15
+ // - destructuring `val (a, b) = pair`
16
+ // - `if`/`when`/`for`/`while` control flow (body treated as straight-line)
17
+ // - infix functions (the call shape isn't recognized)
18
+ // - operator overloading
19
+ //
20
+ // Single-pass v1. Roslyn-equivalent for Kotlin (kotlinc -p ir or PSI via
21
+ // gradle helper) is the upgrade path.
22
+
23
+ import * as crypto from 'node:crypto';
24
+
25
+ const FUN_RE = new RegExp(
26
+ '(?:^|[\\s;{}])(?:public|private|internal|protected|inline|suspend|tailrec|operator|infix|open|abstract|override|final|external)?' +
27
+ '(?:\\s+(?:public|private|internal|protected|inline|suspend|tailrec|operator|infix|open|abstract|override|final|external))*' +
28
+ '\\s*fun\\s+(?:[A-Za-z_][\\w.]*\\.)?' + // optional receiver-type prefix
29
+ '([A-Za-z_][\\w]*)' + // function name (group 1)
30
+ '\\s*\\(([^)]*)\\)' + // params (group 2)
31
+ '\\s*(?::\\s*[A-Za-z_][\\w<>?,\\s.]*)?\\s*\\{', 'g'); // optional return type then '{'
32
+
33
+ function _splitStatements(body) {
34
+ const out = [];
35
+ let buf = '';
36
+ let depth = 0;
37
+ let inStr = null;
38
+ let escape = false;
39
+ for (let i = 0; i < body.length; i++) {
40
+ const c = body[i];
41
+ if (escape) { buf += c; escape = false; continue; }
42
+ if (inStr) {
43
+ buf += c;
44
+ if (inStr === '"' && c === '\\') { escape = true; continue; }
45
+ if (c === inStr) inStr = null;
46
+ continue;
47
+ }
48
+ if (c === '"' || c === "'") { inStr = c; buf += c; continue; }
49
+ if (c === '{' || c === '(' || c === '[') depth++;
50
+ if (c === '}' || c === ')' || c === ']') depth--;
51
+ // Kotlin uses newlines OR semicolons as statement separators.
52
+ if ((c === '\n' || c === ';') && depth === 0) {
53
+ const t = buf.trim();
54
+ if (t) out.push(t);
55
+ buf = '';
56
+ continue;
57
+ }
58
+ buf += c;
59
+ }
60
+ if (buf.trim()) out.push(buf.trim());
61
+ return out;
62
+ }
63
+
64
+ function _splitTopLevelCommas(s) {
65
+ const out = [];
66
+ let buf = '';
67
+ let depth = 0;
68
+ let inStr = null;
69
+ for (let i = 0; i < s.length; i++) {
70
+ const c = s[i];
71
+ if (inStr) {
72
+ buf += c;
73
+ if (c === inStr && s[i-1] !== '\\') inStr = null;
74
+ continue;
75
+ }
76
+ if (c === '"' || c === "'") { inStr = c; buf += c; continue; }
77
+ if (c === '(' || c === '{' || c === '[' || c === '<') depth++;
78
+ if (c === ')' || c === '}' || c === ']' || c === '>') depth--;
79
+ if (c === ',' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
80
+ buf += c;
81
+ }
82
+ if (buf.trim()) out.push(buf.trim());
83
+ return out;
84
+ }
85
+
86
+ function _splitTopLevelPlus(s) {
87
+ const out = [];
88
+ let buf = '';
89
+ let depth = 0;
90
+ let inStr = null;
91
+ for (let i = 0; i < s.length; i++) {
92
+ const c = s[i];
93
+ if (inStr) {
94
+ buf += c;
95
+ if (c === inStr && s[i-1] !== '\\') inStr = null;
96
+ continue;
97
+ }
98
+ if (c === '"' || c === "'") { inStr = c; buf += c; continue; }
99
+ if (c === '(' || c === '{' || c === '[') depth++;
100
+ if (c === ')' || c === '}' || c === ']') depth--;
101
+ if (c === '+' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
102
+ buf += c;
103
+ }
104
+ if (buf.trim()) out.push(buf.trim());
105
+ return out;
106
+ }
107
+
108
+ function _buildMemberChain(parts) {
109
+ let cur = { kind: 'ident', name: parts[0] };
110
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
111
+ return cur;
112
+ }
113
+
114
+ function _lowerExpr(text) {
115
+ const s = String(text || '').trim();
116
+ if (!s) return { kind: 'unknown' };
117
+ // String interpolation: "hi $x" / "hi ${name}".
118
+ if (/^".*"$/.test(s) && /\$/.test(s)) {
119
+ const parts = [];
120
+ const re = /\$\{([^}]+)\}|\$([A-Za-z_]\w*)/g;
121
+ let last = 0;
122
+ let m;
123
+ while ((m = re.exec(s)) !== null) {
124
+ if (m.index > last) parts.push({ kind: 'literal', value: s.slice(last, m.index) });
125
+ parts.push(_lowerExpr((m[1] || m[2]).trim()));
126
+ last = re.lastIndex;
127
+ }
128
+ if (last < s.length) parts.push({ kind: 'literal', value: s.slice(last) });
129
+ return { kind: 'tpl', parts };
130
+ }
131
+ // Plain dotted ident
132
+ if (/^[A-Za-z_][\w.]*$/.test(s)) {
133
+ const parts = s.split('.');
134
+ if (parts.length === 1) return { kind: 'ident', name: parts[0] };
135
+ return _buildMemberChain(parts);
136
+ }
137
+ // Call
138
+ const callMatch = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
139
+ if (callMatch) {
140
+ return {
141
+ kind: 'call',
142
+ callee: callMatch[1],
143
+ args: _splitTopLevelCommas(callMatch[2]).map(_lowerExpr),
144
+ };
145
+ }
146
+ // Concat
147
+ if (s.includes('+') && /["']/.test(s)) {
148
+ return { kind: 'tpl', parts: _splitTopLevelPlus(s).map(_lowerExpr) };
149
+ }
150
+ if (/^"/.test(s) || /^\d/.test(s)) return { kind: 'literal', value: s };
151
+ return { kind: 'unknown' };
152
+ }
153
+
154
+ function _lowerStmt(stmt, line) {
155
+ const s = stmt.trim();
156
+ if (!s || s.startsWith('//') || s.startsWith('/*') || s.startsWith('*')) return null;
157
+ if (/^return\b/.test(s)) {
158
+ const m = s.match(/^return\s*(.*?)\s*$/);
159
+ return { kind: 'return', line, value: m && m[1] ? _lowerExpr(m[1]) : null };
160
+ }
161
+ if (/^throw\b/.test(s)) {
162
+ return { kind: 'throw', line, value: _lowerExpr(s.replace(/^throw\s*/, '')) };
163
+ }
164
+ // Variable declarations: val/var name [: Type] = expr
165
+ const decl = s.match(/^(?:val|var)\s+([A-Za-z_]\w*)\s*(?::\s*[\w<>?,\s.]*?)?\s*=\s*(.+)$/s);
166
+ if (decl) return { kind: 'assign', line, target: decl[1], source: _lowerExpr(decl[2]) };
167
+ // Plain assign: x = expr (also x.y = expr)
168
+ const assign = s.match(/^([A-Za-z_][\w.]*)\s*=\s*(.+)$/s);
169
+ if (assign && !/[=!<>]=/.test(s.slice(0, s.indexOf('=')+1).slice(0, -1))) {
170
+ return { kind: 'assign', line, target: assign[1], source: _lowerExpr(assign[2]) };
171
+ }
172
+ // Statement-form call
173
+ const cm = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
174
+ if (cm) return { kind: 'call', line, callee: cm[1], args: _splitTopLevelCommas(cm[2]).map(_lowerExpr) };
175
+ return { kind: 'unknown', line, text: s };
176
+ }
177
+
178
+ function _extractBody(src, openBrace) {
179
+ let depth = 1;
180
+ let i = openBrace + 1;
181
+ let inStr = null;
182
+ let escape = false;
183
+ while (i < src.length && depth > 0) {
184
+ const c = src[i];
185
+ if (escape) { escape = false; i++; continue; }
186
+ if (inStr) {
187
+ if (inStr === '"' && c === '\\') { escape = true; i++; continue; }
188
+ if (c === inStr) inStr = null;
189
+ i++; continue;
190
+ }
191
+ if (c === '"' || c === "'") { inStr = c; i++; continue; }
192
+ if (c === '{') depth++;
193
+ else if (c === '}') depth--;
194
+ if (depth === 0) return { body: src.slice(openBrace + 1, i), end: i };
195
+ i++;
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function _lineAt(src, idx) {
201
+ let line = 1;
202
+ for (let i = 0; i < idx && i < src.length; i++) if (src[i] === '\n') line++;
203
+ return line;
204
+ }
205
+
206
+ function _qid(file, name, line, body) {
207
+ const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
208
+ return `${file}::${name}@${line}#${sha}`;
209
+ }
210
+
211
+ export function parseKotlinFile(file, code) {
212
+ if (!file || typeof code !== 'string') return null;
213
+ const functions = [];
214
+ FUN_RE.lastIndex = 0;
215
+ let m;
216
+ while ((m = FUN_RE.exec(code)) !== null) {
217
+ const name = m[1];
218
+ const paramsText = m[2] || '';
219
+ const params = paramsText.split(',').map(p => {
220
+ const t = p.trim();
221
+ if (!t) return null;
222
+ // Kotlin params: `name: Type = default` or `vararg name: Type`
223
+ const cleaned = t.replace(/^vararg\s+/, '');
224
+ const colon = cleaned.indexOf(':');
225
+ const namePart = colon > 0 ? cleaned.slice(0, colon).trim() : cleaned.trim();
226
+ return /^[A-Za-z_]\w*$/.test(namePart) ? namePart : null;
227
+ }).filter(Boolean);
228
+ const braceIdx = code.indexOf('{', m.index + m[0].length - 1);
229
+ if (braceIdx < 0) continue;
230
+ const extracted = _extractBody(code, braceIdx);
231
+ if (!extracted) continue;
232
+ const startLine = _lineAt(code, m.index);
233
+ const stmts = _splitStatements(extracted.body);
234
+ const nodes = {};
235
+ nodes.entry = { kind: 'entry', line: startLine, succ: [], pred: [] };
236
+ nodes.exit = { kind: 'exit', line: startLine, succ: [], pred: [] };
237
+ let prev = 'entry';
238
+ let stmtLine = startLine;
239
+ for (let idx = 0; idx < stmts.length; idx++) {
240
+ const node = _lowerStmt(stmts[idx], stmtLine);
241
+ if (!node) continue;
242
+ const id = `n${idx}`;
243
+ nodes[id] = { ...node, succ: [], pred: [prev] };
244
+ nodes[prev].succ.push(id);
245
+ prev = id;
246
+ stmtLine += (stmts[idx].match(/\n/g) || []).length + 1;
247
+ }
248
+ nodes[prev].succ.push('exit');
249
+ nodes.exit.pred.push(prev);
250
+ functions.push({
251
+ qid: _qid(file, name, startLine, extracted.body),
252
+ name, line: startLine, params, file,
253
+ cfg: { entry: 'entry', exit: 'exit', nodes },
254
+ });
255
+ FUN_RE.lastIndex = extracted.end + 1;
256
+ }
257
+ return { file, functions, topLevel: null };
258
+ }