@aroman22/codegraph-vba-darwin-arm64 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codegraph +5 -0
- package/lib/dist/bin/codegraph.d.ts +1 -1
- package/lib/dist/bin/codegraph.d.ts.map +1 -1
- package/lib/dist/bin/codegraph.js +307 -81
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/bin/command-supervision.d.ts +38 -1
- package/lib/dist/bin/command-supervision.d.ts.map +1 -1
- package/lib/dist/bin/command-supervision.js +22 -3
- package/lib/dist/bin/command-supervision.js.map +1 -1
- package/lib/dist/db/index.d.ts +84 -5
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +209 -12
- package/lib/dist/db/index.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +80 -3
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +116 -4
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +294 -7
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +43 -1
- package/lib/dist/db/wal-valve.d.ts +106 -0
- package/lib/dist/db/wal-valve.d.ts.map +1 -0
- package/lib/dist/db/wal-valve.js +208 -0
- package/lib/dist/db/wal-valve.js.map +1 -0
- package/lib/dist/directory.d.ts +9 -5
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +210 -19
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
- package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/cfml-extractor.js +494 -0
- package/lib/dist/extraction/cfml-extractor.js.map +1 -0
- package/lib/dist/extraction/grammars.d.ts +25 -1
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +177 -23
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +47 -2
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +568 -51
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/arkts.d.ts +3 -0
- package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
- package/lib/dist/extraction/languages/arkts.js +127 -0
- package/lib/dist/extraction/languages/arkts.js.map +1 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts +98 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +563 -1
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
- package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfquery.js +28 -0
- package/lib/dist/extraction/languages/cfquery.js.map +1 -0
- package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
- package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfscript.js +73 -0
- package/lib/dist/extraction/languages/cfscript.js.map +1 -0
- package/lib/dist/extraction/languages/cobol.d.ts +33 -0
- package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cobol.js +499 -0
- package/lib/dist/extraction/languages/cobol.js.map +1 -0
- package/lib/dist/extraction/languages/erlang.d.ts +3 -0
- package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/lib/dist/extraction/languages/erlang.js +350 -0
- package/lib/dist/extraction/languages/erlang.js.map +1 -0
- package/lib/dist/extraction/languages/index.d.ts.map +1 -1
- package/lib/dist/extraction/languages/index.js +18 -0
- package/lib/dist/extraction/languages/index.js.map +1 -1
- package/lib/dist/extraction/languages/nix.d.ts +3 -0
- package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
- package/lib/dist/extraction/languages/nix.js +294 -0
- package/lib/dist/extraction/languages/nix.js.map +1 -0
- package/lib/dist/extraction/languages/solidity.d.ts +3 -0
- package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/lib/dist/extraction/languages/solidity.js +293 -0
- package/lib/dist/extraction/languages/solidity.js.map +1 -0
- package/lib/dist/extraction/languages/terraform.d.ts +3 -0
- package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/lib/dist/extraction/languages/terraform.js +641 -0
- package/lib/dist/extraction/languages/terraform.js.map +1 -0
- package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
- package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/lib/dist/extraction/languages/vbnet.js +141 -0
- package/lib/dist/extraction/languages/vbnet.js.map +1 -0
- package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/mybatis-extractor.js +140 -38
- package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
- package/lib/dist/extraction/parse-pool.d.ts +28 -1
- package/lib/dist/extraction/parse-pool.d.ts.map +1 -1
- package/lib/dist/extraction/parse-pool.js +76 -7
- package/lib/dist/extraction/parse-pool.js.map +1 -1
- package/lib/dist/extraction/parse-worker.js +11 -4
- package/lib/dist/extraction/parse-worker.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
- package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +20 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +60 -1
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +905 -20
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/vba/call-sweep.d.ts +3 -0
- package/lib/dist/extraction/vba/call-sweep.d.ts.map +1 -0
- package/lib/dist/extraction/vba/call-sweep.js +206 -0
- package/lib/dist/extraction/vba/call-sweep.js.map +1 -0
- package/lib/dist/extraction/vba/calls.d.ts +55 -0
- package/lib/dist/extraction/vba/calls.d.ts.map +1 -0
- package/lib/dist/extraction/vba/calls.js +439 -0
- package/lib/dist/extraction/vba/calls.js.map +1 -0
- package/lib/dist/extraction/vba/constants.d.ts +42 -0
- package/lib/dist/extraction/vba/constants.d.ts.map +1 -0
- package/lib/dist/extraction/vba/constants.js +112 -0
- package/lib/dist/extraction/vba/constants.js.map +1 -0
- package/lib/dist/extraction/vba/context.d.ts +175 -0
- package/lib/dist/extraction/vba/context.d.ts.map +1 -0
- package/lib/dist/extraction/vba/context.js +268 -0
- package/lib/dist/extraction/vba/context.js.map +1 -0
- package/lib/dist/extraction/vba/controls.d.ts +24 -0
- package/lib/dist/extraction/vba/controls.d.ts.map +1 -0
- package/lib/dist/extraction/vba/controls.js +89 -0
- package/lib/dist/extraction/vba/controls.js.map +1 -0
- package/lib/dist/extraction/vba/declarations.d.ts +3 -0
- package/lib/dist/extraction/vba/declarations.d.ts.map +1 -0
- package/lib/dist/extraction/vba/declarations.js +154 -0
- package/lib/dist/extraction/vba/declarations.js.map +1 -0
- package/lib/dist/extraction/vba/dims.d.ts +3 -0
- package/lib/dist/extraction/vba/dims.d.ts.map +1 -0
- package/lib/dist/extraction/vba/dims.js +186 -0
- package/lib/dist/extraction/vba/dims.js.map +1 -0
- package/lib/dist/extraction/vba/docmd.d.ts +16 -0
- package/lib/dist/extraction/vba/docmd.d.ts.map +1 -0
- package/lib/dist/extraction/vba/docmd.js +179 -0
- package/lib/dist/extraction/vba/docmd.js.map +1 -0
- package/lib/dist/extraction/vba/enums-consts.d.ts +15 -0
- package/lib/dist/extraction/vba/enums-consts.d.ts.map +1 -0
- package/lib/dist/extraction/vba/enums-consts.js +178 -0
- package/lib/dist/extraction/vba/enums-consts.js.map +1 -0
- package/lib/dist/extraction/vba/implements.d.ts +3 -0
- package/lib/dist/extraction/vba/implements.d.ts.map +1 -0
- package/lib/dist/extraction/vba/implements.js +52 -0
- package/lib/dist/extraction/vba/implements.js.map +1 -0
- package/lib/dist/extraction/vba/procedures.d.ts +3 -0
- package/lib/dist/extraction/vba/procedures.d.ts.map +1 -0
- package/lib/dist/extraction/vba/procedures.js +239 -0
- package/lib/dist/extraction/vba/procedures.js.map +1 -0
- package/lib/dist/extraction/vba/sql-wrapper.d.ts +17 -0
- package/lib/dist/extraction/vba/sql-wrapper.d.ts.map +1 -0
- package/lib/dist/extraction/vba/sql-wrapper.js +212 -0
- package/lib/dist/extraction/vba/sql-wrapper.js.map +1 -0
- package/lib/dist/extraction/vba/tempvars.d.ts +10 -0
- package/lib/dist/extraction/vba/tempvars.d.ts.map +1 -0
- package/lib/dist/extraction/vba/tempvars.js +138 -0
- package/lib/dist/extraction/vba/tempvars.js.map +1 -0
- package/lib/dist/extraction/vba/text-utils.d.ts +73 -0
- package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -0
- package/lib/dist/extraction/vba/text-utils.js +195 -0
- package/lib/dist/extraction/vba/text-utils.js.map +1 -0
- package/lib/dist/extraction/vba-extractor.d.ts +3 -951
- package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vba-extractor.js +46 -2934
- package/lib/dist/extraction/vba-extractor.js.map +1 -1
- package/lib/dist/extraction/vba-preprocess.d.ts +1 -1
- package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
- package/lib/dist/extraction/vba-preprocess.js +300 -88
- package/lib/dist/extraction/vba-preprocess.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/lib/dist/graph/traversal.d.ts.map +1 -1
- package/lib/dist/graph/traversal.js +76 -17
- package/lib/dist/graph/traversal.js.map +1 -1
- package/lib/dist/index.d.ts +81 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +460 -10
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts +42 -0
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +97 -2
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/mcp/daemon.d.ts +35 -3
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +101 -9
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.js +2 -1
- package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/lib/dist/mcp/early-ppid.d.ts +26 -0
- package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
- package/lib/dist/mcp/early-ppid.js +29 -0
- package/lib/dist/mcp/early-ppid.js.map +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +29 -4
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/lib/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/lib/dist/mcp/liveness-watchdog.js +73 -13
- package/lib/dist/mcp/liveness-watchdog.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +30 -4
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/query-pool.d.ts +14 -0
- package/lib/dist/mcp/query-pool.d.ts.map +1 -1
- package/lib/dist/mcp/query-pool.js +18 -0
- package/lib/dist/mcp/query-pool.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +14 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +28 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/startup-handshake.d.ts +44 -0
- package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/lib/dist/mcp/startup-handshake.js +73 -0
- package/lib/dist/mcp/startup-handshake.js.map +1 -0
- package/lib/dist/mcp/tools.d.ts +22 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +289 -23
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/mcp/transport.d.ts.map +1 -1
- package/lib/dist/mcp/transport.js +18 -0
- package/lib/dist/mcp/transport.js.map +1 -1
- package/lib/dist/project-config.d.ts +44 -0
- package/lib/dist/project-config.d.ts.map +1 -1
- package/lib/dist/project-config.js +151 -2
- package/lib/dist/project-config.js.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.js +170 -146
- package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +1109 -97
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/cooperative-yield.d.ts +32 -0
- package/lib/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/lib/dist/resolution/cooperative-yield.js +42 -0
- package/lib/dist/resolution/cooperative-yield.js.map +1 -0
- package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
- package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/cics.js +90 -0
- package/lib/dist/resolution/frameworks/cics.js.map +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +6 -0
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/java.js +14 -6
- package/lib/dist/resolution/frameworks/java.js.map +1 -1
- package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/terraform.js +277 -0
- package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
- package/lib/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/goframe-synthesizer.js +8 -3
- package/lib/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +7 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +129 -4
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +64 -2
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +476 -58
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +22 -3
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +613 -39
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/strip-comments.d.ts +1 -1
- package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
- package/lib/dist/resolution/strip-comments.js +49 -0
- package/lib/dist/resolution/strip-comments.js.map +1 -1
- package/lib/dist/resolution/types.d.ts +29 -0
- package/lib/dist/resolution/types.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.d.ts +10 -0
- package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.js +142 -4
- package/lib/dist/resolution/workspace-packages.js.map +1 -1
- package/lib/dist/search/identifier-segments.d.ts +60 -0
- package/lib/dist/search/identifier-segments.d.ts.map +1 -0
- package/lib/dist/search/identifier-segments.js +176 -0
- package/lib/dist/search/identifier-segments.js.map +1 -0
- package/lib/dist/sync/git-hooks.d.ts.map +1 -1
- package/lib/dist/sync/git-hooks.js +2 -0
- package/lib/dist/sync/git-hooks.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +10 -5
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +51 -14
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/sync/worktree.d.ts +9 -0
- package/lib/dist/sync/worktree.d.ts.map +1 -1
- package/lib/dist/sync/worktree.js +45 -0
- package/lib/dist/sync/worktree.js.map +1 -1
- package/lib/dist/types.d.ts +25 -2
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +10 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/dist/upgrade/index.d.ts +32 -0
- package/lib/dist/upgrade/index.d.ts.map +1 -1
- package/lib/dist/upgrade/index.js +163 -14
- package/lib/dist/upgrade/index.js.map +1 -1
- package/lib/dist/upgrade/remove-binary.d.ts +87 -0
- package/lib/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/lib/dist/upgrade/remove-binary.js +289 -0
- package/lib/dist/upgrade/remove-binary.js.map +1 -0
- package/lib/dist/upgrade/update-check.d.ts +92 -0
- package/lib/dist/upgrade/update-check.d.ts.map +1 -0
- package/lib/dist/upgrade/update-check.js +258 -0
- package/lib/dist/upgrade/update-check.js.map +1 -0
- package/lib/node_modules/.modules.yaml +1 -1
- package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/lib/dist/reasoning/config.d.ts +0 -45
- package/lib/dist/reasoning/config.d.ts.map +0 -1
- package/lib/dist/reasoning/config.js +0 -171
- package/lib/dist/reasoning/config.js.map +0 -1
- package/lib/dist/reasoning/credentials.d.ts +0 -5
- package/lib/dist/reasoning/credentials.d.ts.map +0 -1
- package/lib/dist/reasoning/credentials.js +0 -83
- package/lib/dist/reasoning/credentials.js.map +0 -1
- package/lib/dist/reasoning/login.d.ts +0 -21
- package/lib/dist/reasoning/login.d.ts.map +0 -1
- package/lib/dist/reasoning/login.js +0 -85
- package/lib/dist/reasoning/login.js.map +0 -1
- package/lib/dist/reasoning/reasoner.d.ts +0 -43
- package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
- package/lib/dist/reasoning/reasoner.js +0 -308
- package/lib/dist/reasoning/reasoner.js.map +0 -1
|
@@ -5,6 +5,7 @@ const generated_detection_1 = require("../extraction/generated-detection");
|
|
|
5
5
|
const strip_comments_1 = require("./strip-comments");
|
|
6
6
|
const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
|
|
7
7
|
const goframe_synthesizer_1 = require("./goframe-synthesizer");
|
|
8
|
+
const cooperative_yield_1 = require("./cooperative-yield");
|
|
8
9
|
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
9
10
|
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
10
11
|
const MAX_CALLBACKS_PER_CHANNEL = 40;
|
|
@@ -39,6 +40,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
|
|
|
39
40
|
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
|
|
40
41
|
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
|
|
41
42
|
const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
|
|
43
|
+
// The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
|
|
44
|
+
// trailing-closure syntax, so ONLY those languages can ever contribute a
|
|
45
|
+
// dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
|
|
46
|
+
// Swift dispatcher's field name) would be a wrong edge, not a missed one.
|
|
47
|
+
// Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
|
|
48
|
+
// in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
|
|
49
|
+
// where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
|
|
50
|
+
// 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
|
|
51
|
+
const CC_LANGUAGES = new Set(['swift', 'kotlin']);
|
|
42
52
|
function kebabToPascal(s) {
|
|
43
53
|
return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
44
54
|
}
|
|
@@ -74,6 +84,35 @@ function sliceLines(content, startLine, endLine) {
|
|
|
74
84
|
return null;
|
|
75
85
|
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Per-match line resolver over `src`, 1-based at `baseLine`. The inline
|
|
89
|
+
* `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
|
|
90
|
+
* which goes quadratic on a match-dense source (a generated function full of
|
|
91
|
+
* `.push(` calls re-scanned tens of thousands of times was most of the #1235
|
|
92
|
+
* indexing wedge). Builds the newline index once — lazily, since most sources
|
|
93
|
+
* never produce a match — then answers each call with a binary search.
|
|
94
|
+
*/
|
|
95
|
+
function makeLineAt(src, baseLine) {
|
|
96
|
+
let nl = null;
|
|
97
|
+
return (idx) => {
|
|
98
|
+
if (!nl) {
|
|
99
|
+
nl = [];
|
|
100
|
+
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
|
|
101
|
+
nl.push(i);
|
|
102
|
+
}
|
|
103
|
+
// Count newlines strictly before idx.
|
|
104
|
+
let lo = 0;
|
|
105
|
+
let hi = nl.length;
|
|
106
|
+
while (lo < hi) {
|
|
107
|
+
const mid = (lo + hi) >> 1;
|
|
108
|
+
if (nl[mid] < idx)
|
|
109
|
+
lo = mid + 1;
|
|
110
|
+
else
|
|
111
|
+
hi = mid;
|
|
112
|
+
}
|
|
113
|
+
return baseLine + lo;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
77
116
|
function registrarField(src) {
|
|
78
117
|
const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
|
|
79
118
|
return m ? m[1] : null;
|
|
@@ -113,10 +152,13 @@ function* methodAndFunctionNodes(queries) {
|
|
|
113
152
|
yield* queries.iterateNodesByKind('function');
|
|
114
153
|
}
|
|
115
154
|
/** Phase 1: field-backed observer channels (registrar/dispatcher share a store). */
|
|
116
|
-
function fieldChannelEdges(queries, ctx) {
|
|
155
|
+
async function fieldChannelEdges(queries, ctx, onYield) {
|
|
117
156
|
const registrars = [];
|
|
118
157
|
const dispatchers = [];
|
|
158
|
+
let scanned = 0;
|
|
119
159
|
for (const m of methodAndFunctionNodes(queries)) {
|
|
160
|
+
if ((++scanned & 255) === 0)
|
|
161
|
+
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
120
162
|
const isReg = REGISTRAR_NAME.test(m.name);
|
|
121
163
|
const isDisp = DISPATCHER_NAME.test(m.name);
|
|
122
164
|
if (!isReg && !isDisp)
|
|
@@ -197,7 +239,7 @@ function fieldChannelEdges(queries, ctx) {
|
|
|
197
239
|
* subclass `DataRequest.validate`), bounded by a fan-out cap so a generic field
|
|
198
240
|
* name shared across unrelated classes can't fan out into noise.
|
|
199
241
|
*/
|
|
200
|
-
function closureCollectionEdges(queries, ctx) {
|
|
242
|
+
async function closureCollectionEdges(queries, ctx, onYield) {
|
|
201
243
|
const dispatchers = new Map(); // field → dispatcher methods + forEach line
|
|
202
244
|
const registrars = new Map(); // field → registrar methods + append line
|
|
203
245
|
const addReg = (field, node, absLine) => {
|
|
@@ -208,7 +250,18 @@ function closureCollectionEdges(queries, ctx) {
|
|
|
208
250
|
arr.push({ node, line: absLine });
|
|
209
251
|
registrars.set(field, arr);
|
|
210
252
|
};
|
|
253
|
+
// Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
|
|
254
|
+
// on a repo with a huge file this is the heaviest synthesis pass — yield
|
|
255
|
+
// mid-scan (and mid-match-loop below: a single generated function dense with
|
|
256
|
+
// matches must not starve the watchdog either) so it can't wedge the #850
|
|
257
|
+
// watchdog on its own (#1091, #1235).
|
|
258
|
+
let scanned = 0;
|
|
259
|
+
let matchTick = 0;
|
|
211
260
|
for (const m of methodAndFunctionNodes(queries)) {
|
|
261
|
+
if ((++scanned & 127) === 0)
|
|
262
|
+
await onYield();
|
|
263
|
+
if (!CC_LANGUAGES.has(m.language))
|
|
264
|
+
continue;
|
|
212
265
|
const content = ctx.readFile(m.filePath);
|
|
213
266
|
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
214
267
|
if (!src)
|
|
@@ -217,11 +270,13 @@ function closureCollectionEdges(queries, ctx) {
|
|
|
217
270
|
const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
|
|
218
271
|
if (!hasForEach && !hasAppend)
|
|
219
272
|
continue;
|
|
220
|
-
const lineAt = (
|
|
273
|
+
const lineAt = makeLineAt(src, m.startLine ?? 1);
|
|
221
274
|
if (hasForEach) {
|
|
222
275
|
CC_DISPATCH_RE.lastIndex = 0;
|
|
223
276
|
let d;
|
|
224
277
|
while ((d = CC_DISPATCH_RE.exec(src))) {
|
|
278
|
+
if ((++matchTick & 255) === 0)
|
|
279
|
+
await onYield();
|
|
225
280
|
const arr = dispatchers.get(d[1]) ?? [];
|
|
226
281
|
if (!arr.some((n) => n.node.id === m.id))
|
|
227
282
|
arr.push({ node: m, line: lineAt(d.index) });
|
|
@@ -231,12 +286,18 @@ function closureCollectionEdges(queries, ctx) {
|
|
|
231
286
|
if (hasAppend) {
|
|
232
287
|
CC_APPEND_WRITE_RE.lastIndex = 0;
|
|
233
288
|
let w;
|
|
234
|
-
while ((w = CC_APPEND_WRITE_RE.exec(src)))
|
|
289
|
+
while ((w = CC_APPEND_WRITE_RE.exec(src))) {
|
|
290
|
+
if ((++matchTick & 255) === 0)
|
|
291
|
+
await onYield();
|
|
235
292
|
addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
|
293
|
+
}
|
|
236
294
|
CC_APPEND_DIRECT_RE.lastIndex = 0;
|
|
237
295
|
let a;
|
|
238
|
-
while ((a = CC_APPEND_DIRECT_RE.exec(src)))
|
|
296
|
+
while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
|
|
297
|
+
if ((++matchTick & 255) === 0)
|
|
298
|
+
await onYield();
|
|
239
299
|
addReg(a[1], m, lineAt(a.index));
|
|
300
|
+
}
|
|
240
301
|
}
|
|
241
302
|
}
|
|
242
303
|
const edges = [];
|
|
@@ -265,10 +326,16 @@ function closureCollectionEdges(queries, ctx) {
|
|
|
265
326
|
return edges;
|
|
266
327
|
}
|
|
267
328
|
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
268
|
-
function eventEmitterEdges(ctx) {
|
|
329
|
+
async function eventEmitterEdges(ctx, onYield) {
|
|
330
|
+
let scannedFiles = 0;
|
|
269
331
|
const emitsByEvent = new Map(); // event → dispatcher node ids
|
|
270
332
|
const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
|
|
333
|
+
let scanned = 0;
|
|
271
334
|
for (const file of ctx.getAllFiles()) {
|
|
335
|
+
if ((++scannedFiles & 15) === 0)
|
|
336
|
+
await onYield();
|
|
337
|
+
if ((++scanned & 255) === 0)
|
|
338
|
+
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
272
339
|
const content = ctx.readFile(file);
|
|
273
340
|
if (!content)
|
|
274
341
|
continue;
|
|
@@ -277,7 +344,7 @@ function eventEmitterEdges(ctx) {
|
|
|
277
344
|
if (!hasEmit && !hasOn)
|
|
278
345
|
continue;
|
|
279
346
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
280
|
-
const lineOf = (
|
|
347
|
+
const lineOf = makeLineAt(content, 1);
|
|
281
348
|
if (hasEmit) {
|
|
282
349
|
EMIT_RE.lastIndex = 0;
|
|
283
350
|
let m;
|
|
@@ -340,10 +407,13 @@ function eventEmitterEdges(ctx) {
|
|
|
340
407
|
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
341
408
|
* accepted — it's reachability-correct, like the callback channels.
|
|
342
409
|
*/
|
|
343
|
-
function reactRenderEdges(queries, ctx) {
|
|
410
|
+
async function reactRenderEdges(queries, ctx, onYield) {
|
|
411
|
+
let scanned255 = 0;
|
|
344
412
|
const edges = [];
|
|
345
413
|
const seen = new Set();
|
|
346
|
-
for (const cls of queries.
|
|
414
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
415
|
+
if ((++scanned255 & 63) === 0)
|
|
416
|
+
await onYield();
|
|
347
417
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
348
418
|
.map((e) => queries.getNodeById(e.target))
|
|
349
419
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -383,10 +453,13 @@ function reactRenderEdges(queries, ctx) {
|
|
|
383
453
|
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
384
454
|
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
385
455
|
*/
|
|
386
|
-
function flutterBuildEdges(queries, ctx) {
|
|
456
|
+
async function flutterBuildEdges(queries, ctx, onYield) {
|
|
457
|
+
let scanned255 = 0;
|
|
387
458
|
const edges = [];
|
|
388
459
|
const seen = new Set();
|
|
389
|
-
for (const cls of queries.
|
|
460
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
461
|
+
if ((++scanned255 & 63) === 0)
|
|
462
|
+
await onYield();
|
|
390
463
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
391
464
|
.map((e) => queries.getNodeById(e.target))
|
|
392
465
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -417,6 +490,279 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
417
490
|
}
|
|
418
491
|
return edges;
|
|
419
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Reactive ArkUI property decorators: assigning a property carrying one of
|
|
495
|
+
* these re-runs the owning struct's `build()`. Covers both state models —
|
|
496
|
+
* V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
|
|
497
|
+
* (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
|
|
498
|
+
* the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
|
|
499
|
+
* data classes, not struct properties).
|
|
500
|
+
*/
|
|
501
|
+
const ARKUI_REACTIVE_DECORATORS = new Set([
|
|
502
|
+
'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
|
|
503
|
+
'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
|
|
504
|
+
'Local', 'Provider', 'Consumer',
|
|
505
|
+
]);
|
|
506
|
+
/** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
|
|
507
|
+
const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
508
|
+
/**
|
|
509
|
+
* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
|
|
510
|
+
* flutter-build). Assigning a reactive-decorated property (`@State count`,
|
|
511
|
+
* `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
|
|
512
|
+
* hop is framework-internal — no static edge — so "onClick → markAllDone →
|
|
513
|
+
* this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
|
|
514
|
+
* for each arkts struct with a `build()` method and at least one reactive
|
|
515
|
+
* property, link every sibling method whose body ASSIGNS (or array-mutates)
|
|
516
|
+
* one of those properties → `build`. Assignment-gated on the struct's OWN
|
|
517
|
+
* reactive property names — a method that merely reads state, or a struct
|
|
518
|
+
* with no reactive properties, gets nothing (this is the precision line the
|
|
519
|
+
* all-sibling-methods design would erase).
|
|
520
|
+
*/
|
|
521
|
+
async function arkuiStateBuildEdges(queries, ctx, onYield) {
|
|
522
|
+
let scanned255 = 0;
|
|
523
|
+
const edges = [];
|
|
524
|
+
const seen = new Set();
|
|
525
|
+
for (const struct of queries.iterateNodesByKind('struct')) {
|
|
526
|
+
if ((++scanned255 & 63) === 0)
|
|
527
|
+
await onYield();
|
|
528
|
+
if (struct.language !== 'arkts')
|
|
529
|
+
continue;
|
|
530
|
+
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
531
|
+
.map((e) => queries.getNodeById(e.target))
|
|
532
|
+
.filter((n) => !!n);
|
|
533
|
+
const build = children.find((n) => n.kind === 'method' && n.name === 'build');
|
|
534
|
+
if (!build)
|
|
535
|
+
continue;
|
|
536
|
+
const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
|
|
537
|
+
if (reactiveProps.length === 0)
|
|
538
|
+
continue;
|
|
539
|
+
const propAlternation = reactiveProps
|
|
540
|
+
.map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
541
|
+
.join('|');
|
|
542
|
+
// `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
|
|
543
|
+
// `=(?!=)` keeps `this.done == x` comparisons out.
|
|
544
|
+
const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
|
|
545
|
+
let added = 0;
|
|
546
|
+
for (const m of children) {
|
|
547
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
548
|
+
break;
|
|
549
|
+
if (m.kind !== 'method' || m.id === build.id)
|
|
550
|
+
continue;
|
|
551
|
+
const content = ctx.readFile(m.filePath);
|
|
552
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
553
|
+
if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
|
|
554
|
+
continue;
|
|
555
|
+
const key = `${m.id}>${build.id}`;
|
|
556
|
+
if (seen.has(key))
|
|
557
|
+
continue;
|
|
558
|
+
seen.add(key);
|
|
559
|
+
edges.push({
|
|
560
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
561
|
+
provenance: 'heuristic',
|
|
562
|
+
metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
563
|
+
});
|
|
564
|
+
added++;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return edges;
|
|
568
|
+
}
|
|
569
|
+
/** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
|
|
570
|
+
const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
|
|
571
|
+
/** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
|
|
572
|
+
const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
573
|
+
/**
|
|
574
|
+
* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
|
|
575
|
+
* bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
|
|
576
|
+
* framework-internal, so an order flow riding it (OrangeShopping's
|
|
577
|
+
* add-to-cart) dead-ends at the emit. Link emit-site enclosing
|
|
578
|
+
* function/method → on/once-site enclosing function/method when both
|
|
579
|
+
* reference the SAME statically-recoverable event key.
|
|
580
|
+
*
|
|
581
|
+
* Key recovery, per call site (comment-stripped enclosing-file source): the
|
|
582
|
+
* first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
|
|
583
|
+
* a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
|
|
584
|
+
* level. Precision scoping learned from the samples monorepo (thousands of
|
|
585
|
+
* unrelated samples, most using eventId 1): NUMERIC keys pair within the same
|
|
586
|
+
* FILE only; NAMED keys pair within the same workspace module directory (or
|
|
587
|
+
* the whole project when it declares no modules — the single-app case), both
|
|
588
|
+
* behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
|
|
589
|
+
* handling — their bodies' calls already attribute to the registering method,
|
|
590
|
+
* so targeting that method keeps the chain connected.
|
|
591
|
+
*/
|
|
592
|
+
async function arkuiEmitterEdges(ctx, onYield) {
|
|
593
|
+
let scannedFiles = 0;
|
|
594
|
+
// bucket key -> emit sites / handler sites
|
|
595
|
+
const emits = new Map();
|
|
596
|
+
const handlers = new Map();
|
|
597
|
+
const moduleDirs = (() => {
|
|
598
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
599
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
600
|
+
})();
|
|
601
|
+
const moduleScopeOf = (file) => {
|
|
602
|
+
for (const dir of moduleDirs) {
|
|
603
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
604
|
+
return dir;
|
|
605
|
+
}
|
|
606
|
+
return '';
|
|
607
|
+
};
|
|
608
|
+
for (const file of ctx.getAllFiles()) {
|
|
609
|
+
if ((++scannedFiles & 15) === 0)
|
|
610
|
+
await onYield();
|
|
611
|
+
if (!file.endsWith('.ets'))
|
|
612
|
+
continue;
|
|
613
|
+
const content = ctx.readFile(file);
|
|
614
|
+
if (!content || !content.includes('emitter.'))
|
|
615
|
+
continue;
|
|
616
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
617
|
+
const nodes = ctx.getNodesInFile(file)
|
|
618
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
619
|
+
ARKUI_EMITTER_CALL_RE.lastIndex = 0;
|
|
620
|
+
let m;
|
|
621
|
+
while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
|
|
622
|
+
const verb = m[1];
|
|
623
|
+
const arg = m[2].trim();
|
|
624
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
625
|
+
const encl = nodes
|
|
626
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
627
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
628
|
+
if (!encl)
|
|
629
|
+
continue;
|
|
630
|
+
// Recover the event key from the first argument.
|
|
631
|
+
let key = null;
|
|
632
|
+
const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
|
|
633
|
+
const token = idLit ?? arg;
|
|
634
|
+
if (token !== undefined) {
|
|
635
|
+
if (/^\d+$/.test(token)) {
|
|
636
|
+
key = `num:${file}:${token}`; // numeric: same-file only
|
|
637
|
+
}
|
|
638
|
+
else if (token.includes('.')) {
|
|
639
|
+
key = `name:${moduleScopeOf(file)}:${token}`;
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// Local variable — chase its same-file declaration one level:
|
|
643
|
+
// `let x = new EventsId(K)` / `const x = K`.
|
|
644
|
+
const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
|
|
645
|
+
const decl = safe.match(declRe);
|
|
646
|
+
const inner = (decl?.[1] ?? decl?.[2])?.trim();
|
|
647
|
+
if (inner && /^\d+$/.test(inner))
|
|
648
|
+
key = `num:${file}:${inner}`;
|
|
649
|
+
else if (inner && /^[\w$.]+$/.test(inner))
|
|
650
|
+
key = `name:${moduleScopeOf(file)}:${inner}`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (!key)
|
|
654
|
+
continue;
|
|
655
|
+
const site = { nodeId: encl.id, file, line };
|
|
656
|
+
if (verb === 'emit') {
|
|
657
|
+
(emits.get(key) ?? emits.set(key, []).get(key)).push(site);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
(handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const edges = [];
|
|
665
|
+
const seen = new Set();
|
|
666
|
+
for (const [key, emitSites] of emits) {
|
|
667
|
+
const handlerSites = handlers.get(key);
|
|
668
|
+
if (!handlerSites)
|
|
669
|
+
continue;
|
|
670
|
+
if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
|
|
671
|
+
continue;
|
|
672
|
+
const eventLabel = key.slice(key.lastIndexOf(':') + 1);
|
|
673
|
+
for (const e of emitSites)
|
|
674
|
+
for (const h of handlerSites) {
|
|
675
|
+
if (e.nodeId === h.nodeId)
|
|
676
|
+
continue;
|
|
677
|
+
const dedupe = `${e.nodeId}>${h.nodeId}`;
|
|
678
|
+
if (seen.has(dedupe))
|
|
679
|
+
continue;
|
|
680
|
+
seen.add(dedupe);
|
|
681
|
+
edges.push({
|
|
682
|
+
source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
|
|
683
|
+
provenance: 'heuristic',
|
|
684
|
+
metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return edges;
|
|
689
|
+
}
|
|
690
|
+
/** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
|
|
691
|
+
const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
|
|
692
|
+
/**
|
|
693
|
+
* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
|
|
694
|
+
* 'pages/Detail' })` reaches the `@Entry struct` of
|
|
695
|
+
* `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
|
|
696
|
+
* static edge — so "tap → openDetail → ???" ends at the router call. Bridge
|
|
697
|
+
* literal urls to the page struct: the url resolves against the standard
|
|
698
|
+
* `src/main/ets/` layout (what main_pages.json entries name); candidates
|
|
699
|
+
* prefer the caller's own workspace module (routes are module-scoped), and
|
|
700
|
+
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
701
|
+
* structs qualify as targets — the decorator is what makes a file a page.
|
|
702
|
+
*/
|
|
703
|
+
async function arkuiRouterEdges(ctx, onYield) {
|
|
704
|
+
const edges = [];
|
|
705
|
+
const seen = new Set();
|
|
706
|
+
const allFiles = ctx.getAllFiles();
|
|
707
|
+
const moduleDirs = (() => {
|
|
708
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
709
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
710
|
+
})();
|
|
711
|
+
const moduleScopeOf = (file) => {
|
|
712
|
+
for (const dir of moduleDirs) {
|
|
713
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
714
|
+
return dir;
|
|
715
|
+
}
|
|
716
|
+
return '';
|
|
717
|
+
};
|
|
718
|
+
let scannedFiles = 0;
|
|
719
|
+
for (const file of allFiles) {
|
|
720
|
+
if ((++scannedFiles & 15) === 0)
|
|
721
|
+
await onYield();
|
|
722
|
+
if (!file.endsWith('.ets'))
|
|
723
|
+
continue;
|
|
724
|
+
const content = ctx.readFile(file);
|
|
725
|
+
if (!content || !content.includes('router.'))
|
|
726
|
+
continue;
|
|
727
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
728
|
+
const nodes = ctx.getNodesInFile(file)
|
|
729
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
730
|
+
ARKUI_ROUTER_RE.lastIndex = 0;
|
|
731
|
+
let m;
|
|
732
|
+
while ((m = ARKUI_ROUTER_RE.exec(safe))) {
|
|
733
|
+
const url = m[1];
|
|
734
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
735
|
+
const encl = nodes
|
|
736
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
737
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
738
|
+
if (!encl)
|
|
739
|
+
continue;
|
|
740
|
+
const suffix = `/src/main/ets/${url}.ets`;
|
|
741
|
+
let candidates = allFiles.filter((f) => f.endsWith(suffix));
|
|
742
|
+
if (candidates.length > 1) {
|
|
743
|
+
const scope = moduleScopeOf(file);
|
|
744
|
+
const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
|
|
745
|
+
if (sameModule.length > 0)
|
|
746
|
+
candidates = sameModule;
|
|
747
|
+
}
|
|
748
|
+
if (candidates.length !== 1)
|
|
749
|
+
continue; // ambiguous or unresolved — never guess
|
|
750
|
+
const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
|
|
751
|
+
if (!page)
|
|
752
|
+
continue;
|
|
753
|
+
const key = `${encl.id}>${page.id}`;
|
|
754
|
+
if (seen.has(key))
|
|
755
|
+
continue;
|
|
756
|
+
seen.add(key);
|
|
757
|
+
edges.push({
|
|
758
|
+
source: encl.id, target: page.id, kind: 'calls', line,
|
|
759
|
+
provenance: 'heuristic',
|
|
760
|
+
metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return edges;
|
|
765
|
+
}
|
|
420
766
|
/**
|
|
421
767
|
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
422
768
|
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
@@ -427,14 +773,17 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
427
773
|
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
428
774
|
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
429
775
|
*/
|
|
430
|
-
function cppOverrideEdges(queries) {
|
|
776
|
+
async function cppOverrideEdges(queries, onYield) {
|
|
777
|
+
let scanned255 = 0;
|
|
431
778
|
const edges = [];
|
|
432
779
|
const seen = new Set();
|
|
433
780
|
const methodsOf = (classId) => queries
|
|
434
781
|
.getOutgoingEdges(classId, ['contains'])
|
|
435
782
|
.map((e) => queries.getNodeById(e.target))
|
|
436
783
|
.filter((n) => !!n && n.kind === 'method');
|
|
437
|
-
for (const cls of queries.
|
|
784
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
785
|
+
if ((++scanned255 & 63) === 0)
|
|
786
|
+
await onYield();
|
|
438
787
|
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
439
788
|
if (subMethods.length === 0)
|
|
440
789
|
continue;
|
|
@@ -488,6 +837,7 @@ function cppOverrideEdges(queries) {
|
|
|
488
837
|
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
489
838
|
const IFACE_OVERRIDE_LANGS = new Set([
|
|
490
839
|
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
840
|
+
'arkts',
|
|
491
841
|
]);
|
|
492
842
|
/**
|
|
493
843
|
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
@@ -501,7 +851,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
|
|
|
501
851
|
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
502
852
|
* (`any`) are skipped so they don't match every struct.
|
|
503
853
|
*/
|
|
504
|
-
function goImplementsEdges(queries) {
|
|
854
|
+
async function goImplementsEdges(queries, onYield) {
|
|
855
|
+
let scanned255 = 0;
|
|
505
856
|
const edges = [];
|
|
506
857
|
const seen = new Set();
|
|
507
858
|
const methodNameSet = (id) => new Set(queries
|
|
@@ -509,11 +860,24 @@ function goImplementsEdges(queries) {
|
|
|
509
860
|
.map((e) => queries.getNodeById(e.target))
|
|
510
861
|
.filter((n) => !!n && n.kind === 'method')
|
|
511
862
|
.map((n) => n.name));
|
|
512
|
-
|
|
863
|
+
// Materializes GO structs only (the pass is language-gated by the caller),
|
|
864
|
+
// never the whole struct kind — that array is O(nodes) on struct-heavy
|
|
865
|
+
// repos like the Linux kernel (#1212).
|
|
866
|
+
const goStructs = [];
|
|
867
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
868
|
+
if ((++scanned255 & 63) === 0)
|
|
869
|
+
await onYield();
|
|
870
|
+
if (s.language === 'go')
|
|
871
|
+
goStructs.push(s);
|
|
872
|
+
}
|
|
513
873
|
const structMethods = new Map();
|
|
514
874
|
for (const s of goStructs)
|
|
515
875
|
structMethods.set(s.id, methodNameSet(s.id));
|
|
516
|
-
for (const iface of queries.
|
|
876
|
+
for (const iface of queries.iterateNodesByKind('interface')) {
|
|
877
|
+
if ((++scanned255 & 63) === 0)
|
|
878
|
+
await onYield();
|
|
879
|
+
if ((++scanned255 & 63) === 0)
|
|
880
|
+
await onYield();
|
|
517
881
|
if (iface.language !== 'go')
|
|
518
882
|
continue;
|
|
519
883
|
const want = methodNameSet(iface.id);
|
|
@@ -573,7 +937,8 @@ function goImplementsEdges(queries) {
|
|
|
573
937
|
* matching the same-file edges extraction already emits). Skips methods that
|
|
574
938
|
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
575
939
|
*/
|
|
576
|
-
function goCrossFileMethodContainsEdges(queries) {
|
|
940
|
+
async function goCrossFileMethodContainsEdges(queries, onYield) {
|
|
941
|
+
let scanned255 = 0;
|
|
577
942
|
const edges = [];
|
|
578
943
|
const seen = new Set();
|
|
579
944
|
const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
@@ -581,7 +946,11 @@ function goCrossFileMethodContainsEdges(queries) {
|
|
|
581
946
|
const i = p.replace(/\\/g, '/').lastIndexOf('/');
|
|
582
947
|
return i >= 0 ? p.slice(0, i) : '';
|
|
583
948
|
};
|
|
584
|
-
for (const method of queries.
|
|
949
|
+
for (const method of queries.iterateNodesByKind('method')) {
|
|
950
|
+
if ((++scanned255 & 63) === 0)
|
|
951
|
+
await onYield();
|
|
952
|
+
if ((++scanned255 & 63) === 0)
|
|
953
|
+
await onYield();
|
|
585
954
|
if (method.language !== 'go')
|
|
586
955
|
continue;
|
|
587
956
|
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
@@ -651,13 +1020,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
|
|
|
651
1020
|
function kmpKindsCompatible(a, b) {
|
|
652
1021
|
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
653
1022
|
}
|
|
654
|
-
function kotlinExpectActualEdges(queries) {
|
|
1023
|
+
async function kotlinExpectActualEdges(queries, onYield) {
|
|
1024
|
+
let scanned255 = 0;
|
|
655
1025
|
const edges = [];
|
|
656
1026
|
const seen = new Set();
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1027
|
+
// SQL-side language+decorator pre-filter, streamed. The old
|
|
1028
|
+
// `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
|
|
1029
|
+
// just to find kotlin `actual` declarations — on a 2M-node graph that alone
|
|
1030
|
+
// exceeded Node's default heap and killed the index (#1212). The LIKE
|
|
1031
|
+
// pre-filter can over-match (substring), so the exact decorator check stays.
|
|
1032
|
+
for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
|
|
1033
|
+
if ((++scanned255 & 63) === 0)
|
|
1034
|
+
await onYield();
|
|
1035
|
+
if (!act.decorators?.includes('actual'))
|
|
1036
|
+
continue;
|
|
661
1037
|
let added = 0;
|
|
662
1038
|
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
663
1039
|
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
@@ -691,7 +1067,8 @@ function kotlinExpectActualEdges(queries) {
|
|
|
691
1067
|
}
|
|
692
1068
|
return edges;
|
|
693
1069
|
}
|
|
694
|
-
function interfaceOverrideEdges(queries) {
|
|
1070
|
+
async function interfaceOverrideEdges(queries, onYield) {
|
|
1071
|
+
let scanned255 = 0;
|
|
695
1072
|
const edges = [];
|
|
696
1073
|
const seen = new Set();
|
|
697
1074
|
const methodsOf = (classId) => queries
|
|
@@ -703,7 +1080,9 @@ function interfaceOverrideEdges(queries) {
|
|
|
703
1080
|
// types that conform to protocols. Iterate both.
|
|
704
1081
|
const concreteKinds = ['class', 'struct'];
|
|
705
1082
|
for (const kind of concreteKinds) {
|
|
706
|
-
for (const cls of queries.
|
|
1083
|
+
for (const cls of queries.iterateNodesByKind(kind)) {
|
|
1084
|
+
if ((++scanned255 & 63) === 0)
|
|
1085
|
+
await onYield();
|
|
707
1086
|
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
708
1087
|
if (implMethods.length === 0)
|
|
709
1088
|
continue;
|
|
@@ -777,7 +1156,8 @@ function interfaceOverrideEdges(queries) {
|
|
|
777
1156
|
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
778
1157
|
* stub's source line is the wiring site shown in the trace trail.
|
|
779
1158
|
*/
|
|
780
|
-
function goGrpcStubImplEdges(queries) {
|
|
1159
|
+
async function goGrpcStubImplEdges(queries, onYield) {
|
|
1160
|
+
let scanned255 = 0;
|
|
781
1161
|
const edges = [];
|
|
782
1162
|
const seen = new Set();
|
|
783
1163
|
const STUB_RE = /^Unimplemented.*Server$/;
|
|
@@ -789,7 +1169,9 @@ function goGrpcStubImplEdges(queries) {
|
|
|
789
1169
|
const methodNamesByStruct = new Map();
|
|
790
1170
|
const methodNodesByStruct = new Map();
|
|
791
1171
|
const goStructs = [];
|
|
792
|
-
for (const s of queries.
|
|
1172
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
1173
|
+
if ((++scanned255 & 63) === 0)
|
|
1174
|
+
await onYield();
|
|
793
1175
|
if (s.language !== 'go')
|
|
794
1176
|
continue;
|
|
795
1177
|
goStructs.push(s);
|
|
@@ -872,11 +1254,17 @@ function goGrpcStubImplEdges(queries) {
|
|
|
872
1254
|
* component/function/class node — TS generics like `Array<Foo>` resolve to a type
|
|
873
1255
|
* (or nothing) and are dropped.
|
|
874
1256
|
*/
|
|
875
|
-
function reactJsxChildEdges(ctx) {
|
|
1257
|
+
async function reactJsxChildEdges(ctx, onYield) {
|
|
1258
|
+
let scannedFiles = 0;
|
|
876
1259
|
const edges = [];
|
|
877
1260
|
const seen = new Set();
|
|
878
1261
|
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
1262
|
+
let scanned = 0;
|
|
879
1263
|
for (const file of ctx.getAllFiles()) {
|
|
1264
|
+
if ((++scannedFiles & 15) === 0)
|
|
1265
|
+
await onYield();
|
|
1266
|
+
if ((++scanned & 255) === 0)
|
|
1267
|
+
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
880
1268
|
const content = ctx.readFile(file);
|
|
881
1269
|
if (!content || (!content.includes('</') && !content.includes('/>')))
|
|
882
1270
|
continue; // JSX-file gate
|
|
@@ -924,7 +1312,9 @@ function reactJsxChildEdges(ctx) {
|
|
|
924
1312
|
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
925
1313
|
* skipped.
|
|
926
1314
|
*/
|
|
927
|
-
function vueTemplateEdges(ctx) {
|
|
1315
|
+
async function vueTemplateEdges(ctx, onYield) {
|
|
1316
|
+
let scannedFiles = 0;
|
|
1317
|
+
let scanned255 = 0;
|
|
928
1318
|
const edges = [];
|
|
929
1319
|
const seen = new Set();
|
|
930
1320
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
@@ -938,12 +1328,16 @@ function vueTemplateEdges(ctx) {
|
|
|
938
1328
|
// misses it (flat components match by basename and don't need this). Map each
|
|
939
1329
|
// nested component's Nuxt name → node so those template usages resolve.
|
|
940
1330
|
const nuxtComponents = new Map();
|
|
941
|
-
for (const c of ctx.getNodesByKind('component')) {
|
|
1331
|
+
for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1332
|
+
if ((++scanned255 & 63) === 0)
|
|
1333
|
+
await onYield();
|
|
942
1334
|
const nn = nuxtComponentName(c.filePath);
|
|
943
1335
|
if (nn && !nuxtComponents.has(nn))
|
|
944
1336
|
nuxtComponents.set(nn, c);
|
|
945
1337
|
}
|
|
946
1338
|
for (const file of ctx.getAllFiles()) {
|
|
1339
|
+
if ((++scannedFiles & 15) === 0)
|
|
1340
|
+
await onYield();
|
|
947
1341
|
if (!file.endsWith('.vue'))
|
|
948
1342
|
continue;
|
|
949
1343
|
const content = ctx.readFile(file);
|
|
@@ -1077,18 +1471,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
|
1077
1471
|
// statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
|
|
1078
1472
|
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
1079
1473
|
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
1080
|
-
function rnEventEdges(ctx) {
|
|
1474
|
+
async function rnEventEdges(ctx, onYield) {
|
|
1475
|
+
let scannedFiles = 0;
|
|
1081
1476
|
// Native dispatchers (source = the native method whose body sends the
|
|
1082
1477
|
// event) and JS handlers (target = the function/method registered as
|
|
1083
1478
|
// the listener) keyed by event name.
|
|
1084
1479
|
const nativeDispatchersByEvent = new Map();
|
|
1085
1480
|
const jsHandlersByEvent = new Map();
|
|
1086
1481
|
for (const file of ctx.getAllFiles()) {
|
|
1482
|
+
if ((++scannedFiles & 15) === 0)
|
|
1483
|
+
await onYield();
|
|
1087
1484
|
const content = ctx.readFile(file);
|
|
1088
1485
|
if (!content)
|
|
1089
1486
|
continue;
|
|
1090
1487
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
1091
|
-
const lineOf = (
|
|
1488
|
+
const lineOf = makeLineAt(content, 1);
|
|
1092
1489
|
const addDispatcher = (event, line) => {
|
|
1093
1490
|
const disp = enclosingFn(nodesInFile, line);
|
|
1094
1491
|
if (!disp)
|
|
@@ -1271,11 +1668,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
|
|
|
1271
1668
|
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
1272
1669
|
* `<file>::<module>.<method>` by the framework extractor.
|
|
1273
1670
|
*/
|
|
1274
|
-
function expoCrossPlatformEdges(queries) {
|
|
1671
|
+
async function expoCrossPlatformEdges(queries, onYield) {
|
|
1672
|
+
let scanned255 = 0;
|
|
1275
1673
|
const edges = [];
|
|
1276
1674
|
const seen = new Set();
|
|
1277
1675
|
const byKey = new Map();
|
|
1278
|
-
for (const m of queries.
|
|
1676
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1677
|
+
if ((++scanned255 & 63) === 0)
|
|
1678
|
+
await onYield();
|
|
1279
1679
|
if (!m.id.startsWith('expo-module:'))
|
|
1280
1680
|
continue;
|
|
1281
1681
|
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
@@ -1324,7 +1724,8 @@ function expoCrossPlatformEdges(queries) {
|
|
|
1324
1724
|
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
1325
1725
|
* lines up with the bare Android method name.
|
|
1326
1726
|
*/
|
|
1327
|
-
function rnCrossPlatformEdges(queries) {
|
|
1727
|
+
async function rnCrossPlatformEdges(queries, onYield) {
|
|
1728
|
+
let scanned255 = 0;
|
|
1328
1729
|
const edges = [];
|
|
1329
1730
|
const seen = new Set();
|
|
1330
1731
|
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
@@ -1346,6 +1747,8 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1346
1747
|
// below only runs for genuine cross-platform candidates.
|
|
1347
1748
|
const byName = new Map();
|
|
1348
1749
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1750
|
+
if ((++scanned255 & 63) === 0)
|
|
1751
|
+
await onYield();
|
|
1349
1752
|
if (!NATIVE.has(m.language))
|
|
1350
1753
|
continue;
|
|
1351
1754
|
const key = norm(m.name);
|
|
@@ -1396,17 +1799,27 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1396
1799
|
}
|
|
1397
1800
|
return edges;
|
|
1398
1801
|
}
|
|
1399
|
-
function fabricNativeImplEdges(ctx) {
|
|
1802
|
+
async function fabricNativeImplEdges(ctx, onYield) {
|
|
1803
|
+
let scanned255 = 0;
|
|
1400
1804
|
const edges = [];
|
|
1401
1805
|
const seen = new Set();
|
|
1402
1806
|
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
1403
|
-
// filter to just those
|
|
1404
|
-
|
|
1807
|
+
// filter to just those while streaming — never materializing the whole
|
|
1808
|
+
// `component` kind (#1212).
|
|
1809
|
+
const components = [];
|
|
1810
|
+
for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1811
|
+
if ((++scanned255 & 63) === 0)
|
|
1812
|
+
await onYield();
|
|
1813
|
+
if (n.id.startsWith('fabric-component:'))
|
|
1814
|
+
components.push(n);
|
|
1815
|
+
}
|
|
1405
1816
|
if (components.length === 0)
|
|
1406
1817
|
return edges;
|
|
1407
1818
|
// Pre-index native classes by name for O(1) lookup.
|
|
1408
1819
|
const nativeClassesByName = new Map();
|
|
1409
|
-
for (const n of ctx.getNodesByKind('class')) {
|
|
1820
|
+
for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
|
|
1821
|
+
if ((++scanned255 & 63) === 0)
|
|
1822
|
+
await onYield();
|
|
1410
1823
|
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
|
|
1411
1824
|
continue;
|
|
1412
1825
|
const arr = nativeClassesByName.get(n.name);
|
|
@@ -1459,12 +1872,15 @@ function fabricNativeImplEdges(ctx) {
|
|
|
1459
1872
|
* same simple name) are dropped. We need-not bridge by package because Java
|
|
1460
1873
|
* mapper interfaces are typically uniquely named within a project.
|
|
1461
1874
|
*/
|
|
1462
|
-
function mybatisJavaXmlEdges(queries) {
|
|
1875
|
+
async function mybatisJavaXmlEdges(queries, onYield) {
|
|
1876
|
+
let scanned255 = 0;
|
|
1463
1877
|
const edges = [];
|
|
1464
1878
|
const seen = new Set();
|
|
1465
1879
|
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
1466
1880
|
const javaIndex = new Map();
|
|
1467
1881
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1882
|
+
if ((++scanned255 & 63) === 0)
|
|
1883
|
+
await onYield();
|
|
1468
1884
|
if (m.language !== 'java' && m.language !== 'kotlin')
|
|
1469
1885
|
continue;
|
|
1470
1886
|
const parts = m.qualifiedName.split('::');
|
|
@@ -1480,6 +1896,8 @@ function mybatisJavaXmlEdges(queries) {
|
|
|
1480
1896
|
javaIndex.set(key, [m]);
|
|
1481
1897
|
}
|
|
1482
1898
|
for (const xml of queries.iterateNodesByKind('method')) {
|
|
1899
|
+
if ((++scanned255 & 63) === 0)
|
|
1900
|
+
await onYield();
|
|
1483
1901
|
if (xml.language !== 'xml')
|
|
1484
1902
|
continue;
|
|
1485
1903
|
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
@@ -1593,10 +2011,14 @@ function goHandlerIdent(expr) {
|
|
|
1593
2011
|
const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
1594
2012
|
return m ? m[1] : null;
|
|
1595
2013
|
}
|
|
1596
|
-
function ginMiddlewareChainEdges(queries, ctx) {
|
|
2014
|
+
async function ginMiddlewareChainEdges(queries, ctx, onYield) {
|
|
2015
|
+
let scanned255 = 0;
|
|
2016
|
+
let scannedFiles = 0;
|
|
1597
2017
|
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
1598
2018
|
const dispatchers = [];
|
|
1599
2019
|
for (const n of queries.iterateNodesByKind('method')) {
|
|
2020
|
+
if ((++scanned255 & 63) === 0)
|
|
2021
|
+
await onYield();
|
|
1600
2022
|
if (n.language !== 'go')
|
|
1601
2023
|
continue;
|
|
1602
2024
|
const content = ctx.readFile(n.filePath);
|
|
@@ -1611,6 +2033,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1611
2033
|
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
1612
2034
|
const registered = new Map(); // name → registeredAt (file:line)
|
|
1613
2035
|
for (const file of ctx.getAllFiles()) {
|
|
2036
|
+
if ((++scannedFiles & 15) === 0)
|
|
2037
|
+
await onYield();
|
|
1614
2038
|
if (!file.endsWith('.go'))
|
|
1615
2039
|
continue;
|
|
1616
2040
|
const content = ctx.readFile(file);
|
|
@@ -1666,10 +2090,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1666
2090
|
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
1667
2091
|
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
1668
2092
|
*/
|
|
1669
|
-
function pascalFormEdges(ctx) {
|
|
2093
|
+
async function pascalFormEdges(ctx, onYield) {
|
|
2094
|
+
let scannedFiles = 0;
|
|
1670
2095
|
const edges = [];
|
|
1671
2096
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1672
2097
|
for (const file of allFiles) {
|
|
2098
|
+
if ((++scannedFiles & 255) === 0)
|
|
2099
|
+
await onYield();
|
|
1673
2100
|
if (!/\.(dfm|fmx)$/i.test(file))
|
|
1674
2101
|
continue;
|
|
1675
2102
|
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
@@ -1705,12 +2132,15 @@ function pascalFormEdges(ctx) {
|
|
|
1705
2132
|
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
1706
2133
|
* its loader.
|
|
1707
2134
|
*/
|
|
1708
|
-
function svelteKitLoadEdges(ctx) {
|
|
2135
|
+
async function svelteKitLoadEdges(ctx, onYield) {
|
|
2136
|
+
let scannedFiles = 0;
|
|
1709
2137
|
const edges = [];
|
|
1710
2138
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1711
2139
|
const HOOKS = new Set(['load', 'actions']);
|
|
1712
2140
|
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
1713
2141
|
for (const file of allFiles) {
|
|
2142
|
+
if ((++scannedFiles & 255) === 0)
|
|
2143
|
+
await onYield();
|
|
1714
2144
|
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
1715
2145
|
if (!m)
|
|
1716
2146
|
continue;
|
|
@@ -1759,10 +2189,13 @@ function svelteKitLoadEdges(ctx) {
|
|
|
1759
2189
|
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1760
2190
|
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1761
2191
|
const THUNK_FANOUT_CAP = 24;
|
|
1762
|
-
function reduxThunkEdges(queries, ctx) {
|
|
2192
|
+
async function reduxThunkEdges(queries, ctx, onYield) {
|
|
2193
|
+
let scanned255 = 0;
|
|
1763
2194
|
const edges = [];
|
|
1764
2195
|
const seen = new Set();
|
|
1765
2196
|
for (const node of queries.iterateNodesByKind('constant')) {
|
|
2197
|
+
if ((++scanned255 & 63) === 0)
|
|
2198
|
+
await onYield();
|
|
1766
2199
|
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1767
2200
|
// avoids reading every constant's body on a large repo.
|
|
1768
2201
|
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
@@ -1893,10 +2326,16 @@ function resolveRegistryHandler(ctx, name, chained) {
|
|
|
1893
2326
|
// entry resolving to the global URL constant).
|
|
1894
2327
|
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1895
2328
|
}
|
|
1896
|
-
function objectRegistryEdges(ctx) {
|
|
2329
|
+
async function objectRegistryEdges(ctx, onYield) {
|
|
2330
|
+
let scannedFiles = 0;
|
|
1897
2331
|
const edges = [];
|
|
1898
2332
|
const seen = new Set();
|
|
2333
|
+
let scanned = 0;
|
|
1899
2334
|
for (const file of ctx.getAllFiles()) {
|
|
2335
|
+
if ((++scannedFiles & 15) === 0)
|
|
2336
|
+
await onYield();
|
|
2337
|
+
if ((++scanned & 255) === 0)
|
|
2338
|
+
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
1900
2339
|
if (!REGISTRY_JS_EXT.test(file))
|
|
1901
2340
|
continue;
|
|
1902
2341
|
const content = ctx.readFile(file);
|
|
@@ -2006,10 +2445,13 @@ function rtkEndpointNameFromHook(hook) {
|
|
|
2006
2445
|
return null;
|
|
2007
2446
|
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2008
2447
|
}
|
|
2009
|
-
function rtkQueryEdges(queries, ctx) {
|
|
2448
|
+
async function rtkQueryEdges(queries, ctx, onYield) {
|
|
2449
|
+
let scanned255 = 0;
|
|
2010
2450
|
const edges = [];
|
|
2011
2451
|
const seen = new Set();
|
|
2012
2452
|
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2453
|
+
if ((++scanned255 & 63) === 0)
|
|
2454
|
+
await onYield();
|
|
2013
2455
|
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2014
2456
|
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2015
2457
|
continue;
|
|
@@ -2053,10 +2495,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
|
|
|
2053
2495
|
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2054
2496
|
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2055
2497
|
const PINIA_FANOUT_CAP = 80;
|
|
2056
|
-
function piniaStoreEdges(ctx) {
|
|
2498
|
+
async function piniaStoreEdges(ctx, onYield) {
|
|
2499
|
+
let scannedFiles = 0;
|
|
2057
2500
|
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2058
2501
|
const factoryFile = new Map();
|
|
2059
2502
|
for (const file of ctx.getAllFiles()) {
|
|
2503
|
+
if ((++scannedFiles & 15) === 0)
|
|
2504
|
+
await onYield();
|
|
2060
2505
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2061
2506
|
continue;
|
|
2062
2507
|
const content = ctx.readFile(file);
|
|
@@ -2072,6 +2517,8 @@ function piniaStoreEdges(ctx) {
|
|
|
2072
2517
|
const edges = [];
|
|
2073
2518
|
const seen = new Set();
|
|
2074
2519
|
for (const file of ctx.getAllFiles()) {
|
|
2520
|
+
if ((++scannedFiles & 15) === 0)
|
|
2521
|
+
await onYield();
|
|
2075
2522
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2076
2523
|
continue;
|
|
2077
2524
|
const content = ctx.readFile(file);
|
|
@@ -2144,7 +2591,8 @@ const VUEX_FANOUT_CAP = 120;
|
|
|
2144
2591
|
function pathHasSegment(filePath, seg) {
|
|
2145
2592
|
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2146
2593
|
}
|
|
2147
|
-
function vuexDispatchEdges(ctx) {
|
|
2594
|
+
async function vuexDispatchEdges(ctx, onYield) {
|
|
2595
|
+
let scannedFiles = 0;
|
|
2148
2596
|
const storeFileCache = new Map();
|
|
2149
2597
|
const isStoreFile = (file) => {
|
|
2150
2598
|
let v = storeFileCache.get(file);
|
|
@@ -2182,6 +2630,8 @@ function vuexDispatchEdges(ctx) {
|
|
|
2182
2630
|
const edges = [];
|
|
2183
2631
|
const seen = new Set();
|
|
2184
2632
|
for (const file of ctx.getAllFiles()) {
|
|
2633
|
+
if ((++scannedFiles & 15) === 0)
|
|
2634
|
+
await onYield();
|
|
2185
2635
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2186
2636
|
continue;
|
|
2187
2637
|
const content = ctx.readFile(file);
|
|
@@ -2241,7 +2691,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
|
|
|
2241
2691
|
const CELERY_PY_EXT = /\.py$/;
|
|
2242
2692
|
const CELERY_FANOUT_CAP = 80;
|
|
2243
2693
|
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2244
|
-
function celeryDispatchEdges(ctx) {
|
|
2694
|
+
async function celeryDispatchEdges(ctx, onYield) {
|
|
2695
|
+
let scannedFiles = 0;
|
|
2245
2696
|
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2246
2697
|
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2247
2698
|
// receivers, so the candidate set stays small.
|
|
@@ -2285,6 +2736,8 @@ function celeryDispatchEdges(ctx) {
|
|
|
2285
2736
|
const edges = [];
|
|
2286
2737
|
const seen = new Set();
|
|
2287
2738
|
for (const file of ctx.getAllFiles()) {
|
|
2739
|
+
if ((++scannedFiles & 15) === 0)
|
|
2740
|
+
await onYield();
|
|
2288
2741
|
if (!CELERY_PY_EXT.test(file))
|
|
2289
2742
|
continue;
|
|
2290
2743
|
const content = ctx.readFile(file);
|
|
@@ -2363,15 +2816,24 @@ function springFirstParamType(sig) {
|
|
|
2363
2816
|
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2364
2817
|
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2365
2818
|
}
|
|
2366
|
-
function springEventEdges(ctx) {
|
|
2819
|
+
async function springEventEdges(ctx, onYield) {
|
|
2820
|
+
let scannedFiles = 0;
|
|
2367
2821
|
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2822
|
+
// This is the ONLY full read sweep: publisher files are recorded here so
|
|
2823
|
+
// pass 2 re-reads just those instead of every .java file again (#1212 —
|
|
2824
|
+
// the double full-repo read was one of the tail's longest unyielded spans).
|
|
2368
2825
|
const listeners = new Map();
|
|
2826
|
+
const publisherFiles = [];
|
|
2369
2827
|
for (const file of ctx.getAllFiles()) {
|
|
2828
|
+
if ((++scannedFiles & 15) === 0)
|
|
2829
|
+
await onYield();
|
|
2370
2830
|
if (!SPRING_JAVA_EXT.test(file))
|
|
2371
2831
|
continue;
|
|
2372
2832
|
const content = ctx.readFile(file);
|
|
2373
2833
|
if (!content)
|
|
2374
2834
|
continue;
|
|
2835
|
+
if (content.includes('.publishEvent('))
|
|
2836
|
+
publisherFiles.push(file);
|
|
2375
2837
|
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2376
2838
|
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2377
2839
|
if (!hasAnno && !hasAppListener)
|
|
@@ -2411,12 +2873,13 @@ function springEventEdges(ctx) {
|
|
|
2411
2873
|
}
|
|
2412
2874
|
if (!listeners.size)
|
|
2413
2875
|
return [];
|
|
2414
|
-
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2876
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2877
|
+
// XEvent. Only the publisher files recorded in pass 1 are (re-)read.
|
|
2415
2878
|
const edges = [];
|
|
2416
2879
|
const seen = new Set();
|
|
2417
|
-
for (const file of
|
|
2418
|
-
if (
|
|
2419
|
-
|
|
2880
|
+
for (const file of publisherFiles) {
|
|
2881
|
+
if ((++scannedFiles & 15) === 0)
|
|
2882
|
+
await onYield();
|
|
2420
2883
|
const content = ctx.readFile(file);
|
|
2421
2884
|
if (!content || !content.includes('.publishEvent('))
|
|
2422
2885
|
continue;
|
|
@@ -2501,10 +2964,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
|
2501
2964
|
}
|
|
2502
2965
|
return declType;
|
|
2503
2966
|
}
|
|
2504
|
-
function mediatrDispatchEdges(ctx) {
|
|
2967
|
+
async function mediatrDispatchEdges(ctx, onYield) {
|
|
2968
|
+
let scannedFiles = 0;
|
|
2505
2969
|
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2506
2970
|
const handlers = new Map();
|
|
2507
2971
|
for (const file of ctx.getAllFiles()) {
|
|
2972
|
+
if ((++scannedFiles & 15) === 0)
|
|
2973
|
+
await onYield();
|
|
2508
2974
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2509
2975
|
continue;
|
|
2510
2976
|
const content = ctx.readFile(file);
|
|
@@ -2538,6 +3004,8 @@ function mediatrDispatchEdges(ctx) {
|
|
|
2538
3004
|
const edges = [];
|
|
2539
3005
|
const seen = new Set();
|
|
2540
3006
|
for (const file of ctx.getAllFiles()) {
|
|
3007
|
+
if ((++scannedFiles & 15) === 0)
|
|
3008
|
+
await onYield();
|
|
2541
3009
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2542
3010
|
continue;
|
|
2543
3011
|
const content = ctx.readFile(file);
|
|
@@ -2601,7 +3069,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
|
|
|
2601
3069
|
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2602
3070
|
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2603
3071
|
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2604
|
-
function sidekiqDispatchEdges(ctx) {
|
|
3072
|
+
async function sidekiqDispatchEdges(ctx, onYield) {
|
|
3073
|
+
let scannedFiles = 0;
|
|
2605
3074
|
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2606
3075
|
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2607
3076
|
const performCache = new Map();
|
|
@@ -2638,6 +3107,8 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2638
3107
|
const edges = [];
|
|
2639
3108
|
const seen = new Set();
|
|
2640
3109
|
for (const file of ctx.getAllFiles()) {
|
|
3110
|
+
if ((++scannedFiles & 15) === 0)
|
|
3111
|
+
await onYield();
|
|
2641
3112
|
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2642
3113
|
continue;
|
|
2643
3114
|
const content = ctx.readFile(file);
|
|
@@ -2673,6 +3144,414 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2673
3144
|
}
|
|
2674
3145
|
return edges;
|
|
2675
3146
|
}
|
|
3147
|
+
// ── Erlang behaviour-callback dispatch ────────────────────────────────────────
|
|
3148
|
+
// An Erlang behaviour is a compile-checked callback contract: the behaviour
|
|
3149
|
+
// module declares `-callback init(...) -> ...`, implementers declare
|
|
3150
|
+
// `-behaviour(B)` and export the callbacks, and the framework side dispatches
|
|
3151
|
+
// through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
|
|
3152
|
+
// `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
|
|
3153
|
+
// deliberately leaves var-module calls silent (no static target), so the flow
|
|
3154
|
+
// breaks at exactly the hop agents ask about (request → handler init). Bridge:
|
|
3155
|
+
//
|
|
3156
|
+
// dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
|
|
3157
|
+
// declaring `fn` with the SITE's arity — provided exactly ONE in-repo
|
|
3158
|
+
// behaviour declares (fn, arity); a name+arity collision across behaviours
|
|
3159
|
+
// bails (silent beats wrong) — and the implementer defines and exports `fn`.
|
|
3160
|
+
//
|
|
3161
|
+
// Behaviours are discovered by scanning every Erlang file for `-callback`
|
|
3162
|
+
// declarations (not just `implements` targets), so a behaviour with zero
|
|
3163
|
+
// implementers still participates in the ambiguity gate. Fan-out control: a
|
|
3164
|
+
// mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
|
|
3165
|
+
// hundreds of edges per site that READ as complete coverage while being
|
|
3166
|
+
// arbitrary — above the cap the site is skipped entirely and the boundary
|
|
3167
|
+
// stays visibly dynamic (explore's boundary announcer covers it) instead of
|
|
3168
|
+
// silently truncated.
|
|
3169
|
+
const ERLANG_EXT = /\.(?:erl|hrl)$/;
|
|
3170
|
+
// `Var:fn(` — variable (capitalized) module, lowercase function, immediate
|
|
3171
|
+
// open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
|
|
3172
|
+
// (static remote call, already linked), and mid-word matches.
|
|
3173
|
+
const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
|
|
3174
|
+
const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
|
|
3175
|
+
const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
|
|
3176
|
+
/**
|
|
3177
|
+
* Argument count of the call/declaration whose `(` sits at `openIdx` —
|
|
3178
|
+
* top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
|
|
3179
|
+
* (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
|
|
3180
|
+
* so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
|
|
3181
|
+
*/
|
|
3182
|
+
function erlangArityAt(src, openIdx) {
|
|
3183
|
+
let depth = 1;
|
|
3184
|
+
let commas = 0;
|
|
3185
|
+
let sawArg = false;
|
|
3186
|
+
const limit = Math.min(src.length, openIdx + 4000);
|
|
3187
|
+
for (let i = openIdx + 1; i < limit; i++) {
|
|
3188
|
+
const ch = src[i];
|
|
3189
|
+
if (ch === '"' || ch === "'") {
|
|
3190
|
+
i++;
|
|
3191
|
+
while (i < limit && src[i] !== ch) {
|
|
3192
|
+
if (src[i] === '\\')
|
|
3193
|
+
i++;
|
|
3194
|
+
i++;
|
|
3195
|
+
}
|
|
3196
|
+
sawArg = true;
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
if (ch === '$') {
|
|
3200
|
+
i++;
|
|
3201
|
+
if (src[i] === '\\')
|
|
3202
|
+
i++;
|
|
3203
|
+
sawArg = true;
|
|
3204
|
+
continue;
|
|
3205
|
+
}
|
|
3206
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
3207
|
+
depth++;
|
|
3208
|
+
sawArg = true;
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
3212
|
+
depth--;
|
|
3213
|
+
if (depth === 0)
|
|
3214
|
+
return sawArg ? commas + 1 : 0;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
if (ch === ',' && depth === 1) {
|
|
3218
|
+
commas++;
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
if (!/\s/.test(ch))
|
|
3222
|
+
sawArg = true;
|
|
3223
|
+
}
|
|
3224
|
+
return -1;
|
|
3225
|
+
}
|
|
3226
|
+
/**
|
|
3227
|
+
* Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
|
|
3228
|
+
* DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
|
|
3229
|
+
* and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
|
|
3230
|
+
* config) — the connection happens by option-path unification inside the
|
|
3231
|
+
* module-system evaluator, so there is no static call/import edge to follow
|
|
3232
|
+
* and flow questions ("how does services.yabai.enable become a launchd
|
|
3233
|
+
* service?") go dark at the module boundary.
|
|
3234
|
+
*
|
|
3235
|
+
* This pass links each config-write binding to the option declaration whose
|
|
3236
|
+
* path is the longest static-segment prefix of the write path. Precision gates:
|
|
3237
|
+
* - only STATIC segments participate: plain identifiers, plus quoted segments
|
|
3238
|
+
* (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
|
|
3239
|
+
* only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
|
|
3240
|
+
* so dynamic paths never match beyond their static head;
|
|
3241
|
+
* - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
|
|
3242
|
+
* every package's `meta = { ... }` attrset to nixos's `options.meta`;
|
|
3243
|
+
* - a prefix declared in more than one file is ambiguous → no edge (a wrong
|
|
3244
|
+
* edge is worse than none);
|
|
3245
|
+
* - writes physically inside an options block are declaration internals
|
|
3246
|
+
* (types, defaults, examples), never config writes → excluded.
|
|
3247
|
+
* Both declaration spellings register: flat (`options.a.b = ...`) by name, and
|
|
3248
|
+
* nested (`options = { a.b = ...; }`) by line-span containment.
|
|
3249
|
+
*/
|
|
3250
|
+
function nixLeadingPlainSegments(name) {
|
|
3251
|
+
const segs = [];
|
|
3252
|
+
let i = 0;
|
|
3253
|
+
const n = name.length;
|
|
3254
|
+
while (i < n) {
|
|
3255
|
+
if (name[i] === '"') {
|
|
3256
|
+
// Quoted segment — an opaque verbatim token (quotes kept, so it can
|
|
3257
|
+
// never collide with a plain identifier). `NSGlobalDomain."com.apple.
|
|
3258
|
+
// mouse.tapBehavior"` must match ITS OWN quoted declaration, not
|
|
3259
|
+
// whichever sibling registered the shared plain prefix first.
|
|
3260
|
+
let j = i + 1;
|
|
3261
|
+
while (j < n && name[j] !== '"') {
|
|
3262
|
+
if (name[j] === '\\')
|
|
3263
|
+
j++;
|
|
3264
|
+
j++;
|
|
3265
|
+
}
|
|
3266
|
+
if (j >= n)
|
|
3267
|
+
return segs; // unterminated — stop at the static head
|
|
3268
|
+
const tok = name.slice(i, j + 1);
|
|
3269
|
+
if (tok.includes('${'))
|
|
3270
|
+
return segs; // interpolated → dynamic → stop
|
|
3271
|
+
segs.push(tok);
|
|
3272
|
+
i = j + 1;
|
|
3273
|
+
if (i >= n)
|
|
3274
|
+
break;
|
|
3275
|
+
if (name[i] !== '.')
|
|
3276
|
+
return segs;
|
|
3277
|
+
i++;
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
let j = i;
|
|
3281
|
+
while (j < n && name[j] !== '.') {
|
|
3282
|
+
if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
|
|
3283
|
+
return segs;
|
|
3284
|
+
j++;
|
|
3285
|
+
}
|
|
3286
|
+
const seg = name.slice(i, j);
|
|
3287
|
+
if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
|
|
3288
|
+
return segs;
|
|
3289
|
+
segs.push(seg);
|
|
3290
|
+
i = j + 1;
|
|
3291
|
+
}
|
|
3292
|
+
return segs;
|
|
3293
|
+
}
|
|
3294
|
+
async function nixOptionPathEdges(queries, onYield) {
|
|
3295
|
+
let scanned255 = 0;
|
|
3296
|
+
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
3297
|
+
// option); memory stays O(bindings-kept), not O(all nodes) (#610).
|
|
3298
|
+
const byFile = new Map();
|
|
3299
|
+
let scanned = 0;
|
|
3300
|
+
for (const kind of ['variable', 'function']) {
|
|
3301
|
+
for (const node of queries.iterateNodesByKind(kind)) {
|
|
3302
|
+
if ((++scanned255 & 63) === 0)
|
|
3303
|
+
await onYield();
|
|
3304
|
+
if ((++scanned & 0x3fff) === 0 && onYield)
|
|
3305
|
+
await onYield();
|
|
3306
|
+
if (node.language !== 'nix')
|
|
3307
|
+
continue;
|
|
3308
|
+
const segs = nixLeadingPlainSegments(node.name);
|
|
3309
|
+
if (segs.length === 0)
|
|
3310
|
+
continue;
|
|
3311
|
+
const rec = {
|
|
3312
|
+
id: node.id,
|
|
3313
|
+
filePath: node.filePath,
|
|
3314
|
+
startLine: node.startLine,
|
|
3315
|
+
endLine: node.endLine,
|
|
3316
|
+
segs,
|
|
3317
|
+
};
|
|
3318
|
+
const arr = byFile.get(node.filePath);
|
|
3319
|
+
if (arr)
|
|
3320
|
+
arr.push(rec);
|
|
3321
|
+
else
|
|
3322
|
+
byFile.set(node.filePath, [rec]);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
// Per file: walk bindings outermost-first with a stack of active option
|
|
3326
|
+
// spans, composing nested declaration paths (`options = { services.foo = {
|
|
3327
|
+
// enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
|
|
3328
|
+
// An `options` binding nested inside another option span is a SUBMODULE's
|
|
3329
|
+
// own namespace (`attrsOf (submodule { options = ...; })`) — its internals
|
|
3330
|
+
// are not globally addressable, so the sentinel blocks registration below it
|
|
3331
|
+
// while still excluding the region from write candidates.
|
|
3332
|
+
const SUBMODULE = 'submodule';
|
|
3333
|
+
const decls = new Map();
|
|
3334
|
+
const writes = [];
|
|
3335
|
+
const register = (path, rec) => {
|
|
3336
|
+
if (path.length < 2 || path.includes(SUBMODULE))
|
|
3337
|
+
return;
|
|
3338
|
+
const key = path.join('.');
|
|
3339
|
+
const arr = decls.get(key);
|
|
3340
|
+
if (arr)
|
|
3341
|
+
arr.push(rec);
|
|
3342
|
+
else
|
|
3343
|
+
decls.set(key, [rec]);
|
|
3344
|
+
};
|
|
3345
|
+
for (const recs of byFile.values()) {
|
|
3346
|
+
recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
3347
|
+
const stack = [];
|
|
3348
|
+
for (const rec of recs) {
|
|
3349
|
+
while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
|
|
3350
|
+
stack.pop();
|
|
3351
|
+
// Strict containment at line granularity: a one-line nested binding is
|
|
3352
|
+
// indistinguishable from its container, so it stays unclassified (rare
|
|
3353
|
+
// in module code, where option blocks are multi-line).
|
|
3354
|
+
const enclosing = stack.length > 0 &&
|
|
3355
|
+
rec.startLine >= stack[stack.length - 1].start &&
|
|
3356
|
+
rec.endLine <= stack[stack.length - 1].end &&
|
|
3357
|
+
!(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
|
|
3358
|
+
? stack[stack.length - 1]
|
|
3359
|
+
: null;
|
|
3360
|
+
if (rec.segs[0] === 'options') {
|
|
3361
|
+
const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
|
|
3362
|
+
const prefix = enclosing ? [SUBMODULE] : ownPath;
|
|
3363
|
+
register(prefix, rec);
|
|
3364
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix });
|
|
3365
|
+
continue;
|
|
3366
|
+
}
|
|
3367
|
+
if (enclosing) {
|
|
3368
|
+
const composed = [...enclosing.prefix, ...rec.segs];
|
|
3369
|
+
register(composed, rec);
|
|
3370
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
if (rec.segs.length >= 2) {
|
|
3374
|
+
writes.push(rec);
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
if (decls.size === 0 || writes.length === 0)
|
|
3379
|
+
return [];
|
|
3380
|
+
const edges = [];
|
|
3381
|
+
for (const w of writes) {
|
|
3382
|
+
// `config.services.x = ...` spells the same write with an explicit prefix.
|
|
3383
|
+
const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
|
|
3384
|
+
if (segs.length < 2)
|
|
3385
|
+
continue;
|
|
3386
|
+
// Longest prefix wins; an ambiguous longest match does NOT fall back to a
|
|
3387
|
+
// shorter one (that would link `services.nginx.virtualHosts.x` to
|
|
3388
|
+
// `options.services.nginx` when virtualHosts is the contested path).
|
|
3389
|
+
for (let len = Math.min(segs.length, 6); len >= 2; len--) {
|
|
3390
|
+
const candidates = decls.get(segs.slice(0, len).join('.'));
|
|
3391
|
+
if (!candidates || candidates.length === 0)
|
|
3392
|
+
continue;
|
|
3393
|
+
const files = new Set(candidates.map((c) => c.filePath));
|
|
3394
|
+
if (files.size === 1) {
|
|
3395
|
+
const target = candidates[0];
|
|
3396
|
+
if (target.id !== w.id) {
|
|
3397
|
+
edges.push({
|
|
3398
|
+
source: w.id,
|
|
3399
|
+
target: target.id,
|
|
3400
|
+
kind: 'references',
|
|
3401
|
+
line: w.startLine,
|
|
3402
|
+
provenance: 'heuristic',
|
|
3403
|
+
metadata: {
|
|
3404
|
+
synthesizedBy: 'nix-option-path',
|
|
3405
|
+
optionPath: segs.slice(0, len).join('.'),
|
|
3406
|
+
registeredAt: `${target.filePath}:${target.startLine}`,
|
|
3407
|
+
},
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
break; // longest hit decides, matched or ambiguous
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return edges;
|
|
3415
|
+
}
|
|
3416
|
+
async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
|
|
3417
|
+
let scannedFiles = 0;
|
|
3418
|
+
let scanned255 = 0;
|
|
3419
|
+
// Cheap language gate: no Erlang modules → no cost beyond one streamed
|
|
3420
|
+
// kind scan (never a materialized array of every namespace — #1212).
|
|
3421
|
+
const erlangModules = [];
|
|
3422
|
+
for (const n of queries.iterateNodesByKind('namespace')) {
|
|
3423
|
+
if ((++scanned255 & 63) === 0)
|
|
3424
|
+
await onYield();
|
|
3425
|
+
if (n.language === 'erlang')
|
|
3426
|
+
erlangModules.push(n);
|
|
3427
|
+
}
|
|
3428
|
+
if (erlangModules.length === 0)
|
|
3429
|
+
return [];
|
|
3430
|
+
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
3431
|
+
// its (name, arity) callback set, and the global `name/arity` → declaring
|
|
3432
|
+
// behaviours map that drives the ambiguity gate.
|
|
3433
|
+
const moduleByFile = new Map();
|
|
3434
|
+
for (const ns of erlangModules) {
|
|
3435
|
+
if (!moduleByFile.has(ns.filePath))
|
|
3436
|
+
moduleByFile.set(ns.filePath, ns);
|
|
3437
|
+
}
|
|
3438
|
+
const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
|
|
3439
|
+
const callbackNames = new Set();
|
|
3440
|
+
for (const file of ctx.getAllFiles()) {
|
|
3441
|
+
if ((++scannedFiles & 15) === 0)
|
|
3442
|
+
await onYield();
|
|
3443
|
+
if (!ERLANG_EXT.test(file))
|
|
3444
|
+
continue;
|
|
3445
|
+
const behaviour = moduleByFile.get(file);
|
|
3446
|
+
if (!behaviour)
|
|
3447
|
+
continue; // a .hrl or module-less file can't be a behaviour
|
|
3448
|
+
const content = ctx.readFile(file);
|
|
3449
|
+
if (!content || !content.includes('-callback'))
|
|
3450
|
+
continue;
|
|
3451
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3452
|
+
ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
|
|
3453
|
+
let m;
|
|
3454
|
+
while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
|
|
3455
|
+
const name = m[2].replace(/^'|'$/g, '');
|
|
3456
|
+
const arity = erlangArityAt(safe, m.index + m[0].length - 1);
|
|
3457
|
+
if (arity < 0)
|
|
3458
|
+
continue;
|
|
3459
|
+
const key = `${name}/${arity}`;
|
|
3460
|
+
const arr = declaringBehaviours.get(key);
|
|
3461
|
+
if (arr) {
|
|
3462
|
+
if (!arr.some((b) => b.id === behaviour.id))
|
|
3463
|
+
arr.push(behaviour);
|
|
3464
|
+
}
|
|
3465
|
+
else {
|
|
3466
|
+
declaringBehaviours.set(key, [behaviour]);
|
|
3467
|
+
}
|
|
3468
|
+
callbackNames.add(name);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
if (declaringBehaviours.size === 0)
|
|
3472
|
+
return [];
|
|
3473
|
+
// Implementer target lookup, lazy per (behaviour, fn): implementers come
|
|
3474
|
+
// from the `implements` edges extraction resolved, and the target is the
|
|
3475
|
+
// implementer module's own exported `fn` function node.
|
|
3476
|
+
const targetCache = new Map();
|
|
3477
|
+
const targetsOf = (behaviour, fn) => {
|
|
3478
|
+
const cacheKey = `${behaviour.id}#${fn}`;
|
|
3479
|
+
let targets = targetCache.get(cacheKey);
|
|
3480
|
+
if (targets)
|
|
3481
|
+
return targets;
|
|
3482
|
+
targets = [];
|
|
3483
|
+
for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
|
|
3484
|
+
const impl = queries.getNodeById(e.source);
|
|
3485
|
+
if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
|
|
3486
|
+
continue;
|
|
3487
|
+
const fnNode = ctx
|
|
3488
|
+
.getNodesInFile(impl.filePath)
|
|
3489
|
+
.find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
|
|
3490
|
+
if (fnNode)
|
|
3491
|
+
targets.push(fnNode);
|
|
3492
|
+
}
|
|
3493
|
+
targetCache.set(cacheKey, targets);
|
|
3494
|
+
return targets;
|
|
3495
|
+
};
|
|
3496
|
+
// Pass 2 — dispatch sites. Only files containing a var-module call shape are
|
|
3497
|
+
// scanned in full.
|
|
3498
|
+
const edges = [];
|
|
3499
|
+
const seen = new Set();
|
|
3500
|
+
for (const file of ctx.getAllFiles()) {
|
|
3501
|
+
if ((++scannedFiles & 15) === 0)
|
|
3502
|
+
await onYield();
|
|
3503
|
+
if (!ERLANG_EXT.test(file))
|
|
3504
|
+
continue;
|
|
3505
|
+
const content = ctx.readFile(file);
|
|
3506
|
+
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
|
|
3507
|
+
continue;
|
|
3508
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3509
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
3510
|
+
ERLANG_DISPATCH_RE.lastIndex = 0;
|
|
3511
|
+
let m;
|
|
3512
|
+
while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
|
|
3513
|
+
const fn = m[3];
|
|
3514
|
+
if (!callbackNames.has(fn))
|
|
3515
|
+
continue;
|
|
3516
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3517
|
+
const arity = erlangArityAt(safe, openIdx);
|
|
3518
|
+
if (arity < 0)
|
|
3519
|
+
continue;
|
|
3520
|
+
const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
|
|
3521
|
+
if (!behaviours || behaviours.length !== 1)
|
|
3522
|
+
continue; // unknown or ambiguous
|
|
3523
|
+
const behaviour = behaviours[0];
|
|
3524
|
+
const targets = targetsOf(behaviour, fn);
|
|
3525
|
+
if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
|
|
3526
|
+
continue;
|
|
3527
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
3528
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
3529
|
+
if (!disp)
|
|
3530
|
+
continue;
|
|
3531
|
+
for (const target of targets) {
|
|
3532
|
+
if (target.id === disp.id)
|
|
3533
|
+
continue;
|
|
3534
|
+
const key = `${disp.id}>${target.id}`;
|
|
3535
|
+
if (seen.has(key))
|
|
3536
|
+
continue;
|
|
3537
|
+
seen.add(key);
|
|
3538
|
+
edges.push({
|
|
3539
|
+
source: disp.id,
|
|
3540
|
+
target: target.id,
|
|
3541
|
+
kind: 'calls',
|
|
3542
|
+
line,
|
|
3543
|
+
provenance: 'heuristic',
|
|
3544
|
+
metadata: {
|
|
3545
|
+
synthesizedBy: 'erlang-behaviour',
|
|
3546
|
+
via: `${behaviour.name}:${fn}/${arity}`,
|
|
3547
|
+
registeredAt: `${file}:${line}`,
|
|
3548
|
+
},
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
return edges;
|
|
3554
|
+
}
|
|
2676
3555
|
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2677
3556
|
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2678
3557
|
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
@@ -2719,7 +3598,8 @@ function phpArrayBody(src, openIdx) {
|
|
|
2719
3598
|
}
|
|
2720
3599
|
return null;
|
|
2721
3600
|
}
|
|
2722
|
-
function laravelEventEdges(ctx) {
|
|
3601
|
+
async function laravelEventEdges(ctx, onYield) {
|
|
3602
|
+
let scannedFiles = 0;
|
|
2723
3603
|
// event short name → its listener `handle` methods (deduped by node id).
|
|
2724
3604
|
const listeners = new Map();
|
|
2725
3605
|
const add = (event, handle) => {
|
|
@@ -2736,6 +3616,8 @@ function laravelEventEdges(ctx) {
|
|
|
2736
3616
|
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2737
3617
|
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2738
3618
|
for (const file of ctx.getAllFiles()) {
|
|
3619
|
+
if ((++scannedFiles & 15) === 0)
|
|
3620
|
+
await onYield();
|
|
2739
3621
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2740
3622
|
continue;
|
|
2741
3623
|
const content = ctx.readFile(file);
|
|
@@ -2781,6 +3663,8 @@ function laravelEventEdges(ctx) {
|
|
|
2781
3663
|
const edges = [];
|
|
2782
3664
|
const seen = new Set();
|
|
2783
3665
|
for (const file of ctx.getAllFiles()) {
|
|
3666
|
+
if ((++scannedFiles & 15) === 0)
|
|
3667
|
+
await onYield();
|
|
2784
3668
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2785
3669
|
continue;
|
|
2786
3670
|
const content = ctx.readFile(file);
|
|
@@ -2831,53 +3715,169 @@ function laravelEventEdges(ctx) {
|
|
|
2831
3715
|
* Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
|
|
2832
3716
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
2833
3717
|
*/
|
|
2834
|
-
function synthesizeCallbackEdges(queries, ctx) {
|
|
3718
|
+
async function synthesizeCallbackEdges(queries, ctx) {
|
|
3719
|
+
// Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
|
|
3720
|
+
// running synchronously on the indexer's main thread. Their AGGREGATE can run
|
|
3721
|
+
// for well over a minute on a large repo — long enough for the #850 liveness
|
|
3722
|
+
// watchdog to SIGKILL the process mid-index (#1091), since its heartbeat lives
|
|
3723
|
+
// on this same thread. Yield between passes so the heartbeat can fire; a pass
|
|
3724
|
+
// that itself hangs (a real wedge) never reaches the next yield, so the
|
|
3725
|
+
// watchdog still catches that. See ./cooperative-yield.
|
|
3726
|
+
const yieldToLoop = (0, cooperative_yield_1.createYielder)();
|
|
3727
|
+
// Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
|
|
3728
|
+
// (=1: passes over 250ms; =all: every pass). This is the diagnostic that
|
|
3729
|
+
// located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
|
|
3730
|
+
const markT = { t: Date.now() };
|
|
3731
|
+
const __mark = (label) => {
|
|
3732
|
+
const now = Date.now();
|
|
3733
|
+
const dt = now - markT.t;
|
|
3734
|
+
markT.t = now;
|
|
3735
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.CODEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
3736
|
+
console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
// Language gating: one indexed DISTINCT over the files table lets a pass
|
|
3740
|
+
// whose own filters reference a specific language/extension be skipped
|
|
3741
|
+
// outright when the project has no such files — its result is provably
|
|
3742
|
+
// empty, so skipping is behavior-identical and the cost drops to zero
|
|
3743
|
+
// (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
|
|
3744
|
+
// Passes without an explicit language filter always run.
|
|
3745
|
+
const langs = queries.getDistinctFileLanguages();
|
|
3746
|
+
const has = (...ls) => ls.some((l) => langs.has(l));
|
|
3747
|
+
const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
|
|
3748
|
+
const NONE = [];
|
|
2835
3749
|
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
2836
3750
|
// FIRST: a method declared in a different file from its receiver type is
|
|
2837
3751
|
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
2838
3752
|
// struct's method set from its `contains` edges — so without this it would
|
|
2839
3753
|
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
2840
|
-
const goMethodContains = goCrossFileMethodContainsEdges(queries);
|
|
2841
|
-
|
|
2842
|
-
queries.insertEdges(goMethodContains);
|
|
3754
|
+
const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
|
|
3755
|
+
for (let i = 0; i < goMethodContains.length; i += 2000) {
|
|
3756
|
+
queries.insertEdges(goMethodContains.slice(i, i + 2000));
|
|
3757
|
+
await yieldToLoop();
|
|
3758
|
+
}
|
|
3759
|
+
await yieldToLoop();
|
|
3760
|
+
__mark('goMethodContains');
|
|
2843
3761
|
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
2844
3762
|
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
2845
3763
|
// Go has none statically. (Other languages already have static implements
|
|
2846
3764
|
// edges from extraction, so they don't need this pre-pass.)
|
|
2847
|
-
const goImpl = goImplementsEdges(queries);
|
|
2848
|
-
|
|
2849
|
-
queries.insertEdges(goImpl);
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
const
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
const
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
const
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
const
|
|
2879
|
-
|
|
2880
|
-
|
|
3765
|
+
const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
|
|
3766
|
+
for (let i = 0; i < goImpl.length; i += 2000) {
|
|
3767
|
+
queries.insertEdges(goImpl.slice(i, i + 2000));
|
|
3768
|
+
await yieldToLoop();
|
|
3769
|
+
}
|
|
3770
|
+
await yieldToLoop();
|
|
3771
|
+
__mark('goImplements');
|
|
3772
|
+
const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
|
|
3773
|
+
await yieldToLoop();
|
|
3774
|
+
__mark('fieldEdges');
|
|
3775
|
+
const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
|
|
3776
|
+
await yieldToLoop();
|
|
3777
|
+
__mark('closureCollEdges');
|
|
3778
|
+
const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
|
|
3779
|
+
await yieldToLoop();
|
|
3780
|
+
__mark('emitterEdges');
|
|
3781
|
+
const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
|
|
3782
|
+
await yieldToLoop();
|
|
3783
|
+
__mark('renderEdges');
|
|
3784
|
+
const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
|
|
3785
|
+
await yieldToLoop();
|
|
3786
|
+
__mark('jsxEdges');
|
|
3787
|
+
const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
|
|
3788
|
+
await yieldToLoop();
|
|
3789
|
+
__mark('vueEdges');
|
|
3790
|
+
const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
|
|
3791
|
+
await yieldToLoop();
|
|
3792
|
+
__mark('svelteKitEdges');
|
|
3793
|
+
const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
|
|
3794
|
+
await yieldToLoop();
|
|
3795
|
+
__mark('pascalEdges');
|
|
3796
|
+
const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3797
|
+
await yieldToLoop();
|
|
3798
|
+
__mark('flutterEdges');
|
|
3799
|
+
const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3800
|
+
await yieldToLoop();
|
|
3801
|
+
__mark('arkuiStateEdges');
|
|
3802
|
+
const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
|
|
3803
|
+
await yieldToLoop();
|
|
3804
|
+
__mark('arkuiEmitter');
|
|
3805
|
+
const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
|
|
3806
|
+
await yieldToLoop();
|
|
3807
|
+
__mark('arkuiRoutes');
|
|
3808
|
+
const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
|
|
3809
|
+
await yieldToLoop();
|
|
3810
|
+
__mark('cppEdges');
|
|
3811
|
+
const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
|
|
3812
|
+
? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
|
|
3813
|
+
await yieldToLoop();
|
|
3814
|
+
__mark('ifaceEdges');
|
|
3815
|
+
const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
|
|
3816
|
+
await yieldToLoop();
|
|
3817
|
+
__mark('kotlinExpectActual');
|
|
3818
|
+
const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
|
|
3819
|
+
await yieldToLoop();
|
|
3820
|
+
__mark('goGrpcEdges');
|
|
3821
|
+
const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
|
|
3822
|
+
await yieldToLoop();
|
|
3823
|
+
__mark('rnEventEdgesList');
|
|
3824
|
+
const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
|
|
3825
|
+
await yieldToLoop();
|
|
3826
|
+
__mark('fabricNativeEdges');
|
|
3827
|
+
const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
|
|
3828
|
+
await yieldToLoop();
|
|
3829
|
+
__mark('expoXPlatEdges');
|
|
3830
|
+
const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
|
|
3831
|
+
await yieldToLoop();
|
|
3832
|
+
__mark('rnXPlatEdges');
|
|
3833
|
+
const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE;
|
|
3834
|
+
await yieldToLoop();
|
|
3835
|
+
__mark('mybatisEdges');
|
|
3836
|
+
const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3837
|
+
await yieldToLoop();
|
|
3838
|
+
__mark('ginEdges');
|
|
3839
|
+
const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3840
|
+
await yieldToLoop();
|
|
3841
|
+
__mark('thunkEdges');
|
|
3842
|
+
const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
|
|
3843
|
+
await yieldToLoop();
|
|
3844
|
+
__mark('registryEdges');
|
|
3845
|
+
const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3846
|
+
await yieldToLoop();
|
|
3847
|
+
__mark('rtkEdges');
|
|
3848
|
+
const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
|
|
3849
|
+
await yieldToLoop();
|
|
3850
|
+
__mark('piniaEdges');
|
|
3851
|
+
const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3852
|
+
await yieldToLoop();
|
|
3853
|
+
__mark('vuexEdges');
|
|
3854
|
+
const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3855
|
+
await yieldToLoop();
|
|
3856
|
+
__mark('celeryEdges');
|
|
3857
|
+
const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
|
|
3858
|
+
await yieldToLoop();
|
|
3859
|
+
__mark('springEdges');
|
|
3860
|
+
const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3861
|
+
await yieldToLoop();
|
|
3862
|
+
__mark('mediatrEdges');
|
|
3863
|
+
const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
3864
|
+
await yieldToLoop();
|
|
3865
|
+
__mark('sidekiqEdges');
|
|
3866
|
+
const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3867
|
+
await yieldToLoop();
|
|
3868
|
+
__mark('erlangBehaviourEdges');
|
|
3869
|
+
const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
|
|
3870
|
+
await yieldToLoop();
|
|
3871
|
+
__mark('laravelEdges');
|
|
3872
|
+
const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop) : NONE;
|
|
3873
|
+
await yieldToLoop();
|
|
3874
|
+
__mark('cFnPtrEdges');
|
|
3875
|
+
const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
|
|
3876
|
+
await yieldToLoop();
|
|
3877
|
+
__mark('goframeEdges');
|
|
3878
|
+
const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE;
|
|
3879
|
+
await yieldToLoop();
|
|
3880
|
+
__mark('nixOptionEdges');
|
|
2881
3881
|
const merged = [];
|
|
2882
3882
|
const seen = new Set();
|
|
2883
3883
|
for (const e of [
|
|
@@ -2890,6 +3890,9 @@ function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2890
3890
|
...svelteKitEdges,
|
|
2891
3891
|
...pascalEdges,
|
|
2892
3892
|
...flutterEdges,
|
|
3893
|
+
...arkuiStateEdges,
|
|
3894
|
+
...arkuiEmitter,
|
|
3895
|
+
...arkuiRoutes,
|
|
2893
3896
|
...cppEdges,
|
|
2894
3897
|
...ifaceEdges,
|
|
2895
3898
|
...kotlinExpectActual,
|
|
@@ -2909,9 +3912,11 @@ function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2909
3912
|
...springEdges,
|
|
2910
3913
|
...mediatrEdges,
|
|
2911
3914
|
...sidekiqEdges,
|
|
3915
|
+
...erlangBehaviourEdges,
|
|
2912
3916
|
...laravelEdges,
|
|
2913
3917
|
...cFnPtrEdges,
|
|
2914
3918
|
...goframeEdges,
|
|
3919
|
+
...nixOptionEdges,
|
|
2915
3920
|
]) {
|
|
2916
3921
|
const key = `${e.source}>${e.target}`;
|
|
2917
3922
|
if (seen.has(key))
|
|
@@ -2919,8 +3924,15 @@ function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2919
3924
|
seen.add(key);
|
|
2920
3925
|
merged.push(e);
|
|
2921
3926
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
3927
|
+
__mark('dedupe-merge');
|
|
3928
|
+
// Chunked insert with yields: on the Linux kernel the merged synthesized
|
|
3929
|
+
// edge set is ~275k rows, and one transaction for all of them was a 20s
|
|
3930
|
+
// unyielded main-thread span (#1212 follow-up) — the last one in the tail.
|
|
3931
|
+
for (let i = 0; i < merged.length; i += 2000) {
|
|
3932
|
+
queries.insertEdges(merged.slice(i, i + 2000));
|
|
3933
|
+
await yieldToLoop();
|
|
3934
|
+
}
|
|
3935
|
+
__mark('insertMergedEdges');
|
|
2924
3936
|
return merged.length + goImpl.length + goMethodContains.length;
|
|
2925
3937
|
}
|
|
2926
3938
|
//# sourceMappingURL=callback-synthesizer.js.map
|