homegraph 1.2.0 → 1.4.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.
- package/dist/bin/homegraph.js +3 -0
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +85 -0
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +64 -5
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +178 -14
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +19 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +117 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +299 -6
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +32 -1
- package/dist/db/wal-valve.d.ts +106 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +208 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +9 -5
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +210 -19
- package/dist/directory.js.map +1 -1
- package/dist/extraction/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.js.map +1 -0
- package/dist/extraction/grammars.d.ts +25 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +177 -24
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +43 -4
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +308 -5
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +42 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +369 -4
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +16 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- package/dist/extraction/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/dist/extraction/mybatis-extractor.js +140 -38
- package/dist/extraction/mybatis-extractor.js.map +1 -1
- package/dist/extraction/parse-pool.d.ts +27 -1
- package/dist/extraction/parse-pool.d.ts.map +1 -1
- package/dist/extraction/parse-pool.js +75 -7
- package/dist/extraction/parse-pool.js.map +1 -1
- package/dist/extraction/parse-worker.js +9 -2
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +1 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +3 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +38 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +754 -10
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +1 -1
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +348 -5
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +42 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +97 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/mcp/daemon.d.ts +25 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +62 -8
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/dist/mcp/dynamic-boundaries.js +2 -1
- package/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +4 -5
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +27 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/dist/mcp/liveness-watchdog.js +73 -13
- package/dist/mcp/liveness-watchdog.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +30 -0
- package/dist/mcp/memory-budget.d.ts.map +1 -0
- package/dist/mcp/memory-budget.js +123 -0
- package/dist/mcp/memory-budget.js.map +1 -0
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +88 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +35 -4
- package/dist/mcp/query-cache.d.ts.map +1 -1
- package/dist/mcp/query-cache.js +89 -19
- package/dist/mcp/query-cache.js.map +1 -1
- package/dist/mcp/query-pool.d.ts +46 -12
- package/dist/mcp/query-pool.d.ts.map +1 -1
- package/dist/mcp/query-pool.js +100 -23
- package/dist/mcp/query-pool.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +4 -27
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -84
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -0
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +28 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/tools.d.ts +104 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2390 -191
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/project-config.d.ts +38 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +101 -2
- package/dist/project-config.js.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +178 -146
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +9 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1164 -176
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +3 -0
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +14 -6
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/dist/resolution/goframe-synthesizer.js +8 -3
- package/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +7 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +129 -4
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +60 -7
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +430 -80
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +0 -3
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +354 -33
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +49 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +29 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +10 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.js +142 -4
- package/dist/resolution/workspace-packages.js.map +1 -1
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-utils.d.ts +201 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +919 -2
- package/dist/search/query-utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/watcher.d.ts +10 -5
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +51 -14
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +5 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/upgrade/index.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +162 -14
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/package.json +2 -2
- package/scripts/agent-eval/run-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/npm-shim.js +8 -1
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/data/agents.json +0 -109
- package/scripts/exp_boundary_eval/data/experiments.json +0 -140
- package/scripts/qa_eval/README.md +0 -407
- package/scripts/qa_eval/_test_deveco_probe.py +0 -41
- package/scripts/qa_eval/agent_runner.py +0 -526
- package/scripts/qa_eval/data/.gitignore +0 -4
- package/scripts/qa_eval/data/test-set.jsonl +0 -2
- package/scripts/qa_eval/eval_metrics.py +0 -274
- package/scripts/qa_eval/external_agent.py +0 -976
- package/scripts/qa_eval/llm_config.py +0 -92
- package/scripts/qa_eval/memory_monitor.py +0 -132
- package/scripts/qa_eval/my_answer_accuracy.py +0 -187
- package/scripts/qa_eval/requirements.txt +0 -2
- package/scripts/qa_eval/run_pipeline.py +0 -804
- package/scripts/qa_eval/stats_efficiency.py +0 -279
- package/scripts/qa_eval/stats_scores.py +0 -207
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SYNTH_PROGRESS_STEPS = void 0;
|
|
3
4
|
exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
|
|
4
5
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
5
6
|
const strip_comments_1 = require("./strip-comments");
|
|
@@ -41,6 +42,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
|
|
|
41
42
|
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
|
|
42
43
|
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
|
|
43
44
|
const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
|
|
45
|
+
// The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
|
|
46
|
+
// trailing-closure syntax, so ONLY those languages can ever contribute a
|
|
47
|
+
// dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
|
|
48
|
+
// Swift dispatcher's field name) would be a wrong edge, not a missed one.
|
|
49
|
+
// Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
|
|
50
|
+
// in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
|
|
51
|
+
// where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
|
|
52
|
+
// 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
|
|
53
|
+
const CC_LANGUAGES = new Set(['swift', 'kotlin']);
|
|
44
54
|
function kebabToPascal(s) {
|
|
45
55
|
return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
46
56
|
}
|
|
@@ -76,6 +86,35 @@ function sliceLines(content, startLine, endLine) {
|
|
|
76
86
|
return null;
|
|
77
87
|
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
|
78
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Per-match line resolver over `src`, 1-based at `baseLine`. The inline
|
|
91
|
+
* `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
|
|
92
|
+
* which goes quadratic on a match-dense source (a generated function full of
|
|
93
|
+
* `.push(` calls re-scanned tens of thousands of times was most of the #1235
|
|
94
|
+
* indexing wedge). Builds the newline index once — lazily, since most sources
|
|
95
|
+
* never produce a match — then answers each call with a binary search.
|
|
96
|
+
*/
|
|
97
|
+
function makeLineAt(src, baseLine) {
|
|
98
|
+
let nl = null;
|
|
99
|
+
return (idx) => {
|
|
100
|
+
if (!nl) {
|
|
101
|
+
nl = [];
|
|
102
|
+
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
|
|
103
|
+
nl.push(i);
|
|
104
|
+
}
|
|
105
|
+
// Count newlines strictly before idx.
|
|
106
|
+
let lo = 0;
|
|
107
|
+
let hi = nl.length;
|
|
108
|
+
while (lo < hi) {
|
|
109
|
+
const mid = (lo + hi) >> 1;
|
|
110
|
+
if (nl[mid] < idx)
|
|
111
|
+
lo = mid + 1;
|
|
112
|
+
else
|
|
113
|
+
hi = mid;
|
|
114
|
+
}
|
|
115
|
+
return baseLine + lo;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
79
118
|
function registrarField(src) {
|
|
80
119
|
const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
|
|
81
120
|
return m ? m[1] : null;
|
|
@@ -213,13 +252,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
213
252
|
arr.push({ node, line: absLine });
|
|
214
253
|
registrars.set(field, arr);
|
|
215
254
|
};
|
|
216
|
-
// Slices EVERY method/function's source (no cheap name-gate), so
|
|
217
|
-
// with a huge file this is the heaviest synthesis pass — yield
|
|
218
|
-
//
|
|
255
|
+
// Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
|
|
256
|
+
// on a repo with a huge file this is the heaviest synthesis pass — yield
|
|
257
|
+
// mid-scan (and mid-match-loop below: a single generated function dense with
|
|
258
|
+
// matches must not starve the watchdog either) so it can't wedge the #850
|
|
259
|
+
// watchdog on its own (#1091, #1235).
|
|
219
260
|
let scanned = 0;
|
|
261
|
+
let matchTick = 0;
|
|
220
262
|
for (const m of methodAndFunctionNodes(queries)) {
|
|
221
263
|
if ((++scanned & 127) === 0)
|
|
222
264
|
await onYield();
|
|
265
|
+
if (!CC_LANGUAGES.has(m.language))
|
|
266
|
+
continue;
|
|
223
267
|
const content = ctx.readFile(m.filePath);
|
|
224
268
|
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
225
269
|
if (!src)
|
|
@@ -228,11 +272,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
228
272
|
const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
|
|
229
273
|
if (!hasForEach && !hasAppend)
|
|
230
274
|
continue;
|
|
231
|
-
const lineAt = (
|
|
275
|
+
const lineAt = makeLineAt(src, m.startLine ?? 1);
|
|
232
276
|
if (hasForEach) {
|
|
233
277
|
CC_DISPATCH_RE.lastIndex = 0;
|
|
234
278
|
let d;
|
|
235
279
|
while ((d = CC_DISPATCH_RE.exec(src))) {
|
|
280
|
+
if ((++matchTick & 255) === 0)
|
|
281
|
+
await onYield();
|
|
236
282
|
const arr = dispatchers.get(d[1]) ?? [];
|
|
237
283
|
if (!arr.some((n) => n.node.id === m.id))
|
|
238
284
|
arr.push({ node: m, line: lineAt(d.index) });
|
|
@@ -242,12 +288,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
242
288
|
if (hasAppend) {
|
|
243
289
|
CC_APPEND_WRITE_RE.lastIndex = 0;
|
|
244
290
|
let w;
|
|
245
|
-
while ((w = CC_APPEND_WRITE_RE.exec(src)))
|
|
291
|
+
while ((w = CC_APPEND_WRITE_RE.exec(src))) {
|
|
292
|
+
if ((++matchTick & 255) === 0)
|
|
293
|
+
await onYield();
|
|
246
294
|
addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
|
295
|
+
}
|
|
247
296
|
CC_APPEND_DIRECT_RE.lastIndex = 0;
|
|
248
297
|
let a;
|
|
249
|
-
while ((a = CC_APPEND_DIRECT_RE.exec(src)))
|
|
298
|
+
while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
|
|
299
|
+
if ((++matchTick & 255) === 0)
|
|
300
|
+
await onYield();
|
|
250
301
|
addReg(a[1], m, lineAt(a.index));
|
|
302
|
+
}
|
|
251
303
|
}
|
|
252
304
|
}
|
|
253
305
|
const edges = [];
|
|
@@ -277,10 +329,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
277
329
|
}
|
|
278
330
|
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
279
331
|
async function eventEmitterEdges(ctx, onYield) {
|
|
332
|
+
let scannedFiles = 0;
|
|
280
333
|
const emitsByEvent = new Map(); // event → dispatcher node ids
|
|
281
334
|
const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
|
|
282
335
|
let scanned = 0;
|
|
283
336
|
for (const file of ctx.getAllFiles()) {
|
|
337
|
+
if ((++scannedFiles & 15) === 0)
|
|
338
|
+
await onYield();
|
|
284
339
|
if ((++scanned & 255) === 0)
|
|
285
340
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
286
341
|
const content = ctx.readFile(file);
|
|
@@ -291,7 +346,7 @@ async function eventEmitterEdges(ctx, onYield) {
|
|
|
291
346
|
if (!hasEmit && !hasOn)
|
|
292
347
|
continue;
|
|
293
348
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
294
|
-
const lineOf = (
|
|
349
|
+
const lineOf = makeLineAt(content, 1);
|
|
295
350
|
if (hasEmit) {
|
|
296
351
|
EMIT_RE.lastIndex = 0;
|
|
297
352
|
let m;
|
|
@@ -354,10 +409,13 @@ async function eventEmitterEdges(ctx, onYield) {
|
|
|
354
409
|
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
355
410
|
* accepted — it's reachability-correct, like the callback channels.
|
|
356
411
|
*/
|
|
357
|
-
function reactRenderEdges(queries, ctx) {
|
|
412
|
+
async function reactRenderEdges(queries, ctx, onYield) {
|
|
413
|
+
let scanned255 = 0;
|
|
358
414
|
const edges = [];
|
|
359
415
|
const seen = new Set();
|
|
360
|
-
for (const cls of queries.
|
|
416
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
417
|
+
if ((++scanned255 & 63) === 0)
|
|
418
|
+
await onYield();
|
|
361
419
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
362
420
|
.map((e) => queries.getNodeById(e.target))
|
|
363
421
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -397,10 +455,13 @@ function reactRenderEdges(queries, ctx) {
|
|
|
397
455
|
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
398
456
|
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
399
457
|
*/
|
|
400
|
-
function flutterBuildEdges(queries, ctx) {
|
|
458
|
+
async function flutterBuildEdges(queries, ctx, onYield) {
|
|
459
|
+
let scanned255 = 0;
|
|
401
460
|
const edges = [];
|
|
402
461
|
const seen = new Set();
|
|
403
|
-
for (const cls of queries.
|
|
462
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
463
|
+
if ((++scanned255 & 63) === 0)
|
|
464
|
+
await onYield();
|
|
404
465
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
405
466
|
.map((e) => queries.getNodeById(e.target))
|
|
406
467
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -431,6 +492,279 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
431
492
|
}
|
|
432
493
|
return edges;
|
|
433
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Reactive ArkUI property decorators: assigning a property carrying one of
|
|
497
|
+
* these re-runs the owning struct's `build()`. Covers both state models —
|
|
498
|
+
* V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
|
|
499
|
+
* (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
|
|
500
|
+
* the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
|
|
501
|
+
* data classes, not struct properties).
|
|
502
|
+
*/
|
|
503
|
+
const ARKUI_REACTIVE_DECORATORS = new Set([
|
|
504
|
+
'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
|
|
505
|
+
'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
|
|
506
|
+
'Local', 'Provider', 'Consumer',
|
|
507
|
+
]);
|
|
508
|
+
/** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
|
|
509
|
+
const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
510
|
+
/**
|
|
511
|
+
* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
|
|
512
|
+
* flutter-build). Assigning a reactive-decorated property (`@State count`,
|
|
513
|
+
* `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
|
|
514
|
+
* hop is framework-internal — no static edge — so "onClick → markAllDone →
|
|
515
|
+
* this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
|
|
516
|
+
* for each arkts struct with a `build()` method and at least one reactive
|
|
517
|
+
* property, link every sibling method whose body ASSIGNS (or array-mutates)
|
|
518
|
+
* one of those properties → `build`. Assignment-gated on the struct's OWN
|
|
519
|
+
* reactive property names — a method that merely reads state, or a struct
|
|
520
|
+
* with no reactive properties, gets nothing (this is the precision line the
|
|
521
|
+
* all-sibling-methods design would erase).
|
|
522
|
+
*/
|
|
523
|
+
async function arkuiStateBuildEdges(queries, ctx, onYield) {
|
|
524
|
+
let scanned255 = 0;
|
|
525
|
+
const edges = [];
|
|
526
|
+
const seen = new Set();
|
|
527
|
+
for (const struct of queries.iterateNodesByKind('struct')) {
|
|
528
|
+
if ((++scanned255 & 63) === 0)
|
|
529
|
+
await onYield();
|
|
530
|
+
if (struct.language !== 'arkts')
|
|
531
|
+
continue;
|
|
532
|
+
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
533
|
+
.map((e) => queries.getNodeById(e.target))
|
|
534
|
+
.filter((n) => !!n);
|
|
535
|
+
const build = children.find((n) => n.kind === 'method' && n.name === 'build');
|
|
536
|
+
if (!build)
|
|
537
|
+
continue;
|
|
538
|
+
const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
|
|
539
|
+
if (reactiveProps.length === 0)
|
|
540
|
+
continue;
|
|
541
|
+
const propAlternation = reactiveProps
|
|
542
|
+
.map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
543
|
+
.join('|');
|
|
544
|
+
// `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
|
|
545
|
+
// `=(?!=)` keeps `this.done == x` comparisons out.
|
|
546
|
+
const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
|
|
547
|
+
let added = 0;
|
|
548
|
+
for (const m of children) {
|
|
549
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
550
|
+
break;
|
|
551
|
+
if (m.kind !== 'method' || m.id === build.id)
|
|
552
|
+
continue;
|
|
553
|
+
const content = ctx.readFile(m.filePath);
|
|
554
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
555
|
+
if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
|
|
556
|
+
continue;
|
|
557
|
+
const key = `${m.id}>${build.id}`;
|
|
558
|
+
if (seen.has(key))
|
|
559
|
+
continue;
|
|
560
|
+
seen.add(key);
|
|
561
|
+
edges.push({
|
|
562
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
563
|
+
provenance: 'heuristic',
|
|
564
|
+
metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
565
|
+
});
|
|
566
|
+
added++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return edges;
|
|
570
|
+
}
|
|
571
|
+
/** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
|
|
572
|
+
const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
|
|
573
|
+
/** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
|
|
574
|
+
const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
575
|
+
/**
|
|
576
|
+
* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
|
|
577
|
+
* bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
|
|
578
|
+
* framework-internal, so an order flow riding it (OrangeShopping's
|
|
579
|
+
* add-to-cart) dead-ends at the emit. Link emit-site enclosing
|
|
580
|
+
* function/method → on/once-site enclosing function/method when both
|
|
581
|
+
* reference the SAME statically-recoverable event key.
|
|
582
|
+
*
|
|
583
|
+
* Key recovery, per call site (comment-stripped enclosing-file source): the
|
|
584
|
+
* first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
|
|
585
|
+
* a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
|
|
586
|
+
* level. Precision scoping learned from the samples monorepo (thousands of
|
|
587
|
+
* unrelated samples, most using eventId 1): NUMERIC keys pair within the same
|
|
588
|
+
* FILE only; NAMED keys pair within the same workspace module directory (or
|
|
589
|
+
* the whole project when it declares no modules — the single-app case), both
|
|
590
|
+
* behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
|
|
591
|
+
* handling — their bodies' calls already attribute to the registering method,
|
|
592
|
+
* so targeting that method keeps the chain connected.
|
|
593
|
+
*/
|
|
594
|
+
async function arkuiEmitterEdges(ctx, onYield) {
|
|
595
|
+
let scannedFiles = 0;
|
|
596
|
+
// bucket key -> emit sites / handler sites
|
|
597
|
+
const emits = new Map();
|
|
598
|
+
const handlers = new Map();
|
|
599
|
+
const moduleDirs = (() => {
|
|
600
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
601
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
602
|
+
})();
|
|
603
|
+
const moduleScopeOf = (file) => {
|
|
604
|
+
for (const dir of moduleDirs) {
|
|
605
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
606
|
+
return dir;
|
|
607
|
+
}
|
|
608
|
+
return '';
|
|
609
|
+
};
|
|
610
|
+
for (const file of ctx.getAllFiles()) {
|
|
611
|
+
if ((++scannedFiles & 15) === 0)
|
|
612
|
+
await onYield();
|
|
613
|
+
if (!file.endsWith('.ets'))
|
|
614
|
+
continue;
|
|
615
|
+
const content = ctx.readFile(file);
|
|
616
|
+
if (!content || !content.includes('emitter.'))
|
|
617
|
+
continue;
|
|
618
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
619
|
+
const nodes = ctx.getNodesInFile(file)
|
|
620
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
621
|
+
ARKUI_EMITTER_CALL_RE.lastIndex = 0;
|
|
622
|
+
let m;
|
|
623
|
+
while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
|
|
624
|
+
const verb = m[1];
|
|
625
|
+
const arg = m[2].trim();
|
|
626
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
627
|
+
const encl = nodes
|
|
628
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
629
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
630
|
+
if (!encl)
|
|
631
|
+
continue;
|
|
632
|
+
// Recover the event key from the first argument.
|
|
633
|
+
let key = null;
|
|
634
|
+
const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
|
|
635
|
+
const token = idLit ?? arg;
|
|
636
|
+
if (token !== undefined) {
|
|
637
|
+
if (/^\d+$/.test(token)) {
|
|
638
|
+
key = `num:${file}:${token}`; // numeric: same-file only
|
|
639
|
+
}
|
|
640
|
+
else if (token.includes('.')) {
|
|
641
|
+
key = `name:${moduleScopeOf(file)}:${token}`;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// Local variable — chase its same-file declaration one level:
|
|
645
|
+
// `let x = new EventsId(K)` / `const x = K`.
|
|
646
|
+
const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
|
|
647
|
+
const decl = safe.match(declRe);
|
|
648
|
+
const inner = (decl?.[1] ?? decl?.[2])?.trim();
|
|
649
|
+
if (inner && /^\d+$/.test(inner))
|
|
650
|
+
key = `num:${file}:${inner}`;
|
|
651
|
+
else if (inner && /^[\w$.]+$/.test(inner))
|
|
652
|
+
key = `name:${moduleScopeOf(file)}:${inner}`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!key)
|
|
656
|
+
continue;
|
|
657
|
+
const site = { nodeId: encl.id, file, line };
|
|
658
|
+
if (verb === 'emit') {
|
|
659
|
+
(emits.get(key) ?? emits.set(key, []).get(key)).push(site);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
(handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const edges = [];
|
|
667
|
+
const seen = new Set();
|
|
668
|
+
for (const [key, emitSites] of emits) {
|
|
669
|
+
const handlerSites = handlers.get(key);
|
|
670
|
+
if (!handlerSites)
|
|
671
|
+
continue;
|
|
672
|
+
if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
|
|
673
|
+
continue;
|
|
674
|
+
const eventLabel = key.slice(key.lastIndexOf(':') + 1);
|
|
675
|
+
for (const e of emitSites)
|
|
676
|
+
for (const h of handlerSites) {
|
|
677
|
+
if (e.nodeId === h.nodeId)
|
|
678
|
+
continue;
|
|
679
|
+
const dedupe = `${e.nodeId}>${h.nodeId}`;
|
|
680
|
+
if (seen.has(dedupe))
|
|
681
|
+
continue;
|
|
682
|
+
seen.add(dedupe);
|
|
683
|
+
edges.push({
|
|
684
|
+
source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
|
|
685
|
+
provenance: 'heuristic',
|
|
686
|
+
metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return edges;
|
|
691
|
+
}
|
|
692
|
+
/** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
|
|
693
|
+
const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
|
|
694
|
+
/**
|
|
695
|
+
* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
|
|
696
|
+
* 'pages/Detail' })` reaches the `@Entry struct` of
|
|
697
|
+
* `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
|
|
698
|
+
* static edge — so "tap → openDetail → ???" ends at the router call. Bridge
|
|
699
|
+
* literal urls to the page struct: the url resolves against the standard
|
|
700
|
+
* `src/main/ets/` layout (what main_pages.json entries name); candidates
|
|
701
|
+
* prefer the caller's own workspace module (routes are module-scoped), and
|
|
702
|
+
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
703
|
+
* structs qualify as targets — the decorator is what makes a file a page.
|
|
704
|
+
*/
|
|
705
|
+
async function arkuiRouterEdges(ctx, onYield) {
|
|
706
|
+
const edges = [];
|
|
707
|
+
const seen = new Set();
|
|
708
|
+
const allFiles = ctx.getAllFiles();
|
|
709
|
+
const moduleDirs = (() => {
|
|
710
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
711
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
712
|
+
})();
|
|
713
|
+
const moduleScopeOf = (file) => {
|
|
714
|
+
for (const dir of moduleDirs) {
|
|
715
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
716
|
+
return dir;
|
|
717
|
+
}
|
|
718
|
+
return '';
|
|
719
|
+
};
|
|
720
|
+
let scannedFiles = 0;
|
|
721
|
+
for (const file of allFiles) {
|
|
722
|
+
if ((++scannedFiles & 15) === 0)
|
|
723
|
+
await onYield();
|
|
724
|
+
if (!file.endsWith('.ets'))
|
|
725
|
+
continue;
|
|
726
|
+
const content = ctx.readFile(file);
|
|
727
|
+
if (!content || !content.includes('router.'))
|
|
728
|
+
continue;
|
|
729
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
730
|
+
const nodes = ctx.getNodesInFile(file)
|
|
731
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
732
|
+
ARKUI_ROUTER_RE.lastIndex = 0;
|
|
733
|
+
let m;
|
|
734
|
+
while ((m = ARKUI_ROUTER_RE.exec(safe))) {
|
|
735
|
+
const url = m[1];
|
|
736
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
737
|
+
const encl = nodes
|
|
738
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
739
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
740
|
+
if (!encl)
|
|
741
|
+
continue;
|
|
742
|
+
const suffix = `/src/main/ets/${url}.ets`;
|
|
743
|
+
let candidates = allFiles.filter((f) => f.endsWith(suffix));
|
|
744
|
+
if (candidates.length > 1) {
|
|
745
|
+
const scope = moduleScopeOf(file);
|
|
746
|
+
const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
|
|
747
|
+
if (sameModule.length > 0)
|
|
748
|
+
candidates = sameModule;
|
|
749
|
+
}
|
|
750
|
+
if (candidates.length !== 1)
|
|
751
|
+
continue; // ambiguous or unresolved — never guess
|
|
752
|
+
const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
|
|
753
|
+
if (!page)
|
|
754
|
+
continue;
|
|
755
|
+
const key = `${encl.id}>${page.id}`;
|
|
756
|
+
if (seen.has(key))
|
|
757
|
+
continue;
|
|
758
|
+
seen.add(key);
|
|
759
|
+
edges.push({
|
|
760
|
+
source: encl.id, target: page.id, kind: 'calls', line,
|
|
761
|
+
provenance: 'heuristic',
|
|
762
|
+
metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return edges;
|
|
767
|
+
}
|
|
434
768
|
/**
|
|
435
769
|
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
436
770
|
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
@@ -441,14 +775,17 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
441
775
|
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
442
776
|
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
443
777
|
*/
|
|
444
|
-
function cppOverrideEdges(queries) {
|
|
778
|
+
async function cppOverrideEdges(queries, onYield) {
|
|
779
|
+
let scanned255 = 0;
|
|
445
780
|
const edges = [];
|
|
446
781
|
const seen = new Set();
|
|
447
782
|
const methodsOf = (classId) => queries
|
|
448
783
|
.getOutgoingEdges(classId, ['contains'])
|
|
449
784
|
.map((e) => queries.getNodeById(e.target))
|
|
450
785
|
.filter((n) => !!n && n.kind === 'method');
|
|
451
|
-
for (const cls of queries.
|
|
786
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
787
|
+
if ((++scanned255 & 63) === 0)
|
|
788
|
+
await onYield();
|
|
452
789
|
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
453
790
|
if (subMethods.length === 0)
|
|
454
791
|
continue;
|
|
@@ -502,6 +839,7 @@ function cppOverrideEdges(queries) {
|
|
|
502
839
|
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
503
840
|
const IFACE_OVERRIDE_LANGS = new Set([
|
|
504
841
|
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
842
|
+
'arkts',
|
|
505
843
|
]);
|
|
506
844
|
/**
|
|
507
845
|
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
@@ -515,7 +853,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
|
|
|
515
853
|
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
516
854
|
* (`any`) are skipped so they don't match every struct.
|
|
517
855
|
*/
|
|
518
|
-
function goImplementsEdges(queries) {
|
|
856
|
+
async function goImplementsEdges(queries, onYield) {
|
|
857
|
+
let scanned255 = 0;
|
|
519
858
|
const edges = [];
|
|
520
859
|
const seen = new Set();
|
|
521
860
|
const methodNameSet = (id) => new Set(queries
|
|
@@ -523,11 +862,24 @@ function goImplementsEdges(queries) {
|
|
|
523
862
|
.map((e) => queries.getNodeById(e.target))
|
|
524
863
|
.filter((n) => !!n && n.kind === 'method')
|
|
525
864
|
.map((n) => n.name));
|
|
526
|
-
|
|
865
|
+
// Materializes GO structs only (the pass is language-gated by the caller),
|
|
866
|
+
// never the whole struct kind — that array is O(nodes) on struct-heavy
|
|
867
|
+
// repos like the Linux kernel (#1212).
|
|
868
|
+
const goStructs = [];
|
|
869
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
870
|
+
if ((++scanned255 & 63) === 0)
|
|
871
|
+
await onYield();
|
|
872
|
+
if (s.language === 'go')
|
|
873
|
+
goStructs.push(s);
|
|
874
|
+
}
|
|
527
875
|
const structMethods = new Map();
|
|
528
876
|
for (const s of goStructs)
|
|
529
877
|
structMethods.set(s.id, methodNameSet(s.id));
|
|
530
|
-
for (const iface of queries.
|
|
878
|
+
for (const iface of queries.iterateNodesByKind('interface')) {
|
|
879
|
+
if ((++scanned255 & 63) === 0)
|
|
880
|
+
await onYield();
|
|
881
|
+
if ((++scanned255 & 63) === 0)
|
|
882
|
+
await onYield();
|
|
531
883
|
if (iface.language !== 'go')
|
|
532
884
|
continue;
|
|
533
885
|
const want = methodNameSet(iface.id);
|
|
@@ -587,7 +939,8 @@ function goImplementsEdges(queries) {
|
|
|
587
939
|
* matching the same-file edges extraction already emits). Skips methods that
|
|
588
940
|
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
589
941
|
*/
|
|
590
|
-
function goCrossFileMethodContainsEdges(queries) {
|
|
942
|
+
async function goCrossFileMethodContainsEdges(queries, onYield) {
|
|
943
|
+
let scanned255 = 0;
|
|
591
944
|
const edges = [];
|
|
592
945
|
const seen = new Set();
|
|
593
946
|
const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
@@ -595,7 +948,11 @@ function goCrossFileMethodContainsEdges(queries) {
|
|
|
595
948
|
const i = p.replace(/\\/g, '/').lastIndexOf('/');
|
|
596
949
|
return i >= 0 ? p.slice(0, i) : '';
|
|
597
950
|
};
|
|
598
|
-
for (const method of queries.
|
|
951
|
+
for (const method of queries.iterateNodesByKind('method')) {
|
|
952
|
+
if ((++scanned255 & 63) === 0)
|
|
953
|
+
await onYield();
|
|
954
|
+
if ((++scanned255 & 63) === 0)
|
|
955
|
+
await onYield();
|
|
599
956
|
if (method.language !== 'go')
|
|
600
957
|
continue;
|
|
601
958
|
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
@@ -665,13 +1022,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
|
|
|
665
1022
|
function kmpKindsCompatible(a, b) {
|
|
666
1023
|
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
667
1024
|
}
|
|
668
|
-
function kotlinExpectActualEdges(queries) {
|
|
1025
|
+
async function kotlinExpectActualEdges(queries, onYield) {
|
|
1026
|
+
let scanned255 = 0;
|
|
669
1027
|
const edges = [];
|
|
670
1028
|
const seen = new Set();
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1029
|
+
// SQL-side language+decorator pre-filter, streamed. The old
|
|
1030
|
+
// `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
|
|
1031
|
+
// just to find kotlin `actual` declarations — on a 2M-node graph that alone
|
|
1032
|
+
// exceeded Node's default heap and killed the index (#1212). The LIKE
|
|
1033
|
+
// pre-filter can over-match (substring), so the exact decorator check stays.
|
|
1034
|
+
for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
|
|
1035
|
+
if ((++scanned255 & 63) === 0)
|
|
1036
|
+
await onYield();
|
|
1037
|
+
if (!act.decorators?.includes('actual'))
|
|
1038
|
+
continue;
|
|
675
1039
|
let added = 0;
|
|
676
1040
|
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
677
1041
|
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
@@ -705,7 +1069,8 @@ function kotlinExpectActualEdges(queries) {
|
|
|
705
1069
|
}
|
|
706
1070
|
return edges;
|
|
707
1071
|
}
|
|
708
|
-
function interfaceOverrideEdges(queries) {
|
|
1072
|
+
async function interfaceOverrideEdges(queries, onYield) {
|
|
1073
|
+
let scanned255 = 0;
|
|
709
1074
|
const edges = [];
|
|
710
1075
|
const seen = new Set();
|
|
711
1076
|
const methodsOf = (classId) => queries
|
|
@@ -717,7 +1082,9 @@ function interfaceOverrideEdges(queries) {
|
|
|
717
1082
|
// types that conform to protocols. Iterate both.
|
|
718
1083
|
const concreteKinds = ['class', 'struct'];
|
|
719
1084
|
for (const kind of concreteKinds) {
|
|
720
|
-
for (const cls of queries.
|
|
1085
|
+
for (const cls of queries.iterateNodesByKind(kind)) {
|
|
1086
|
+
if ((++scanned255 & 63) === 0)
|
|
1087
|
+
await onYield();
|
|
721
1088
|
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
722
1089
|
if (implMethods.length === 0)
|
|
723
1090
|
continue;
|
|
@@ -791,7 +1158,8 @@ function interfaceOverrideEdges(queries) {
|
|
|
791
1158
|
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
792
1159
|
* stub's source line is the wiring site shown in the trace trail.
|
|
793
1160
|
*/
|
|
794
|
-
function goGrpcStubImplEdges(queries) {
|
|
1161
|
+
async function goGrpcStubImplEdges(queries, onYield) {
|
|
1162
|
+
let scanned255 = 0;
|
|
795
1163
|
const edges = [];
|
|
796
1164
|
const seen = new Set();
|
|
797
1165
|
const STUB_RE = /^Unimplemented.*Server$/;
|
|
@@ -803,7 +1171,9 @@ function goGrpcStubImplEdges(queries) {
|
|
|
803
1171
|
const methodNamesByStruct = new Map();
|
|
804
1172
|
const methodNodesByStruct = new Map();
|
|
805
1173
|
const goStructs = [];
|
|
806
|
-
for (const s of queries.
|
|
1174
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
1175
|
+
if ((++scanned255 & 63) === 0)
|
|
1176
|
+
await onYield();
|
|
807
1177
|
if (s.language !== 'go')
|
|
808
1178
|
continue;
|
|
809
1179
|
goStructs.push(s);
|
|
@@ -887,11 +1257,14 @@ function goGrpcStubImplEdges(queries) {
|
|
|
887
1257
|
* (or nothing) and are dropped.
|
|
888
1258
|
*/
|
|
889
1259
|
async function reactJsxChildEdges(ctx, onYield) {
|
|
1260
|
+
let scannedFiles = 0;
|
|
890
1261
|
const edges = [];
|
|
891
1262
|
const seen = new Set();
|
|
892
1263
|
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
893
1264
|
let scanned = 0;
|
|
894
1265
|
for (const file of ctx.getAllFiles()) {
|
|
1266
|
+
if ((++scannedFiles & 15) === 0)
|
|
1267
|
+
await onYield();
|
|
895
1268
|
if ((++scanned & 255) === 0)
|
|
896
1269
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
897
1270
|
const content = ctx.readFile(file);
|
|
@@ -941,7 +1314,9 @@ async function reactJsxChildEdges(ctx, onYield) {
|
|
|
941
1314
|
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
942
1315
|
* skipped.
|
|
943
1316
|
*/
|
|
944
|
-
function vueTemplateEdges(ctx) {
|
|
1317
|
+
async function vueTemplateEdges(ctx, onYield) {
|
|
1318
|
+
let scannedFiles = 0;
|
|
1319
|
+
let scanned255 = 0;
|
|
945
1320
|
const edges = [];
|
|
946
1321
|
const seen = new Set();
|
|
947
1322
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
@@ -955,12 +1330,16 @@ function vueTemplateEdges(ctx) {
|
|
|
955
1330
|
// misses it (flat components match by basename and don't need this). Map each
|
|
956
1331
|
// nested component's Nuxt name → node so those template usages resolve.
|
|
957
1332
|
const nuxtComponents = new Map();
|
|
958
|
-
for (const c of ctx.getNodesByKind('component')) {
|
|
1333
|
+
for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1334
|
+
if ((++scanned255 & 63) === 0)
|
|
1335
|
+
await onYield();
|
|
959
1336
|
const nn = nuxtComponentName(c.filePath);
|
|
960
1337
|
if (nn && !nuxtComponents.has(nn))
|
|
961
1338
|
nuxtComponents.set(nn, c);
|
|
962
1339
|
}
|
|
963
1340
|
for (const file of ctx.getAllFiles()) {
|
|
1341
|
+
if ((++scannedFiles & 15) === 0)
|
|
1342
|
+
await onYield();
|
|
964
1343
|
if (!file.endsWith('.vue'))
|
|
965
1344
|
continue;
|
|
966
1345
|
const content = ctx.readFile(file);
|
|
@@ -1094,18 +1473,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
|
1094
1473
|
// statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
|
|
1095
1474
|
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
1096
1475
|
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
1097
|
-
function rnEventEdges(ctx) {
|
|
1476
|
+
async function rnEventEdges(ctx, onYield) {
|
|
1477
|
+
let scannedFiles = 0;
|
|
1098
1478
|
// Native dispatchers (source = the native method whose body sends the
|
|
1099
1479
|
// event) and JS handlers (target = the function/method registered as
|
|
1100
1480
|
// the listener) keyed by event name.
|
|
1101
1481
|
const nativeDispatchersByEvent = new Map();
|
|
1102
1482
|
const jsHandlersByEvent = new Map();
|
|
1103
1483
|
for (const file of ctx.getAllFiles()) {
|
|
1484
|
+
if ((++scannedFiles & 15) === 0)
|
|
1485
|
+
await onYield();
|
|
1104
1486
|
const content = ctx.readFile(file);
|
|
1105
1487
|
if (!content)
|
|
1106
1488
|
continue;
|
|
1107
1489
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
1108
|
-
const lineOf = (
|
|
1490
|
+
const lineOf = makeLineAt(content, 1);
|
|
1109
1491
|
const addDispatcher = (event, line) => {
|
|
1110
1492
|
const disp = enclosingFn(nodesInFile, line);
|
|
1111
1493
|
if (!disp)
|
|
@@ -1288,11 +1670,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
|
|
|
1288
1670
|
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
1289
1671
|
* `<file>::<module>.<method>` by the framework extractor.
|
|
1290
1672
|
*/
|
|
1291
|
-
function expoCrossPlatformEdges(queries) {
|
|
1673
|
+
async function expoCrossPlatformEdges(queries, onYield) {
|
|
1674
|
+
let scanned255 = 0;
|
|
1292
1675
|
const edges = [];
|
|
1293
1676
|
const seen = new Set();
|
|
1294
1677
|
const byKey = new Map();
|
|
1295
|
-
for (const m of queries.
|
|
1678
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1679
|
+
if ((++scanned255 & 63) === 0)
|
|
1680
|
+
await onYield();
|
|
1296
1681
|
if (!m.id.startsWith('expo-module:'))
|
|
1297
1682
|
continue;
|
|
1298
1683
|
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
@@ -1341,7 +1726,8 @@ function expoCrossPlatformEdges(queries) {
|
|
|
1341
1726
|
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
1342
1727
|
* lines up with the bare Android method name.
|
|
1343
1728
|
*/
|
|
1344
|
-
function rnCrossPlatformEdges(queries) {
|
|
1729
|
+
async function rnCrossPlatformEdges(queries, onYield) {
|
|
1730
|
+
let scanned255 = 0;
|
|
1345
1731
|
const edges = [];
|
|
1346
1732
|
const seen = new Set();
|
|
1347
1733
|
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
@@ -1363,6 +1749,8 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1363
1749
|
// below only runs for genuine cross-platform candidates.
|
|
1364
1750
|
const byName = new Map();
|
|
1365
1751
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1752
|
+
if ((++scanned255 & 63) === 0)
|
|
1753
|
+
await onYield();
|
|
1366
1754
|
if (!NATIVE.has(m.language))
|
|
1367
1755
|
continue;
|
|
1368
1756
|
const key = norm(m.name);
|
|
@@ -1413,17 +1801,27 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1413
1801
|
}
|
|
1414
1802
|
return edges;
|
|
1415
1803
|
}
|
|
1416
|
-
function fabricNativeImplEdges(ctx) {
|
|
1804
|
+
async function fabricNativeImplEdges(ctx, onYield) {
|
|
1805
|
+
let scanned255 = 0;
|
|
1417
1806
|
const edges = [];
|
|
1418
1807
|
const seen = new Set();
|
|
1419
1808
|
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
1420
|
-
// filter to just those
|
|
1421
|
-
|
|
1809
|
+
// filter to just those while streaming — never materializing the whole
|
|
1810
|
+
// `component` kind (#1212).
|
|
1811
|
+
const components = [];
|
|
1812
|
+
for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1813
|
+
if ((++scanned255 & 63) === 0)
|
|
1814
|
+
await onYield();
|
|
1815
|
+
if (n.id.startsWith('fabric-component:'))
|
|
1816
|
+
components.push(n);
|
|
1817
|
+
}
|
|
1422
1818
|
if (components.length === 0)
|
|
1423
1819
|
return edges;
|
|
1424
1820
|
// Pre-index native classes by name for O(1) lookup.
|
|
1425
1821
|
const nativeClassesByName = new Map();
|
|
1426
|
-
for (const n of ctx.getNodesByKind('class')) {
|
|
1822
|
+
for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
|
|
1823
|
+
if ((++scanned255 & 63) === 0)
|
|
1824
|
+
await onYield();
|
|
1427
1825
|
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
|
|
1428
1826
|
continue;
|
|
1429
1827
|
const arr = nativeClassesByName.get(n.name);
|
|
@@ -1476,12 +1874,15 @@ function fabricNativeImplEdges(ctx) {
|
|
|
1476
1874
|
* same simple name) are dropped. We need-not bridge by package because Java
|
|
1477
1875
|
* mapper interfaces are typically uniquely named within a project.
|
|
1478
1876
|
*/
|
|
1479
|
-
function mybatisJavaXmlEdges(queries) {
|
|
1877
|
+
async function mybatisJavaXmlEdges(queries, onYield) {
|
|
1878
|
+
let scanned255 = 0;
|
|
1480
1879
|
const edges = [];
|
|
1481
1880
|
const seen = new Set();
|
|
1482
1881
|
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
1483
1882
|
const javaIndex = new Map();
|
|
1484
1883
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1884
|
+
if ((++scanned255 & 63) === 0)
|
|
1885
|
+
await onYield();
|
|
1485
1886
|
if (m.language !== 'java' && m.language !== 'kotlin')
|
|
1486
1887
|
continue;
|
|
1487
1888
|
const parts = m.qualifiedName.split('::');
|
|
@@ -1497,6 +1898,8 @@ function mybatisJavaXmlEdges(queries) {
|
|
|
1497
1898
|
javaIndex.set(key, [m]);
|
|
1498
1899
|
}
|
|
1499
1900
|
for (const xml of queries.iterateNodesByKind('method')) {
|
|
1901
|
+
if ((++scanned255 & 63) === 0)
|
|
1902
|
+
await onYield();
|
|
1500
1903
|
if (xml.language !== 'xml')
|
|
1501
1904
|
continue;
|
|
1502
1905
|
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
@@ -1610,10 +2013,14 @@ function goHandlerIdent(expr) {
|
|
|
1610
2013
|
const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
1611
2014
|
return m ? m[1] : null;
|
|
1612
2015
|
}
|
|
1613
|
-
function ginMiddlewareChainEdges(queries, ctx) {
|
|
2016
|
+
async function ginMiddlewareChainEdges(queries, ctx, onYield) {
|
|
2017
|
+
let scanned255 = 0;
|
|
2018
|
+
let scannedFiles = 0;
|
|
1614
2019
|
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
1615
2020
|
const dispatchers = [];
|
|
1616
2021
|
for (const n of queries.iterateNodesByKind('method')) {
|
|
2022
|
+
if ((++scanned255 & 63) === 0)
|
|
2023
|
+
await onYield();
|
|
1617
2024
|
if (n.language !== 'go')
|
|
1618
2025
|
continue;
|
|
1619
2026
|
const content = ctx.readFile(n.filePath);
|
|
@@ -1628,6 +2035,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1628
2035
|
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
1629
2036
|
const registered = new Map(); // name → registeredAt (file:line)
|
|
1630
2037
|
for (const file of ctx.getAllFiles()) {
|
|
2038
|
+
if ((++scannedFiles & 15) === 0)
|
|
2039
|
+
await onYield();
|
|
1631
2040
|
if (!file.endsWith('.go'))
|
|
1632
2041
|
continue;
|
|
1633
2042
|
const content = ctx.readFile(file);
|
|
@@ -1683,10 +2092,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1683
2092
|
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
1684
2093
|
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
1685
2094
|
*/
|
|
1686
|
-
function pascalFormEdges(ctx) {
|
|
2095
|
+
async function pascalFormEdges(ctx, onYield) {
|
|
2096
|
+
let scannedFiles = 0;
|
|
1687
2097
|
const edges = [];
|
|
1688
2098
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1689
2099
|
for (const file of allFiles) {
|
|
2100
|
+
if ((++scannedFiles & 255) === 0)
|
|
2101
|
+
await onYield();
|
|
1690
2102
|
if (!/\.(dfm|fmx)$/i.test(file))
|
|
1691
2103
|
continue;
|
|
1692
2104
|
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
@@ -1722,12 +2134,15 @@ function pascalFormEdges(ctx) {
|
|
|
1722
2134
|
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
1723
2135
|
* its loader.
|
|
1724
2136
|
*/
|
|
1725
|
-
function svelteKitLoadEdges(ctx) {
|
|
2137
|
+
async function svelteKitLoadEdges(ctx, onYield) {
|
|
2138
|
+
let scannedFiles = 0;
|
|
1726
2139
|
const edges = [];
|
|
1727
2140
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1728
2141
|
const HOOKS = new Set(['load', 'actions']);
|
|
1729
2142
|
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
1730
2143
|
for (const file of allFiles) {
|
|
2144
|
+
if ((++scannedFiles & 255) === 0)
|
|
2145
|
+
await onYield();
|
|
1731
2146
|
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
1732
2147
|
if (!m)
|
|
1733
2148
|
continue;
|
|
@@ -1760,91 +2175,6 @@ function svelteKitLoadEdges(ctx) {
|
|
|
1760
2175
|
}
|
|
1761
2176
|
return edges;
|
|
1762
2177
|
}
|
|
1763
|
-
/** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
|
|
1764
|
-
function arktsEntryEdges(queries, ctx) {
|
|
1765
|
-
const edges = [];
|
|
1766
|
-
const seen = new Set();
|
|
1767
|
-
const add = (source, target, synthesizedBy, extra = {}) => {
|
|
1768
|
-
const key = `${source}>${target}`;
|
|
1769
|
-
if (seen.has(key))
|
|
1770
|
-
return;
|
|
1771
|
-
seen.add(key);
|
|
1772
|
-
const targetNode = queries.getNodeById(target);
|
|
1773
|
-
edges.push({
|
|
1774
|
-
source,
|
|
1775
|
-
target,
|
|
1776
|
-
kind: 'calls',
|
|
1777
|
-
provenance: 'heuristic',
|
|
1778
|
-
metadata: {
|
|
1779
|
-
synthesizedBy,
|
|
1780
|
-
registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
|
|
1781
|
-
...extra,
|
|
1782
|
-
},
|
|
1783
|
-
});
|
|
1784
|
-
};
|
|
1785
|
-
for (const m of methodAndFunctionNodes(queries)) {
|
|
1786
|
-
if (m.language !== 'arkts')
|
|
1787
|
-
continue;
|
|
1788
|
-
const content = ctx.readFile(m.filePath);
|
|
1789
|
-
if (!content)
|
|
1790
|
-
continue;
|
|
1791
|
-
const lines = content.split('\n');
|
|
1792
|
-
const start = Math.max((m.startLine ?? 1) - 1, 0);
|
|
1793
|
-
const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
|
|
1794
|
-
const body = lines.slice(start, end).join('\n');
|
|
1795
|
-
const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
|
|
1796
|
-
if (!loadMatch)
|
|
1797
|
-
continue;
|
|
1798
|
-
const page = loadMatch[1];
|
|
1799
|
-
const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
|
|
1800
|
-
if (pageComponent) {
|
|
1801
|
-
const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
|
|
1802
|
-
if (aboutToAppear) {
|
|
1803
|
-
add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
|
|
1808
|
-
if (cls.language !== 'arkts')
|
|
1809
|
-
continue;
|
|
1810
|
-
const methods = queries
|
|
1811
|
-
.getOutgoingEdges(cls.id, ['contains'])
|
|
1812
|
-
.map((e) => queries.getNodeById(e.target))
|
|
1813
|
-
.filter((n) => !!n && n.kind === 'method');
|
|
1814
|
-
const onCreate = methods.find((n) => n.name === 'onCreate');
|
|
1815
|
-
const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
|
|
1816
|
-
if (onCreate && onWindowStageCreate) {
|
|
1817
|
-
add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
for (const comp of [
|
|
1821
|
-
...queries.getNodesByKind('component'),
|
|
1822
|
-
...queries.getNodesByKind('class'),
|
|
1823
|
-
...queries.getNodesByKind('struct'),
|
|
1824
|
-
]) {
|
|
1825
|
-
if (comp.language !== 'arkts')
|
|
1826
|
-
continue;
|
|
1827
|
-
const owner = comp.kind === 'component'
|
|
1828
|
-
? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
1829
|
-
queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
1830
|
-
comp
|
|
1831
|
-
: comp;
|
|
1832
|
-
const methods = queries
|
|
1833
|
-
.getOutgoingEdges(owner.id, ['contains'])
|
|
1834
|
-
.map((e) => queries.getNodeById(e.target))
|
|
1835
|
-
.filter((n) => !!n && n.kind === 'method');
|
|
1836
|
-
const build = methods.find((n) => n.name === 'build');
|
|
1837
|
-
if (!build)
|
|
1838
|
-
continue;
|
|
1839
|
-
for (const life of ['aboutToAppear', 'onPageShow']) {
|
|
1840
|
-
const lifeMethod = methods.find((n) => n.name === life);
|
|
1841
|
-
if (lifeMethod) {
|
|
1842
|
-
add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
return edges;
|
|
1847
|
-
}
|
|
1848
2178
|
/**
|
|
1849
2179
|
* Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
|
|
1850
2180
|
* (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
|
|
@@ -1861,10 +2191,13 @@ function arktsEntryEdges(queries, ctx) {
|
|
|
1861
2191
|
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1862
2192
|
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1863
2193
|
const THUNK_FANOUT_CAP = 24;
|
|
1864
|
-
function reduxThunkEdges(queries, ctx) {
|
|
2194
|
+
async function reduxThunkEdges(queries, ctx, onYield) {
|
|
2195
|
+
let scanned255 = 0;
|
|
1865
2196
|
const edges = [];
|
|
1866
2197
|
const seen = new Set();
|
|
1867
2198
|
for (const node of queries.iterateNodesByKind('constant')) {
|
|
2199
|
+
if ((++scanned255 & 63) === 0)
|
|
2200
|
+
await onYield();
|
|
1868
2201
|
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1869
2202
|
// avoids reading every constant's body on a large repo.
|
|
1870
2203
|
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
@@ -1996,10 +2329,13 @@ function resolveRegistryHandler(ctx, name, chained) {
|
|
|
1996
2329
|
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1997
2330
|
}
|
|
1998
2331
|
async function objectRegistryEdges(ctx, onYield) {
|
|
2332
|
+
let scannedFiles = 0;
|
|
1999
2333
|
const edges = [];
|
|
2000
2334
|
const seen = new Set();
|
|
2001
2335
|
let scanned = 0;
|
|
2002
2336
|
for (const file of ctx.getAllFiles()) {
|
|
2337
|
+
if ((++scannedFiles & 15) === 0)
|
|
2338
|
+
await onYield();
|
|
2003
2339
|
if ((++scanned & 255) === 0)
|
|
2004
2340
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
2005
2341
|
if (!REGISTRY_JS_EXT.test(file))
|
|
@@ -2111,10 +2447,13 @@ function rtkEndpointNameFromHook(hook) {
|
|
|
2111
2447
|
return null;
|
|
2112
2448
|
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2113
2449
|
}
|
|
2114
|
-
function rtkQueryEdges(queries, ctx) {
|
|
2450
|
+
async function rtkQueryEdges(queries, ctx, onYield) {
|
|
2451
|
+
let scanned255 = 0;
|
|
2115
2452
|
const edges = [];
|
|
2116
2453
|
const seen = new Set();
|
|
2117
2454
|
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2455
|
+
if ((++scanned255 & 63) === 0)
|
|
2456
|
+
await onYield();
|
|
2118
2457
|
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2119
2458
|
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2120
2459
|
continue;
|
|
@@ -2158,10 +2497,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
|
|
|
2158
2497
|
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2159
2498
|
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2160
2499
|
const PINIA_FANOUT_CAP = 80;
|
|
2161
|
-
function piniaStoreEdges(ctx) {
|
|
2500
|
+
async function piniaStoreEdges(ctx, onYield) {
|
|
2501
|
+
let scannedFiles = 0;
|
|
2162
2502
|
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2163
2503
|
const factoryFile = new Map();
|
|
2164
2504
|
for (const file of ctx.getAllFiles()) {
|
|
2505
|
+
if ((++scannedFiles & 15) === 0)
|
|
2506
|
+
await onYield();
|
|
2165
2507
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2166
2508
|
continue;
|
|
2167
2509
|
const content = ctx.readFile(file);
|
|
@@ -2177,6 +2519,8 @@ function piniaStoreEdges(ctx) {
|
|
|
2177
2519
|
const edges = [];
|
|
2178
2520
|
const seen = new Set();
|
|
2179
2521
|
for (const file of ctx.getAllFiles()) {
|
|
2522
|
+
if ((++scannedFiles & 15) === 0)
|
|
2523
|
+
await onYield();
|
|
2180
2524
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2181
2525
|
continue;
|
|
2182
2526
|
const content = ctx.readFile(file);
|
|
@@ -2249,7 +2593,8 @@ const VUEX_FANOUT_CAP = 120;
|
|
|
2249
2593
|
function pathHasSegment(filePath, seg) {
|
|
2250
2594
|
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2251
2595
|
}
|
|
2252
|
-
function vuexDispatchEdges(ctx) {
|
|
2596
|
+
async function vuexDispatchEdges(ctx, onYield) {
|
|
2597
|
+
let scannedFiles = 0;
|
|
2253
2598
|
const storeFileCache = new Map();
|
|
2254
2599
|
const isStoreFile = (file) => {
|
|
2255
2600
|
let v = storeFileCache.get(file);
|
|
@@ -2287,6 +2632,8 @@ function vuexDispatchEdges(ctx) {
|
|
|
2287
2632
|
const edges = [];
|
|
2288
2633
|
const seen = new Set();
|
|
2289
2634
|
for (const file of ctx.getAllFiles()) {
|
|
2635
|
+
if ((++scannedFiles & 15) === 0)
|
|
2636
|
+
await onYield();
|
|
2290
2637
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2291
2638
|
continue;
|
|
2292
2639
|
const content = ctx.readFile(file);
|
|
@@ -2346,7 +2693,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
|
|
|
2346
2693
|
const CELERY_PY_EXT = /\.py$/;
|
|
2347
2694
|
const CELERY_FANOUT_CAP = 80;
|
|
2348
2695
|
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2349
|
-
function celeryDispatchEdges(ctx) {
|
|
2696
|
+
async function celeryDispatchEdges(ctx, onYield) {
|
|
2697
|
+
let scannedFiles = 0;
|
|
2350
2698
|
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2351
2699
|
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2352
2700
|
// receivers, so the candidate set stays small.
|
|
@@ -2390,6 +2738,8 @@ function celeryDispatchEdges(ctx) {
|
|
|
2390
2738
|
const edges = [];
|
|
2391
2739
|
const seen = new Set();
|
|
2392
2740
|
for (const file of ctx.getAllFiles()) {
|
|
2741
|
+
if ((++scannedFiles & 15) === 0)
|
|
2742
|
+
await onYield();
|
|
2393
2743
|
if (!CELERY_PY_EXT.test(file))
|
|
2394
2744
|
continue;
|
|
2395
2745
|
const content = ctx.readFile(file);
|
|
@@ -2468,15 +2818,24 @@ function springFirstParamType(sig) {
|
|
|
2468
2818
|
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2469
2819
|
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2470
2820
|
}
|
|
2471
|
-
function springEventEdges(ctx) {
|
|
2821
|
+
async function springEventEdges(ctx, onYield) {
|
|
2822
|
+
let scannedFiles = 0;
|
|
2472
2823
|
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2824
|
+
// This is the ONLY full read sweep: publisher files are recorded here so
|
|
2825
|
+
// pass 2 re-reads just those instead of every .java file again (#1212 —
|
|
2826
|
+
// the double full-repo read was one of the tail's longest unyielded spans).
|
|
2473
2827
|
const listeners = new Map();
|
|
2828
|
+
const publisherFiles = [];
|
|
2474
2829
|
for (const file of ctx.getAllFiles()) {
|
|
2830
|
+
if ((++scannedFiles & 15) === 0)
|
|
2831
|
+
await onYield();
|
|
2475
2832
|
if (!SPRING_JAVA_EXT.test(file))
|
|
2476
2833
|
continue;
|
|
2477
2834
|
const content = ctx.readFile(file);
|
|
2478
2835
|
if (!content)
|
|
2479
2836
|
continue;
|
|
2837
|
+
if (content.includes('.publishEvent('))
|
|
2838
|
+
publisherFiles.push(file);
|
|
2480
2839
|
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2481
2840
|
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2482
2841
|
if (!hasAnno && !hasAppListener)
|
|
@@ -2516,12 +2875,13 @@ function springEventEdges(ctx) {
|
|
|
2516
2875
|
}
|
|
2517
2876
|
if (!listeners.size)
|
|
2518
2877
|
return [];
|
|
2519
|
-
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2878
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2879
|
+
// XEvent. Only the publisher files recorded in pass 1 are (re-)read.
|
|
2520
2880
|
const edges = [];
|
|
2521
2881
|
const seen = new Set();
|
|
2522
|
-
for (const file of
|
|
2523
|
-
if (
|
|
2524
|
-
|
|
2882
|
+
for (const file of publisherFiles) {
|
|
2883
|
+
if ((++scannedFiles & 15) === 0)
|
|
2884
|
+
await onYield();
|
|
2525
2885
|
const content = ctx.readFile(file);
|
|
2526
2886
|
if (!content || !content.includes('.publishEvent('))
|
|
2527
2887
|
continue;
|
|
@@ -2606,10 +2966,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
|
2606
2966
|
}
|
|
2607
2967
|
return declType;
|
|
2608
2968
|
}
|
|
2609
|
-
function mediatrDispatchEdges(ctx) {
|
|
2969
|
+
async function mediatrDispatchEdges(ctx, onYield) {
|
|
2970
|
+
let scannedFiles = 0;
|
|
2610
2971
|
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2611
2972
|
const handlers = new Map();
|
|
2612
2973
|
for (const file of ctx.getAllFiles()) {
|
|
2974
|
+
if ((++scannedFiles & 15) === 0)
|
|
2975
|
+
await onYield();
|
|
2613
2976
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2614
2977
|
continue;
|
|
2615
2978
|
const content = ctx.readFile(file);
|
|
@@ -2643,6 +3006,8 @@ function mediatrDispatchEdges(ctx) {
|
|
|
2643
3006
|
const edges = [];
|
|
2644
3007
|
const seen = new Set();
|
|
2645
3008
|
for (const file of ctx.getAllFiles()) {
|
|
3009
|
+
if ((++scannedFiles & 15) === 0)
|
|
3010
|
+
await onYield();
|
|
2646
3011
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2647
3012
|
continue;
|
|
2648
3013
|
const content = ctx.readFile(file);
|
|
@@ -2706,7 +3071,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
|
|
|
2706
3071
|
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2707
3072
|
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2708
3073
|
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2709
|
-
function sidekiqDispatchEdges(ctx) {
|
|
3074
|
+
async function sidekiqDispatchEdges(ctx, onYield) {
|
|
3075
|
+
let scannedFiles = 0;
|
|
2710
3076
|
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2711
3077
|
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2712
3078
|
const performCache = new Map();
|
|
@@ -2743,6 +3109,8 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2743
3109
|
const edges = [];
|
|
2744
3110
|
const seen = new Set();
|
|
2745
3111
|
for (const file of ctx.getAllFiles()) {
|
|
3112
|
+
if ((++scannedFiles & 15) === 0)
|
|
3113
|
+
await onYield();
|
|
2746
3114
|
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2747
3115
|
continue;
|
|
2748
3116
|
const content = ctx.readFile(file);
|
|
@@ -2778,6 +3146,414 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2778
3146
|
}
|
|
2779
3147
|
return edges;
|
|
2780
3148
|
}
|
|
3149
|
+
// ── Erlang behaviour-callback dispatch ────────────────────────────────────────
|
|
3150
|
+
// An Erlang behaviour is a compile-checked callback contract: the behaviour
|
|
3151
|
+
// module declares `-callback init(...) -> ...`, implementers declare
|
|
3152
|
+
// `-behaviour(B)` and export the callbacks, and the framework side dispatches
|
|
3153
|
+
// through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
|
|
3154
|
+
// `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
|
|
3155
|
+
// deliberately leaves var-module calls silent (no static target), so the flow
|
|
3156
|
+
// breaks at exactly the hop agents ask about (request → handler init). Bridge:
|
|
3157
|
+
//
|
|
3158
|
+
// dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
|
|
3159
|
+
// declaring `fn` with the SITE's arity — provided exactly ONE in-repo
|
|
3160
|
+
// behaviour declares (fn, arity); a name+arity collision across behaviours
|
|
3161
|
+
// bails (silent beats wrong) — and the implementer defines and exports `fn`.
|
|
3162
|
+
//
|
|
3163
|
+
// Behaviours are discovered by scanning every Erlang file for `-callback`
|
|
3164
|
+
// declarations (not just `implements` targets), so a behaviour with zero
|
|
3165
|
+
// implementers still participates in the ambiguity gate. Fan-out control: a
|
|
3166
|
+
// mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
|
|
3167
|
+
// hundreds of edges per site that READ as complete coverage while being
|
|
3168
|
+
// arbitrary — above the cap the site is skipped entirely and the boundary
|
|
3169
|
+
// stays visibly dynamic (explore's boundary announcer covers it) instead of
|
|
3170
|
+
// silently truncated.
|
|
3171
|
+
const ERLANG_EXT = /\.(?:erl|hrl)$/;
|
|
3172
|
+
// `Var:fn(` — variable (capitalized) module, lowercase function, immediate
|
|
3173
|
+
// open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
|
|
3174
|
+
// (static remote call, already linked), and mid-word matches.
|
|
3175
|
+
const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
|
|
3176
|
+
const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
|
|
3177
|
+
const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
|
|
3178
|
+
/**
|
|
3179
|
+
* Argument count of the call/declaration whose `(` sits at `openIdx` —
|
|
3180
|
+
* top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
|
|
3181
|
+
* (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
|
|
3182
|
+
* so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
|
|
3183
|
+
*/
|
|
3184
|
+
function erlangArityAt(src, openIdx) {
|
|
3185
|
+
let depth = 1;
|
|
3186
|
+
let commas = 0;
|
|
3187
|
+
let sawArg = false;
|
|
3188
|
+
const limit = Math.min(src.length, openIdx + 4000);
|
|
3189
|
+
for (let i = openIdx + 1; i < limit; i++) {
|
|
3190
|
+
const ch = src[i];
|
|
3191
|
+
if (ch === '"' || ch === "'") {
|
|
3192
|
+
i++;
|
|
3193
|
+
while (i < limit && src[i] !== ch) {
|
|
3194
|
+
if (src[i] === '\\')
|
|
3195
|
+
i++;
|
|
3196
|
+
i++;
|
|
3197
|
+
}
|
|
3198
|
+
sawArg = true;
|
|
3199
|
+
continue;
|
|
3200
|
+
}
|
|
3201
|
+
if (ch === '$') {
|
|
3202
|
+
i++;
|
|
3203
|
+
if (src[i] === '\\')
|
|
3204
|
+
i++;
|
|
3205
|
+
sawArg = true;
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
3209
|
+
depth++;
|
|
3210
|
+
sawArg = true;
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
3214
|
+
depth--;
|
|
3215
|
+
if (depth === 0)
|
|
3216
|
+
return sawArg ? commas + 1 : 0;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (ch === ',' && depth === 1) {
|
|
3220
|
+
commas++;
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
if (!/\s/.test(ch))
|
|
3224
|
+
sawArg = true;
|
|
3225
|
+
}
|
|
3226
|
+
return -1;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
|
|
3230
|
+
* DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
|
|
3231
|
+
* and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
|
|
3232
|
+
* config) — the connection happens by option-path unification inside the
|
|
3233
|
+
* module-system evaluator, so there is no static call/import edge to follow
|
|
3234
|
+
* and flow questions ("how does services.yabai.enable become a launchd
|
|
3235
|
+
* service?") go dark at the module boundary.
|
|
3236
|
+
*
|
|
3237
|
+
* This pass links each config-write binding to the option declaration whose
|
|
3238
|
+
* path is the longest static-segment prefix of the write path. Precision gates:
|
|
3239
|
+
* - only STATIC segments participate: plain identifiers, plus quoted segments
|
|
3240
|
+
* (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
|
|
3241
|
+
* only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
|
|
3242
|
+
* so dynamic paths never match beyond their static head;
|
|
3243
|
+
* - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
|
|
3244
|
+
* every package's `meta = { ... }` attrset to nixos's `options.meta`;
|
|
3245
|
+
* - a prefix declared in more than one file is ambiguous → no edge (a wrong
|
|
3246
|
+
* edge is worse than none);
|
|
3247
|
+
* - writes physically inside an options block are declaration internals
|
|
3248
|
+
* (types, defaults, examples), never config writes → excluded.
|
|
3249
|
+
* Both declaration spellings register: flat (`options.a.b = ...`) by name, and
|
|
3250
|
+
* nested (`options = { a.b = ...; }`) by line-span containment.
|
|
3251
|
+
*/
|
|
3252
|
+
function nixLeadingPlainSegments(name) {
|
|
3253
|
+
const segs = [];
|
|
3254
|
+
let i = 0;
|
|
3255
|
+
const n = name.length;
|
|
3256
|
+
while (i < n) {
|
|
3257
|
+
if (name[i] === '"') {
|
|
3258
|
+
// Quoted segment — an opaque verbatim token (quotes kept, so it can
|
|
3259
|
+
// never collide with a plain identifier). `NSGlobalDomain."com.apple.
|
|
3260
|
+
// mouse.tapBehavior"` must match ITS OWN quoted declaration, not
|
|
3261
|
+
// whichever sibling registered the shared plain prefix first.
|
|
3262
|
+
let j = i + 1;
|
|
3263
|
+
while (j < n && name[j] !== '"') {
|
|
3264
|
+
if (name[j] === '\\')
|
|
3265
|
+
j++;
|
|
3266
|
+
j++;
|
|
3267
|
+
}
|
|
3268
|
+
if (j >= n)
|
|
3269
|
+
return segs; // unterminated — stop at the static head
|
|
3270
|
+
const tok = name.slice(i, j + 1);
|
|
3271
|
+
if (tok.includes('${'))
|
|
3272
|
+
return segs; // interpolated → dynamic → stop
|
|
3273
|
+
segs.push(tok);
|
|
3274
|
+
i = j + 1;
|
|
3275
|
+
if (i >= n)
|
|
3276
|
+
break;
|
|
3277
|
+
if (name[i] !== '.')
|
|
3278
|
+
return segs;
|
|
3279
|
+
i++;
|
|
3280
|
+
continue;
|
|
3281
|
+
}
|
|
3282
|
+
let j = i;
|
|
3283
|
+
while (j < n && name[j] !== '.') {
|
|
3284
|
+
if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
|
|
3285
|
+
return segs;
|
|
3286
|
+
j++;
|
|
3287
|
+
}
|
|
3288
|
+
const seg = name.slice(i, j);
|
|
3289
|
+
if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
|
|
3290
|
+
return segs;
|
|
3291
|
+
segs.push(seg);
|
|
3292
|
+
i = j + 1;
|
|
3293
|
+
}
|
|
3294
|
+
return segs;
|
|
3295
|
+
}
|
|
3296
|
+
async function nixOptionPathEdges(queries, onYield) {
|
|
3297
|
+
let scanned255 = 0;
|
|
3298
|
+
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
3299
|
+
// option); memory stays O(bindings-kept), not O(all nodes) (#610).
|
|
3300
|
+
const byFile = new Map();
|
|
3301
|
+
let scanned = 0;
|
|
3302
|
+
for (const kind of ['variable', 'function']) {
|
|
3303
|
+
for (const node of queries.iterateNodesByKind(kind)) {
|
|
3304
|
+
if ((++scanned255 & 63) === 0)
|
|
3305
|
+
await onYield();
|
|
3306
|
+
if ((++scanned & 0x3fff) === 0 && onYield)
|
|
3307
|
+
await onYield();
|
|
3308
|
+
if (node.language !== 'nix')
|
|
3309
|
+
continue;
|
|
3310
|
+
const segs = nixLeadingPlainSegments(node.name);
|
|
3311
|
+
if (segs.length === 0)
|
|
3312
|
+
continue;
|
|
3313
|
+
const rec = {
|
|
3314
|
+
id: node.id,
|
|
3315
|
+
filePath: node.filePath,
|
|
3316
|
+
startLine: node.startLine,
|
|
3317
|
+
endLine: node.endLine,
|
|
3318
|
+
segs,
|
|
3319
|
+
};
|
|
3320
|
+
const arr = byFile.get(node.filePath);
|
|
3321
|
+
if (arr)
|
|
3322
|
+
arr.push(rec);
|
|
3323
|
+
else
|
|
3324
|
+
byFile.set(node.filePath, [rec]);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
// Per file: walk bindings outermost-first with a stack of active option
|
|
3328
|
+
// spans, composing nested declaration paths (`options = { services.foo = {
|
|
3329
|
+
// enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
|
|
3330
|
+
// An `options` binding nested inside another option span is a SUBMODULE's
|
|
3331
|
+
// own namespace (`attrsOf (submodule { options = ...; })`) — its internals
|
|
3332
|
+
// are not globally addressable, so the sentinel blocks registration below it
|
|
3333
|
+
// while still excluding the region from write candidates.
|
|
3334
|
+
const SUBMODULE = 'submodule';
|
|
3335
|
+
const decls = new Map();
|
|
3336
|
+
const writes = [];
|
|
3337
|
+
const register = (path, rec) => {
|
|
3338
|
+
if (path.length < 2 || path.includes(SUBMODULE))
|
|
3339
|
+
return;
|
|
3340
|
+
const key = path.join('.');
|
|
3341
|
+
const arr = decls.get(key);
|
|
3342
|
+
if (arr)
|
|
3343
|
+
arr.push(rec);
|
|
3344
|
+
else
|
|
3345
|
+
decls.set(key, [rec]);
|
|
3346
|
+
};
|
|
3347
|
+
for (const recs of byFile.values()) {
|
|
3348
|
+
recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
3349
|
+
const stack = [];
|
|
3350
|
+
for (const rec of recs) {
|
|
3351
|
+
while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
|
|
3352
|
+
stack.pop();
|
|
3353
|
+
// Strict containment at line granularity: a one-line nested binding is
|
|
3354
|
+
// indistinguishable from its container, so it stays unclassified (rare
|
|
3355
|
+
// in module code, where option blocks are multi-line).
|
|
3356
|
+
const enclosing = stack.length > 0 &&
|
|
3357
|
+
rec.startLine >= stack[stack.length - 1].start &&
|
|
3358
|
+
rec.endLine <= stack[stack.length - 1].end &&
|
|
3359
|
+
!(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
|
|
3360
|
+
? stack[stack.length - 1]
|
|
3361
|
+
: null;
|
|
3362
|
+
if (rec.segs[0] === 'options') {
|
|
3363
|
+
const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
|
|
3364
|
+
const prefix = enclosing ? [SUBMODULE] : ownPath;
|
|
3365
|
+
register(prefix, rec);
|
|
3366
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix });
|
|
3367
|
+
continue;
|
|
3368
|
+
}
|
|
3369
|
+
if (enclosing) {
|
|
3370
|
+
const composed = [...enclosing.prefix, ...rec.segs];
|
|
3371
|
+
register(composed, rec);
|
|
3372
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
|
|
3373
|
+
continue;
|
|
3374
|
+
}
|
|
3375
|
+
if (rec.segs.length >= 2) {
|
|
3376
|
+
writes.push(rec);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (decls.size === 0 || writes.length === 0)
|
|
3381
|
+
return [];
|
|
3382
|
+
const edges = [];
|
|
3383
|
+
for (const w of writes) {
|
|
3384
|
+
// `config.services.x = ...` spells the same write with an explicit prefix.
|
|
3385
|
+
const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
|
|
3386
|
+
if (segs.length < 2)
|
|
3387
|
+
continue;
|
|
3388
|
+
// Longest prefix wins; an ambiguous longest match does NOT fall back to a
|
|
3389
|
+
// shorter one (that would link `services.nginx.virtualHosts.x` to
|
|
3390
|
+
// `options.services.nginx` when virtualHosts is the contested path).
|
|
3391
|
+
for (let len = Math.min(segs.length, 6); len >= 2; len--) {
|
|
3392
|
+
const candidates = decls.get(segs.slice(0, len).join('.'));
|
|
3393
|
+
if (!candidates || candidates.length === 0)
|
|
3394
|
+
continue;
|
|
3395
|
+
const files = new Set(candidates.map((c) => c.filePath));
|
|
3396
|
+
if (files.size === 1) {
|
|
3397
|
+
const target = candidates[0];
|
|
3398
|
+
if (target.id !== w.id) {
|
|
3399
|
+
edges.push({
|
|
3400
|
+
source: w.id,
|
|
3401
|
+
target: target.id,
|
|
3402
|
+
kind: 'references',
|
|
3403
|
+
line: w.startLine,
|
|
3404
|
+
provenance: 'heuristic',
|
|
3405
|
+
metadata: {
|
|
3406
|
+
synthesizedBy: 'nix-option-path',
|
|
3407
|
+
optionPath: segs.slice(0, len).join('.'),
|
|
3408
|
+
registeredAt: `${target.filePath}:${target.startLine}`,
|
|
3409
|
+
},
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
break; // longest hit decides, matched or ambiguous
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
return edges;
|
|
3417
|
+
}
|
|
3418
|
+
async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
|
|
3419
|
+
let scannedFiles = 0;
|
|
3420
|
+
let scanned255 = 0;
|
|
3421
|
+
// Cheap language gate: no Erlang modules → no cost beyond one streamed
|
|
3422
|
+
// kind scan (never a materialized array of every namespace — #1212).
|
|
3423
|
+
const erlangModules = [];
|
|
3424
|
+
for (const n of queries.iterateNodesByKind('namespace')) {
|
|
3425
|
+
if ((++scanned255 & 63) === 0)
|
|
3426
|
+
await onYield();
|
|
3427
|
+
if (n.language === 'erlang')
|
|
3428
|
+
erlangModules.push(n);
|
|
3429
|
+
}
|
|
3430
|
+
if (erlangModules.length === 0)
|
|
3431
|
+
return [];
|
|
3432
|
+
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
3433
|
+
// its (name, arity) callback set, and the global `name/arity` → declaring
|
|
3434
|
+
// behaviours map that drives the ambiguity gate.
|
|
3435
|
+
const moduleByFile = new Map();
|
|
3436
|
+
for (const ns of erlangModules) {
|
|
3437
|
+
if (!moduleByFile.has(ns.filePath))
|
|
3438
|
+
moduleByFile.set(ns.filePath, ns);
|
|
3439
|
+
}
|
|
3440
|
+
const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
|
|
3441
|
+
const callbackNames = new Set();
|
|
3442
|
+
for (const file of ctx.getAllFiles()) {
|
|
3443
|
+
if ((++scannedFiles & 15) === 0)
|
|
3444
|
+
await onYield();
|
|
3445
|
+
if (!ERLANG_EXT.test(file))
|
|
3446
|
+
continue;
|
|
3447
|
+
const behaviour = moduleByFile.get(file);
|
|
3448
|
+
if (!behaviour)
|
|
3449
|
+
continue; // a .hrl or module-less file can't be a behaviour
|
|
3450
|
+
const content = ctx.readFile(file);
|
|
3451
|
+
if (!content || !content.includes('-callback'))
|
|
3452
|
+
continue;
|
|
3453
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3454
|
+
ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
|
|
3455
|
+
let m;
|
|
3456
|
+
while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
|
|
3457
|
+
const name = m[2].replace(/^'|'$/g, '');
|
|
3458
|
+
const arity = erlangArityAt(safe, m.index + m[0].length - 1);
|
|
3459
|
+
if (arity < 0)
|
|
3460
|
+
continue;
|
|
3461
|
+
const key = `${name}/${arity}`;
|
|
3462
|
+
const arr = declaringBehaviours.get(key);
|
|
3463
|
+
if (arr) {
|
|
3464
|
+
if (!arr.some((b) => b.id === behaviour.id))
|
|
3465
|
+
arr.push(behaviour);
|
|
3466
|
+
}
|
|
3467
|
+
else {
|
|
3468
|
+
declaringBehaviours.set(key, [behaviour]);
|
|
3469
|
+
}
|
|
3470
|
+
callbackNames.add(name);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
if (declaringBehaviours.size === 0)
|
|
3474
|
+
return [];
|
|
3475
|
+
// Implementer target lookup, lazy per (behaviour, fn): implementers come
|
|
3476
|
+
// from the `implements` edges extraction resolved, and the target is the
|
|
3477
|
+
// implementer module's own exported `fn` function node.
|
|
3478
|
+
const targetCache = new Map();
|
|
3479
|
+
const targetsOf = (behaviour, fn) => {
|
|
3480
|
+
const cacheKey = `${behaviour.id}#${fn}`;
|
|
3481
|
+
let targets = targetCache.get(cacheKey);
|
|
3482
|
+
if (targets)
|
|
3483
|
+
return targets;
|
|
3484
|
+
targets = [];
|
|
3485
|
+
for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
|
|
3486
|
+
const impl = queries.getNodeById(e.source);
|
|
3487
|
+
if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
|
|
3488
|
+
continue;
|
|
3489
|
+
const fnNode = ctx
|
|
3490
|
+
.getNodesInFile(impl.filePath)
|
|
3491
|
+
.find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
|
|
3492
|
+
if (fnNode)
|
|
3493
|
+
targets.push(fnNode);
|
|
3494
|
+
}
|
|
3495
|
+
targetCache.set(cacheKey, targets);
|
|
3496
|
+
return targets;
|
|
3497
|
+
};
|
|
3498
|
+
// Pass 2 — dispatch sites. Only files containing a var-module call shape are
|
|
3499
|
+
// scanned in full.
|
|
3500
|
+
const edges = [];
|
|
3501
|
+
const seen = new Set();
|
|
3502
|
+
for (const file of ctx.getAllFiles()) {
|
|
3503
|
+
if ((++scannedFiles & 15) === 0)
|
|
3504
|
+
await onYield();
|
|
3505
|
+
if (!ERLANG_EXT.test(file))
|
|
3506
|
+
continue;
|
|
3507
|
+
const content = ctx.readFile(file);
|
|
3508
|
+
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
|
|
3509
|
+
continue;
|
|
3510
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3511
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
3512
|
+
ERLANG_DISPATCH_RE.lastIndex = 0;
|
|
3513
|
+
let m;
|
|
3514
|
+
while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
|
|
3515
|
+
const fn = m[3];
|
|
3516
|
+
if (!callbackNames.has(fn))
|
|
3517
|
+
continue;
|
|
3518
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3519
|
+
const arity = erlangArityAt(safe, openIdx);
|
|
3520
|
+
if (arity < 0)
|
|
3521
|
+
continue;
|
|
3522
|
+
const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
|
|
3523
|
+
if (!behaviours || behaviours.length !== 1)
|
|
3524
|
+
continue; // unknown or ambiguous
|
|
3525
|
+
const behaviour = behaviours[0];
|
|
3526
|
+
const targets = targetsOf(behaviour, fn);
|
|
3527
|
+
if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
|
|
3528
|
+
continue;
|
|
3529
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
3530
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
3531
|
+
if (!disp)
|
|
3532
|
+
continue;
|
|
3533
|
+
for (const target of targets) {
|
|
3534
|
+
if (target.id === disp.id)
|
|
3535
|
+
continue;
|
|
3536
|
+
const key = `${disp.id}>${target.id}`;
|
|
3537
|
+
if (seen.has(key))
|
|
3538
|
+
continue;
|
|
3539
|
+
seen.add(key);
|
|
3540
|
+
edges.push({
|
|
3541
|
+
source: disp.id,
|
|
3542
|
+
target: target.id,
|
|
3543
|
+
kind: 'calls',
|
|
3544
|
+
line,
|
|
3545
|
+
provenance: 'heuristic',
|
|
3546
|
+
metadata: {
|
|
3547
|
+
synthesizedBy: 'erlang-behaviour',
|
|
3548
|
+
via: `${behaviour.name}:${fn}/${arity}`,
|
|
3549
|
+
registeredAt: `${file}:${line}`,
|
|
3550
|
+
},
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return edges;
|
|
3556
|
+
}
|
|
2781
3557
|
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2782
3558
|
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2783
3559
|
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
@@ -2824,7 +3600,8 @@ function phpArrayBody(src, openIdx) {
|
|
|
2824
3600
|
}
|
|
2825
3601
|
return null;
|
|
2826
3602
|
}
|
|
2827
|
-
function laravelEventEdges(ctx) {
|
|
3603
|
+
async function laravelEventEdges(ctx, onYield) {
|
|
3604
|
+
let scannedFiles = 0;
|
|
2828
3605
|
// event short name → its listener `handle` methods (deduped by node id).
|
|
2829
3606
|
const listeners = new Map();
|
|
2830
3607
|
const add = (event, handle) => {
|
|
@@ -2841,6 +3618,8 @@ function laravelEventEdges(ctx) {
|
|
|
2841
3618
|
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2842
3619
|
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2843
3620
|
for (const file of ctx.getAllFiles()) {
|
|
3621
|
+
if ((++scannedFiles & 15) === 0)
|
|
3622
|
+
await onYield();
|
|
2844
3623
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2845
3624
|
continue;
|
|
2846
3625
|
const content = ctx.readFile(file);
|
|
@@ -2886,6 +3665,8 @@ function laravelEventEdges(ctx) {
|
|
|
2886
3665
|
const edges = [];
|
|
2887
3666
|
const seen = new Set();
|
|
2888
3667
|
for (const file of ctx.getAllFiles()) {
|
|
3668
|
+
if ((++scannedFiles & 15) === 0)
|
|
3669
|
+
await onYield();
|
|
2889
3670
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2890
3671
|
continue;
|
|
2891
3672
|
const content = ctx.readFile(file);
|
|
@@ -2925,6 +3706,91 @@ function laravelEventEdges(ctx) {
|
|
|
2925
3706
|
}
|
|
2926
3707
|
return edges;
|
|
2927
3708
|
}
|
|
3709
|
+
/** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
|
|
3710
|
+
function arktsEntryEdges(queries, ctx) {
|
|
3711
|
+
const edges = [];
|
|
3712
|
+
const seen = new Set();
|
|
3713
|
+
const add = (source, target, synthesizedBy, extra = {}) => {
|
|
3714
|
+
const key = `${source}>${target}`;
|
|
3715
|
+
if (seen.has(key))
|
|
3716
|
+
return;
|
|
3717
|
+
seen.add(key);
|
|
3718
|
+
const targetNode = queries.getNodeById(target);
|
|
3719
|
+
edges.push({
|
|
3720
|
+
source,
|
|
3721
|
+
target,
|
|
3722
|
+
kind: 'calls',
|
|
3723
|
+
provenance: 'heuristic',
|
|
3724
|
+
metadata: {
|
|
3725
|
+
synthesizedBy,
|
|
3726
|
+
registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
|
|
3727
|
+
...extra,
|
|
3728
|
+
},
|
|
3729
|
+
});
|
|
3730
|
+
};
|
|
3731
|
+
for (const m of methodAndFunctionNodes(queries)) {
|
|
3732
|
+
if (m.language !== 'arkts')
|
|
3733
|
+
continue;
|
|
3734
|
+
const content = ctx.readFile(m.filePath);
|
|
3735
|
+
if (!content)
|
|
3736
|
+
continue;
|
|
3737
|
+
const lines = content.split('\n');
|
|
3738
|
+
const start = Math.max((m.startLine ?? 1) - 1, 0);
|
|
3739
|
+
const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
|
|
3740
|
+
const body = lines.slice(start, end).join('\n');
|
|
3741
|
+
const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
|
|
3742
|
+
if (!loadMatch)
|
|
3743
|
+
continue;
|
|
3744
|
+
const page = loadMatch[1];
|
|
3745
|
+
const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
|
|
3746
|
+
if (pageComponent) {
|
|
3747
|
+
const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
|
|
3748
|
+
if (aboutToAppear) {
|
|
3749
|
+
add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
|
|
3754
|
+
if (cls.language !== 'arkts')
|
|
3755
|
+
continue;
|
|
3756
|
+
const methods = queries
|
|
3757
|
+
.getOutgoingEdges(cls.id, ['contains'])
|
|
3758
|
+
.map((e) => queries.getNodeById(e.target))
|
|
3759
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
3760
|
+
const onCreate = methods.find((n) => n.name === 'onCreate');
|
|
3761
|
+
const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
|
|
3762
|
+
if (onCreate && onWindowStageCreate) {
|
|
3763
|
+
add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
for (const comp of [
|
|
3767
|
+
...queries.getNodesByKind('component'),
|
|
3768
|
+
...queries.getNodesByKind('class'),
|
|
3769
|
+
...queries.getNodesByKind('struct'),
|
|
3770
|
+
]) {
|
|
3771
|
+
if (comp.language !== 'arkts')
|
|
3772
|
+
continue;
|
|
3773
|
+
const owner = comp.kind === 'component'
|
|
3774
|
+
? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
3775
|
+
queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
3776
|
+
comp
|
|
3777
|
+
: comp;
|
|
3778
|
+
const methods = queries
|
|
3779
|
+
.getOutgoingEdges(owner.id, ['contains'])
|
|
3780
|
+
.map((e) => queries.getNodeById(e.target))
|
|
3781
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
3782
|
+
const build = methods.find((n) => n.name === 'build');
|
|
3783
|
+
if (!build)
|
|
3784
|
+
continue;
|
|
3785
|
+
for (const life of ['aboutToAppear', 'onPageShow']) {
|
|
3786
|
+
const lifeMethod = methods.find((n) => n.name === life);
|
|
3787
|
+
if (lifeMethod) {
|
|
3788
|
+
add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return edges;
|
|
3793
|
+
}
|
|
2928
3794
|
/**
|
|
2929
3795
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
2930
3796
|
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
|
@@ -2936,7 +3802,15 @@ function laravelEventEdges(ctx) {
|
|
|
2936
3802
|
* Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
|
|
2937
3803
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
2938
3804
|
*/
|
|
2939
|
-
|
|
3805
|
+
/**
|
|
3806
|
+
* Number of progress steps synthesizeCallbackEdges reports: one per `__mark()`
|
|
3807
|
+
* call (every synthesis pass, plus the dedupe-merge and edge-insert steps).
|
|
3808
|
+
* Cosmetic only — drift just makes the progress bar end early or jump — and a
|
|
3809
|
+
* test pins it to the actual `__mark(` call count so adding a pass without
|
|
3810
|
+
* bumping this fails loudly instead of silently skewing the bar.
|
|
3811
|
+
*/
|
|
3812
|
+
exports.SYNTH_PROGRESS_STEPS = 40;
|
|
3813
|
+
async function synthesizeCallbackEdges(queries, ctx, onProgress) {
|
|
2940
3814
|
// Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
|
|
2941
3815
|
// running synchronously on the indexer's main thread. Their AGGREGATE can run
|
|
2942
3816
|
// for well over a minute on a large repo — long enough for the #850 liveness
|
|
@@ -2945,87 +3819,189 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2945
3819
|
// that itself hangs (a real wedge) never reaches the next yield, so the
|
|
2946
3820
|
// watchdog still catches that. See ./cooperative-yield.
|
|
2947
3821
|
const yieldToLoop = (0, cooperative_yield_1.createYielder)();
|
|
3822
|
+
// Synthesis runs AFTER the resolution progress bar reaches 100%, so without
|
|
3823
|
+
// its own progress the UI freezes at "Resolving refs 100%" for the whole
|
|
3824
|
+
// tail — long enough on big repos that users conclude the index hung and
|
|
3825
|
+
// kill it. Report each completed pass; the caller surfaces it as its own
|
|
3826
|
+
// progress phase. Emit 0/total up front so the phase flips immediately.
|
|
3827
|
+
// Emissions are throttled to whole-percent movement (each consumes a UI
|
|
3828
|
+
// message); values may be fractional steps from within-pass reporting.
|
|
3829
|
+
let passesDone = 0;
|
|
3830
|
+
let lastPct = -1;
|
|
3831
|
+
const emit = (value) => {
|
|
3832
|
+
if (!onProgress)
|
|
3833
|
+
return;
|
|
3834
|
+
const v = Math.min(value, exports.SYNTH_PROGRESS_STEPS);
|
|
3835
|
+
const pct = Math.floor((v / exports.SYNTH_PROGRESS_STEPS) * 100);
|
|
3836
|
+
if (pct === lastPct)
|
|
3837
|
+
return;
|
|
3838
|
+
lastPct = pct;
|
|
3839
|
+
onProgress(v, exports.SYNTH_PROGRESS_STEPS);
|
|
3840
|
+
};
|
|
3841
|
+
// A single long pass otherwise parks the bar between steps; a pass that
|
|
3842
|
+
// takes this callback reports a 0..1 fraction of its own work, surfaced
|
|
3843
|
+
// here as fractional progress within its step.
|
|
3844
|
+
const subProgress = (fraction) => emit(passesDone + Math.max(0, Math.min(fraction, 1)));
|
|
3845
|
+
emit(0);
|
|
3846
|
+
// Per-pass wall-clock timing to stderr, opt-in via HOMEGRAPH_SYNTH_TIMINGS
|
|
3847
|
+
// (=1: passes over 250ms; =all: every pass). This is the diagnostic that
|
|
3848
|
+
// located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
|
|
3849
|
+
const markT = { t: Date.now() };
|
|
3850
|
+
const __mark = (label) => {
|
|
3851
|
+
const now = Date.now();
|
|
3852
|
+
const dt = now - markT.t;
|
|
3853
|
+
markT.t = now;
|
|
3854
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.HOMEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
3855
|
+
console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
3856
|
+
}
|
|
3857
|
+
passesDone++;
|
|
3858
|
+
emit(passesDone);
|
|
3859
|
+
};
|
|
3860
|
+
// Language gating: one indexed DISTINCT over the files table lets a pass
|
|
3861
|
+
// whose own filters reference a specific language/extension be skipped
|
|
3862
|
+
// outright when the project has no such files — its result is provably
|
|
3863
|
+
// empty, so skipping is behavior-identical and the cost drops to zero
|
|
3864
|
+
// (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
|
|
3865
|
+
// Passes without an explicit language filter always run.
|
|
3866
|
+
const langs = queries.getDistinctFileLanguages();
|
|
3867
|
+
const has = (...ls) => ls.some((l) => langs.has(l));
|
|
3868
|
+
const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
|
|
3869
|
+
const NONE = [];
|
|
2948
3870
|
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
2949
3871
|
// FIRST: a method declared in a different file from its receiver type is
|
|
2950
3872
|
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
2951
3873
|
// struct's method set from its `contains` edges — so without this it would
|
|
2952
3874
|
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
2953
|
-
const goMethodContains = goCrossFileMethodContainsEdges(queries);
|
|
2954
|
-
|
|
2955
|
-
queries.insertEdges(goMethodContains);
|
|
3875
|
+
const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
|
|
3876
|
+
for (let i = 0; i < goMethodContains.length; i += 2000) {
|
|
3877
|
+
queries.insertEdges(goMethodContains.slice(i, i + 2000));
|
|
3878
|
+
await yieldToLoop();
|
|
3879
|
+
}
|
|
2956
3880
|
await yieldToLoop();
|
|
3881
|
+
__mark('goMethodContains');
|
|
2957
3882
|
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
2958
3883
|
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
2959
3884
|
// Go has none statically. (Other languages already have static implements
|
|
2960
3885
|
// edges from extraction, so they don't need this pre-pass.)
|
|
2961
|
-
const goImpl = goImplementsEdges(queries);
|
|
2962
|
-
|
|
2963
|
-
queries.insertEdges(goImpl);
|
|
3886
|
+
const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
|
|
3887
|
+
for (let i = 0; i < goImpl.length; i += 2000) {
|
|
3888
|
+
queries.insertEdges(goImpl.slice(i, i + 2000));
|
|
3889
|
+
await yieldToLoop();
|
|
3890
|
+
}
|
|
2964
3891
|
await yieldToLoop();
|
|
3892
|
+
__mark('goImplements');
|
|
2965
3893
|
const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
|
|
2966
3894
|
await yieldToLoop();
|
|
3895
|
+
__mark('fieldEdges');
|
|
2967
3896
|
const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
|
|
2968
3897
|
await yieldToLoop();
|
|
3898
|
+
__mark('closureCollEdges');
|
|
2969
3899
|
const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
|
|
2970
3900
|
await yieldToLoop();
|
|
2971
|
-
|
|
3901
|
+
__mark('emitterEdges');
|
|
3902
|
+
const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
|
|
2972
3903
|
await yieldToLoop();
|
|
3904
|
+
__mark('renderEdges');
|
|
2973
3905
|
const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
|
|
2974
3906
|
await yieldToLoop();
|
|
2975
|
-
|
|
3907
|
+
__mark('jsxEdges');
|
|
3908
|
+
const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
|
|
2976
3909
|
await yieldToLoop();
|
|
2977
|
-
|
|
3910
|
+
__mark('vueEdges');
|
|
3911
|
+
const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
|
|
2978
3912
|
await yieldToLoop();
|
|
2979
|
-
|
|
3913
|
+
__mark('svelteKitEdges');
|
|
3914
|
+
const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
|
|
2980
3915
|
await yieldToLoop();
|
|
2981
|
-
|
|
3916
|
+
__mark('pascalEdges');
|
|
3917
|
+
const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2982
3918
|
await yieldToLoop();
|
|
2983
|
-
|
|
3919
|
+
__mark('flutterEdges');
|
|
3920
|
+
const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2984
3921
|
await yieldToLoop();
|
|
2985
|
-
|
|
3922
|
+
__mark('arkuiStateEdges');
|
|
3923
|
+
const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
|
|
2986
3924
|
await yieldToLoop();
|
|
2987
|
-
|
|
3925
|
+
__mark('arkuiEmitter');
|
|
3926
|
+
const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
|
|
2988
3927
|
await yieldToLoop();
|
|
2989
|
-
|
|
3928
|
+
__mark('arkuiRoutes');
|
|
3929
|
+
const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
|
|
2990
3930
|
await yieldToLoop();
|
|
2991
|
-
|
|
3931
|
+
__mark('cppEdges');
|
|
3932
|
+
const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
|
|
3933
|
+
? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
|
|
2992
3934
|
await yieldToLoop();
|
|
2993
|
-
|
|
3935
|
+
__mark('ifaceEdges');
|
|
3936
|
+
const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
|
|
2994
3937
|
await yieldToLoop();
|
|
2995
|
-
|
|
3938
|
+
__mark('kotlinExpectActual');
|
|
3939
|
+
const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
|
|
2996
3940
|
await yieldToLoop();
|
|
2997
|
-
|
|
3941
|
+
__mark('goGrpcEdges');
|
|
3942
|
+
const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
|
|
2998
3943
|
await yieldToLoop();
|
|
2999
|
-
|
|
3944
|
+
__mark('rnEventEdgesList');
|
|
3945
|
+
const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
|
|
3000
3946
|
await yieldToLoop();
|
|
3001
|
-
|
|
3947
|
+
__mark('fabricNativeEdges');
|
|
3948
|
+
const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
|
|
3002
3949
|
await yieldToLoop();
|
|
3003
|
-
|
|
3950
|
+
__mark('expoXPlatEdges');
|
|
3951
|
+
const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
|
|
3004
3952
|
await yieldToLoop();
|
|
3953
|
+
__mark('rnXPlatEdges');
|
|
3954
|
+
const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE;
|
|
3955
|
+
await yieldToLoop();
|
|
3956
|
+
__mark('mybatisEdges');
|
|
3957
|
+
const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3958
|
+
await yieldToLoop();
|
|
3959
|
+
__mark('ginEdges');
|
|
3960
|
+
const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3961
|
+
await yieldToLoop();
|
|
3962
|
+
__mark('thunkEdges');
|
|
3005
3963
|
const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
|
|
3006
3964
|
await yieldToLoop();
|
|
3007
|
-
|
|
3965
|
+
__mark('registryEdges');
|
|
3966
|
+
const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3008
3967
|
await yieldToLoop();
|
|
3009
|
-
|
|
3968
|
+
__mark('rtkEdges');
|
|
3969
|
+
const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
|
|
3010
3970
|
await yieldToLoop();
|
|
3011
|
-
|
|
3971
|
+
__mark('piniaEdges');
|
|
3972
|
+
const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3012
3973
|
await yieldToLoop();
|
|
3013
|
-
|
|
3974
|
+
__mark('vuexEdges');
|
|
3975
|
+
const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3014
3976
|
await yieldToLoop();
|
|
3015
|
-
|
|
3977
|
+
__mark('celeryEdges');
|
|
3978
|
+
const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
|
|
3016
3979
|
await yieldToLoop();
|
|
3017
|
-
|
|
3980
|
+
__mark('springEdges');
|
|
3981
|
+
const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3018
3982
|
await yieldToLoop();
|
|
3019
|
-
|
|
3983
|
+
__mark('mediatrEdges');
|
|
3984
|
+
const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3020
3985
|
await yieldToLoop();
|
|
3021
|
-
|
|
3986
|
+
__mark('sidekiqEdges');
|
|
3987
|
+
const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3022
3988
|
await yieldToLoop();
|
|
3023
|
-
|
|
3989
|
+
__mark('erlangBehaviourEdges');
|
|
3990
|
+
const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
|
|
3024
3991
|
await yieldToLoop();
|
|
3025
|
-
|
|
3992
|
+
__mark('laravelEdges');
|
|
3993
|
+
const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop, subProgress) : NONE;
|
|
3026
3994
|
await yieldToLoop();
|
|
3027
|
-
|
|
3995
|
+
__mark('cFnPtrEdges');
|
|
3996
|
+
const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
|
|
3028
3997
|
await yieldToLoop();
|
|
3998
|
+
__mark('goframeEdges');
|
|
3999
|
+
const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE;
|
|
4000
|
+
await yieldToLoop();
|
|
4001
|
+
__mark('nixOptionEdges');
|
|
4002
|
+
const arktsStartupEdges = has('arkts') ? arktsEntryEdges(queries, ctx) : NONE;
|
|
4003
|
+
await yieldToLoop();
|
|
4004
|
+
__mark('arktsStartupEdges');
|
|
3029
4005
|
const merged = [];
|
|
3030
4006
|
const seen = new Set();
|
|
3031
4007
|
for (const e of [
|
|
@@ -3038,6 +4014,9 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
3038
4014
|
...svelteKitEdges,
|
|
3039
4015
|
...pascalEdges,
|
|
3040
4016
|
...flutterEdges,
|
|
4017
|
+
...arkuiStateEdges,
|
|
4018
|
+
...arkuiEmitter,
|
|
4019
|
+
...arkuiRoutes,
|
|
3041
4020
|
...cppEdges,
|
|
3042
4021
|
...ifaceEdges,
|
|
3043
4022
|
...kotlinExpectActual,
|
|
@@ -3057,9 +4036,11 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
3057
4036
|
...springEdges,
|
|
3058
4037
|
...mediatrEdges,
|
|
3059
4038
|
...sidekiqEdges,
|
|
4039
|
+
...erlangBehaviourEdges,
|
|
3060
4040
|
...laravelEdges,
|
|
3061
4041
|
...cFnPtrEdges,
|
|
3062
4042
|
...goframeEdges,
|
|
4043
|
+
...nixOptionEdges,
|
|
3063
4044
|
...arktsStartupEdges,
|
|
3064
4045
|
]) {
|
|
3065
4046
|
const key = `${e.source}>${e.target}`;
|
|
@@ -3068,8 +4049,15 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
3068
4049
|
seen.add(key);
|
|
3069
4050
|
merged.push(e);
|
|
3070
4051
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
4052
|
+
__mark('dedupe-merge');
|
|
4053
|
+
// Chunked insert with yields: on the Linux kernel the merged synthesized
|
|
4054
|
+
// edge set is ~275k rows, and one transaction for all of them was a 20s
|
|
4055
|
+
// unyielded main-thread span (#1212 follow-up) — the last one in the tail.
|
|
4056
|
+
for (let i = 0; i < merged.length; i += 2000) {
|
|
4057
|
+
queries.insertEdges(merged.slice(i, i + 2000));
|
|
4058
|
+
await yieldToLoop();
|
|
4059
|
+
}
|
|
4060
|
+
__mark('insertMergedEdges');
|
|
3073
4061
|
return merged.length + goImpl.length + goMethodContains.length;
|
|
3074
4062
|
}
|
|
3075
4063
|
//# sourceMappingURL=callback-synthesizer.js.map
|