homegraph 1.2.0 → 1.5.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/README.md +48 -11
- package/dist/bin/homegraph.js +298 -72
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +117 -11
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +207 -6
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +461 -20
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +19 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +157 -3
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +426 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +32 -1
- package/dist/db/sqlite-adapter.d.ts +24 -17
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +292 -36
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/db/wal-valve.d.ts +119 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +290 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +9 -5
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +210 -19
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.js +3 -2
- package/dist/extraction/arkts-batch-worker.js.map +1 -1
- package/dist/extraction/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.js.map +1 -0
- package/dist/extraction/default-ignore.d.ts +39 -0
- package/dist/extraction/default-ignore.d.ts.map +1 -0
- package/dist/extraction/default-ignore.js +221 -0
- package/dist/extraction/default-ignore.js.map +1 -0
- package/dist/extraction/grammars.d.ts +25 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +177 -24
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +52 -14
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +521 -314
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +90 -6
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1579 -128
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +42 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +369 -4
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +16 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- package/dist/extraction/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/dist/extraction/mybatis-extractor.js +140 -38
- package/dist/extraction/mybatis-extractor.js.map +1 -1
- package/dist/extraction/parse-pool.d.ts +27 -1
- package/dist/extraction/parse-pool.d.ts.map +1 -1
- package/dist/extraction/parse-pool.js +75 -7
- package/dist/extraction/parse-pool.js.map +1 -1
- package/dist/extraction/parse-worker.js +9 -2
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/store-worker.d.ts +20 -0
- package/dist/extraction/store-worker.d.ts.map +1 -0
- package/dist/extraction/store-worker.js +102 -0
- package/dist/extraction/store-worker.js.map +1 -0
- package/dist/extraction/store-writer.d.ts +48 -0
- package/dist/extraction/store-writer.d.ts.map +1 -0
- package/dist/extraction/store-writer.js +167 -0
- package/dist/extraction/store-writer.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +1 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +3 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +38 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +801 -10
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +55 -7
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +1 -1
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +74 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +436 -55
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +42 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +97 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/mcp/daemon.d.ts +25 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +62 -8
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/dist/mcp/dynamic-boundaries.js +2 -1
- package/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +4 -5
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +27 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/dist/mcp/liveness-watchdog.js +73 -13
- package/dist/mcp/liveness-watchdog.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +30 -0
- package/dist/mcp/memory-budget.d.ts.map +1 -0
- package/dist/mcp/memory-budget.js +123 -0
- package/dist/mcp/memory-budget.js.map +1 -0
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +88 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +35 -4
- package/dist/mcp/query-cache.d.ts.map +1 -1
- package/dist/mcp/query-cache.js +89 -19
- package/dist/mcp/query-cache.js.map +1 -1
- package/dist/mcp/query-pool.d.ts +46 -12
- package/dist/mcp/query-pool.d.ts.map +1 -1
- package/dist/mcp/query-pool.js +100 -23
- package/dist/mcp/query-pool.js.map +1 -1
- package/dist/mcp/query-worker.d.ts +2 -2
- package/dist/mcp/query-worker.js +2 -2
- package/dist/mcp/server-instructions.d.ts +4 -27
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -84
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -0
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +28 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/tools.d.ts +104 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2432 -217
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/project-config.d.ts +38 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +101 -2
- package/dist/project-config.js.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +178 -146
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +54 -10
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1299 -257
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +10 -2
- package/dist/resolution/cooperative-yield.d.ts.map +1 -1
- package/dist/resolution/cooperative-yield.js +6 -4
- package/dist/resolution/cooperative-yield.js.map +1 -1
- package/dist/resolution/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -1
- package/dist/resolution/frameworks/go.js +29 -6
- package/dist/resolution/frameworks/go.js.map +1 -1
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +3 -0
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +14 -6
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/dist/resolution/goframe-synthesizer.js +8 -3
- package/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +9 -3
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +295 -11
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +146 -8
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +1089 -131
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/memory-budget.d.ts +48 -0
- package/dist/resolution/memory-budget.d.ts.map +1 -0
- package/dist/resolution/memory-budget.js +162 -0
- package/dist/resolution/memory-budget.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +12 -4
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +793 -130
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/resolver-pool.d.ts +106 -0
- package/dist/resolution/resolver-pool.d.ts.map +1 -0
- package/dist/resolution/resolver-pool.js +344 -0
- package/dist/resolution/resolver-pool.js.map +1 -0
- package/dist/resolution/resolver-worker.d.ts +17 -0
- package/dist/resolution/resolver-worker.d.ts.map +1 -0
- package/dist/resolution/resolver-worker.js +141 -0
- package/dist/resolution/resolver-worker.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +49 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +32 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +10 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.js +142 -4
- package/dist/resolution/workspace-packages.js.map +1 -1
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-utils.d.ts +201 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +919 -2
- package/dist/search/query-utils.js.map +1 -1
- package/dist/spec/build/diff-parser.d.ts +55 -0
- package/dist/spec/build/diff-parser.d.ts.map +1 -0
- package/dist/spec/build/diff-parser.js +205 -0
- package/dist/spec/build/diff-parser.js.map +1 -0
- package/dist/spec/build/git-scanner.d.ts +93 -0
- package/dist/spec/build/git-scanner.d.ts.map +1 -0
- package/dist/spec/build/git-scanner.js +254 -0
- package/dist/spec/build/git-scanner.js.map +1 -0
- package/dist/spec/build/pipeline.d.ts +54 -0
- package/dist/spec/build/pipeline.d.ts.map +1 -0
- package/dist/spec/build/pipeline.js +170 -0
- package/dist/spec/build/pipeline.js.map +1 -0
- package/dist/spec/build/scan.d.ts +48 -0
- package/dist/spec/build/scan.d.ts.map +1 -0
- package/dist/spec/build/scan.js +89 -0
- package/dist/spec/build/scan.js.map +1 -0
- package/dist/spec/build/scope-resolver.d.ts +50 -0
- package/dist/spec/build/scope-resolver.d.ts.map +1 -0
- package/dist/spec/build/scope-resolver.js +105 -0
- package/dist/spec/build/scope-resolver.js.map +1 -0
- package/dist/spec/build/spec-extractor.d.ts +74 -0
- package/dist/spec/build/spec-extractor.d.ts.map +1 -0
- package/dist/spec/build/spec-extractor.js +344 -0
- package/dist/spec/build/spec-extractor.js.map +1 -0
- package/dist/spec/config.d.ts +35 -1
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +30 -5
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +19 -50
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/index.d.ts +5 -3
- package/dist/spec/db/index.d.ts.map +1 -1
- package/dist/spec/db/index.js +16 -1
- package/dist/spec/db/index.js.map +1 -1
- package/dist/spec/db/persist.d.ts +45 -0
- package/dist/spec/db/persist.d.ts.map +1 -0
- package/dist/spec/db/persist.js +66 -0
- package/dist/spec/db/persist.js.map +1 -0
- package/dist/spec/db/relations.d.ts +80 -1
- package/dist/spec/db/relations.d.ts.map +1 -1
- package/dist/spec/db/relations.js +175 -4
- package/dist/spec/db/relations.js.map +1 -1
- package/dist/spec/db/schema.d.ts +4 -0
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +4 -0
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +5 -1
- package/dist/spec/db/spec-node.d.ts +5 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -1
- package/dist/spec/db/spec-node.js +11 -5
- package/dist/spec/db/spec-node.js.map +1 -1
- package/dist/spec/db/sql-utils.d.ts +11 -0
- package/dist/spec/db/sql-utils.d.ts.map +1 -0
- package/dist/spec/db/sql-utils.js +19 -0
- package/dist/spec/db/sql-utils.js.map +1 -0
- package/dist/spec/evolve/cluster-context.d.ts +30 -0
- package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
- package/dist/spec/evolve/cluster-context.js +78 -0
- package/dist/spec/evolve/cluster-context.js.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
- package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.js +93 -0
- package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +23 -7
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
- package/dist/spec/evolve/impact-locator.js +58 -14
- package/dist/spec/evolve/impact-locator.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +62 -25
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +436 -457
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +45 -45
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/git/commits.d.ts +85 -0
- package/dist/spec/git/commits.d.ts.map +1 -0
- package/dist/spec/git/commits.js +218 -0
- package/dist/spec/git/commits.js.map +1 -0
- package/dist/spec/git/exec.d.ts +13 -0
- package/dist/spec/git/exec.d.ts.map +1 -0
- package/dist/spec/git/exec.js +19 -0
- package/dist/spec/git/exec.js.map +1 -0
- package/dist/spec/git/index.d.ts +9 -0
- package/dist/spec/git/index.d.ts.map +1 -0
- package/dist/spec/git/index.js +22 -0
- package/dist/spec/git/index.js.map +1 -0
- package/dist/spec/git-utils.d.ts +8 -0
- package/dist/spec/git-utils.d.ts.map +1 -0
- package/dist/spec/git-utils.js +14 -0
- package/dist/spec/git-utils.js.map +1 -0
- package/dist/spec/graph/index.d.ts +8 -0
- package/dist/spec/graph/index.d.ts.map +1 -0
- package/dist/spec/graph/index.js +17 -0
- package/dist/spec/graph/index.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +6 -35
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +71 -248
- package/dist/spec/graph/queries.js.map +1 -1
- package/dist/spec/llm/agent-client.d.ts +59 -0
- package/dist/spec/llm/agent-client.d.ts.map +1 -0
- package/dist/spec/llm/agent-client.js +213 -0
- package/dist/spec/llm/agent-client.js.map +1 -0
- package/dist/spec/llm/agents/claude-code.d.ts +32 -0
- package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/claude-code.js +125 -0
- package/dist/spec/llm/agents/claude-code.js.map +1 -0
- package/dist/spec/llm/agents/codex.d.ts +32 -0
- package/dist/spec/llm/agents/codex.d.ts.map +1 -0
- package/dist/spec/llm/agents/codex.js +145 -0
- package/dist/spec/llm/agents/codex.js.map +1 -0
- package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
- package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
- package/dist/spec/llm/agents/detect-utils.js +91 -0
- package/dist/spec/llm/agents/detect-utils.js.map +1 -0
- package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
- package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/deveco-code.js +165 -0
- package/dist/spec/llm/agents/deveco-code.js.map +1 -0
- package/dist/spec/llm/agents/index.d.ts +31 -0
- package/dist/spec/llm/agents/index.d.ts.map +1 -0
- package/dist/spec/llm/agents/index.js +65 -0
- package/dist/spec/llm/agents/index.js.map +1 -0
- package/dist/spec/llm/agents/types.d.ts +76 -0
- package/dist/spec/llm/agents/types.d.ts.map +1 -0
- package/dist/spec/llm/agents/types.js +15 -0
- package/dist/spec/llm/agents/types.js.map +1 -0
- package/dist/spec/llm/client.d.ts +31 -11
- package/dist/spec/llm/client.d.ts.map +1 -1
- package/dist/spec/llm/client.js +143 -81
- package/dist/spec/llm/client.js.map +1 -1
- package/dist/spec/llm/factory.d.ts +41 -0
- package/dist/spec/llm/factory.d.ts.map +1 -0
- package/dist/spec/llm/factory.js +80 -0
- package/dist/spec/llm/factory.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +24 -8
- package/dist/spec/llm/prompts.d.ts.map +1 -1
- package/dist/spec/llm/prompts.js +100 -50
- package/dist/spec/llm/prompts.js.map +1 -1
- package/dist/spec/llm/retry.d.ts +53 -0
- package/dist/spec/llm/retry.d.ts.map +1 -0
- package/dist/spec/llm/retry.js +153 -0
- package/dist/spec/llm/retry.js.map +1 -0
- package/dist/spec/mine/clusterer.d.ts +63 -0
- package/dist/spec/mine/clusterer.d.ts.map +1 -0
- package/dist/spec/mine/clusterer.js +904 -0
- package/dist/spec/mine/clusterer.js.map +1 -0
- package/dist/spec/mine/clustering/features.d.ts +38 -0
- package/dist/spec/mine/clustering/features.d.ts.map +1 -0
- package/dist/spec/mine/clustering/features.js +108 -0
- package/dist/spec/mine/clustering/features.js.map +1 -0
- package/dist/spec/mine/clustering/index.d.ts +62 -0
- package/dist/spec/mine/clustering/index.d.ts.map +1 -0
- package/dist/spec/mine/clustering/index.js +259 -0
- package/dist/spec/mine/clustering/index.js.map +1 -0
- package/dist/spec/mine/clustering/leiden.d.ts +61 -0
- package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
- package/dist/spec/mine/clustering/leiden.js +489 -0
- package/dist/spec/mine/clustering/leiden.js.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.js +98 -0
- package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
- package/dist/spec/mine/generator.d.ts +54 -0
- package/dist/spec/mine/generator.d.ts.map +1 -0
- package/dist/spec/mine/generator.js +267 -0
- package/dist/spec/mine/generator.js.map +1 -0
- package/dist/spec/mine/persist.d.ts +37 -0
- package/dist/spec/mine/persist.d.ts.map +1 -0
- package/dist/spec/mine/persist.js +170 -0
- package/dist/spec/mine/persist.js.map +1 -0
- package/dist/spec/mine/pipeline.d.ts +41 -0
- package/dist/spec/mine/pipeline.d.ts.map +1 -0
- package/dist/spec/mine/pipeline.js +236 -0
- package/dist/spec/mine/pipeline.js.map +1 -0
- package/dist/spec/mine/progress-handler.d.ts +22 -0
- package/dist/spec/mine/progress-handler.d.ts.map +1 -0
- package/dist/spec/mine/progress-handler.js +108 -0
- package/dist/spec/mine/progress-handler.js.map +1 -0
- package/dist/spec/mine/progress.d.ts +23 -0
- package/dist/spec/mine/progress.d.ts.map +1 -0
- package/dist/spec/mine/progress.js +12 -0
- package/dist/spec/mine/progress.js.map +1 -0
- package/dist/spec/mine/scanner.d.ts +59 -0
- package/dist/spec/mine/scanner.d.ts.map +1 -0
- package/dist/spec/mine/scanner.js +383 -0
- package/dist/spec/mine/scanner.js.map +1 -0
- package/dist/spec/types.d.ts +32 -0
- package/dist/spec/types.d.ts.map +1 -1
- package/dist/spec/ui/index.d.ts +9 -0
- package/dist/spec/ui/index.d.ts.map +1 -0
- package/dist/spec/ui/index.js +16 -0
- package/dist/spec/ui/index.js.map +1 -0
- package/dist/spec/ui/progress-handler.d.ts +33 -0
- package/dist/spec/ui/progress-handler.d.ts.map +1 -0
- package/dist/spec/ui/progress-handler.js +132 -0
- package/dist/spec/ui/progress-handler.js.map +1 -0
- package/dist/spec/ui/progress.d.ts +24 -0
- package/dist/spec/ui/progress.d.ts.map +1 -0
- package/dist/spec/ui/progress.js +13 -0
- package/dist/spec/ui/progress.js.map +1 -0
- package/dist/spec/utils/fs.d.ts +45 -0
- package/dist/spec/utils/fs.d.ts.map +1 -0
- package/dist/spec/utils/fs.js +163 -0
- package/dist/spec/utils/fs.js.map +1 -0
- package/dist/spec/utils/index.d.ts +12 -0
- package/dist/spec/utils/index.d.ts.map +1 -0
- package/dist/spec/utils/index.js +26 -0
- package/dist/spec/utils/index.js.map +1 -0
- package/dist/spec/utils/meta.d.ts +32 -0
- package/dist/spec/utils/meta.d.ts.map +1 -0
- package/dist/spec/utils/meta.js +122 -0
- package/dist/spec/utils/meta.js.map +1 -0
- package/dist/spec/utils/truncate.d.ts +86 -0
- package/dist/spec/utils/truncate.d.ts.map +1 -0
- package/dist/spec/utils/truncate.js +172 -0
- package/dist/spec/utils/truncate.js.map +1 -0
- package/dist/spec/utils.d.ts +4 -16
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +6 -58
- package/dist/spec/utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/watcher.d.ts +35 -6
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +127 -17
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +5 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/telemetry/index.js +3 -3
- package/dist/telemetry/index.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/upgrade/index.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +162 -14
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/package.json +6 -2
- package/scripts/agent-eval/run-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/npm-shim.js +8 -1
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/data/agents.json +0 -109
- package/scripts/exp_boundary_eval/data/experiments.json +0 -140
- package/scripts/qa_eval/README.md +0 -407
- package/scripts/qa_eval/_test_deveco_probe.py +0 -41
- package/scripts/qa_eval/agent_runner.py +0 -526
- package/scripts/qa_eval/data/.gitignore +0 -4
- package/scripts/qa_eval/data/test-set.jsonl +0 -2
- package/scripts/qa_eval/eval_metrics.py +0 -274
- package/scripts/qa_eval/external_agent.py +0 -976
- package/scripts/qa_eval/llm_config.py +0 -92
- package/scripts/qa_eval/memory_monitor.py +0 -132
- package/scripts/qa_eval/my_answer_accuracy.py +0 -187
- package/scripts/qa_eval/requirements.txt +0 -2
- package/scripts/qa_eval/run_pipeline.py +0 -804
- package/scripts/qa_eval/stats_efficiency.py +0 -279
- package/scripts/qa_eval/stats_scores.py +0 -207
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SYNTH_PROGRESS_STEPS = exports.SYNTH_PASSES = void 0;
|
|
3
4
|
exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
|
|
4
5
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
5
6
|
const strip_comments_1 = require("./strip-comments");
|
|
6
7
|
const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
|
|
7
8
|
const goframe_synthesizer_1 = require("./goframe-synthesizer");
|
|
8
|
-
const cooperative_yield_1 = require("./cooperative-yield");
|
|
9
9
|
const arkts_entry_1 = require("./frameworks/arkts-entry");
|
|
10
|
+
const cooperative_yield_1 = require("./cooperative-yield");
|
|
10
11
|
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
11
12
|
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
12
13
|
const MAX_CALLBACKS_PER_CHANNEL = 40;
|
|
@@ -41,6 +42,15 @@ const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
|
|
|
41
42
|
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
|
|
42
43
|
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
|
|
43
44
|
const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
|
|
45
|
+
// The dispatcher gate — `{ $0( ` / `{ it( ` element-invocation — is Swift/Kotlin
|
|
46
|
+
// trailing-closure syntax, so ONLY those languages can ever contribute a
|
|
47
|
+
// dispatcher, and a cross-language registrar pairing (a JS `.push(` against a
|
|
48
|
+
// Swift dispatcher's field name) would be a wrong edge, not a missed one.
|
|
49
|
+
// Gating both sides here isn't just precision: `.push(`/`.add(` is everywhere
|
|
50
|
+
// in JS/PHP, so an ungated scan slices + regexes nearly every function on repos
|
|
51
|
+
// where the pass cannot emit a single edge — on a 12k-file PHP/JS app that was
|
|
52
|
+
// 20+ minutes of the "Resolving refs" tail and a #850 watchdog kill (#1235).
|
|
53
|
+
const CC_LANGUAGES = new Set(['swift', 'kotlin']);
|
|
44
54
|
function kebabToPascal(s) {
|
|
45
55
|
return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
46
56
|
}
|
|
@@ -76,6 +86,35 @@ function sliceLines(content, startLine, endLine) {
|
|
|
76
86
|
return null;
|
|
77
87
|
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
|
78
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Per-match line resolver over `src`, 1-based at `baseLine`. The inline
|
|
91
|
+
* `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
|
|
92
|
+
* which goes quadratic on a match-dense source (a generated function full of
|
|
93
|
+
* `.push(` calls re-scanned tens of thousands of times was most of the #1235
|
|
94
|
+
* indexing wedge). Builds the newline index once — lazily, since most sources
|
|
95
|
+
* never produce a match — then answers each call with a binary search.
|
|
96
|
+
*/
|
|
97
|
+
function makeLineAt(src, baseLine) {
|
|
98
|
+
let nl = null;
|
|
99
|
+
return (idx) => {
|
|
100
|
+
if (!nl) {
|
|
101
|
+
nl = [];
|
|
102
|
+
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1))
|
|
103
|
+
nl.push(i);
|
|
104
|
+
}
|
|
105
|
+
// Count newlines strictly before idx.
|
|
106
|
+
let lo = 0;
|
|
107
|
+
let hi = nl.length;
|
|
108
|
+
while (lo < hi) {
|
|
109
|
+
const mid = (lo + hi) >> 1;
|
|
110
|
+
if (nl[mid] < idx)
|
|
111
|
+
lo = mid + 1;
|
|
112
|
+
else
|
|
113
|
+
hi = mid;
|
|
114
|
+
}
|
|
115
|
+
return baseLine + lo;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
79
118
|
function registrarField(src) {
|
|
80
119
|
const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
|
|
81
120
|
return m ? m[1] : null;
|
|
@@ -213,13 +252,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
213
252
|
arr.push({ node, line: absLine });
|
|
214
253
|
registrars.set(field, arr);
|
|
215
254
|
};
|
|
216
|
-
// Slices EVERY method/function's source (no cheap name-gate), so
|
|
217
|
-
// with a huge file this is the heaviest synthesis pass — yield
|
|
218
|
-
//
|
|
255
|
+
// Slices EVERY Swift/Kotlin method/function's source (no cheap name-gate), so
|
|
256
|
+
// on a repo with a huge file this is the heaviest synthesis pass — yield
|
|
257
|
+
// mid-scan (and mid-match-loop below: a single generated function dense with
|
|
258
|
+
// matches must not starve the watchdog either) so it can't wedge the #850
|
|
259
|
+
// watchdog on its own (#1091, #1235).
|
|
219
260
|
let scanned = 0;
|
|
261
|
+
let matchTick = 0;
|
|
220
262
|
for (const m of methodAndFunctionNodes(queries)) {
|
|
221
263
|
if ((++scanned & 127) === 0)
|
|
222
264
|
await onYield();
|
|
265
|
+
if (!CC_LANGUAGES.has(m.language))
|
|
266
|
+
continue;
|
|
223
267
|
const content = ctx.readFile(m.filePath);
|
|
224
268
|
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
225
269
|
if (!src)
|
|
@@ -228,11 +272,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
228
272
|
const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
|
|
229
273
|
if (!hasForEach && !hasAppend)
|
|
230
274
|
continue;
|
|
231
|
-
const lineAt = (
|
|
275
|
+
const lineAt = makeLineAt(src, m.startLine ?? 1);
|
|
232
276
|
if (hasForEach) {
|
|
233
277
|
CC_DISPATCH_RE.lastIndex = 0;
|
|
234
278
|
let d;
|
|
235
279
|
while ((d = CC_DISPATCH_RE.exec(src))) {
|
|
280
|
+
if ((++matchTick & 255) === 0)
|
|
281
|
+
await onYield();
|
|
236
282
|
const arr = dispatchers.get(d[1]) ?? [];
|
|
237
283
|
if (!arr.some((n) => n.node.id === m.id))
|
|
238
284
|
arr.push({ node: m, line: lineAt(d.index) });
|
|
@@ -242,12 +288,18 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
242
288
|
if (hasAppend) {
|
|
243
289
|
CC_APPEND_WRITE_RE.lastIndex = 0;
|
|
244
290
|
let w;
|
|
245
|
-
while ((w = CC_APPEND_WRITE_RE.exec(src)))
|
|
291
|
+
while ((w = CC_APPEND_WRITE_RE.exec(src))) {
|
|
292
|
+
if ((++matchTick & 255) === 0)
|
|
293
|
+
await onYield();
|
|
246
294
|
addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
|
295
|
+
}
|
|
247
296
|
CC_APPEND_DIRECT_RE.lastIndex = 0;
|
|
248
297
|
let a;
|
|
249
|
-
while ((a = CC_APPEND_DIRECT_RE.exec(src)))
|
|
298
|
+
while ((a = CC_APPEND_DIRECT_RE.exec(src))) {
|
|
299
|
+
if ((++matchTick & 255) === 0)
|
|
300
|
+
await onYield();
|
|
250
301
|
addReg(a[1], m, lineAt(a.index));
|
|
302
|
+
}
|
|
251
303
|
}
|
|
252
304
|
}
|
|
253
305
|
const edges = [];
|
|
@@ -277,10 +329,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
277
329
|
}
|
|
278
330
|
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
279
331
|
async function eventEmitterEdges(ctx, onYield) {
|
|
332
|
+
let scannedFiles = 0;
|
|
280
333
|
const emitsByEvent = new Map(); // event → dispatcher node ids
|
|
281
334
|
const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
|
|
282
335
|
let scanned = 0;
|
|
283
336
|
for (const file of ctx.getAllFiles()) {
|
|
337
|
+
if ((++scannedFiles & 15) === 0)
|
|
338
|
+
await onYield();
|
|
284
339
|
if ((++scanned & 255) === 0)
|
|
285
340
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
286
341
|
const content = ctx.readFile(file);
|
|
@@ -291,7 +346,7 @@ async function eventEmitterEdges(ctx, onYield) {
|
|
|
291
346
|
if (!hasEmit && !hasOn)
|
|
292
347
|
continue;
|
|
293
348
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
294
|
-
const lineOf = (
|
|
349
|
+
const lineOf = makeLineAt(content, 1);
|
|
295
350
|
if (hasEmit) {
|
|
296
351
|
EMIT_RE.lastIndex = 0;
|
|
297
352
|
let m;
|
|
@@ -354,10 +409,32 @@ async function eventEmitterEdges(ctx, onYield) {
|
|
|
354
409
|
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
355
410
|
* accepted — it's reachability-correct, like the callback channels.
|
|
356
411
|
*/
|
|
357
|
-
function reactRenderEdges(queries, ctx) {
|
|
412
|
+
async function reactRenderEdges(queries, ctx, onYield) {
|
|
413
|
+
let scanned255 = 0;
|
|
358
414
|
const edges = [];
|
|
359
415
|
const seen = new Set();
|
|
360
|
-
|
|
416
|
+
// A class can only emit here if it CONTAINS a method named `render` — so one
|
|
417
|
+
// indexed name lookup bounds the candidate set up front, and the class scan
|
|
418
|
+
// below skips everything else before its per-class edge/node queries. On a
|
|
419
|
+
// repo with few/no render methods (any non-React codebase) this collapses
|
|
420
|
+
// the pass from every-class fan-out to ~zero DB work, with identical output:
|
|
421
|
+
// the skipped classes fail the same `render` check today, just after paying
|
|
422
|
+
// for their children. (Not a language gate: `render` + `this.setState(` in
|
|
423
|
+
// Java — e.g. Litho — legitimately matches today and still does.)
|
|
424
|
+
const renderOwners = new Set();
|
|
425
|
+
for (const n of ctx.getNodesByName('render')) {
|
|
426
|
+
if (n.kind !== 'method')
|
|
427
|
+
continue;
|
|
428
|
+
for (const e of queries.getIncomingEdges(n.id, ['contains']))
|
|
429
|
+
renderOwners.add(e.source);
|
|
430
|
+
}
|
|
431
|
+
if (renderOwners.size === 0)
|
|
432
|
+
return edges;
|
|
433
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
434
|
+
if ((++scanned255 & 63) === 0)
|
|
435
|
+
await onYield();
|
|
436
|
+
if (!renderOwners.has(cls.id))
|
|
437
|
+
continue;
|
|
361
438
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
362
439
|
.map((e) => queries.getNodeById(e.target))
|
|
363
440
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -397,10 +474,13 @@ function reactRenderEdges(queries, ctx) {
|
|
|
397
474
|
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
398
475
|
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
399
476
|
*/
|
|
400
|
-
function flutterBuildEdges(queries, ctx) {
|
|
477
|
+
async function flutterBuildEdges(queries, ctx, onYield) {
|
|
478
|
+
let scanned255 = 0;
|
|
401
479
|
const edges = [];
|
|
402
480
|
const seen = new Set();
|
|
403
|
-
for (const cls of queries.
|
|
481
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
482
|
+
if ((++scanned255 & 63) === 0)
|
|
483
|
+
await onYield();
|
|
404
484
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
405
485
|
.map((e) => queries.getNodeById(e.target))
|
|
406
486
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -431,6 +511,279 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
431
511
|
}
|
|
432
512
|
return edges;
|
|
433
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Reactive ArkUI property decorators: assigning a property carrying one of
|
|
516
|
+
* these re-runs the owning struct's `build()`. Covers both state models —
|
|
517
|
+
* V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
|
|
518
|
+
* (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
|
|
519
|
+
* the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
|
|
520
|
+
* data classes, not struct properties).
|
|
521
|
+
*/
|
|
522
|
+
const ARKUI_REACTIVE_DECORATORS = new Set([
|
|
523
|
+
'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
|
|
524
|
+
'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
|
|
525
|
+
'Local', 'Provider', 'Consumer',
|
|
526
|
+
]);
|
|
527
|
+
/** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
|
|
528
|
+
const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
529
|
+
/**
|
|
530
|
+
* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
|
|
531
|
+
* flutter-build). Assigning a reactive-decorated property (`@State count`,
|
|
532
|
+
* `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
|
|
533
|
+
* hop is framework-internal — no static edge — so "onClick → markAllDone →
|
|
534
|
+
* this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
|
|
535
|
+
* for each arkts struct with a `build()` method and at least one reactive
|
|
536
|
+
* property, link every sibling method whose body ASSIGNS (or array-mutates)
|
|
537
|
+
* one of those properties → `build`. Assignment-gated on the struct's OWN
|
|
538
|
+
* reactive property names — a method that merely reads state, or a struct
|
|
539
|
+
* with no reactive properties, gets nothing (this is the precision line the
|
|
540
|
+
* all-sibling-methods design would erase).
|
|
541
|
+
*/
|
|
542
|
+
async function arkuiStateBuildEdges(queries, ctx, onYield) {
|
|
543
|
+
let scanned255 = 0;
|
|
544
|
+
const edges = [];
|
|
545
|
+
const seen = new Set();
|
|
546
|
+
for (const struct of queries.iterateNodesByKind('struct')) {
|
|
547
|
+
if ((++scanned255 & 63) === 0)
|
|
548
|
+
await onYield();
|
|
549
|
+
if (struct.language !== 'arkts')
|
|
550
|
+
continue;
|
|
551
|
+
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
552
|
+
.map((e) => queries.getNodeById(e.target))
|
|
553
|
+
.filter((n) => !!n);
|
|
554
|
+
const build = children.find((n) => n.kind === 'method' && n.name === 'build');
|
|
555
|
+
if (!build)
|
|
556
|
+
continue;
|
|
557
|
+
const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
|
|
558
|
+
if (reactiveProps.length === 0)
|
|
559
|
+
continue;
|
|
560
|
+
const propAlternation = reactiveProps
|
|
561
|
+
.map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
562
|
+
.join('|');
|
|
563
|
+
// `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
|
|
564
|
+
// `=(?!=)` keeps `this.done == x` comparisons out.
|
|
565
|
+
const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
|
|
566
|
+
let added = 0;
|
|
567
|
+
for (const m of children) {
|
|
568
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
569
|
+
break;
|
|
570
|
+
if (m.kind !== 'method' || m.id === build.id)
|
|
571
|
+
continue;
|
|
572
|
+
const content = ctx.readFile(m.filePath);
|
|
573
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
574
|
+
if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
|
|
575
|
+
continue;
|
|
576
|
+
const key = `${m.id}>${build.id}`;
|
|
577
|
+
if (seen.has(key))
|
|
578
|
+
continue;
|
|
579
|
+
seen.add(key);
|
|
580
|
+
edges.push({
|
|
581
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
582
|
+
provenance: 'heuristic',
|
|
583
|
+
metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
584
|
+
});
|
|
585
|
+
added++;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return edges;
|
|
589
|
+
}
|
|
590
|
+
/** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
|
|
591
|
+
const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
|
|
592
|
+
/** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
|
|
593
|
+
const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
594
|
+
/**
|
|
595
|
+
* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
|
|
596
|
+
* bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
|
|
597
|
+
* framework-internal, so an order flow riding it (OrangeShopping's
|
|
598
|
+
* add-to-cart) dead-ends at the emit. Link emit-site enclosing
|
|
599
|
+
* function/method → on/once-site enclosing function/method when both
|
|
600
|
+
* reference the SAME statically-recoverable event key.
|
|
601
|
+
*
|
|
602
|
+
* Key recovery, per call site (comment-stripped enclosing-file source): the
|
|
603
|
+
* first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
|
|
604
|
+
* a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
|
|
605
|
+
* level. Precision scoping learned from the samples monorepo (thousands of
|
|
606
|
+
* unrelated samples, most using eventId 1): NUMERIC keys pair within the same
|
|
607
|
+
* FILE only; NAMED keys pair within the same workspace module directory (or
|
|
608
|
+
* the whole project when it declares no modules — the single-app case), both
|
|
609
|
+
* behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
|
|
610
|
+
* handling — their bodies' calls already attribute to the registering method,
|
|
611
|
+
* so targeting that method keeps the chain connected.
|
|
612
|
+
*/
|
|
613
|
+
async function arkuiEmitterEdges(ctx, onYield) {
|
|
614
|
+
let scannedFiles = 0;
|
|
615
|
+
// bucket key -> emit sites / handler sites
|
|
616
|
+
const emits = new Map();
|
|
617
|
+
const handlers = new Map();
|
|
618
|
+
const moduleDirs = (() => {
|
|
619
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
620
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
621
|
+
})();
|
|
622
|
+
const moduleScopeOf = (file) => {
|
|
623
|
+
for (const dir of moduleDirs) {
|
|
624
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
625
|
+
return dir;
|
|
626
|
+
}
|
|
627
|
+
return '';
|
|
628
|
+
};
|
|
629
|
+
for (const file of ctx.getAllFiles()) {
|
|
630
|
+
if ((++scannedFiles & 15) === 0)
|
|
631
|
+
await onYield();
|
|
632
|
+
if (!file.endsWith('.ets'))
|
|
633
|
+
continue;
|
|
634
|
+
const content = ctx.readFile(file);
|
|
635
|
+
if (!content || !content.includes('emitter.'))
|
|
636
|
+
continue;
|
|
637
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
638
|
+
const nodes = ctx.getNodesInFile(file)
|
|
639
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
640
|
+
ARKUI_EMITTER_CALL_RE.lastIndex = 0;
|
|
641
|
+
let m;
|
|
642
|
+
while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
|
|
643
|
+
const verb = m[1];
|
|
644
|
+
const arg = m[2].trim();
|
|
645
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
646
|
+
const encl = nodes
|
|
647
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
648
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
649
|
+
if (!encl)
|
|
650
|
+
continue;
|
|
651
|
+
// Recover the event key from the first argument.
|
|
652
|
+
let key = null;
|
|
653
|
+
const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
|
|
654
|
+
const token = idLit ?? arg;
|
|
655
|
+
if (token !== undefined) {
|
|
656
|
+
if (/^\d+$/.test(token)) {
|
|
657
|
+
key = `num:${file}:${token}`; // numeric: same-file only
|
|
658
|
+
}
|
|
659
|
+
else if (token.includes('.')) {
|
|
660
|
+
key = `name:${moduleScopeOf(file)}:${token}`;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
// Local variable — chase its same-file declaration one level:
|
|
664
|
+
// `let x = new EventsId(K)` / `const x = K`.
|
|
665
|
+
const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
|
|
666
|
+
const decl = safe.match(declRe);
|
|
667
|
+
const inner = (decl?.[1] ?? decl?.[2])?.trim();
|
|
668
|
+
if (inner && /^\d+$/.test(inner))
|
|
669
|
+
key = `num:${file}:${inner}`;
|
|
670
|
+
else if (inner && /^[\w$.]+$/.test(inner))
|
|
671
|
+
key = `name:${moduleScopeOf(file)}:${inner}`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (!key)
|
|
675
|
+
continue;
|
|
676
|
+
const site = { nodeId: encl.id, file, line };
|
|
677
|
+
if (verb === 'emit') {
|
|
678
|
+
(emits.get(key) ?? emits.set(key, []).get(key)).push(site);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
(handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const edges = [];
|
|
686
|
+
const seen = new Set();
|
|
687
|
+
for (const [key, emitSites] of emits) {
|
|
688
|
+
const handlerSites = handlers.get(key);
|
|
689
|
+
if (!handlerSites)
|
|
690
|
+
continue;
|
|
691
|
+
if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
|
|
692
|
+
continue;
|
|
693
|
+
const eventLabel = key.slice(key.lastIndexOf(':') + 1);
|
|
694
|
+
for (const e of emitSites)
|
|
695
|
+
for (const h of handlerSites) {
|
|
696
|
+
if (e.nodeId === h.nodeId)
|
|
697
|
+
continue;
|
|
698
|
+
const dedupe = `${e.nodeId}>${h.nodeId}`;
|
|
699
|
+
if (seen.has(dedupe))
|
|
700
|
+
continue;
|
|
701
|
+
seen.add(dedupe);
|
|
702
|
+
edges.push({
|
|
703
|
+
source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
|
|
704
|
+
provenance: 'heuristic',
|
|
705
|
+
metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return edges;
|
|
710
|
+
}
|
|
711
|
+
/** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
|
|
712
|
+
const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
|
|
713
|
+
/**
|
|
714
|
+
* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
|
|
715
|
+
* 'pages/Detail' })` reaches the `@Entry struct` of
|
|
716
|
+
* `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
|
|
717
|
+
* static edge — so "tap → openDetail → ???" ends at the router call. Bridge
|
|
718
|
+
* literal urls to the page struct: the url resolves against the standard
|
|
719
|
+
* `src/main/ets/` layout (what main_pages.json entries name); candidates
|
|
720
|
+
* prefer the caller's own workspace module (routes are module-scoped), and
|
|
721
|
+
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
722
|
+
* structs qualify as targets — the decorator is what makes a file a page.
|
|
723
|
+
*/
|
|
724
|
+
async function arkuiRouterEdges(ctx, onYield) {
|
|
725
|
+
const edges = [];
|
|
726
|
+
const seen = new Set();
|
|
727
|
+
const allFiles = ctx.getAllFiles();
|
|
728
|
+
const moduleDirs = (() => {
|
|
729
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
730
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
731
|
+
})();
|
|
732
|
+
const moduleScopeOf = (file) => {
|
|
733
|
+
for (const dir of moduleDirs) {
|
|
734
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
735
|
+
return dir;
|
|
736
|
+
}
|
|
737
|
+
return '';
|
|
738
|
+
};
|
|
739
|
+
let scannedFiles = 0;
|
|
740
|
+
for (const file of allFiles) {
|
|
741
|
+
if ((++scannedFiles & 15) === 0)
|
|
742
|
+
await onYield();
|
|
743
|
+
if (!file.endsWith('.ets'))
|
|
744
|
+
continue;
|
|
745
|
+
const content = ctx.readFile(file);
|
|
746
|
+
if (!content || !content.includes('router.'))
|
|
747
|
+
continue;
|
|
748
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
749
|
+
const nodes = ctx.getNodesInFile(file)
|
|
750
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
751
|
+
ARKUI_ROUTER_RE.lastIndex = 0;
|
|
752
|
+
let m;
|
|
753
|
+
while ((m = ARKUI_ROUTER_RE.exec(safe))) {
|
|
754
|
+
const url = m[1];
|
|
755
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
756
|
+
const encl = nodes
|
|
757
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
758
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
759
|
+
if (!encl)
|
|
760
|
+
continue;
|
|
761
|
+
const suffix = `/src/main/ets/${url}.ets`;
|
|
762
|
+
let candidates = allFiles.filter((f) => f.endsWith(suffix));
|
|
763
|
+
if (candidates.length > 1) {
|
|
764
|
+
const scope = moduleScopeOf(file);
|
|
765
|
+
const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
|
|
766
|
+
if (sameModule.length > 0)
|
|
767
|
+
candidates = sameModule;
|
|
768
|
+
}
|
|
769
|
+
if (candidates.length !== 1)
|
|
770
|
+
continue; // ambiguous or unresolved — never guess
|
|
771
|
+
const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
|
|
772
|
+
if (!page)
|
|
773
|
+
continue;
|
|
774
|
+
const key = `${encl.id}>${page.id}`;
|
|
775
|
+
if (seen.has(key))
|
|
776
|
+
continue;
|
|
777
|
+
seen.add(key);
|
|
778
|
+
edges.push({
|
|
779
|
+
source: encl.id, target: page.id, kind: 'calls', line,
|
|
780
|
+
provenance: 'heuristic',
|
|
781
|
+
metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return edges;
|
|
786
|
+
}
|
|
434
787
|
/**
|
|
435
788
|
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
436
789
|
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
@@ -441,14 +794,17 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
441
794
|
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
442
795
|
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
443
796
|
*/
|
|
444
|
-
function cppOverrideEdges(queries) {
|
|
797
|
+
async function cppOverrideEdges(queries, onYield) {
|
|
798
|
+
let scanned255 = 0;
|
|
445
799
|
const edges = [];
|
|
446
800
|
const seen = new Set();
|
|
447
801
|
const methodsOf = (classId) => queries
|
|
448
802
|
.getOutgoingEdges(classId, ['contains'])
|
|
449
803
|
.map((e) => queries.getNodeById(e.target))
|
|
450
804
|
.filter((n) => !!n && n.kind === 'method');
|
|
451
|
-
for (const cls of queries.
|
|
805
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
806
|
+
if ((++scanned255 & 63) === 0)
|
|
807
|
+
await onYield();
|
|
452
808
|
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
453
809
|
if (subMethods.length === 0)
|
|
454
810
|
continue;
|
|
@@ -502,6 +858,7 @@ function cppOverrideEdges(queries) {
|
|
|
502
858
|
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
503
859
|
const IFACE_OVERRIDE_LANGS = new Set([
|
|
504
860
|
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
861
|
+
'arkts',
|
|
505
862
|
]);
|
|
506
863
|
/**
|
|
507
864
|
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
@@ -515,7 +872,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
|
|
|
515
872
|
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
516
873
|
* (`any`) are skipped so they don't match every struct.
|
|
517
874
|
*/
|
|
518
|
-
function goImplementsEdges(queries) {
|
|
875
|
+
async function goImplementsEdges(queries, onYield) {
|
|
876
|
+
let scanned255 = 0;
|
|
519
877
|
const edges = [];
|
|
520
878
|
const seen = new Set();
|
|
521
879
|
const methodNameSet = (id) => new Set(queries
|
|
@@ -523,11 +881,24 @@ function goImplementsEdges(queries) {
|
|
|
523
881
|
.map((e) => queries.getNodeById(e.target))
|
|
524
882
|
.filter((n) => !!n && n.kind === 'method')
|
|
525
883
|
.map((n) => n.name));
|
|
526
|
-
|
|
884
|
+
// Materializes GO structs only (the pass is language-gated by the caller),
|
|
885
|
+
// never the whole struct kind — that array is O(nodes) on struct-heavy
|
|
886
|
+
// repos like the Linux kernel (#1212).
|
|
887
|
+
const goStructs = [];
|
|
888
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
889
|
+
if ((++scanned255 & 63) === 0)
|
|
890
|
+
await onYield();
|
|
891
|
+
if (s.language === 'go')
|
|
892
|
+
goStructs.push(s);
|
|
893
|
+
}
|
|
527
894
|
const structMethods = new Map();
|
|
528
895
|
for (const s of goStructs)
|
|
529
896
|
structMethods.set(s.id, methodNameSet(s.id));
|
|
530
|
-
for (const iface of queries.
|
|
897
|
+
for (const iface of queries.iterateNodesByKind('interface')) {
|
|
898
|
+
if ((++scanned255 & 63) === 0)
|
|
899
|
+
await onYield();
|
|
900
|
+
if ((++scanned255 & 63) === 0)
|
|
901
|
+
await onYield();
|
|
531
902
|
if (iface.language !== 'go')
|
|
532
903
|
continue;
|
|
533
904
|
const want = methodNameSet(iface.id);
|
|
@@ -587,7 +958,8 @@ function goImplementsEdges(queries) {
|
|
|
587
958
|
* matching the same-file edges extraction already emits). Skips methods that
|
|
588
959
|
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
589
960
|
*/
|
|
590
|
-
function goCrossFileMethodContainsEdges(queries) {
|
|
961
|
+
async function goCrossFileMethodContainsEdges(queries, onYield) {
|
|
962
|
+
let scanned255 = 0;
|
|
591
963
|
const edges = [];
|
|
592
964
|
const seen = new Set();
|
|
593
965
|
const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
@@ -595,7 +967,11 @@ function goCrossFileMethodContainsEdges(queries) {
|
|
|
595
967
|
const i = p.replace(/\\/g, '/').lastIndexOf('/');
|
|
596
968
|
return i >= 0 ? p.slice(0, i) : '';
|
|
597
969
|
};
|
|
598
|
-
for (const method of queries.
|
|
970
|
+
for (const method of queries.iterateNodesByKind('method')) {
|
|
971
|
+
if ((++scanned255 & 63) === 0)
|
|
972
|
+
await onYield();
|
|
973
|
+
if ((++scanned255 & 63) === 0)
|
|
974
|
+
await onYield();
|
|
599
975
|
if (method.language !== 'go')
|
|
600
976
|
continue;
|
|
601
977
|
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
@@ -665,13 +1041,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
|
|
|
665
1041
|
function kmpKindsCompatible(a, b) {
|
|
666
1042
|
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
667
1043
|
}
|
|
668
|
-
function kotlinExpectActualEdges(queries) {
|
|
1044
|
+
async function kotlinExpectActualEdges(queries, onYield) {
|
|
1045
|
+
let scanned255 = 0;
|
|
669
1046
|
const edges = [];
|
|
670
1047
|
const seen = new Set();
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1048
|
+
// SQL-side language+decorator pre-filter, streamed. The old
|
|
1049
|
+
// `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
|
|
1050
|
+
// just to find kotlin `actual` declarations — on a 2M-node graph that alone
|
|
1051
|
+
// exceeded Node's default heap and killed the index (#1212). The LIKE
|
|
1052
|
+
// pre-filter can over-match (substring), so the exact decorator check stays.
|
|
1053
|
+
for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
|
|
1054
|
+
if ((++scanned255 & 63) === 0)
|
|
1055
|
+
await onYield();
|
|
1056
|
+
if (!act.decorators?.includes('actual'))
|
|
1057
|
+
continue;
|
|
675
1058
|
let added = 0;
|
|
676
1059
|
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
677
1060
|
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
@@ -705,23 +1088,43 @@ function kotlinExpectActualEdges(queries) {
|
|
|
705
1088
|
}
|
|
706
1089
|
return edges;
|
|
707
1090
|
}
|
|
708
|
-
function interfaceOverrideEdges(queries) {
|
|
1091
|
+
async function interfaceOverrideEdges(queries, onYield) {
|
|
1092
|
+
let scanned255 = 0;
|
|
709
1093
|
const edges = [];
|
|
710
1094
|
const seen = new Set();
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1095
|
+
// Memoized: a popular base interface's method list is otherwise re-fetched
|
|
1096
|
+
// once per implementer (dubbo-style hub interfaces have hundreds), and the
|
|
1097
|
+
// memo only ever serves reads. Same rows, same order — byte-identical.
|
|
1098
|
+
const methodsMemo = new Map();
|
|
1099
|
+
const methodsOf = (classId) => {
|
|
1100
|
+
const hit = methodsMemo.get(classId);
|
|
1101
|
+
if (hit)
|
|
1102
|
+
return hit;
|
|
1103
|
+
const methods = queries
|
|
1104
|
+
.getOutgoingEdges(classId, ['contains'])
|
|
1105
|
+
.map((e) => queries.getNodeById(e.target))
|
|
1106
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
1107
|
+
methodsMemo.set(classId, methods);
|
|
1108
|
+
return methods;
|
|
1109
|
+
};
|
|
715
1110
|
// Concrete-side kinds vary by language: `class` covers Java / Kotlin /
|
|
716
1111
|
// C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value
|
|
717
1112
|
// types that conform to protocols. Iterate both.
|
|
718
1113
|
const concreteKinds = ['class', 'struct'];
|
|
719
1114
|
for (const kind of concreteKinds) {
|
|
720
|
-
for (const cls of queries.
|
|
1115
|
+
for (const cls of queries.iterateNodesByKind(kind)) {
|
|
1116
|
+
if ((++scanned255 & 63) === 0)
|
|
1117
|
+
await onYield();
|
|
1118
|
+
// A class can only emit here if it HAS a supertype edge — check that
|
|
1119
|
+
// (one edge query) before materializing its methods: most classes in a
|
|
1120
|
+
// typical graph extend/implement nothing and skip in one hop.
|
|
1121
|
+
const sups = queries.getOutgoingEdges(cls.id, ['implements', 'extends']);
|
|
1122
|
+
if (sups.length === 0)
|
|
1123
|
+
continue;
|
|
721
1124
|
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
722
1125
|
if (implMethods.length === 0)
|
|
723
1126
|
continue;
|
|
724
|
-
for (const sup of
|
|
1127
|
+
for (const sup of sups) {
|
|
725
1128
|
const base = queries.getNodeById(sup.target);
|
|
726
1129
|
if (!base || !IFACE_OVERRIDE_LANGS.has(base.language) || base.id === cls.id)
|
|
727
1130
|
continue;
|
|
@@ -791,7 +1194,8 @@ function interfaceOverrideEdges(queries) {
|
|
|
791
1194
|
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
792
1195
|
* stub's source line is the wiring site shown in the trace trail.
|
|
793
1196
|
*/
|
|
794
|
-
function goGrpcStubImplEdges(queries) {
|
|
1197
|
+
async function goGrpcStubImplEdges(queries, onYield) {
|
|
1198
|
+
let scanned255 = 0;
|
|
795
1199
|
const edges = [];
|
|
796
1200
|
const seen = new Set();
|
|
797
1201
|
const STUB_RE = /^Unimplemented.*Server$/;
|
|
@@ -803,7 +1207,9 @@ function goGrpcStubImplEdges(queries) {
|
|
|
803
1207
|
const methodNamesByStruct = new Map();
|
|
804
1208
|
const methodNodesByStruct = new Map();
|
|
805
1209
|
const goStructs = [];
|
|
806
|
-
for (const s of queries.
|
|
1210
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
1211
|
+
if ((++scanned255 & 63) === 0)
|
|
1212
|
+
await onYield();
|
|
807
1213
|
if (s.language !== 'go')
|
|
808
1214
|
continue;
|
|
809
1215
|
goStructs.push(s);
|
|
@@ -887,11 +1293,14 @@ function goGrpcStubImplEdges(queries) {
|
|
|
887
1293
|
* (or nothing) and are dropped.
|
|
888
1294
|
*/
|
|
889
1295
|
async function reactJsxChildEdges(ctx, onYield) {
|
|
1296
|
+
let scannedFiles = 0;
|
|
890
1297
|
const edges = [];
|
|
891
1298
|
const seen = new Set();
|
|
892
1299
|
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
893
1300
|
let scanned = 0;
|
|
894
1301
|
for (const file of ctx.getAllFiles()) {
|
|
1302
|
+
if ((++scannedFiles & 15) === 0)
|
|
1303
|
+
await onYield();
|
|
895
1304
|
if ((++scanned & 255) === 0)
|
|
896
1305
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
897
1306
|
const content = ctx.readFile(file);
|
|
@@ -941,7 +1350,9 @@ async function reactJsxChildEdges(ctx, onYield) {
|
|
|
941
1350
|
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
942
1351
|
* skipped.
|
|
943
1352
|
*/
|
|
944
|
-
function vueTemplateEdges(ctx) {
|
|
1353
|
+
async function vueTemplateEdges(ctx, onYield) {
|
|
1354
|
+
let scannedFiles = 0;
|
|
1355
|
+
let scanned255 = 0;
|
|
945
1356
|
const edges = [];
|
|
946
1357
|
const seen = new Set();
|
|
947
1358
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
@@ -955,12 +1366,16 @@ function vueTemplateEdges(ctx) {
|
|
|
955
1366
|
// misses it (flat components match by basename and don't need this). Map each
|
|
956
1367
|
// nested component's Nuxt name → node so those template usages resolve.
|
|
957
1368
|
const nuxtComponents = new Map();
|
|
958
|
-
for (const c of ctx.getNodesByKind('component')) {
|
|
1369
|
+
for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1370
|
+
if ((++scanned255 & 63) === 0)
|
|
1371
|
+
await onYield();
|
|
959
1372
|
const nn = nuxtComponentName(c.filePath);
|
|
960
1373
|
if (nn && !nuxtComponents.has(nn))
|
|
961
1374
|
nuxtComponents.set(nn, c);
|
|
962
1375
|
}
|
|
963
1376
|
for (const file of ctx.getAllFiles()) {
|
|
1377
|
+
if ((++scannedFiles & 15) === 0)
|
|
1378
|
+
await onYield();
|
|
964
1379
|
if (!file.endsWith('.vue'))
|
|
965
1380
|
continue;
|
|
966
1381
|
const content = ctx.readFile(file);
|
|
@@ -1094,18 +1509,21 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
|
1094
1509
|
// statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
|
|
1095
1510
|
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
1096
1511
|
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
1097
|
-
function rnEventEdges(ctx) {
|
|
1512
|
+
async function rnEventEdges(ctx, onYield) {
|
|
1513
|
+
let scannedFiles = 0;
|
|
1098
1514
|
// Native dispatchers (source = the native method whose body sends the
|
|
1099
1515
|
// event) and JS handlers (target = the function/method registered as
|
|
1100
1516
|
// the listener) keyed by event name.
|
|
1101
1517
|
const nativeDispatchersByEvent = new Map();
|
|
1102
1518
|
const jsHandlersByEvent = new Map();
|
|
1103
1519
|
for (const file of ctx.getAllFiles()) {
|
|
1520
|
+
if ((++scannedFiles & 15) === 0)
|
|
1521
|
+
await onYield();
|
|
1104
1522
|
const content = ctx.readFile(file);
|
|
1105
1523
|
if (!content)
|
|
1106
1524
|
continue;
|
|
1107
1525
|
const nodesInFile = ctx.getNodesInFile(file);
|
|
1108
|
-
const lineOf = (
|
|
1526
|
+
const lineOf = makeLineAt(content, 1);
|
|
1109
1527
|
const addDispatcher = (event, line) => {
|
|
1110
1528
|
const disp = enclosingFn(nodesInFile, line);
|
|
1111
1529
|
if (!disp)
|
|
@@ -1288,11 +1706,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
|
|
|
1288
1706
|
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
1289
1707
|
* `<file>::<module>.<method>` by the framework extractor.
|
|
1290
1708
|
*/
|
|
1291
|
-
function expoCrossPlatformEdges(queries) {
|
|
1709
|
+
async function expoCrossPlatformEdges(queries, onYield) {
|
|
1710
|
+
let scanned255 = 0;
|
|
1292
1711
|
const edges = [];
|
|
1293
1712
|
const seen = new Set();
|
|
1294
1713
|
const byKey = new Map();
|
|
1295
|
-
for (const m of queries.
|
|
1714
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1715
|
+
if ((++scanned255 & 63) === 0)
|
|
1716
|
+
await onYield();
|
|
1296
1717
|
if (!m.id.startsWith('expo-module:'))
|
|
1297
1718
|
continue;
|
|
1298
1719
|
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
@@ -1341,7 +1762,8 @@ function expoCrossPlatformEdges(queries) {
|
|
|
1341
1762
|
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
1342
1763
|
* lines up with the bare Android method name.
|
|
1343
1764
|
*/
|
|
1344
|
-
function rnCrossPlatformEdges(queries) {
|
|
1765
|
+
async function rnCrossPlatformEdges(queries, onYield) {
|
|
1766
|
+
let scanned255 = 0;
|
|
1345
1767
|
const edges = [];
|
|
1346
1768
|
const seen = new Set();
|
|
1347
1769
|
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
@@ -1363,6 +1785,8 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1363
1785
|
// below only runs for genuine cross-platform candidates.
|
|
1364
1786
|
const byName = new Map();
|
|
1365
1787
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1788
|
+
if ((++scanned255 & 63) === 0)
|
|
1789
|
+
await onYield();
|
|
1366
1790
|
if (!NATIVE.has(m.language))
|
|
1367
1791
|
continue;
|
|
1368
1792
|
const key = norm(m.name);
|
|
@@ -1413,17 +1837,27 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1413
1837
|
}
|
|
1414
1838
|
return edges;
|
|
1415
1839
|
}
|
|
1416
|
-
function fabricNativeImplEdges(ctx) {
|
|
1840
|
+
async function fabricNativeImplEdges(ctx, onYield) {
|
|
1841
|
+
let scanned255 = 0;
|
|
1417
1842
|
const edges = [];
|
|
1418
1843
|
const seen = new Set();
|
|
1419
1844
|
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
1420
|
-
// filter to just those
|
|
1421
|
-
|
|
1845
|
+
// filter to just those while streaming — never materializing the whole
|
|
1846
|
+
// `component` kind (#1212).
|
|
1847
|
+
const components = [];
|
|
1848
|
+
for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1849
|
+
if ((++scanned255 & 63) === 0)
|
|
1850
|
+
await onYield();
|
|
1851
|
+
if (n.id.startsWith('fabric-component:'))
|
|
1852
|
+
components.push(n);
|
|
1853
|
+
}
|
|
1422
1854
|
if (components.length === 0)
|
|
1423
1855
|
return edges;
|
|
1424
1856
|
// Pre-index native classes by name for O(1) lookup.
|
|
1425
1857
|
const nativeClassesByName = new Map();
|
|
1426
|
-
for (const n of ctx.getNodesByKind('class')) {
|
|
1858
|
+
for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
|
|
1859
|
+
if ((++scanned255 & 63) === 0)
|
|
1860
|
+
await onYield();
|
|
1427
1861
|
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
|
|
1428
1862
|
continue;
|
|
1429
1863
|
const arr = nativeClassesByName.get(n.name);
|
|
@@ -1476,12 +1910,29 @@ function fabricNativeImplEdges(ctx) {
|
|
|
1476
1910
|
* same simple name) are dropped. We need-not bridge by package because Java
|
|
1477
1911
|
* mapper interfaces are typically uniquely named within a project.
|
|
1478
1912
|
*/
|
|
1479
|
-
function mybatisJavaXmlEdges(queries) {
|
|
1913
|
+
async function mybatisJavaXmlEdges(queries, onYield) {
|
|
1914
|
+
let scanned255 = 0;
|
|
1480
1915
|
const edges = [];
|
|
1481
1916
|
const seen = new Set();
|
|
1917
|
+
// Collect the XML side FIRST (mapper `<select id=…>` statements extracted as
|
|
1918
|
+
// xml-language method nodes): if the project has none — every Java+XML repo
|
|
1919
|
+
// that doesn't use MyBatis — return before paying the full java-method
|
|
1920
|
+
// stream below. Same rowid stream order as matching inline, so the edge
|
|
1921
|
+
// output is byte-identical when mappers do exist.
|
|
1922
|
+
const xmlMethods = [];
|
|
1923
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1924
|
+
if ((++scanned255 & 63) === 0)
|
|
1925
|
+
await onYield();
|
|
1926
|
+
if (m.language === 'xml')
|
|
1927
|
+
xmlMethods.push(m);
|
|
1928
|
+
}
|
|
1929
|
+
if (xmlMethods.length === 0)
|
|
1930
|
+
return edges;
|
|
1482
1931
|
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
1483
1932
|
const javaIndex = new Map();
|
|
1484
1933
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1934
|
+
if ((++scanned255 & 63) === 0)
|
|
1935
|
+
await onYield();
|
|
1485
1936
|
if (m.language !== 'java' && m.language !== 'kotlin')
|
|
1486
1937
|
continue;
|
|
1487
1938
|
const parts = m.qualifiedName.split('::');
|
|
@@ -1496,9 +1947,9 @@ function mybatisJavaXmlEdges(queries) {
|
|
|
1496
1947
|
else
|
|
1497
1948
|
javaIndex.set(key, [m]);
|
|
1498
1949
|
}
|
|
1499
|
-
for (const xml of
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1950
|
+
for (const xml of xmlMethods) {
|
|
1951
|
+
if ((++scanned255 & 63) === 0)
|
|
1952
|
+
await onYield();
|
|
1502
1953
|
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
1503
1954
|
const colonIdx = xml.qualifiedName.lastIndexOf('::');
|
|
1504
1955
|
if (colonIdx < 0)
|
|
@@ -1610,10 +2061,14 @@ function goHandlerIdent(expr) {
|
|
|
1610
2061
|
const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
1611
2062
|
return m ? m[1] : null;
|
|
1612
2063
|
}
|
|
1613
|
-
function ginMiddlewareChainEdges(queries, ctx) {
|
|
2064
|
+
async function ginMiddlewareChainEdges(queries, ctx, onYield) {
|
|
2065
|
+
let scanned255 = 0;
|
|
2066
|
+
let scannedFiles = 0;
|
|
1614
2067
|
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
1615
2068
|
const dispatchers = [];
|
|
1616
2069
|
for (const n of queries.iterateNodesByKind('method')) {
|
|
2070
|
+
if ((++scanned255 & 63) === 0)
|
|
2071
|
+
await onYield();
|
|
1617
2072
|
if (n.language !== 'go')
|
|
1618
2073
|
continue;
|
|
1619
2074
|
const content = ctx.readFile(n.filePath);
|
|
@@ -1628,6 +2083,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1628
2083
|
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
1629
2084
|
const registered = new Map(); // name → registeredAt (file:line)
|
|
1630
2085
|
for (const file of ctx.getAllFiles()) {
|
|
2086
|
+
if ((++scannedFiles & 15) === 0)
|
|
2087
|
+
await onYield();
|
|
1631
2088
|
if (!file.endsWith('.go'))
|
|
1632
2089
|
continue;
|
|
1633
2090
|
const content = ctx.readFile(file);
|
|
@@ -1683,10 +2140,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1683
2140
|
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
1684
2141
|
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
1685
2142
|
*/
|
|
1686
|
-
function pascalFormEdges(ctx) {
|
|
2143
|
+
async function pascalFormEdges(ctx, onYield) {
|
|
2144
|
+
let scannedFiles = 0;
|
|
1687
2145
|
const edges = [];
|
|
1688
2146
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1689
2147
|
for (const file of allFiles) {
|
|
2148
|
+
if ((++scannedFiles & 255) === 0)
|
|
2149
|
+
await onYield();
|
|
1690
2150
|
if (!/\.(dfm|fmx)$/i.test(file))
|
|
1691
2151
|
continue;
|
|
1692
2152
|
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
@@ -1722,12 +2182,15 @@ function pascalFormEdges(ctx) {
|
|
|
1722
2182
|
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
1723
2183
|
* its loader.
|
|
1724
2184
|
*/
|
|
1725
|
-
function svelteKitLoadEdges(ctx) {
|
|
2185
|
+
async function svelteKitLoadEdges(ctx, onYield) {
|
|
2186
|
+
let scannedFiles = 0;
|
|
1726
2187
|
const edges = [];
|
|
1727
2188
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1728
2189
|
const HOOKS = new Set(['load', 'actions']);
|
|
1729
2190
|
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
1730
2191
|
for (const file of allFiles) {
|
|
2192
|
+
if ((++scannedFiles & 255) === 0)
|
|
2193
|
+
await onYield();
|
|
1731
2194
|
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
1732
2195
|
if (!m)
|
|
1733
2196
|
continue;
|
|
@@ -1760,91 +2223,6 @@ function svelteKitLoadEdges(ctx) {
|
|
|
1760
2223
|
}
|
|
1761
2224
|
return edges;
|
|
1762
2225
|
}
|
|
1763
|
-
/** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
|
|
1764
|
-
function arktsEntryEdges(queries, ctx) {
|
|
1765
|
-
const edges = [];
|
|
1766
|
-
const seen = new Set();
|
|
1767
|
-
const add = (source, target, synthesizedBy, extra = {}) => {
|
|
1768
|
-
const key = `${source}>${target}`;
|
|
1769
|
-
if (seen.has(key))
|
|
1770
|
-
return;
|
|
1771
|
-
seen.add(key);
|
|
1772
|
-
const targetNode = queries.getNodeById(target);
|
|
1773
|
-
edges.push({
|
|
1774
|
-
source,
|
|
1775
|
-
target,
|
|
1776
|
-
kind: 'calls',
|
|
1777
|
-
provenance: 'heuristic',
|
|
1778
|
-
metadata: {
|
|
1779
|
-
synthesizedBy,
|
|
1780
|
-
registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
|
|
1781
|
-
...extra,
|
|
1782
|
-
},
|
|
1783
|
-
});
|
|
1784
|
-
};
|
|
1785
|
-
for (const m of methodAndFunctionNodes(queries)) {
|
|
1786
|
-
if (m.language !== 'arkts')
|
|
1787
|
-
continue;
|
|
1788
|
-
const content = ctx.readFile(m.filePath);
|
|
1789
|
-
if (!content)
|
|
1790
|
-
continue;
|
|
1791
|
-
const lines = content.split('\n');
|
|
1792
|
-
const start = Math.max((m.startLine ?? 1) - 1, 0);
|
|
1793
|
-
const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
|
|
1794
|
-
const body = lines.slice(start, end).join('\n');
|
|
1795
|
-
const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
|
|
1796
|
-
if (!loadMatch)
|
|
1797
|
-
continue;
|
|
1798
|
-
const page = loadMatch[1];
|
|
1799
|
-
const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
|
|
1800
|
-
if (pageComponent) {
|
|
1801
|
-
const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
|
|
1802
|
-
if (aboutToAppear) {
|
|
1803
|
-
add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
|
|
1808
|
-
if (cls.language !== 'arkts')
|
|
1809
|
-
continue;
|
|
1810
|
-
const methods = queries
|
|
1811
|
-
.getOutgoingEdges(cls.id, ['contains'])
|
|
1812
|
-
.map((e) => queries.getNodeById(e.target))
|
|
1813
|
-
.filter((n) => !!n && n.kind === 'method');
|
|
1814
|
-
const onCreate = methods.find((n) => n.name === 'onCreate');
|
|
1815
|
-
const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
|
|
1816
|
-
if (onCreate && onWindowStageCreate) {
|
|
1817
|
-
add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
for (const comp of [
|
|
1821
|
-
...queries.getNodesByKind('component'),
|
|
1822
|
-
...queries.getNodesByKind('class'),
|
|
1823
|
-
...queries.getNodesByKind('struct'),
|
|
1824
|
-
]) {
|
|
1825
|
-
if (comp.language !== 'arkts')
|
|
1826
|
-
continue;
|
|
1827
|
-
const owner = comp.kind === 'component'
|
|
1828
|
-
? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
1829
|
-
queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
1830
|
-
comp
|
|
1831
|
-
: comp;
|
|
1832
|
-
const methods = queries
|
|
1833
|
-
.getOutgoingEdges(owner.id, ['contains'])
|
|
1834
|
-
.map((e) => queries.getNodeById(e.target))
|
|
1835
|
-
.filter((n) => !!n && n.kind === 'method');
|
|
1836
|
-
const build = methods.find((n) => n.name === 'build');
|
|
1837
|
-
if (!build)
|
|
1838
|
-
continue;
|
|
1839
|
-
for (const life of ['aboutToAppear', 'onPageShow']) {
|
|
1840
|
-
const lifeMethod = methods.find((n) => n.name === life);
|
|
1841
|
-
if (lifeMethod) {
|
|
1842
|
-
add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
return edges;
|
|
1847
|
-
}
|
|
1848
2226
|
/**
|
|
1849
2227
|
* Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
|
|
1850
2228
|
* (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
|
|
@@ -1861,10 +2239,13 @@ function arktsEntryEdges(queries, ctx) {
|
|
|
1861
2239
|
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1862
2240
|
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1863
2241
|
const THUNK_FANOUT_CAP = 24;
|
|
1864
|
-
function reduxThunkEdges(queries, ctx) {
|
|
2242
|
+
async function reduxThunkEdges(queries, ctx, onYield) {
|
|
2243
|
+
let scanned255 = 0;
|
|
1865
2244
|
const edges = [];
|
|
1866
2245
|
const seen = new Set();
|
|
1867
2246
|
for (const node of queries.iterateNodesByKind('constant')) {
|
|
2247
|
+
if ((++scanned255 & 63) === 0)
|
|
2248
|
+
await onYield();
|
|
1868
2249
|
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1869
2250
|
// avoids reading every constant's body on a large repo.
|
|
1870
2251
|
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
@@ -1996,10 +2377,13 @@ function resolveRegistryHandler(ctx, name, chained) {
|
|
|
1996
2377
|
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1997
2378
|
}
|
|
1998
2379
|
async function objectRegistryEdges(ctx, onYield) {
|
|
2380
|
+
let scannedFiles = 0;
|
|
1999
2381
|
const edges = [];
|
|
2000
2382
|
const seen = new Set();
|
|
2001
2383
|
let scanned = 0;
|
|
2002
2384
|
for (const file of ctx.getAllFiles()) {
|
|
2385
|
+
if ((++scannedFiles & 15) === 0)
|
|
2386
|
+
await onYield();
|
|
2003
2387
|
if ((++scanned & 255) === 0)
|
|
2004
2388
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
2005
2389
|
if (!REGISTRY_JS_EXT.test(file))
|
|
@@ -2111,10 +2495,13 @@ function rtkEndpointNameFromHook(hook) {
|
|
|
2111
2495
|
return null;
|
|
2112
2496
|
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2113
2497
|
}
|
|
2114
|
-
function rtkQueryEdges(queries, ctx) {
|
|
2498
|
+
async function rtkQueryEdges(queries, ctx, onYield) {
|
|
2499
|
+
let scanned255 = 0;
|
|
2115
2500
|
const edges = [];
|
|
2116
2501
|
const seen = new Set();
|
|
2117
2502
|
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2503
|
+
if ((++scanned255 & 63) === 0)
|
|
2504
|
+
await onYield();
|
|
2118
2505
|
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2119
2506
|
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2120
2507
|
continue;
|
|
@@ -2158,10 +2545,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
|
|
|
2158
2545
|
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2159
2546
|
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2160
2547
|
const PINIA_FANOUT_CAP = 80;
|
|
2161
|
-
function piniaStoreEdges(ctx) {
|
|
2548
|
+
async function piniaStoreEdges(ctx, onYield) {
|
|
2549
|
+
let scannedFiles = 0;
|
|
2162
2550
|
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2163
2551
|
const factoryFile = new Map();
|
|
2164
2552
|
for (const file of ctx.getAllFiles()) {
|
|
2553
|
+
if ((++scannedFiles & 15) === 0)
|
|
2554
|
+
await onYield();
|
|
2165
2555
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2166
2556
|
continue;
|
|
2167
2557
|
const content = ctx.readFile(file);
|
|
@@ -2177,6 +2567,8 @@ function piniaStoreEdges(ctx) {
|
|
|
2177
2567
|
const edges = [];
|
|
2178
2568
|
const seen = new Set();
|
|
2179
2569
|
for (const file of ctx.getAllFiles()) {
|
|
2570
|
+
if ((++scannedFiles & 15) === 0)
|
|
2571
|
+
await onYield();
|
|
2180
2572
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2181
2573
|
continue;
|
|
2182
2574
|
const content = ctx.readFile(file);
|
|
@@ -2249,7 +2641,8 @@ const VUEX_FANOUT_CAP = 120;
|
|
|
2249
2641
|
function pathHasSegment(filePath, seg) {
|
|
2250
2642
|
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2251
2643
|
}
|
|
2252
|
-
function vuexDispatchEdges(ctx) {
|
|
2644
|
+
async function vuexDispatchEdges(ctx, onYield) {
|
|
2645
|
+
let scannedFiles = 0;
|
|
2253
2646
|
const storeFileCache = new Map();
|
|
2254
2647
|
const isStoreFile = (file) => {
|
|
2255
2648
|
let v = storeFileCache.get(file);
|
|
@@ -2287,6 +2680,8 @@ function vuexDispatchEdges(ctx) {
|
|
|
2287
2680
|
const edges = [];
|
|
2288
2681
|
const seen = new Set();
|
|
2289
2682
|
for (const file of ctx.getAllFiles()) {
|
|
2683
|
+
if ((++scannedFiles & 15) === 0)
|
|
2684
|
+
await onYield();
|
|
2290
2685
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2291
2686
|
continue;
|
|
2292
2687
|
const content = ctx.readFile(file);
|
|
@@ -2346,7 +2741,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
|
|
|
2346
2741
|
const CELERY_PY_EXT = /\.py$/;
|
|
2347
2742
|
const CELERY_FANOUT_CAP = 80;
|
|
2348
2743
|
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2349
|
-
function celeryDispatchEdges(ctx) {
|
|
2744
|
+
async function celeryDispatchEdges(ctx, onYield) {
|
|
2745
|
+
let scannedFiles = 0;
|
|
2350
2746
|
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2351
2747
|
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2352
2748
|
// receivers, so the candidate set stays small.
|
|
@@ -2390,6 +2786,8 @@ function celeryDispatchEdges(ctx) {
|
|
|
2390
2786
|
const edges = [];
|
|
2391
2787
|
const seen = new Set();
|
|
2392
2788
|
for (const file of ctx.getAllFiles()) {
|
|
2789
|
+
if ((++scannedFiles & 15) === 0)
|
|
2790
|
+
await onYield();
|
|
2393
2791
|
if (!CELERY_PY_EXT.test(file))
|
|
2394
2792
|
continue;
|
|
2395
2793
|
const content = ctx.readFile(file);
|
|
@@ -2468,15 +2866,24 @@ function springFirstParamType(sig) {
|
|
|
2468
2866
|
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2469
2867
|
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2470
2868
|
}
|
|
2471
|
-
function springEventEdges(ctx) {
|
|
2869
|
+
async function springEventEdges(ctx, onYield) {
|
|
2870
|
+
let scannedFiles = 0;
|
|
2472
2871
|
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2872
|
+
// This is the ONLY full read sweep: publisher files are recorded here so
|
|
2873
|
+
// pass 2 re-reads just those instead of every .java file again (#1212 —
|
|
2874
|
+
// the double full-repo read was one of the tail's longest unyielded spans).
|
|
2473
2875
|
const listeners = new Map();
|
|
2876
|
+
const publisherFiles = [];
|
|
2474
2877
|
for (const file of ctx.getAllFiles()) {
|
|
2878
|
+
if ((++scannedFiles & 15) === 0)
|
|
2879
|
+
await onYield();
|
|
2475
2880
|
if (!SPRING_JAVA_EXT.test(file))
|
|
2476
2881
|
continue;
|
|
2477
2882
|
const content = ctx.readFile(file);
|
|
2478
2883
|
if (!content)
|
|
2479
2884
|
continue;
|
|
2885
|
+
if (content.includes('.publishEvent('))
|
|
2886
|
+
publisherFiles.push(file);
|
|
2480
2887
|
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2481
2888
|
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2482
2889
|
if (!hasAnno && !hasAppListener)
|
|
@@ -2516,12 +2923,13 @@ function springEventEdges(ctx) {
|
|
|
2516
2923
|
}
|
|
2517
2924
|
if (!listeners.size)
|
|
2518
2925
|
return [];
|
|
2519
|
-
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2926
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2927
|
+
// XEvent. Only the publisher files recorded in pass 1 are (re-)read.
|
|
2520
2928
|
const edges = [];
|
|
2521
2929
|
const seen = new Set();
|
|
2522
|
-
for (const file of
|
|
2523
|
-
if (
|
|
2524
|
-
|
|
2930
|
+
for (const file of publisherFiles) {
|
|
2931
|
+
if ((++scannedFiles & 15) === 0)
|
|
2932
|
+
await onYield();
|
|
2525
2933
|
const content = ctx.readFile(file);
|
|
2526
2934
|
if (!content || !content.includes('.publishEvent('))
|
|
2527
2935
|
continue;
|
|
@@ -2606,10 +3014,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
|
2606
3014
|
}
|
|
2607
3015
|
return declType;
|
|
2608
3016
|
}
|
|
2609
|
-
function mediatrDispatchEdges(ctx) {
|
|
3017
|
+
async function mediatrDispatchEdges(ctx, onYield) {
|
|
3018
|
+
let scannedFiles = 0;
|
|
2610
3019
|
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2611
3020
|
const handlers = new Map();
|
|
2612
3021
|
for (const file of ctx.getAllFiles()) {
|
|
3022
|
+
if ((++scannedFiles & 15) === 0)
|
|
3023
|
+
await onYield();
|
|
2613
3024
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2614
3025
|
continue;
|
|
2615
3026
|
const content = ctx.readFile(file);
|
|
@@ -2643,6 +3054,8 @@ function mediatrDispatchEdges(ctx) {
|
|
|
2643
3054
|
const edges = [];
|
|
2644
3055
|
const seen = new Set();
|
|
2645
3056
|
for (const file of ctx.getAllFiles()) {
|
|
3057
|
+
if ((++scannedFiles & 15) === 0)
|
|
3058
|
+
await onYield();
|
|
2646
3059
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2647
3060
|
continue;
|
|
2648
3061
|
const content = ctx.readFile(file);
|
|
@@ -2706,7 +3119,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
|
|
|
2706
3119
|
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2707
3120
|
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2708
3121
|
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2709
|
-
function sidekiqDispatchEdges(ctx) {
|
|
3122
|
+
async function sidekiqDispatchEdges(ctx, onYield) {
|
|
3123
|
+
let scannedFiles = 0;
|
|
2710
3124
|
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2711
3125
|
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2712
3126
|
const performCache = new Map();
|
|
@@ -2743,6 +3157,8 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2743
3157
|
const edges = [];
|
|
2744
3158
|
const seen = new Set();
|
|
2745
3159
|
for (const file of ctx.getAllFiles()) {
|
|
3160
|
+
if ((++scannedFiles & 15) === 0)
|
|
3161
|
+
await onYield();
|
|
2746
3162
|
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2747
3163
|
continue;
|
|
2748
3164
|
const content = ctx.readFile(file);
|
|
@@ -2778,7 +3194,415 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2778
3194
|
}
|
|
2779
3195
|
return edges;
|
|
2780
3196
|
}
|
|
2781
|
-
// ──
|
|
3197
|
+
// ── Erlang behaviour-callback dispatch ────────────────────────────────────────
|
|
3198
|
+
// An Erlang behaviour is a compile-checked callback contract: the behaviour
|
|
3199
|
+
// module declares `-callback init(...) -> ...`, implementers declare
|
|
3200
|
+
// `-behaviour(B)` and export the callbacks, and the framework side dispatches
|
|
3201
|
+
// through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
|
|
3202
|
+
// `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
|
|
3203
|
+
// deliberately leaves var-module calls silent (no static target), so the flow
|
|
3204
|
+
// breaks at exactly the hop agents ask about (request → handler init). Bridge:
|
|
3205
|
+
//
|
|
3206
|
+
// dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
|
|
3207
|
+
// declaring `fn` with the SITE's arity — provided exactly ONE in-repo
|
|
3208
|
+
// behaviour declares (fn, arity); a name+arity collision across behaviours
|
|
3209
|
+
// bails (silent beats wrong) — and the implementer defines and exports `fn`.
|
|
3210
|
+
//
|
|
3211
|
+
// Behaviours are discovered by scanning every Erlang file for `-callback`
|
|
3212
|
+
// declarations (not just `implements` targets), so a behaviour with zero
|
|
3213
|
+
// implementers still participates in the ambiguity gate. Fan-out control: a
|
|
3214
|
+
// mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
|
|
3215
|
+
// hundreds of edges per site that READ as complete coverage while being
|
|
3216
|
+
// arbitrary — above the cap the site is skipped entirely and the boundary
|
|
3217
|
+
// stays visibly dynamic (explore's boundary announcer covers it) instead of
|
|
3218
|
+
// silently truncated.
|
|
3219
|
+
const ERLANG_EXT = /\.(?:erl|hrl)$/;
|
|
3220
|
+
// `Var:fn(` — variable (capitalized) module, lowercase function, immediate
|
|
3221
|
+
// open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
|
|
3222
|
+
// (static remote call, already linked), and mid-word matches.
|
|
3223
|
+
const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
|
|
3224
|
+
const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
|
|
3225
|
+
const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
|
|
3226
|
+
/**
|
|
3227
|
+
* Argument count of the call/declaration whose `(` sits at `openIdx` —
|
|
3228
|
+
* top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
|
|
3229
|
+
* (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
|
|
3230
|
+
* so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
|
|
3231
|
+
*/
|
|
3232
|
+
function erlangArityAt(src, openIdx) {
|
|
3233
|
+
let depth = 1;
|
|
3234
|
+
let commas = 0;
|
|
3235
|
+
let sawArg = false;
|
|
3236
|
+
const limit = Math.min(src.length, openIdx + 4000);
|
|
3237
|
+
for (let i = openIdx + 1; i < limit; i++) {
|
|
3238
|
+
const ch = src[i];
|
|
3239
|
+
if (ch === '"' || ch === "'") {
|
|
3240
|
+
i++;
|
|
3241
|
+
while (i < limit && src[i] !== ch) {
|
|
3242
|
+
if (src[i] === '\\')
|
|
3243
|
+
i++;
|
|
3244
|
+
i++;
|
|
3245
|
+
}
|
|
3246
|
+
sawArg = true;
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (ch === '$') {
|
|
3250
|
+
i++;
|
|
3251
|
+
if (src[i] === '\\')
|
|
3252
|
+
i++;
|
|
3253
|
+
sawArg = true;
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
3257
|
+
depth++;
|
|
3258
|
+
sawArg = true;
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
3262
|
+
depth--;
|
|
3263
|
+
if (depth === 0)
|
|
3264
|
+
return sawArg ? commas + 1 : 0;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
if (ch === ',' && depth === 1) {
|
|
3268
|
+
commas++;
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3271
|
+
if (!/\s/.test(ch))
|
|
3272
|
+
sawArg = true;
|
|
3273
|
+
}
|
|
3274
|
+
return -1;
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
|
|
3278
|
+
* DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
|
|
3279
|
+
* and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
|
|
3280
|
+
* config) — the connection happens by option-path unification inside the
|
|
3281
|
+
* module-system evaluator, so there is no static call/import edge to follow
|
|
3282
|
+
* and flow questions ("how does services.yabai.enable become a launchd
|
|
3283
|
+
* service?") go dark at the module boundary.
|
|
3284
|
+
*
|
|
3285
|
+
* This pass links each config-write binding to the option declaration whose
|
|
3286
|
+
* path is the longest static-segment prefix of the write path. Precision gates:
|
|
3287
|
+
* - only STATIC segments participate: plain identifiers, plus quoted segments
|
|
3288
|
+
* (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
|
|
3289
|
+
* only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
|
|
3290
|
+
* so dynamic paths never match beyond their static head;
|
|
3291
|
+
* - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
|
|
3292
|
+
* every package's `meta = { ... }` attrset to nixos's `options.meta`;
|
|
3293
|
+
* - a prefix declared in more than one file is ambiguous → no edge (a wrong
|
|
3294
|
+
* edge is worse than none);
|
|
3295
|
+
* - writes physically inside an options block are declaration internals
|
|
3296
|
+
* (types, defaults, examples), never config writes → excluded.
|
|
3297
|
+
* Both declaration spellings register: flat (`options.a.b = ...`) by name, and
|
|
3298
|
+
* nested (`options = { a.b = ...; }`) by line-span containment.
|
|
3299
|
+
*/
|
|
3300
|
+
function nixLeadingPlainSegments(name) {
|
|
3301
|
+
const segs = [];
|
|
3302
|
+
let i = 0;
|
|
3303
|
+
const n = name.length;
|
|
3304
|
+
while (i < n) {
|
|
3305
|
+
if (name[i] === '"') {
|
|
3306
|
+
// Quoted segment — an opaque verbatim token (quotes kept, so it can
|
|
3307
|
+
// never collide with a plain identifier). `NSGlobalDomain."com.apple.
|
|
3308
|
+
// mouse.tapBehavior"` must match ITS OWN quoted declaration, not
|
|
3309
|
+
// whichever sibling registered the shared plain prefix first.
|
|
3310
|
+
let j = i + 1;
|
|
3311
|
+
while (j < n && name[j] !== '"') {
|
|
3312
|
+
if (name[j] === '\\')
|
|
3313
|
+
j++;
|
|
3314
|
+
j++;
|
|
3315
|
+
}
|
|
3316
|
+
if (j >= n)
|
|
3317
|
+
return segs; // unterminated — stop at the static head
|
|
3318
|
+
const tok = name.slice(i, j + 1);
|
|
3319
|
+
if (tok.includes('${'))
|
|
3320
|
+
return segs; // interpolated → dynamic → stop
|
|
3321
|
+
segs.push(tok);
|
|
3322
|
+
i = j + 1;
|
|
3323
|
+
if (i >= n)
|
|
3324
|
+
break;
|
|
3325
|
+
if (name[i] !== '.')
|
|
3326
|
+
return segs;
|
|
3327
|
+
i++;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
let j = i;
|
|
3331
|
+
while (j < n && name[j] !== '.') {
|
|
3332
|
+
if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
|
|
3333
|
+
return segs;
|
|
3334
|
+
j++;
|
|
3335
|
+
}
|
|
3336
|
+
const seg = name.slice(i, j);
|
|
3337
|
+
if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
|
|
3338
|
+
return segs;
|
|
3339
|
+
segs.push(seg);
|
|
3340
|
+
i = j + 1;
|
|
3341
|
+
}
|
|
3342
|
+
return segs;
|
|
3343
|
+
}
|
|
3344
|
+
async function nixOptionPathEdges(queries, onYield) {
|
|
3345
|
+
let scanned255 = 0;
|
|
3346
|
+
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
3347
|
+
// option); memory stays O(bindings-kept), not O(all nodes) (#610).
|
|
3348
|
+
const byFile = new Map();
|
|
3349
|
+
let scanned = 0;
|
|
3350
|
+
for (const kind of ['variable', 'function']) {
|
|
3351
|
+
for (const node of queries.iterateNodesByKind(kind)) {
|
|
3352
|
+
if ((++scanned255 & 63) === 0)
|
|
3353
|
+
await onYield();
|
|
3354
|
+
if ((++scanned & 0x3fff) === 0 && onYield)
|
|
3355
|
+
await onYield();
|
|
3356
|
+
if (node.language !== 'nix')
|
|
3357
|
+
continue;
|
|
3358
|
+
const segs = nixLeadingPlainSegments(node.name);
|
|
3359
|
+
if (segs.length === 0)
|
|
3360
|
+
continue;
|
|
3361
|
+
const rec = {
|
|
3362
|
+
id: node.id,
|
|
3363
|
+
filePath: node.filePath,
|
|
3364
|
+
startLine: node.startLine,
|
|
3365
|
+
endLine: node.endLine,
|
|
3366
|
+
segs,
|
|
3367
|
+
};
|
|
3368
|
+
const arr = byFile.get(node.filePath);
|
|
3369
|
+
if (arr)
|
|
3370
|
+
arr.push(rec);
|
|
3371
|
+
else
|
|
3372
|
+
byFile.set(node.filePath, [rec]);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
// Per file: walk bindings outermost-first with a stack of active option
|
|
3376
|
+
// spans, composing nested declaration paths (`options = { services.foo = {
|
|
3377
|
+
// enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
|
|
3378
|
+
// An `options` binding nested inside another option span is a SUBMODULE's
|
|
3379
|
+
// own namespace (`attrsOf (submodule { options = ...; })`) — its internals
|
|
3380
|
+
// are not globally addressable, so the sentinel blocks registration below it
|
|
3381
|
+
// while still excluding the region from write candidates.
|
|
3382
|
+
const SUBMODULE = 'submodule';
|
|
3383
|
+
const decls = new Map();
|
|
3384
|
+
const writes = [];
|
|
3385
|
+
const register = (path, rec) => {
|
|
3386
|
+
if (path.length < 2 || path.includes(SUBMODULE))
|
|
3387
|
+
return;
|
|
3388
|
+
const key = path.join('.');
|
|
3389
|
+
const arr = decls.get(key);
|
|
3390
|
+
if (arr)
|
|
3391
|
+
arr.push(rec);
|
|
3392
|
+
else
|
|
3393
|
+
decls.set(key, [rec]);
|
|
3394
|
+
};
|
|
3395
|
+
for (const recs of byFile.values()) {
|
|
3396
|
+
recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
3397
|
+
const stack = [];
|
|
3398
|
+
for (const rec of recs) {
|
|
3399
|
+
while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
|
|
3400
|
+
stack.pop();
|
|
3401
|
+
// Strict containment at line granularity: a one-line nested binding is
|
|
3402
|
+
// indistinguishable from its container, so it stays unclassified (rare
|
|
3403
|
+
// in module code, where option blocks are multi-line).
|
|
3404
|
+
const enclosing = stack.length > 0 &&
|
|
3405
|
+
rec.startLine >= stack[stack.length - 1].start &&
|
|
3406
|
+
rec.endLine <= stack[stack.length - 1].end &&
|
|
3407
|
+
!(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
|
|
3408
|
+
? stack[stack.length - 1]
|
|
3409
|
+
: null;
|
|
3410
|
+
if (rec.segs[0] === 'options') {
|
|
3411
|
+
const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
|
|
3412
|
+
const prefix = enclosing ? [SUBMODULE] : ownPath;
|
|
3413
|
+
register(prefix, rec);
|
|
3414
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix });
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
if (enclosing) {
|
|
3418
|
+
const composed = [...enclosing.prefix, ...rec.segs];
|
|
3419
|
+
register(composed, rec);
|
|
3420
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
|
|
3421
|
+
continue;
|
|
3422
|
+
}
|
|
3423
|
+
if (rec.segs.length >= 2) {
|
|
3424
|
+
writes.push(rec);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (decls.size === 0 || writes.length === 0)
|
|
3429
|
+
return [];
|
|
3430
|
+
const edges = [];
|
|
3431
|
+
for (const w of writes) {
|
|
3432
|
+
// `config.services.x = ...` spells the same write with an explicit prefix.
|
|
3433
|
+
const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
|
|
3434
|
+
if (segs.length < 2)
|
|
3435
|
+
continue;
|
|
3436
|
+
// Longest prefix wins; an ambiguous longest match does NOT fall back to a
|
|
3437
|
+
// shorter one (that would link `services.nginx.virtualHosts.x` to
|
|
3438
|
+
// `options.services.nginx` when virtualHosts is the contested path).
|
|
3439
|
+
for (let len = Math.min(segs.length, 6); len >= 2; len--) {
|
|
3440
|
+
const candidates = decls.get(segs.slice(0, len).join('.'));
|
|
3441
|
+
if (!candidates || candidates.length === 0)
|
|
3442
|
+
continue;
|
|
3443
|
+
const files = new Set(candidates.map((c) => c.filePath));
|
|
3444
|
+
if (files.size === 1) {
|
|
3445
|
+
const target = candidates[0];
|
|
3446
|
+
if (target.id !== w.id) {
|
|
3447
|
+
edges.push({
|
|
3448
|
+
source: w.id,
|
|
3449
|
+
target: target.id,
|
|
3450
|
+
kind: 'references',
|
|
3451
|
+
line: w.startLine,
|
|
3452
|
+
provenance: 'heuristic',
|
|
3453
|
+
metadata: {
|
|
3454
|
+
synthesizedBy: 'nix-option-path',
|
|
3455
|
+
optionPath: segs.slice(0, len).join('.'),
|
|
3456
|
+
registeredAt: `${target.filePath}:${target.startLine}`,
|
|
3457
|
+
},
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
break; // longest hit decides, matched or ambiguous
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return edges;
|
|
3465
|
+
}
|
|
3466
|
+
async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
|
|
3467
|
+
let scannedFiles = 0;
|
|
3468
|
+
let scanned255 = 0;
|
|
3469
|
+
// Cheap language gate: no Erlang modules → no cost beyond one streamed
|
|
3470
|
+
// kind scan (never a materialized array of every namespace — #1212).
|
|
3471
|
+
const erlangModules = [];
|
|
3472
|
+
for (const n of queries.iterateNodesByKind('namespace')) {
|
|
3473
|
+
if ((++scanned255 & 63) === 0)
|
|
3474
|
+
await onYield();
|
|
3475
|
+
if (n.language === 'erlang')
|
|
3476
|
+
erlangModules.push(n);
|
|
3477
|
+
}
|
|
3478
|
+
if (erlangModules.length === 0)
|
|
3479
|
+
return [];
|
|
3480
|
+
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
3481
|
+
// its (name, arity) callback set, and the global `name/arity` → declaring
|
|
3482
|
+
// behaviours map that drives the ambiguity gate.
|
|
3483
|
+
const moduleByFile = new Map();
|
|
3484
|
+
for (const ns of erlangModules) {
|
|
3485
|
+
if (!moduleByFile.has(ns.filePath))
|
|
3486
|
+
moduleByFile.set(ns.filePath, ns);
|
|
3487
|
+
}
|
|
3488
|
+
const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
|
|
3489
|
+
const callbackNames = new Set();
|
|
3490
|
+
for (const file of ctx.getAllFiles()) {
|
|
3491
|
+
if ((++scannedFiles & 15) === 0)
|
|
3492
|
+
await onYield();
|
|
3493
|
+
if (!ERLANG_EXT.test(file))
|
|
3494
|
+
continue;
|
|
3495
|
+
const behaviour = moduleByFile.get(file);
|
|
3496
|
+
if (!behaviour)
|
|
3497
|
+
continue; // a .hrl or module-less file can't be a behaviour
|
|
3498
|
+
const content = ctx.readFile(file);
|
|
3499
|
+
if (!content || !content.includes('-callback'))
|
|
3500
|
+
continue;
|
|
3501
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3502
|
+
ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
|
|
3503
|
+
let m;
|
|
3504
|
+
while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
|
|
3505
|
+
const name = m[2].replace(/^'|'$/g, '');
|
|
3506
|
+
const arity = erlangArityAt(safe, m.index + m[0].length - 1);
|
|
3507
|
+
if (arity < 0)
|
|
3508
|
+
continue;
|
|
3509
|
+
const key = `${name}/${arity}`;
|
|
3510
|
+
const arr = declaringBehaviours.get(key);
|
|
3511
|
+
if (arr) {
|
|
3512
|
+
if (!arr.some((b) => b.id === behaviour.id))
|
|
3513
|
+
arr.push(behaviour);
|
|
3514
|
+
}
|
|
3515
|
+
else {
|
|
3516
|
+
declaringBehaviours.set(key, [behaviour]);
|
|
3517
|
+
}
|
|
3518
|
+
callbackNames.add(name);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
if (declaringBehaviours.size === 0)
|
|
3522
|
+
return [];
|
|
3523
|
+
// Implementer target lookup, lazy per (behaviour, fn): implementers come
|
|
3524
|
+
// from the `implements` edges extraction resolved, and the target is the
|
|
3525
|
+
// implementer module's own exported `fn` function node.
|
|
3526
|
+
const targetCache = new Map();
|
|
3527
|
+
const targetsOf = (behaviour, fn) => {
|
|
3528
|
+
const cacheKey = `${behaviour.id}#${fn}`;
|
|
3529
|
+
let targets = targetCache.get(cacheKey);
|
|
3530
|
+
if (targets)
|
|
3531
|
+
return targets;
|
|
3532
|
+
targets = [];
|
|
3533
|
+
for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
|
|
3534
|
+
const impl = queries.getNodeById(e.source);
|
|
3535
|
+
if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
|
|
3536
|
+
continue;
|
|
3537
|
+
const fnNode = ctx
|
|
3538
|
+
.getNodesInFile(impl.filePath)
|
|
3539
|
+
.find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
|
|
3540
|
+
if (fnNode)
|
|
3541
|
+
targets.push(fnNode);
|
|
3542
|
+
}
|
|
3543
|
+
targetCache.set(cacheKey, targets);
|
|
3544
|
+
return targets;
|
|
3545
|
+
};
|
|
3546
|
+
// Pass 2 — dispatch sites. Only files containing a var-module call shape are
|
|
3547
|
+
// scanned in full.
|
|
3548
|
+
const edges = [];
|
|
3549
|
+
const seen = new Set();
|
|
3550
|
+
for (const file of ctx.getAllFiles()) {
|
|
3551
|
+
if ((++scannedFiles & 15) === 0)
|
|
3552
|
+
await onYield();
|
|
3553
|
+
if (!ERLANG_EXT.test(file))
|
|
3554
|
+
continue;
|
|
3555
|
+
const content = ctx.readFile(file);
|
|
3556
|
+
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
|
|
3557
|
+
continue;
|
|
3558
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3559
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
3560
|
+
ERLANG_DISPATCH_RE.lastIndex = 0;
|
|
3561
|
+
let m;
|
|
3562
|
+
while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
|
|
3563
|
+
const fn = m[3];
|
|
3564
|
+
if (!callbackNames.has(fn))
|
|
3565
|
+
continue;
|
|
3566
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3567
|
+
const arity = erlangArityAt(safe, openIdx);
|
|
3568
|
+
if (arity < 0)
|
|
3569
|
+
continue;
|
|
3570
|
+
const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
|
|
3571
|
+
if (!behaviours || behaviours.length !== 1)
|
|
3572
|
+
continue; // unknown or ambiguous
|
|
3573
|
+
const behaviour = behaviours[0];
|
|
3574
|
+
const targets = targetsOf(behaviour, fn);
|
|
3575
|
+
if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
|
|
3576
|
+
continue;
|
|
3577
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
3578
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
3579
|
+
if (!disp)
|
|
3580
|
+
continue;
|
|
3581
|
+
for (const target of targets) {
|
|
3582
|
+
if (target.id === disp.id)
|
|
3583
|
+
continue;
|
|
3584
|
+
const key = `${disp.id}>${target.id}`;
|
|
3585
|
+
if (seen.has(key))
|
|
3586
|
+
continue;
|
|
3587
|
+
seen.add(key);
|
|
3588
|
+
edges.push({
|
|
3589
|
+
source: disp.id,
|
|
3590
|
+
target: target.id,
|
|
3591
|
+
kind: 'calls',
|
|
3592
|
+
line,
|
|
3593
|
+
provenance: 'heuristic',
|
|
3594
|
+
metadata: {
|
|
3595
|
+
synthesizedBy: 'erlang-behaviour',
|
|
3596
|
+
via: `${behaviour.name}:${fn}/${arity}`,
|
|
3597
|
+
registeredAt: `${file}:${line}`,
|
|
3598
|
+
},
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
return edges;
|
|
3604
|
+
}
|
|
3605
|
+
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2782
3606
|
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2783
3607
|
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
2784
3608
|
// class UpdateLastfmNowPlaying { public function handle(PlaybackStarted $event) { … } }
|
|
@@ -2824,7 +3648,8 @@ function phpArrayBody(src, openIdx) {
|
|
|
2824
3648
|
}
|
|
2825
3649
|
return null;
|
|
2826
3650
|
}
|
|
2827
|
-
function laravelEventEdges(ctx) {
|
|
3651
|
+
async function laravelEventEdges(ctx, onYield) {
|
|
3652
|
+
let scannedFiles = 0;
|
|
2828
3653
|
// event short name → its listener `handle` methods (deduped by node id).
|
|
2829
3654
|
const listeners = new Map();
|
|
2830
3655
|
const add = (event, handle) => {
|
|
@@ -2841,6 +3666,8 @@ function laravelEventEdges(ctx) {
|
|
|
2841
3666
|
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2842
3667
|
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2843
3668
|
for (const file of ctx.getAllFiles()) {
|
|
3669
|
+
if ((++scannedFiles & 15) === 0)
|
|
3670
|
+
await onYield();
|
|
2844
3671
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2845
3672
|
continue;
|
|
2846
3673
|
const content = ctx.readFile(file);
|
|
@@ -2886,6 +3713,8 @@ function laravelEventEdges(ctx) {
|
|
|
2886
3713
|
const edges = [];
|
|
2887
3714
|
const seen = new Set();
|
|
2888
3715
|
for (const file of ctx.getAllFiles()) {
|
|
3716
|
+
if ((++scannedFiles & 15) === 0)
|
|
3717
|
+
await onYield();
|
|
2889
3718
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2890
3719
|
continue;
|
|
2891
3720
|
const content = ctx.readFile(file);
|
|
@@ -2936,7 +3765,182 @@ function laravelEventEdges(ctx) {
|
|
|
2936
3765
|
* Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
|
|
2937
3766
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
2938
3767
|
*/
|
|
2939
|
-
|
|
3768
|
+
/**
|
|
3769
|
+
* Number of progress steps synthesizeCallbackEdges reports: one per `__mark()`
|
|
3770
|
+
* call (every synthesis pass, plus the dedupe-merge and edge-insert steps).
|
|
3771
|
+
* Cosmetic only — drift just makes the progress bar end early or jump — and a
|
|
3772
|
+
* test pins it to the actual step count (registry passes + the fixed
|
|
3773
|
+
* pre/post marks) so adding a pass without bumping this fails loudly instead
|
|
3774
|
+
* of silently skewing the bar.
|
|
3775
|
+
*/
|
|
3776
|
+
const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
|
|
3777
|
+
/** ArkTS startup + UI lifecycle: loadContent → first screen, Ability chain, aboutToAppear → build. */
|
|
3778
|
+
function arktsEntryEdges(queries, ctx) {
|
|
3779
|
+
const edges = [];
|
|
3780
|
+
const seen = new Set();
|
|
3781
|
+
const add = (source, target, synthesizedBy, extra = {}) => {
|
|
3782
|
+
const key = `${source}>${target}`;
|
|
3783
|
+
if (seen.has(key))
|
|
3784
|
+
return;
|
|
3785
|
+
seen.add(key);
|
|
3786
|
+
const targetNode = queries.getNodeById(target);
|
|
3787
|
+
edges.push({
|
|
3788
|
+
source,
|
|
3789
|
+
target,
|
|
3790
|
+
kind: 'calls',
|
|
3791
|
+
provenance: 'heuristic',
|
|
3792
|
+
metadata: {
|
|
3793
|
+
synthesizedBy,
|
|
3794
|
+
registeredAt: targetNode ? `${targetNode.filePath}:${targetNode.startLine}` : '',
|
|
3795
|
+
...extra,
|
|
3796
|
+
},
|
|
3797
|
+
});
|
|
3798
|
+
};
|
|
3799
|
+
for (const m of methodAndFunctionNodes(queries)) {
|
|
3800
|
+
if (m.language !== 'arkts')
|
|
3801
|
+
continue;
|
|
3802
|
+
const content = ctx.readFile(m.filePath);
|
|
3803
|
+
if (!content)
|
|
3804
|
+
continue;
|
|
3805
|
+
const lines = content.split('\n');
|
|
3806
|
+
const start = Math.max((m.startLine ?? 1) - 1, 0);
|
|
3807
|
+
const end = Math.min(m.endLine ?? m.startLine ?? lines.length, lines.length);
|
|
3808
|
+
const body = lines.slice(start, end).join('\n');
|
|
3809
|
+
const loadMatch = body.match(/loadContent\s*\(\s*['"]([^'"]+)['"]/);
|
|
3810
|
+
if (!loadMatch)
|
|
3811
|
+
continue;
|
|
3812
|
+
const page = loadMatch[1];
|
|
3813
|
+
const pageComponent = (0, arkts_entry_1.findArktsEntryPageComponent)(ctx, page);
|
|
3814
|
+
if (pageComponent) {
|
|
3815
|
+
const aboutToAppear = (0, arkts_entry_1.findArktsLifecycleMethod)(ctx, pageComponent, 'aboutToAppear');
|
|
3816
|
+
if (aboutToAppear) {
|
|
3817
|
+
add(m.id, aboutToAppear.id, 'arkts-load-content', { page });
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
for (const cls of [...queries.getNodesByKind('class'), ...queries.getNodesByKind('struct')]) {
|
|
3822
|
+
if (cls.language !== 'arkts')
|
|
3823
|
+
continue;
|
|
3824
|
+
const methods = queries
|
|
3825
|
+
.getOutgoingEdges(cls.id, ['contains'])
|
|
3826
|
+
.map((e) => queries.getNodeById(e.target))
|
|
3827
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
3828
|
+
const onCreate = methods.find((n) => n.name === 'onCreate');
|
|
3829
|
+
const onWindowStageCreate = methods.find((n) => n.name === 'onWindowStageCreate');
|
|
3830
|
+
if (onCreate && onWindowStageCreate) {
|
|
3831
|
+
add(onCreate.id, onWindowStageCreate.id, 'arkts-lifecycle', { via: 'ability' });
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
for (const comp of [
|
|
3835
|
+
...queries.getNodesByKind('component'),
|
|
3836
|
+
...queries.getNodesByKind('class'),
|
|
3837
|
+
...queries.getNodesByKind('struct'),
|
|
3838
|
+
]) {
|
|
3839
|
+
if (comp.language !== 'arkts')
|
|
3840
|
+
continue;
|
|
3841
|
+
const owner = comp.kind === 'component'
|
|
3842
|
+
? queries.getNodesByKind('struct').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
3843
|
+
queries.getNodesByKind('class').find((c) => c.filePath === comp.filePath && c.name === comp.name) ??
|
|
3844
|
+
comp
|
|
3845
|
+
: comp;
|
|
3846
|
+
const methods = queries
|
|
3847
|
+
.getOutgoingEdges(owner.id, ['contains'])
|
|
3848
|
+
.map((e) => queries.getNodeById(e.target))
|
|
3849
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
3850
|
+
const build = methods.find((n) => n.name === 'build');
|
|
3851
|
+
if (!build)
|
|
3852
|
+
continue;
|
|
3853
|
+
for (const life of ['aboutToAppear', 'onPageShow']) {
|
|
3854
|
+
const lifeMethod = methods.find((n) => n.name === life);
|
|
3855
|
+
if (lifeMethod) {
|
|
3856
|
+
add(lifeMethod.id, build.id, 'arkts-lifecycle', { via: life });
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return edges;
|
|
3861
|
+
}
|
|
3862
|
+
const ALWAYS = () => true;
|
|
3863
|
+
/**
|
|
3864
|
+
* The independent passes, in MERGE ORDER — the first-seen dedup in
|
|
3865
|
+
* synthesizeCallbackEdges follows this array, so reordering entries changes
|
|
3866
|
+
* which duplicate edge wins. The two Go pre-passes (cross-file method
|
|
3867
|
+
* `contains`, implicit `implements`) are NOT here: they persist before these
|
|
3868
|
+
* run because interfaceOverrideEdges reads their edges from the DB.
|
|
3869
|
+
*/
|
|
3870
|
+
exports.SYNTH_PASSES = [
|
|
3871
|
+
{ name: 'fieldEdges', gate: ALWAYS, run: (q, c, y) => fieldChannelEdges(q, c, y) },
|
|
3872
|
+
{ name: 'closureCollEdges', gate: ALWAYS, run: (q, c, y) => closureCollectionEdges(q, c, y) },
|
|
3873
|
+
{ name: 'emitterEdges', gate: ALWAYS, run: (_q, c, y) => eventEmitterEdges(c, y) },
|
|
3874
|
+
{ name: 'renderEdges', gate: ALWAYS, run: (q, c, y) => reactRenderEdges(q, c, y) },
|
|
3875
|
+
{ name: 'jsxEdges', gate: ALWAYS, run: (_q, c, y) => reactJsxChildEdges(c, y) },
|
|
3876
|
+
{ name: 'vueEdges', gate: (has) => has('vue'), run: (_q, c, y) => vueTemplateEdges(c, y) },
|
|
3877
|
+
{ name: 'svelteKitEdges', gate: (has) => has('svelte'), run: (_q, c, y) => svelteKitLoadEdges(c, y) },
|
|
3878
|
+
{ name: 'pascalEdges', gate: ALWAYS, run: (_q, c, y) => pascalFormEdges(c, y) },
|
|
3879
|
+
{ name: 'flutterEdges', gate: (has) => has('dart'), run: (q, c, y) => flutterBuildEdges(q, c, y) },
|
|
3880
|
+
{ name: 'arkuiStateEdges', gate: (has) => has('arkts'), run: (q, c, y) => arkuiStateBuildEdges(q, c, y) },
|
|
3881
|
+
{ name: 'arkuiEmitter', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiEmitterEdges(c, y) },
|
|
3882
|
+
{ name: 'arkuiRoutes', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiRouterEdges(c, y) },
|
|
3883
|
+
{ name: 'arktsStartupEdges', gate: (has) => has('arkts'), run: async (q, c) => arktsEntryEdges(q, c) },
|
|
3884
|
+
{ name: 'cppEdges', gate: (has) => has('cpp'), run: (q, _c, y) => cppOverrideEdges(q, y) },
|
|
3885
|
+
{
|
|
3886
|
+
name: 'ifaceEdges',
|
|
3887
|
+
gate: (has) => has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY),
|
|
3888
|
+
run: (q, _c, y) => interfaceOverrideEdges(q, y),
|
|
3889
|
+
},
|
|
3890
|
+
{ name: 'kotlinExpectActual', gate: (has) => has('kotlin'), run: (q, _c, y) => kotlinExpectActualEdges(q, y) },
|
|
3891
|
+
{ name: 'goGrpcEdges', gate: (has) => has('go'), run: (q, _c, y) => goGrpcStubImplEdges(q, y) },
|
|
3892
|
+
{ name: 'rnEventEdgesList', gate: (has) => has(...JS_FAMILY), run: (_q, c, y) => rnEventEdges(c, y) },
|
|
3893
|
+
{ name: 'fabricNativeEdges', gate: ALWAYS, run: (_q, c, y) => fabricNativeImplEdges(c, y) },
|
|
3894
|
+
// Expo module nodes (`expo-module:` ids) are emitted only from .swift/.kt
|
|
3895
|
+
// files, and a pair needs BOTH platforms — so without both languages the
|
|
3896
|
+
// pass's only collection loop is provably empty (it was streaming every
|
|
3897
|
+
// method row on pure-Java repos to find nothing).
|
|
3898
|
+
{ name: 'expoXPlatEdges', gate: (has) => has('swift') && has('kotlin'), run: (q, _c, y) => expoCrossPlatformEdges(q, y) },
|
|
3899
|
+
// An RN cross-platform edge requires a JS-language caller on the native
|
|
3900
|
+
// method (`isBridge`) — no JS-family files means no JS-language nodes, so
|
|
3901
|
+
// the result is provably empty.
|
|
3902
|
+
{ name: 'rnXPlatEdges', gate: (has) => has(...JS_FAMILY), run: (q, _c, y) => rnCrossPlatformEdges(q, y) },
|
|
3903
|
+
{
|
|
3904
|
+
name: 'mybatisEdges',
|
|
3905
|
+
gate: (has) => has('java', 'kotlin') && has('xml'),
|
|
3906
|
+
run: (q, _c, y) => mybatisJavaXmlEdges(q, y),
|
|
3907
|
+
},
|
|
3908
|
+
{ name: 'ginEdges', gate: (has) => has('go'), run: (q, c, y) => ginMiddlewareChainEdges(q, c, y) },
|
|
3909
|
+
{ name: 'thunkEdges', gate: (has) => has(...JS_FAMILY), run: (q, c, y) => reduxThunkEdges(q, c, y) },
|
|
3910
|
+
{ name: 'registryEdges', gate: ALWAYS, run: (_q, c, y) => objectRegistryEdges(c, y) },
|
|
3911
|
+
{ name: 'rtkEdges', gate: (has) => has(...JS_FAMILY), run: (q, c, y) => rtkQueryEdges(q, c, y) },
|
|
3912
|
+
{ name: 'piniaEdges', gate: (has) => has('vue', ...JS_FAMILY), run: (_q, c, y) => piniaStoreEdges(c, y) },
|
|
3913
|
+
{ name: 'vuexEdges', gate: (has) => has('vue', ...JS_FAMILY), run: (_q, c, y) => vuexDispatchEdges(c, y) },
|
|
3914
|
+
{ name: 'celeryEdges', gate: (has) => has('python'), run: (_q, c, y) => celeryDispatchEdges(c, y) },
|
|
3915
|
+
{ name: 'springEdges', gate: (has) => has('java'), run: (_q, c, y) => springEventEdges(c, y) },
|
|
3916
|
+
{ name: 'mediatrEdges', gate: (has) => has('csharp'), run: (_q, c, y) => mediatrDispatchEdges(c, y) },
|
|
3917
|
+
{ name: 'sidekiqEdges', gate: (has) => has('ruby'), run: (_q, c, y) => sidekiqDispatchEdges(c, y) },
|
|
3918
|
+
{
|
|
3919
|
+
name: 'erlangBehaviourEdges',
|
|
3920
|
+
gate: (has) => has('erlang'),
|
|
3921
|
+
run: (q, c, y) => erlangBehaviourDispatchEdges(q, c, y),
|
|
3922
|
+
},
|
|
3923
|
+
{ name: 'laravelEdges', gate: (has) => has('php'), run: (_q, c, y) => laravelEventEdges(c, y) },
|
|
3924
|
+
{
|
|
3925
|
+
name: 'cFnPtrEdges',
|
|
3926
|
+
gate: (has) => has('c', 'cpp'),
|
|
3927
|
+
run: (q, c, y, sub) => (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(q, c, y, sub),
|
|
3928
|
+
},
|
|
3929
|
+
{ name: 'goframeEdges', gate: (has) => has('go'), run: (_q, c, y) => (0, goframe_synthesizer_1.goframeRouteEdges)(c, y) },
|
|
3930
|
+
{ name: 'nixOptionEdges', gate: (has) => has('nix'), run: (q, _c, y) => nixOptionPathEdges(q, y) },
|
|
3931
|
+
];
|
|
3932
|
+
/** Fixed non-registry steps: goMethodContains, goImplements, dedupe-merge, insertMergedEdges. */
|
|
3933
|
+
const FIXED_SYNTH_STEPS = 4;
|
|
3934
|
+
exports.SYNTH_PROGRESS_STEPS = exports.SYNTH_PASSES.length + FIXED_SYNTH_STEPS;
|
|
3935
|
+
async function synthesizeCallbackEdges(queries, ctx, onProgress,
|
|
3936
|
+
// A live resolver pool to fan the independent passes across (structural type
|
|
3937
|
+
// so this file never imports the pool — resolver-worker imports THIS file).
|
|
3938
|
+
// Null/omitted → the sequential path, byte-identical to the pool path.
|
|
3939
|
+
pool,
|
|
3940
|
+
// WAL-valve writer backstop (WalCheckpointValve.backpressure), called at
|
|
3941
|
+
// pool-idle points in the edge-insert loops below — the passes themselves
|
|
3942
|
+
// only read; every write in this function happens with the pool idle.
|
|
3943
|
+
backpressure) {
|
|
2940
3944
|
// Each sub-pass below is a whole-graph scan, and there are ~30 of them, all
|
|
2941
3945
|
// running synchronously on the indexer's main thread. Their AGGREGATE can run
|
|
2942
3946
|
// for well over a minute on a large repo — long enough for the #850 liveness
|
|
@@ -2945,131 +3949,169 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2945
3949
|
// that itself hangs (a real wedge) never reaches the next yield, so the
|
|
2946
3950
|
// watchdog still catches that. See ./cooperative-yield.
|
|
2947
3951
|
const yieldToLoop = (0, cooperative_yield_1.createYielder)();
|
|
3952
|
+
// Synthesis runs AFTER the resolution progress bar reaches 100%, so without
|
|
3953
|
+
// its own progress the UI freezes at "Resolving refs 100%" for the whole
|
|
3954
|
+
// tail — long enough on big repos that users conclude the index hung and
|
|
3955
|
+
// kill it. Report each completed pass; the caller surfaces it as its own
|
|
3956
|
+
// progress phase. Emit 0/total up front so the phase flips immediately.
|
|
3957
|
+
// Emissions are throttled to whole-percent movement (each consumes a UI
|
|
3958
|
+
// message); values may be fractional steps from within-pass reporting.
|
|
3959
|
+
let passesDone = 0;
|
|
3960
|
+
let lastPct = -1;
|
|
3961
|
+
const emit = (value) => {
|
|
3962
|
+
if (!onProgress)
|
|
3963
|
+
return;
|
|
3964
|
+
const v = Math.min(value, exports.SYNTH_PROGRESS_STEPS);
|
|
3965
|
+
const pct = Math.floor((v / exports.SYNTH_PROGRESS_STEPS) * 100);
|
|
3966
|
+
if (pct === lastPct)
|
|
3967
|
+
return;
|
|
3968
|
+
lastPct = pct;
|
|
3969
|
+
onProgress(v, exports.SYNTH_PROGRESS_STEPS);
|
|
3970
|
+
};
|
|
3971
|
+
// A single long pass otherwise parks the bar between steps; a pass that
|
|
3972
|
+
// takes this callback reports a 0..1 fraction of its own work, surfaced
|
|
3973
|
+
// here as fractional progress within its step.
|
|
3974
|
+
const subProgress = (fraction) => emit(passesDone + Math.max(0, Math.min(fraction, 1)));
|
|
3975
|
+
emit(0);
|
|
3976
|
+
// Per-pass wall-clock timing to stderr, opt-in via HOMEGRAPH_SYNTH_TIMINGS
|
|
3977
|
+
// (=1: passes over 250ms; =all: every pass). This is the diagnostic that
|
|
3978
|
+
// located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
|
|
3979
|
+
const markT = { t: Date.now() };
|
|
3980
|
+
const __mark = (label) => {
|
|
3981
|
+
const now = Date.now();
|
|
3982
|
+
const dt = now - markT.t;
|
|
3983
|
+
markT.t = now;
|
|
3984
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.HOMEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
3985
|
+
console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
3986
|
+
}
|
|
3987
|
+
passesDone++;
|
|
3988
|
+
emit(passesDone);
|
|
3989
|
+
};
|
|
3990
|
+
// Language gating: one indexed DISTINCT over the files table lets a pass
|
|
3991
|
+
// whose own filters reference a specific language/extension be skipped
|
|
3992
|
+
// outright when the project has no such files — its result is provably
|
|
3993
|
+
// empty, so skipping is behavior-identical and the cost drops to zero
|
|
3994
|
+
// (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
|
|
3995
|
+
// Passes without an explicit language filter always run.
|
|
3996
|
+
const langs = queries.getDistinctFileLanguages();
|
|
3997
|
+
const has = (...ls) => ls.some((l) => langs.has(l));
|
|
3998
|
+
const NONE = [];
|
|
2948
3999
|
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
2949
4000
|
// FIRST: a method declared in a different file from its receiver type is
|
|
2950
4001
|
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
2951
4002
|
// struct's method set from its `contains` edges — so without this it would
|
|
2952
4003
|
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
4004
|
+
// Writer-side WAL backstop for the insert loops here (see the param doc):
|
|
4005
|
+
// one fstat when under the valve's hard cap, a parked full backfill past it.
|
|
4006
|
+
const foldIfOver = async () => {
|
|
4007
|
+
const bp = backpressure?.();
|
|
4008
|
+
if (bp)
|
|
4009
|
+
await bp;
|
|
4010
|
+
};
|
|
4011
|
+
const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
|
|
4012
|
+
for (let i = 0; i < goMethodContains.length; i += 2000) {
|
|
4013
|
+
queries.insertEdges(goMethodContains.slice(i, i + 2000));
|
|
4014
|
+
await yieldToLoop();
|
|
4015
|
+
await foldIfOver();
|
|
4016
|
+
}
|
|
2956
4017
|
await yieldToLoop();
|
|
4018
|
+
__mark('goMethodContains');
|
|
2957
4019
|
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
2958
4020
|
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
2959
4021
|
// Go has none statically. (Other languages already have static implements
|
|
2960
4022
|
// edges from extraction, so they don't need this pre-pass.)
|
|
2961
|
-
const goImpl = goImplementsEdges(queries);
|
|
2962
|
-
|
|
2963
|
-
queries.insertEdges(goImpl);
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
|
|
2968
|
-
await yieldToLoop();
|
|
2969
|
-
const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
|
|
2970
|
-
await yieldToLoop();
|
|
2971
|
-
const renderEdges = reactRenderEdges(queries, ctx);
|
|
2972
|
-
await yieldToLoop();
|
|
2973
|
-
const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
|
|
2974
|
-
await yieldToLoop();
|
|
2975
|
-
const vueEdges = vueTemplateEdges(ctx);
|
|
2976
|
-
await yieldToLoop();
|
|
2977
|
-
const svelteKitEdges = svelteKitLoadEdges(ctx);
|
|
2978
|
-
await yieldToLoop();
|
|
2979
|
-
const pascalEdges = pascalFormEdges(ctx);
|
|
2980
|
-
await yieldToLoop();
|
|
2981
|
-
const flutterEdges = flutterBuildEdges(queries, ctx);
|
|
2982
|
-
await yieldToLoop();
|
|
2983
|
-
const cppEdges = cppOverrideEdges(queries);
|
|
2984
|
-
await yieldToLoop();
|
|
2985
|
-
const ifaceEdges = interfaceOverrideEdges(queries);
|
|
2986
|
-
await yieldToLoop();
|
|
2987
|
-
const kotlinExpectActual = kotlinExpectActualEdges(queries);
|
|
2988
|
-
await yieldToLoop();
|
|
2989
|
-
const goGrpcEdges = goGrpcStubImplEdges(queries);
|
|
2990
|
-
await yieldToLoop();
|
|
2991
|
-
const rnEventEdgesList = rnEventEdges(ctx);
|
|
2992
|
-
await yieldToLoop();
|
|
2993
|
-
const fabricNativeEdges = fabricNativeImplEdges(ctx);
|
|
2994
|
-
await yieldToLoop();
|
|
2995
|
-
const expoXPlatEdges = expoCrossPlatformEdges(queries);
|
|
2996
|
-
await yieldToLoop();
|
|
2997
|
-
const rnXPlatEdges = rnCrossPlatformEdges(queries);
|
|
2998
|
-
await yieldToLoop();
|
|
2999
|
-
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
|
3000
|
-
await yieldToLoop();
|
|
3001
|
-
const ginEdges = ginMiddlewareChainEdges(queries, ctx);
|
|
3002
|
-
await yieldToLoop();
|
|
3003
|
-
const thunkEdges = reduxThunkEdges(queries, ctx);
|
|
3004
|
-
await yieldToLoop();
|
|
3005
|
-
const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
|
|
3006
|
-
await yieldToLoop();
|
|
3007
|
-
const rtkEdges = rtkQueryEdges(queries, ctx);
|
|
3008
|
-
await yieldToLoop();
|
|
3009
|
-
const piniaEdges = piniaStoreEdges(ctx);
|
|
3010
|
-
await yieldToLoop();
|
|
3011
|
-
const vuexEdges = vuexDispatchEdges(ctx);
|
|
3012
|
-
await yieldToLoop();
|
|
3013
|
-
const celeryEdges = celeryDispatchEdges(ctx);
|
|
3014
|
-
await yieldToLoop();
|
|
3015
|
-
const springEdges = springEventEdges(ctx);
|
|
3016
|
-
await yieldToLoop();
|
|
3017
|
-
const mediatrEdges = mediatrDispatchEdges(ctx);
|
|
3018
|
-
await yieldToLoop();
|
|
3019
|
-
const sidekiqEdges = sidekiqDispatchEdges(ctx);
|
|
3020
|
-
await yieldToLoop();
|
|
3021
|
-
const laravelEdges = laravelEventEdges(ctx);
|
|
3022
|
-
await yieldToLoop();
|
|
3023
|
-
const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
|
|
3024
|
-
await yieldToLoop();
|
|
3025
|
-
const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
|
|
3026
|
-
await yieldToLoop();
|
|
3027
|
-
const arktsStartupEdges = arktsEntryEdges(queries, ctx);
|
|
4023
|
+
const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
|
|
4024
|
+
for (let i = 0; i < goImpl.length; i += 2000) {
|
|
4025
|
+
queries.insertEdges(goImpl.slice(i, i + 2000));
|
|
4026
|
+
await yieldToLoop();
|
|
4027
|
+
await foldIfOver();
|
|
4028
|
+
}
|
|
3028
4029
|
await yieldToLoop();
|
|
4030
|
+
__mark('goImplements');
|
|
4031
|
+
// Run the independent passes (see SYNTH_PASSES). Their results are merged in
|
|
4032
|
+
// REGISTRY ORDER below regardless of execution order, and none of their edges
|
|
4033
|
+
// persist until that merge — so every pass sees the same committed
|
|
4034
|
+
// post-resolution DB state whether it runs sequentially here or on a resolver
|
|
4035
|
+
// pool worker. With a live pool (already booted on ≥150k-ref repos), passes
|
|
4036
|
+
// fan out across its read-only workers and the per-pass wall-clock comes from
|
|
4037
|
+
// the worker; a pass that fails on a worker falls back to running on the main
|
|
4038
|
+
// thread, so a worker crash isolates to a retry instead of failing synthesis.
|
|
4039
|
+
const passEdges = new Array(exports.SYNTH_PASSES.length).fill(NONE);
|
|
4040
|
+
const markPass = (label, dt) => {
|
|
4041
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.HOMEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
4042
|
+
console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
4043
|
+
}
|
|
4044
|
+
passesDone++;
|
|
4045
|
+
emit(passesDone);
|
|
4046
|
+
};
|
|
4047
|
+
const runPassOnMain = async (i) => {
|
|
4048
|
+
const pass = exports.SYNTH_PASSES[i];
|
|
4049
|
+
const t0 = Date.now();
|
|
4050
|
+
passEdges[i] = await pass.run(queries, ctx, yieldToLoop, subProgress);
|
|
4051
|
+
await yieldToLoop();
|
|
4052
|
+
markPass(pass.name, Date.now() - t0);
|
|
4053
|
+
};
|
|
4054
|
+
const gatedIn = [];
|
|
4055
|
+
for (let i = 0; i < exports.SYNTH_PASSES.length; i++) {
|
|
4056
|
+
if (exports.SYNTH_PASSES[i].gate(has))
|
|
4057
|
+
gatedIn.push(i);
|
|
4058
|
+
else
|
|
4059
|
+
markPass(exports.SYNTH_PASSES[i].name, 0);
|
|
4060
|
+
}
|
|
4061
|
+
// Above this node count, a pass that OOM-killed its worker must NOT be
|
|
4062
|
+
// retried on the main thread — the retry would OOM the whole process and
|
|
4063
|
+
// take the index with it (the #1212 failure class). Below it, a worker
|
|
4064
|
+
// failure is more likely a transient crash than a memory ceiling, and the
|
|
4065
|
+
// main-thread retry keeps coverage. Skipping loses only that pass's
|
|
4066
|
+
// synthesized edges; the index still completes.
|
|
4067
|
+
const MAIN_RETRY_MAX_NODES = 1_500_000;
|
|
4068
|
+
const graphNodes = queries.getNodeAndEdgeCount().nodes;
|
|
4069
|
+
if (pool && gatedIn.length > 1) {
|
|
4070
|
+
await Promise.all(gatedIn.map(async (i) => {
|
|
4071
|
+
const pass = exports.SYNTH_PASSES[i];
|
|
4072
|
+
try {
|
|
4073
|
+
const out = await pool.runSynthPass(pass.name);
|
|
4074
|
+
passEdges[i] = out.edges;
|
|
4075
|
+
markPass(pass.name, out.ms);
|
|
4076
|
+
}
|
|
4077
|
+
catch (err) {
|
|
4078
|
+
if (graphNodes > MAIN_RETRY_MAX_NODES) {
|
|
4079
|
+
// Worker died at a scale where the main-thread retry is a process
|
|
4080
|
+
// OOM risk: skip the pass, keep the index alive, and say so.
|
|
4081
|
+
console.error(`[synthesis] pass '${pass.name}' failed on a worker at ${graphNodes} nodes — skipped (edges from this pass are absent): ${err instanceof Error ? err.message : String(err)}`);
|
|
4082
|
+
markPass(`${pass.name} (skipped at scale)`, 0);
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
// Worker-side failure (crash, OOM, unknown pass after a version
|
|
4086
|
+
// mismatch): retry this one pass on the main thread.
|
|
4087
|
+
await runPassOnMain(i);
|
|
4088
|
+
}
|
|
4089
|
+
}));
|
|
4090
|
+
}
|
|
4091
|
+
else {
|
|
4092
|
+
for (const i of gatedIn) {
|
|
4093
|
+
await runPassOnMain(i);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
3029
4096
|
const merged = [];
|
|
3030
4097
|
const seen = new Set();
|
|
3031
|
-
for (const e of
|
|
3032
|
-
...fieldEdges,
|
|
3033
|
-
...closureCollEdges,
|
|
3034
|
-
...emitterEdges,
|
|
3035
|
-
...renderEdges,
|
|
3036
|
-
...jsxEdges,
|
|
3037
|
-
...vueEdges,
|
|
3038
|
-
...svelteKitEdges,
|
|
3039
|
-
...pascalEdges,
|
|
3040
|
-
...flutterEdges,
|
|
3041
|
-
...cppEdges,
|
|
3042
|
-
...ifaceEdges,
|
|
3043
|
-
...kotlinExpectActual,
|
|
3044
|
-
...goGrpcEdges,
|
|
3045
|
-
...rnEventEdgesList,
|
|
3046
|
-
...fabricNativeEdges,
|
|
3047
|
-
...expoXPlatEdges,
|
|
3048
|
-
...rnXPlatEdges,
|
|
3049
|
-
...mybatisEdges,
|
|
3050
|
-
...ginEdges,
|
|
3051
|
-
...thunkEdges,
|
|
3052
|
-
...registryEdges,
|
|
3053
|
-
...rtkEdges,
|
|
3054
|
-
...piniaEdges,
|
|
3055
|
-
...vuexEdges,
|
|
3056
|
-
...celeryEdges,
|
|
3057
|
-
...springEdges,
|
|
3058
|
-
...mediatrEdges,
|
|
3059
|
-
...sidekiqEdges,
|
|
3060
|
-
...laravelEdges,
|
|
3061
|
-
...cFnPtrEdges,
|
|
3062
|
-
...goframeEdges,
|
|
3063
|
-
...arktsStartupEdges,
|
|
3064
|
-
]) {
|
|
4098
|
+
for (const e of passEdges.flat()) {
|
|
3065
4099
|
const key = `${e.source}>${e.target}`;
|
|
3066
4100
|
if (seen.has(key))
|
|
3067
4101
|
continue;
|
|
3068
4102
|
seen.add(key);
|
|
3069
4103
|
merged.push(e);
|
|
3070
4104
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
4105
|
+
__mark('dedupe-merge');
|
|
4106
|
+
// Chunked insert with yields: on the Linux kernel the merged synthesized
|
|
4107
|
+
// edge set is ~275k rows, and one transaction for all of them was a 20s
|
|
4108
|
+
// unyielded main-thread span (#1212 follow-up) — the last one in the tail.
|
|
4109
|
+
for (let i = 0; i < merged.length; i += 2000) {
|
|
4110
|
+
queries.insertEdges(merged.slice(i, i + 2000));
|
|
4111
|
+
await yieldToLoop();
|
|
4112
|
+
await foldIfOver();
|
|
4113
|
+
}
|
|
4114
|
+
__mark('insertMergedEdges');
|
|
3073
4115
|
return merged.length + goImpl.length + goMethodContains.length;
|
|
3074
4116
|
}
|
|
3075
4117
|
//# sourceMappingURL=callback-synthesizer.js.map
|