@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.
- package/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- 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
|
+
}
|