cgraphx 1.1.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/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/.claude-template/commands/my-commit.md +9 -0
- package/dist/.claude-template/commands/my-query.md +4 -0
- package/dist/.claude-template/hooks/context-monitor/context-monitor.cjs +216 -0
- package/dist/.claude-template/plugins/claude-hud/dist/claude-config-dir.d.ts +4 -0
- package/dist/.claude-template/plugins/claude-hud/dist/claude-config-dir.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/claude-config-dir.js +24 -0
- package/dist/.claude-template/plugins/claude-hud/dist/claude-config-dir.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config-reader.d.ts +8 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config-reader.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config-reader.js +204 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config-reader.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config.d.ts +46 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config.js +220 -0
- package/dist/.claude-template/plugins/claude-hud/dist/config.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/constants.d.ts +10 -0
- package/dist/.claude-template/plugins/claude-hud/dist/constants.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/constants.js +10 -0
- package/dist/.claude-template/plugins/claude-hud/dist/constants.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/debug.d.ts +6 -0
- package/dist/.claude-template/plugins/claude-hud/dist/debug.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/debug.js +15 -0
- package/dist/.claude-template/plugins/claude-hud/dist/debug.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/extra-cmd.d.ts +23 -0
- package/dist/.claude-template/plugins/claude-hud/dist/extra-cmd.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/extra-cmd.js +103 -0
- package/dist/.claude-template/plugins/claude-hud/dist/extra-cmd.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/git.d.ts +16 -0
- package/dist/.claude-template/plugins/claude-hud/dist/git.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/git.js +86 -0
- package/dist/.claude-template/plugins/claude-hud/dist/git.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/index.d.ts +24 -0
- package/dist/.claude-template/plugins/claude-hud/dist/index.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/index.js +97 -0
- package/dist/.claude-template/plugins/claude-hud/dist/index.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/agents-line.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/agents-line.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/agents-line.js +44 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/agents-line.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/colors.d.ts +12 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/colors.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/colors.js +58 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/colors.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/index.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/index.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/index.js +379 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/index.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/environment.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/environment.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/environment.js +30 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/environment.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/identity.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/identity.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/identity.js +52 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/identity.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/index.d.ts +5 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/index.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/index.js +5 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/index.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/project.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/project.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/project.js +74 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/project.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/usage.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/usage.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/usage.js +92 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/lines/usage.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/session-line.d.ts +7 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/session-line.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/session-line.js +247 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/session-line.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/todos-line.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/todos-line.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/todos-line.js +25 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/todos-line.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/tools-line.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/tools-line.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/tools-line.js +43 -0
- package/dist/.claude-template/plugins/claude-hud/dist/render/tools-line.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/speed-tracker.d.ts +7 -0
- package/dist/.claude-template/plugins/claude-hud/dist/speed-tracker.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/speed-tracker.js +62 -0
- package/dist/.claude-template/plugins/claude-hud/dist/speed-tracker.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/stdin.d.ts +9 -0
- package/dist/.claude-template/plugins/claude-hud/dist/stdin.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/stdin.js +136 -0
- package/dist/.claude-template/plugins/claude-hud/dist/stdin.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/transcript.d.ts +3 -0
- package/dist/.claude-template/plugins/claude-hud/dist/transcript.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/transcript.js +189 -0
- package/dist/.claude-template/plugins/claude-hud/dist/transcript.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/types.d.ts +79 -0
- package/dist/.claude-template/plugins/claude-hud/dist/types.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/types.js +5 -0
- package/dist/.claude-template/plugins/claude-hud/dist/types.js.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/usage-api.d.ts +59 -0
- package/dist/.claude-template/plugins/claude-hud/dist/usage-api.d.ts.map +1 -0
- package/dist/.claude-template/plugins/claude-hud/dist/usage-api.js +733 -0
- package/dist/.claude-template/plugins/claude-hud/dist/usage-api.js.map +1 -0
- package/dist/.claude-template/skills/cgraphx/SKILL.md +143 -0
- package/dist/.claude-template/skills/cgraphx/agent-prompt.md +56 -0
- package/dist/.claude-template/skills/clarify-requirements/SKILL.md +425 -0
- package/dist/.claude-template/skills/code-impact-api/SKILL.md +143 -0
- package/dist/.claude-template/skills/code-impact-api/agent-prompt.md +51 -0
- package/dist/.claude-template/skills/code-impact-docgen/SKILL.md +366 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-business-html.md +242 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-business-md.md +107 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-html.md +205 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-md.md +155 -0
- package/dist/.claude-template/skills/code-impact-init/SKILL.md +800 -0
- package/dist/.claude-template/skills/code-impact-markdown/SKILL.md +345 -0
- package/dist/.claude-template/skills/code-impact-markdown/template-guide.md +68 -0
- package/dist/.claude-template/skills/code-impact-markdown/template-memory.md +82 -0
- package/dist/.claude-template/skills/code-impact-markdown/template-runbook.md +58 -0
- package/dist/.claude-template/skills/db-query/SKILL.md +166 -0
- package/dist/.claude-template/skills/db-query/agent-prompt.md +55 -0
- package/dist/.claude-template/skills/developer-timeline/SKILL.md +302 -0
- package/dist/.claude-template/skills/developer-timeline/demo-single-page-report.html +657 -0
- package/dist/.claude-template/skills/implementation/SKILL.md +136 -0
- package/dist/.claude-template/skills/subagent-implement/SKILL.md +225 -0
- package/dist/.claude-template/skills/subagent-implement/implementer-prompt.md +127 -0
- package/dist/.claude-template/skills/subagent-implement/quality-reviewer-prompt.md +130 -0
- package/dist/.claude-template/skills/subagent-implement/spec-reviewer-prompt.md +112 -0
- package/dist/.claude-template/skills/write-plan/SKILL.md +322 -0
- package/dist/.claude-template/skills/write-plan/plan-document-reviewer-prompt.md +134 -0
- package/dist/.claude-template/skills/write-prd/SKILL.md +242 -0
- package/dist/.claude-template/skills/write-spec/SKILL.md +278 -0
- package/dist/bin/codegraph.d.ts +26 -0
- package/dist/bin/codegraph.d.ts.map +1 -0
- package/dist/bin/codegraph.js +2014 -0
- package/dist/bin/codegraph.js.map +1 -0
- package/dist/bin/fatal-handler.d.ts +20 -0
- package/dist/bin/fatal-handler.d.ts.map +1 -0
- package/dist/bin/fatal-handler.js +118 -0
- package/dist/bin/fatal-handler.js.map +1 -0
- package/dist/bin/node-version-check.d.ts +51 -0
- package/dist/bin/node-version-check.d.ts.map +1 -0
- package/dist/bin/node-version-check.js +114 -0
- package/dist/bin/node-version-check.js.map +1 -0
- package/dist/bin/uninstall.d.ts +14 -0
- package/dist/bin/uninstall.d.ts.map +1 -0
- package/dist/bin/uninstall.js +36 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/context/formatter.d.ts +30 -0
- package/dist/context/formatter.d.ts.map +1 -0
- package/dist/context/formatter.js +263 -0
- package/dist/context/formatter.js.map +1 -0
- package/dist/context/index.d.ts +119 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +1296 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/markers.d.ts +19 -0
- package/dist/context/markers.d.ts.map +1 -0
- package/dist/context/markers.js +22 -0
- package/dist/context/markers.js.map +1 -0
- package/dist/db/index.d.ts +122 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +296 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations.d.ts +44 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +140 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/queries.d.ts +401 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +1591 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.sql +152 -0
- package/dist/db/sqlite-adapter.d.ts +53 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.js +117 -0
- package/dist/db/sqlite-adapter.js.map +1 -0
- package/dist/dbquery/cli.d.ts +17 -0
- package/dist/dbquery/cli.d.ts.map +1 -0
- package/dist/dbquery/cli.js +229 -0
- package/dist/dbquery/cli.js.map +1 -0
- package/dist/dbquery/config.d.ts +38 -0
- package/dist/dbquery/config.d.ts.map +1 -0
- package/dist/dbquery/config.js +244 -0
- package/dist/dbquery/config.js.map +1 -0
- package/dist/dbquery/constants.d.ts +40 -0
- package/dist/dbquery/constants.d.ts.map +1 -0
- package/dist/dbquery/constants.js +65 -0
- package/dist/dbquery/constants.js.map +1 -0
- package/dist/dbquery/drivers/mysql.d.ts +15 -0
- package/dist/dbquery/drivers/mysql.d.ts.map +1 -0
- package/dist/dbquery/drivers/mysql.js +102 -0
- package/dist/dbquery/drivers/mysql.js.map +1 -0
- package/dist/dbquery/drivers/postgres.d.ts +16 -0
- package/dist/dbquery/drivers/postgres.d.ts.map +1 -0
- package/dist/dbquery/drivers/postgres.js +105 -0
- package/dist/dbquery/drivers/postgres.js.map +1 -0
- package/dist/dbquery/errors.d.ts +40 -0
- package/dist/dbquery/errors.d.ts.map +1 -0
- package/dist/dbquery/errors.js +85 -0
- package/dist/dbquery/errors.js.map +1 -0
- package/dist/dbquery/executor.d.ts +30 -0
- package/dist/dbquery/executor.d.ts.map +1 -0
- package/dist/dbquery/executor.js +243 -0
- package/dist/dbquery/executor.js.map +1 -0
- package/dist/dbquery/format.d.ts +18 -0
- package/dist/dbquery/format.d.ts.map +1 -0
- package/dist/dbquery/format.js +174 -0
- package/dist/dbquery/format.js.map +1 -0
- package/dist/dbquery/index.d.ts +10 -0
- package/dist/dbquery/index.d.ts.map +1 -0
- package/dist/dbquery/index.js +23 -0
- package/dist/dbquery/index.js.map +1 -0
- package/dist/dbquery/init.d.ts +33 -0
- package/dist/dbquery/init.d.ts.map +1 -0
- package/dist/dbquery/init.js +125 -0
- package/dist/dbquery/init.js.map +1 -0
- package/dist/dbquery/logging.d.ts +22 -0
- package/dist/dbquery/logging.d.ts.map +1 -0
- package/dist/dbquery/logging.js +140 -0
- package/dist/dbquery/logging.js.map +1 -0
- package/dist/dbquery/mcp-tools.d.ts +29 -0
- package/dist/dbquery/mcp-tools.d.ts.map +1 -0
- package/dist/dbquery/mcp-tools.js +206 -0
- package/dist/dbquery/mcp-tools.js.map +1 -0
- package/dist/dbquery/queries.d.ts +31 -0
- package/dist/dbquery/queries.d.ts.map +1 -0
- package/dist/dbquery/queries.js +160 -0
- package/dist/dbquery/queries.js.map +1 -0
- package/dist/dbquery/safety.d.ts +35 -0
- package/dist/dbquery/safety.d.ts.map +1 -0
- package/dist/dbquery/safety.js +306 -0
- package/dist/dbquery/safety.js.map +1 -0
- package/dist/dbquery/types.d.ts +152 -0
- package/dist/dbquery/types.d.ts.map +1 -0
- package/dist/dbquery/types.js +10 -0
- package/dist/dbquery/types.js.map +1 -0
- package/dist/directory.d.ts +147 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +523 -0
- package/dist/directory.js.map +1 -0
- package/dist/errors.d.ts +136 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +219 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction/astro-extractor.d.ts +79 -0
- package/dist/extraction/astro-extractor.d.ts.map +1 -0
- package/dist/extraction/astro-extractor.js +320 -0
- package/dist/extraction/astro-extractor.js.map +1 -0
- package/dist/extraction/dfm-extractor.d.ts +31 -0
- package/dist/extraction/dfm-extractor.d.ts.map +1 -0
- package/dist/extraction/dfm-extractor.js +151 -0
- package/dist/extraction/dfm-extractor.js.map +1 -0
- package/dist/extraction/extraction-version.d.ts +25 -0
- package/dist/extraction/extraction-version.d.ts.map +1 -0
- package/dist/extraction/extraction-version.js +28 -0
- package/dist/extraction/extraction-version.js.map +1 -0
- package/dist/extraction/function-ref.d.ts +118 -0
- package/dist/extraction/function-ref.d.ts.map +1 -0
- package/dist/extraction/function-ref.js +727 -0
- package/dist/extraction/function-ref.js.map +1 -0
- package/dist/extraction/generated-detection.d.ts +30 -0
- package/dist/extraction/generated-detection.d.ts.map +1 -0
- package/dist/extraction/generated-detection.js +83 -0
- package/dist/extraction/generated-detection.js.map +1 -0
- package/dist/extraction/grammars.d.ts +114 -0
- package/dist/extraction/grammars.d.ts.map +1 -0
- package/dist/extraction/grammars.js +477 -0
- package/dist/extraction/grammars.js.map +1 -0
- package/dist/extraction/index.d.ts +175 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +1887 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/languages/c-cpp.d.ts +12 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
- package/dist/extraction/languages/c-cpp.js +275 -0
- package/dist/extraction/languages/c-cpp.js.map +1 -0
- package/dist/extraction/languages/csharp.d.ts +25 -0
- package/dist/extraction/languages/csharp.d.ts.map +1 -0
- package/dist/extraction/languages/csharp.js +175 -0
- package/dist/extraction/languages/csharp.js.map +1 -0
- package/dist/extraction/languages/dart.d.ts +3 -0
- package/dist/extraction/languages/dart.d.ts.map +1 -0
- package/dist/extraction/languages/dart.js +374 -0
- package/dist/extraction/languages/dart.js.map +1 -0
- package/dist/extraction/languages/go.d.ts +3 -0
- package/dist/extraction/languages/go.d.ts.map +1 -0
- package/dist/extraction/languages/go.js +111 -0
- package/dist/extraction/languages/go.js.map +1 -0
- package/dist/extraction/languages/index.d.ts +10 -0
- package/dist/extraction/languages/index.d.ts.map +1 -0
- package/dist/extraction/languages/index.js +53 -0
- package/dist/extraction/languages/index.js.map +1 -0
- package/dist/extraction/languages/java.d.ts +3 -0
- package/dist/extraction/languages/java.d.ts.map +1 -0
- package/dist/extraction/languages/java.js +315 -0
- package/dist/extraction/languages/java.js.map +1 -0
- package/dist/extraction/languages/javascript.d.ts +3 -0
- package/dist/extraction/languages/javascript.d.ts.map +1 -0
- package/dist/extraction/languages/javascript.js +106 -0
- package/dist/extraction/languages/javascript.js.map +1 -0
- package/dist/extraction/languages/kotlin.d.ts +3 -0
- package/dist/extraction/languages/kotlin.d.ts.map +1 -0
- package/dist/extraction/languages/kotlin.js +379 -0
- package/dist/extraction/languages/kotlin.js.map +1 -0
- package/dist/extraction/languages/lua.d.ts +3 -0
- package/dist/extraction/languages/lua.d.ts.map +1 -0
- package/dist/extraction/languages/lua.js +150 -0
- package/dist/extraction/languages/lua.js.map +1 -0
- package/dist/extraction/languages/luau.d.ts +3 -0
- package/dist/extraction/languages/luau.d.ts.map +1 -0
- package/dist/extraction/languages/luau.js +37 -0
- package/dist/extraction/languages/luau.js.map +1 -0
- package/dist/extraction/languages/objc.d.ts +3 -0
- package/dist/extraction/languages/objc.d.ts.map +1 -0
- package/dist/extraction/languages/objc.js +175 -0
- package/dist/extraction/languages/objc.js.map +1 -0
- package/dist/extraction/languages/pascal.d.ts +3 -0
- package/dist/extraction/languages/pascal.d.ts.map +1 -0
- package/dist/extraction/languages/pascal.js +77 -0
- package/dist/extraction/languages/pascal.js.map +1 -0
- package/dist/extraction/languages/php.d.ts +3 -0
- package/dist/extraction/languages/php.d.ts.map +1 -0
- package/dist/extraction/languages/php.js +196 -0
- package/dist/extraction/languages/php.js.map +1 -0
- package/dist/extraction/languages/python.d.ts +3 -0
- package/dist/extraction/languages/python.d.ts.map +1 -0
- package/dist/extraction/languages/python.js +56 -0
- package/dist/extraction/languages/python.js.map +1 -0
- package/dist/extraction/languages/r.d.ts +3 -0
- package/dist/extraction/languages/r.d.ts.map +1 -0
- package/dist/extraction/languages/r.js +314 -0
- package/dist/extraction/languages/r.js.map +1 -0
- package/dist/extraction/languages/ruby.d.ts +3 -0
- package/dist/extraction/languages/ruby.d.ts.map +1 -0
- package/dist/extraction/languages/ruby.js +149 -0
- package/dist/extraction/languages/ruby.js.map +1 -0
- package/dist/extraction/languages/rust.d.ts +3 -0
- package/dist/extraction/languages/rust.d.ts.map +1 -0
- package/dist/extraction/languages/rust.js +142 -0
- package/dist/extraction/languages/rust.js.map +1 -0
- package/dist/extraction/languages/scala.d.ts +3 -0
- package/dist/extraction/languages/scala.d.ts.map +1 -0
- package/dist/extraction/languages/scala.js +209 -0
- package/dist/extraction/languages/scala.js.map +1 -0
- package/dist/extraction/languages/swift.d.ts +3 -0
- package/dist/extraction/languages/swift.d.ts.map +1 -0
- package/dist/extraction/languages/swift.js +152 -0
- package/dist/extraction/languages/swift.js.map +1 -0
- package/dist/extraction/languages/typescript.d.ts +16 -0
- package/dist/extraction/languages/typescript.d.ts.map +1 -0
- package/dist/extraction/languages/typescript.js +167 -0
- package/dist/extraction/languages/typescript.js.map +1 -0
- package/dist/extraction/liquid-extractor.d.ts +59 -0
- package/dist/extraction/liquid-extractor.d.ts.map +1 -0
- package/dist/extraction/liquid-extractor.js +357 -0
- package/dist/extraction/liquid-extractor.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +48 -0
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
- package/dist/extraction/mybatis-extractor.js +198 -0
- package/dist/extraction/mybatis-extractor.js.map +1 -0
- package/dist/extraction/parse-worker.d.ts +8 -0
- package/dist/extraction/parse-worker.d.ts.map +1 -0
- package/dist/extraction/parse-worker.js +97 -0
- package/dist/extraction/parse-worker.js.map +1 -0
- package/dist/extraction/razor-extractor.d.ts +42 -0
- package/dist/extraction/razor-extractor.d.ts.map +1 -0
- package/dist/extraction/razor-extractor.js +285 -0
- package/dist/extraction/razor-extractor.js.map +1 -0
- package/dist/extraction/svelte-extractor.d.ts +56 -0
- package/dist/extraction/svelte-extractor.d.ts.map +1 -0
- package/dist/extraction/svelte-extractor.js +275 -0
- package/dist/extraction/svelte-extractor.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-helpers.js +152 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +239 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-types.js +10 -0
- package/dist/extraction/tree-sitter-types.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts +647 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -0
- package/dist/extraction/tree-sitter.js +5592 -0
- package/dist/extraction/tree-sitter.js.map +1 -0
- package/dist/extraction/vue-extractor.d.ts +51 -0
- package/dist/extraction/vue-extractor.d.ts.map +1 -0
- package/dist/extraction/vue-extractor.js +254 -0
- package/dist/extraction/vue-extractor.js.map +1 -0
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
- package/dist/extraction/wasm-runtime-flags.js +106 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/queries.d.ts +106 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +339 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/traversal.d.ts +127 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/traversal.js +540 -0
- package/dist/graph/traversal.js.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1041 -0
- package/dist/index.js.map +1 -0
- package/dist/installer/claude-assets.d.ts +45 -0
- package/dist/installer/claude-assets.d.ts.map +1 -0
- package/dist/installer/claude-assets.js +144 -0
- package/dist/installer/claude-assets.js.map +1 -0
- package/dist/installer/config-writer.d.ts +28 -0
- package/dist/installer/config-writer.d.ts.map +1 -0
- package/dist/installer/config-writer.js +91 -0
- package/dist/installer/config-writer.js.map +1 -0
- package/dist/installer/index.d.ts +101 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +692 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/instructions-template.d.ts +41 -0
- package/dist/installer/instructions-template.d.ts.map +1 -0
- package/dist/installer/instructions-template.js +53 -0
- package/dist/installer/instructions-template.js.map +1 -0
- package/dist/installer/targets/antigravity.d.ts +57 -0
- package/dist/installer/targets/antigravity.d.ts.map +1 -0
- package/dist/installer/targets/antigravity.js +308 -0
- package/dist/installer/targets/antigravity.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +66 -0
- package/dist/installer/targets/claude.d.ts.map +1 -0
- package/dist/installer/targets/claude.js +564 -0
- package/dist/installer/targets/claude.js.map +1 -0
- package/dist/installer/targets/codex.d.ts +18 -0
- package/dist/installer/targets/codex.d.ts.map +1 -0
- package/dist/installer/targets/codex.js +185 -0
- package/dist/installer/targets/codex.js.map +1 -0
- package/dist/installer/targets/cursor.d.ts +35 -0
- package/dist/installer/targets/cursor.d.ts.map +1 -0
- package/dist/installer/targets/cursor.js +254 -0
- package/dist/installer/targets/cursor.js.map +1 -0
- package/dist/installer/targets/gemini.d.ts +26 -0
- package/dist/installer/targets/gemini.d.ts.map +1 -0
- package/dist/installer/targets/gemini.js +165 -0
- package/dist/installer/targets/gemini.js.map +1 -0
- package/dist/installer/targets/hermes.d.ts +18 -0
- package/dist/installer/targets/hermes.d.ts.map +1 -0
- package/dist/installer/targets/hermes.js +359 -0
- package/dist/installer/targets/hermes.js.map +1 -0
- package/dist/installer/targets/kiro.d.ts +27 -0
- package/dist/installer/targets/kiro.d.ts.map +1 -0
- package/dist/installer/targets/kiro.js +178 -0
- package/dist/installer/targets/kiro.js.map +1 -0
- package/dist/installer/targets/opencode.d.ts +38 -0
- package/dist/installer/targets/opencode.d.ts.map +1 -0
- package/dist/installer/targets/opencode.js +288 -0
- package/dist/installer/targets/opencode.js.map +1 -0
- package/dist/installer/targets/registry.d.ts +35 -0
- package/dist/installer/targets/registry.d.ts.map +1 -0
- package/dist/installer/targets/registry.js +91 -0
- package/dist/installer/targets/registry.js.map +1 -0
- package/dist/installer/targets/shared.d.ts +101 -0
- package/dist/installer/targets/shared.d.ts.map +1 -0
- package/dist/installer/targets/shared.js +264 -0
- package/dist/installer/targets/shared.js.map +1 -0
- package/dist/installer/targets/toml.d.ts +52 -0
- package/dist/installer/targets/toml.d.ts.map +1 -0
- package/dist/installer/targets/toml.js +147 -0
- package/dist/installer/targets/toml.js.map +1 -0
- package/dist/installer/targets/types.d.ts +108 -0
- package/dist/installer/targets/types.d.ts.map +1 -0
- package/dist/installer/targets/types.js +16 -0
- package/dist/installer/targets/types.js.map +1 -0
- package/dist/markdown/cli.d.ts +16 -0
- package/dist/markdown/cli.d.ts.map +1 -0
- package/dist/markdown/cli.js +533 -0
- package/dist/markdown/cli.js.map +1 -0
- package/dist/markdown/constants.d.ts +22 -0
- package/dist/markdown/constants.d.ts.map +1 -0
- package/dist/markdown/constants.js +71 -0
- package/dist/markdown/constants.js.map +1 -0
- package/dist/markdown/dedup.d.ts +20 -0
- package/dist/markdown/dedup.d.ts.map +1 -0
- package/dist/markdown/dedup.js +64 -0
- package/dist/markdown/dedup.js.map +1 -0
- package/dist/markdown/errors.d.ts +22 -0
- package/dist/markdown/errors.d.ts.map +1 -0
- package/dist/markdown/errors.js +45 -0
- package/dist/markdown/errors.js.map +1 -0
- package/dist/markdown/extractor.d.ts +43 -0
- package/dist/markdown/extractor.d.ts.map +1 -0
- package/dist/markdown/extractor.js +152 -0
- package/dist/markdown/extractor.js.map +1 -0
- package/dist/markdown/frontmatter-parser.d.ts +47 -0
- package/dist/markdown/frontmatter-parser.d.ts.map +1 -0
- package/dist/markdown/frontmatter-parser.js +199 -0
- package/dist/markdown/frontmatter-parser.js.map +1 -0
- package/dist/markdown/indexer.d.ts +34 -0
- package/dist/markdown/indexer.d.ts.map +1 -0
- package/dist/markdown/indexer.js +256 -0
- package/dist/markdown/indexer.js.map +1 -0
- package/dist/markdown/mcp-tools.d.ts +33 -0
- package/dist/markdown/mcp-tools.d.ts.map +1 -0
- package/dist/markdown/mcp-tools.js +300 -0
- package/dist/markdown/mcp-tools.js.map +1 -0
- package/dist/markdown/query.d.ts +108 -0
- package/dist/markdown/query.d.ts.map +1 -0
- package/dist/markdown/query.js +570 -0
- package/dist/markdown/query.js.map +1 -0
- package/dist/markdown/schema-bootstrap.d.ts +40 -0
- package/dist/markdown/schema-bootstrap.d.ts.map +1 -0
- package/dist/markdown/schema-bootstrap.js +85 -0
- package/dist/markdown/schema-bootstrap.js.map +1 -0
- package/dist/markdown/schema.sql +124 -0
- package/dist/markdown/store.d.ts +77 -0
- package/dist/markdown/store.d.ts.map +1 -0
- package/dist/markdown/store.js +194 -0
- package/dist/markdown/store.js.map +1 -0
- package/dist/markdown/summary-extractor.d.ts +22 -0
- package/dist/markdown/summary-extractor.d.ts.map +1 -0
- package/dist/markdown/summary-extractor.js +66 -0
- package/dist/markdown/summary-extractor.js.map +1 -0
- package/dist/markdown/types.d.ts +159 -0
- package/dist/markdown/types.d.ts.map +1 -0
- package/dist/markdown/types.js +9 -0
- package/dist/markdown/types.js.map +1 -0
- package/dist/markdown/validator.d.ts +44 -0
- package/dist/markdown/validator.d.ts.map +1 -0
- package/dist/markdown/validator.js +95 -0
- package/dist/markdown/validator.js.map +1 -0
- package/dist/mcp/daemon-manager.d.ts +42 -0
- package/dist/mcp/daemon-manager.d.ts.map +1 -0
- package/dist/mcp/daemon-manager.js +129 -0
- package/dist/mcp/daemon-manager.js.map +1 -0
- package/dist/mcp/daemon-paths.d.ts +46 -0
- package/dist/mcp/daemon-paths.d.ts.map +1 -0
- package/dist/mcp/daemon-paths.js +125 -0
- package/dist/mcp/daemon-paths.js.map +1 -0
- package/dist/mcp/daemon-registry.d.ts +47 -0
- package/dist/mcp/daemon-registry.d.ts.map +1 -0
- package/dist/mcp/daemon-registry.js +229 -0
- package/dist/mcp/daemon-registry.js.map +1 -0
- package/dist/mcp/daemon.d.ts +220 -0
- package/dist/mcp/daemon.d.ts.map +1 -0
- package/dist/mcp/daemon.js +637 -0
- package/dist/mcp/daemon.js.map +1 -0
- package/dist/mcp/dynamic-boundaries.d.ts +41 -0
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
- package/dist/mcp/dynamic-boundaries.js +359 -0
- package/dist/mcp/dynamic-boundaries.js.map +1 -0
- package/dist/mcp/engine.d.ts +105 -0
- package/dist/mcp/engine.d.ts.map +1 -0
- package/dist/mcp/engine.js +278 -0
- package/dist/mcp/engine.js.map +1 -0
- package/dist/mcp/index.d.ts +113 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +499 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/liveness-watchdog.d.ts +18 -0
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -0
- package/dist/mcp/liveness-watchdog.js +207 -0
- package/dist/mcp/liveness-watchdog.js.map +1 -0
- package/dist/mcp/ppid-watchdog.d.ts +44 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -0
- package/dist/mcp/ppid-watchdog.js +27 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -0
- package/dist/mcp/proxy.d.ts +87 -0
- package/dist/mcp/proxy.d.ts.map +1 -0
- package/dist/mcp/proxy.js +641 -0
- package/dist/mcp/proxy.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +34 -0
- package/dist/mcp/server-instructions.d.ts.map +1 -0
- package/dist/mcp/server-instructions.js +106 -0
- package/dist/mcp/server-instructions.js.map +1 -0
- package/dist/mcp/session.d.ts +79 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +330 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/stdin-teardown.d.ts +27 -0
- package/dist/mcp/stdin-teardown.d.ts.map +1 -0
- package/dist/mcp/stdin-teardown.js +49 -0
- package/dist/mcp/stdin-teardown.js.map +1 -0
- package/dist/mcp/tools.d.ts +547 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +4122 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/transport.d.ts +188 -0
- package/dist/mcp/transport.d.ts.map +1 -0
- package/dist/mcp/transport.js +359 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/mcp/version.d.ts +19 -0
- package/dist/mcp/version.d.ts.map +1 -0
- package/dist/mcp/version.js +71 -0
- package/dist/mcp/version.js.map +1 -0
- package/dist/project-config.d.ts +36 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +235 -0
- package/dist/project-config.js.map +1 -0
- package/dist/reasoning/config.d.ts +45 -0
- package/dist/reasoning/config.d.ts.map +1 -0
- package/dist/reasoning/config.js +171 -0
- package/dist/reasoning/config.js.map +1 -0
- package/dist/reasoning/credentials.d.ts +5 -0
- package/dist/reasoning/credentials.d.ts.map +1 -0
- package/dist/reasoning/credentials.js +83 -0
- package/dist/reasoning/credentials.js.map +1 -0
- package/dist/reasoning/login.d.ts +21 -0
- package/dist/reasoning/login.d.ts.map +1 -0
- package/dist/reasoning/login.js +85 -0
- package/dist/reasoning/login.js.map +1 -0
- package/dist/reasoning/reasoner.d.ts +43 -0
- package/dist/reasoning/reasoner.d.ts.map +1 -0
- package/dist/reasoning/reasoner.js +308 -0
- package/dist/reasoning/reasoner.js.map +1 -0
- package/dist/resolution/c-fnptr-synthesizer.d.ts +33 -0
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -0
- package/dist/resolution/c-fnptr-synthesizer.js +352 -0
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
- package/dist/resolution/callback-synthesizer.d.ts +15 -0
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
- package/dist/resolution/callback-synthesizer.js +2926 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -0
- package/dist/resolution/frameworks/astro.d.ts +9 -0
- package/dist/resolution/frameworks/astro.d.ts.map +1 -0
- package/dist/resolution/frameworks/astro.js +169 -0
- package/dist/resolution/frameworks/astro.js.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.js +225 -0
- package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
- package/dist/resolution/frameworks/csharp.d.ts +8 -0
- package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
- package/dist/resolution/frameworks/csharp.js +241 -0
- package/dist/resolution/frameworks/csharp.js.map +1 -0
- package/dist/resolution/frameworks/drupal.d.ts +51 -0
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
- package/dist/resolution/frameworks/drupal.js +367 -0
- package/dist/resolution/frameworks/drupal.js.map +1 -0
- package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
- package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
- package/dist/resolution/frameworks/expo-modules.js +148 -0
- package/dist/resolution/frameworks/expo-modules.js.map +1 -0
- package/dist/resolution/frameworks/express.d.ts +8 -0
- package/dist/resolution/frameworks/express.d.ts.map +1 -0
- package/dist/resolution/frameworks/express.js +308 -0
- package/dist/resolution/frameworks/express.js.map +1 -0
- package/dist/resolution/frameworks/fabric.d.ts +3 -0
- package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
- package/dist/resolution/frameworks/fabric.js +354 -0
- package/dist/resolution/frameworks/fabric.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts +8 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -0
- package/dist/resolution/frameworks/go.js +161 -0
- package/dist/resolution/frameworks/go.js.map +1 -0
- package/dist/resolution/frameworks/goframe.d.ts +41 -0
- package/dist/resolution/frameworks/goframe.d.ts.map +1 -0
- package/dist/resolution/frameworks/goframe.js +112 -0
- package/dist/resolution/frameworks/goframe.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts +50 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -0
- package/dist/resolution/frameworks/index.js +169 -0
- package/dist/resolution/frameworks/index.js.map +1 -0
- package/dist/resolution/frameworks/java.d.ts +8 -0
- package/dist/resolution/frameworks/java.d.ts.map +1 -0
- package/dist/resolution/frameworks/java.js +509 -0
- package/dist/resolution/frameworks/java.js.map +1 -0
- package/dist/resolution/frameworks/laravel.d.ts +13 -0
- package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
- package/dist/resolution/frameworks/laravel.js +257 -0
- package/dist/resolution/frameworks/laravel.js.map +1 -0
- package/dist/resolution/frameworks/nestjs.d.ts +26 -0
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
- package/dist/resolution/frameworks/nestjs.js +698 -0
- package/dist/resolution/frameworks/nestjs.js.map +1 -0
- package/dist/resolution/frameworks/play.d.ts +19 -0
- package/dist/resolution/frameworks/play.d.ts.map +1 -0
- package/dist/resolution/frameworks/play.js +111 -0
- package/dist/resolution/frameworks/play.js.map +1 -0
- package/dist/resolution/frameworks/python.d.ts +10 -0
- package/dist/resolution/frameworks/python.d.ts.map +1 -0
- package/dist/resolution/frameworks/python.js +400 -0
- package/dist/resolution/frameworks/python.js.map +1 -0
- package/dist/resolution/frameworks/react-native.d.ts +3 -0
- package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
- package/dist/resolution/frameworks/react-native.js +410 -0
- package/dist/resolution/frameworks/react-native.js.map +1 -0
- package/dist/resolution/frameworks/react.d.ts +8 -0
- package/dist/resolution/frameworks/react.d.ts.map +1 -0
- package/dist/resolution/frameworks/react.js +334 -0
- package/dist/resolution/frameworks/react.js.map +1 -0
- package/dist/resolution/frameworks/ruby.d.ts +8 -0
- package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
- package/dist/resolution/frameworks/ruby.js +302 -0
- package/dist/resolution/frameworks/ruby.js.map +1 -0
- package/dist/resolution/frameworks/rust.d.ts +8 -0
- package/dist/resolution/frameworks/rust.d.ts.map +1 -0
- package/dist/resolution/frameworks/rust.js +304 -0
- package/dist/resolution/frameworks/rust.js.map +1 -0
- package/dist/resolution/frameworks/svelte.d.ts +9 -0
- package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
- package/dist/resolution/frameworks/svelte.js +253 -0
- package/dist/resolution/frameworks/svelte.js.map +1 -0
- package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
- package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift-objc.js +252 -0
- package/dist/resolution/frameworks/swift-objc.js.map +1 -0
- package/dist/resolution/frameworks/swift.d.ts +10 -0
- package/dist/resolution/frameworks/swift.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift.js +400 -0
- package/dist/resolution/frameworks/swift.js.map +1 -0
- package/dist/resolution/frameworks/vue.d.ts +9 -0
- package/dist/resolution/frameworks/vue.d.ts.map +1 -0
- package/dist/resolution/frameworks/vue.js +303 -0
- package/dist/resolution/frameworks/vue.js.map +1 -0
- package/dist/resolution/go-module.d.ts +26 -0
- package/dist/resolution/go-module.d.ts.map +1 -0
- package/dist/resolution/go-module.js +78 -0
- package/dist/resolution/go-module.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +28 -0
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -0
- package/dist/resolution/goframe-synthesizer.js +158 -0
- package/dist/resolution/goframe-synthesizer.js.map +1 -0
- package/dist/resolution/import-resolver.d.ts +78 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -0
- package/dist/resolution/import-resolver.js +1849 -0
- package/dist/resolution/import-resolver.js.map +1 -0
- package/dist/resolution/index.d.ts +196 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +1328 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/resolution/lru-cache.d.ts +24 -0
- package/dist/resolution/lru-cache.d.ts.map +1 -0
- package/dist/resolution/lru-cache.js +62 -0
- package/dist/resolution/lru-cache.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +93 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -0
- package/dist/resolution/name-matcher.js +1212 -0
- package/dist/resolution/name-matcher.js.map +1 -0
- package/dist/resolution/path-aliases.d.ts +68 -0
- package/dist/resolution/path-aliases.d.ts.map +1 -0
- package/dist/resolution/path-aliases.js +238 -0
- package/dist/resolution/path-aliases.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +27 -0
- package/dist/resolution/strip-comments.d.ts.map +1 -0
- package/dist/resolution/strip-comments.js +443 -0
- package/dist/resolution/strip-comments.js.map +1 -0
- package/dist/resolution/swift-objc-bridge.d.ts +134 -0
- package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
- package/dist/resolution/swift-objc-bridge.js +256 -0
- package/dist/resolution/swift-objc-bridge.js.map +1 -0
- package/dist/resolution/types.d.ts +233 -0
- package/dist/resolution/types.d.ts.map +1 -0
- package/dist/resolution/types.js +8 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/resolution/workspace-packages.d.ts +48 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/dist/resolution/workspace-packages.js +208 -0
- package/dist/resolution/workspace-packages.js.map +1 -0
- package/dist/search/query-parser.d.ts +57 -0
- package/dist/search/query-parser.d.ts.map +1 -0
- package/dist/search/query-parser.js +177 -0
- package/dist/search/query-parser.js.map +1 -0
- package/dist/search/query-utils.d.ts +87 -0
- package/dist/search/query-utils.d.ts.map +1 -0
- package/dist/search/query-utils.js +449 -0
- package/dist/search/query-utils.js.map +1 -0
- package/dist/sync/git-hooks.d.ts +45 -0
- package/dist/sync/git-hooks.d.ts.map +1 -0
- package/dist/sync/git-hooks.js +225 -0
- package/dist/sync/git-hooks.js.map +1 -0
- package/dist/sync/index.d.ts +19 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +35 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/watch-policy.d.ts +48 -0
- package/dist/sync/watch-policy.d.ts.map +1 -0
- package/dist/sync/watch-policy.js +124 -0
- package/dist/sync/watch-policy.js.map +1 -0
- package/dist/sync/watcher.d.ts +350 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +811 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/sync/worktree.d.ts +54 -0
- package/dist/sync/worktree.d.ts.map +1 -0
- package/dist/sync/worktree.js +137 -0
- package/dist/sync/worktree.js.map +1 -0
- package/dist/telemetry/index.d.ts +143 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +541 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/timeline/bash-semantics.d.ts +52 -0
- package/dist/timeline/bash-semantics.d.ts.map +1 -0
- package/dist/timeline/bash-semantics.js +376 -0
- package/dist/timeline/bash-semantics.js.map +1 -0
- package/dist/timeline/cli.d.ts +50 -0
- package/dist/timeline/cli.d.ts.map +1 -0
- package/dist/timeline/cli.js +367 -0
- package/dist/timeline/cli.js.map +1 -0
- package/dist/timeline/constants.d.ts +62 -0
- package/dist/timeline/constants.d.ts.map +1 -0
- package/dist/timeline/constants.js +73 -0
- package/dist/timeline/constants.js.map +1 -0
- package/dist/timeline/errors.d.ts +27 -0
- package/dist/timeline/errors.d.ts.map +1 -0
- package/dist/timeline/errors.js +51 -0
- package/dist/timeline/errors.js.map +1 -0
- package/dist/timeline/hook-runner.d.ts +36 -0
- package/dist/timeline/hook-runner.d.ts.map +1 -0
- package/dist/timeline/hook-runner.js +61 -0
- package/dist/timeline/hook-runner.js.map +1 -0
- package/dist/timeline/hooks.d.ts +45 -0
- package/dist/timeline/hooks.d.ts.map +1 -0
- package/dist/timeline/hooks.js +364 -0
- package/dist/timeline/hooks.js.map +1 -0
- package/dist/timeline/index.d.ts +12 -0
- package/dist/timeline/index.d.ts.map +1 -0
- package/dist/timeline/index.js +28 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/indexer.d.ts +37 -0
- package/dist/timeline/indexer.d.ts.map +1 -0
- package/dist/timeline/indexer.js +76 -0
- package/dist/timeline/indexer.js.map +1 -0
- package/dist/timeline/installer.d.ts +33 -0
- package/dist/timeline/installer.d.ts.map +1 -0
- package/dist/timeline/installer.js +255 -0
- package/dist/timeline/installer.js.map +1 -0
- package/dist/timeline/payload.d.ts +31 -0
- package/dist/timeline/payload.d.ts.map +1 -0
- package/dist/timeline/payload.js +58 -0
- package/dist/timeline/payload.js.map +1 -0
- package/dist/timeline/post-tool-summary.d.ts +29 -0
- package/dist/timeline/post-tool-summary.d.ts.map +1 -0
- package/dist/timeline/post-tool-summary.js +190 -0
- package/dist/timeline/post-tool-summary.js.map +1 -0
- package/dist/timeline/recorder.d.ts +36 -0
- package/dist/timeline/recorder.d.ts.map +1 -0
- package/dist/timeline/recorder.js +42 -0
- package/dist/timeline/recorder.js.map +1 -0
- package/dist/timeline/schema-bootstrap.d.ts +42 -0
- package/dist/timeline/schema-bootstrap.d.ts.map +1 -0
- package/dist/timeline/schema-bootstrap.js +81 -0
- package/dist/timeline/schema-bootstrap.js.map +1 -0
- package/dist/timeline/schema.sql +37 -0
- package/dist/timeline/store.d.ts +69 -0
- package/dist/timeline/store.d.ts.map +1 -0
- package/dist/timeline/store.js +429 -0
- package/dist/timeline/store.js.map +1 -0
- package/dist/timeline/types.d.ts +78 -0
- package/dist/timeline/types.d.ts.map +1 -0
- package/dist/timeline/types.js +9 -0
- package/dist/timeline/types.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/glyphs.d.ts +42 -0
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +78 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/shimmer-progress.d.ts +11 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -0
- package/dist/ui/shimmer-progress.js +90 -0
- package/dist/ui/shimmer-progress.js.map +1 -0
- package/dist/ui/shimmer-worker.d.ts +2 -0
- package/dist/ui/shimmer-worker.d.ts.map +1 -0
- package/dist/ui/shimmer-worker.js +118 -0
- package/dist/ui/shimmer-worker.js.map +1 -0
- package/dist/ui/types.d.ts +17 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +3 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/upgrade/index.d.ts +132 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +498 -0
- package/dist/upgrade/index.js.map +1 -0
- package/dist/utils.d.ts +224 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +583 -0
- package/dist/utils.js.map +1 -0
- package/package.json +60 -0
- package/scripts/add-lang/bench.sh +60 -0
- package/scripts/add-lang/check-grammar.mjs +75 -0
- package/scripts/add-lang/dump-ast.mjs +103 -0
- package/scripts/add-lang/verify-extraction.mjs +70 -0
- package/scripts/agent-eval/ab-adoption.sh +91 -0
- package/scripts/agent-eval/ab-hook.sh +86 -0
- package/scripts/agent-eval/ab-impl.sh +78 -0
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -0
- package/scripts/agent-eval/ab-sufficiency.sh +78 -0
- package/scripts/agent-eval/arms-F.sh +21 -0
- package/scripts/agent-eval/arms-matrix.sh +37 -0
- package/scripts/agent-eval/audit.sh +68 -0
- package/scripts/agent-eval/bench-readme.sh +28 -0
- package/scripts/agent-eval/bench-why-repo.sh +22 -0
- package/scripts/agent-eval/block-read-hook.sh +19 -0
- package/scripts/agent-eval/hook-settings.json +15 -0
- package/scripts/agent-eval/itrun.sh +120 -0
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -0
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -0
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -0
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -0
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -0
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -0
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -0
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -0
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -0
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -0
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -0
- package/scripts/agent-eval/offload-eval-setup.sh +24 -0
- package/scripts/agent-eval/offload-eval-styles.sh +72 -0
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -0
- package/scripts/agent-eval/offload-eval.md +76 -0
- package/scripts/agent-eval/parse-arms.mjs +116 -0
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -0
- package/scripts/agent-eval/parse-run.mjs +45 -0
- package/scripts/agent-eval/parse-session.mjs +93 -0
- package/scripts/agent-eval/probe-context.mjs +21 -0
- package/scripts/agent-eval/probe-explore.mjs +40 -0
- package/scripts/agent-eval/probe-node.mjs +20 -0
- package/scripts/agent-eval/probe-sweep.mjs +119 -0
- package/scripts/agent-eval/probe-trace.mjs +20 -0
- package/scripts/agent-eval/redirect-read-hook.sh +38 -0
- package/scripts/agent-eval/run-agent.sh +34 -0
- package/scripts/agent-eval/run-all.sh +69 -0
- package/scripts/agent-eval/run-arms.sh +56 -0
- package/scripts/agent-eval/seq-matrix.mjs +137 -0
- package/scripts/build-bundle.sh +118 -0
- package/scripts/extract-release-notes.mjs +130 -0
- package/scripts/local-install.sh +41 -0
- package/scripts/npm-sdk.js +75 -0
- package/scripts/npm-shim.js +246 -0
- package/scripts/pack-npm.sh +118 -0
- package/scripts/prepare-release.mjs +270 -0
|
@@ -0,0 +1,2926 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
|
|
4
|
+
const generated_detection_1 = require("../extraction/generated-detection");
|
|
5
|
+
const strip_comments_1 = require("./strip-comments");
|
|
6
|
+
const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
|
|
7
|
+
const goframe_synthesizer_1 = require("./goframe-synthesizer");
|
|
8
|
+
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
9
|
+
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
10
|
+
const MAX_CALLBACKS_PER_CHANNEL = 40;
|
|
11
|
+
const EVENT_FANOUT_CAP = 6; // skip events with more handlers/dispatchers than this (too generic without type info)
|
|
12
|
+
const ON_RE = /\.(?:on|once|addListener)\(\s*['"]([^'"]+)['"]\s*,\s*(?:function\s+(\w+)|(?:this\.)?(\w+))/g;
|
|
13
|
+
const EMIT_RE = /\.(?:emit|fire|dispatchEvent)\(\s*['"]([^'"]+)['"]/g;
|
|
14
|
+
const SETSTATE_RE = /this\.setState\s*\(/;
|
|
15
|
+
const FLUTTER_SETSTATE_RE = /\bsetState\s*\(/; // Flutter: setState((){…}) / this.setState
|
|
16
|
+
const JSX_TAG_RE = /<([A-Z][A-Za-z0-9_]*)[\s/>]/g;
|
|
17
|
+
const MAX_JSX_CHILDREN = 30;
|
|
18
|
+
// Vue SFC templates: kebab-case child components (<el-button> → ElButton) and
|
|
19
|
+
// event bindings (@click="fn" / v-on:click="fn"). PascalCase children (<VPNav/>)
|
|
20
|
+
// are already caught by JSX_TAG_RE via the SFC component node.
|
|
21
|
+
const VUE_KEBAB_RE = /<([a-z][a-z0-9]*(?:-[a-z0-9]+)+)[\s/>]/g;
|
|
22
|
+
// PascalCase component tags — `<MediaCard ...>`, `<NavBar/>`. HTML elements are
|
|
23
|
+
// lowercase, so an uppercase-initial tag is a component usage; built-ins
|
|
24
|
+
// (`<NuxtLink>`, `<Transition>`) simply resolve to nothing and emit no edge.
|
|
25
|
+
const VUE_PASCAL_RE = /<([A-Z][A-Za-z0-9]*)[\s/>]/g;
|
|
26
|
+
const VUE_HANDLER_RE = /(?:@|v-on:)([a-zA-Z][\w-]*)(?:\.[\w]+)*\s*=\s*"([^"]+)"/g;
|
|
27
|
+
// Composable/hook destructure: `const { close: closeSidebar } = useSidebarControl()`.
|
|
28
|
+
// Captures the destructure body + the called composable; only `use*` calls qualify.
|
|
29
|
+
const VUE_DESTRUCTURE_RE = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*(\w+)\s*\(/g;
|
|
30
|
+
// Closure-collection dynamic dispatch (language-agnostic, Swift-first). A method
|
|
31
|
+
// appends a closure to a collection property; another method iterates that
|
|
32
|
+
// property *invoking each element* (`coll.forEach { $0() }` / `{ it() }`). The
|
|
33
|
+
// element-invoke (`$0(` / `it(`) PROVES the collection holds closures, so pairing
|
|
34
|
+
// a dispatcher to same-named registrars (`.append`/`.add`/`.push`/`.insert`,
|
|
35
|
+
// incl. Swift `prop.write { $0.append }`) is high-precision. Cross-file/class by
|
|
36
|
+
// design: Alamofire appends in `DataRequest.validate` but iterates in the base
|
|
37
|
+
// `Request.didCompleteTask` — neither same-file nor same-class pairing reaches it.
|
|
38
|
+
const CC_DISPATCH_RE = /(\w+)\.forEach\s*\{\s*(?:\$0|it)\s*\(/g;
|
|
39
|
+
const CC_APPEND_WRITE_RE = /(\w+)\.write\s*\{\s*\$0(?:\.(\w+))?\.(?:append|add|push|insert)\s*\(/g;
|
|
40
|
+
const CC_APPEND_DIRECT_RE = /(\w+)\.(?:append|add|push|insert)\s*\(/g;
|
|
41
|
+
const CC_FANOUT_CAP = 8; // skip a field name with more dispatchers/registrars than this (too generic to pair confidently)
|
|
42
|
+
function kebabToPascal(s) {
|
|
43
|
+
return s.split('-').map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Nuxt auto-import name for a component, derived from its path UNDER `components/`:
|
|
47
|
+
* `components/media/Card.vue` → `MediaCard`, `components/base/foo/Bar.vue` →
|
|
48
|
+
* `BaseFooBar`. Each directory segment and the filename is PascalCased and
|
|
49
|
+
* concatenated; a directory whose PascalCase name prefixes the next segment is
|
|
50
|
+
* collapsed (Nuxt's de-dup: `base/BaseButton.vue` → `BaseButton`, not
|
|
51
|
+
* `BaseBaseButton`). Returns null for a flat component (`components/NavBar.vue`)
|
|
52
|
+
* — its node is already named by basename, so a direct tag match finds it.
|
|
53
|
+
*/
|
|
54
|
+
function nuxtComponentName(filePath) {
|
|
55
|
+
const marker = filePath.lastIndexOf('components/');
|
|
56
|
+
if (marker === -1)
|
|
57
|
+
return null;
|
|
58
|
+
const rel = filePath.slice(marker + 'components/'.length).replace(/\.(vue|ts|tsx|js|jsx)$/i, '');
|
|
59
|
+
const segs = rel.split('/').filter(Boolean).map(kebabToPascal);
|
|
60
|
+
if (segs.length < 2)
|
|
61
|
+
return null;
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const s of segs) {
|
|
64
|
+
const prev = out[out.length - 1];
|
|
65
|
+
if (prev && s.startsWith(prev))
|
|
66
|
+
out[out.length - 1] = s;
|
|
67
|
+
else
|
|
68
|
+
out.push(s);
|
|
69
|
+
}
|
|
70
|
+
return out.join('');
|
|
71
|
+
}
|
|
72
|
+
function sliceLines(content, startLine, endLine) {
|
|
73
|
+
if (!startLine || !endLine)
|
|
74
|
+
return null;
|
|
75
|
+
return content.split('\n').slice(startLine - 1, endLine).join('\n');
|
|
76
|
+
}
|
|
77
|
+
function registrarField(src) {
|
|
78
|
+
const m = src.match(/this\.(\w+)\.(?:add|push|set)\(/);
|
|
79
|
+
return m ? m[1] : null;
|
|
80
|
+
}
|
|
81
|
+
function dispatcherField(src) {
|
|
82
|
+
const forOf = src.match(/\bof\s+(?:Array\.from\(\s*)?this\.(\w+)/);
|
|
83
|
+
if (forOf && /\b\w+\s*\(/.test(src))
|
|
84
|
+
return forOf[1];
|
|
85
|
+
const forEach = src.match(/this\.(\w+)\.forEach\(/);
|
|
86
|
+
if (forEach)
|
|
87
|
+
return forEach[1];
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const FN_KINDS = new Set(['method', 'function', 'component']);
|
|
91
|
+
/** Innermost function/method node whose line range contains `line`. */
|
|
92
|
+
function enclosingFn(nodesInFile, line) {
|
|
93
|
+
let best = null;
|
|
94
|
+
for (const n of nodesInFile) {
|
|
95
|
+
if (!FN_KINDS.has(n.kind))
|
|
96
|
+
continue;
|
|
97
|
+
const end = n.endLine ?? n.startLine;
|
|
98
|
+
if (n.startLine <= line && end >= line) {
|
|
99
|
+
if (!best || n.startLine >= best.startLine)
|
|
100
|
+
best = n; // prefer the tightest (latest-starting) encloser
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return best;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Stream method + function nodes lazily. The synthesizers only scan-and-filter
|
|
107
|
+
* down to a tiny matched subset, so materializing every function/method (which
|
|
108
|
+
* is gigabytes on a symbol-dense project) just to iterate it once is what OOM'd
|
|
109
|
+
* #610. Iterating keeps memory O(1) in the node count.
|
|
110
|
+
*/
|
|
111
|
+
function* methodAndFunctionNodes(queries) {
|
|
112
|
+
yield* queries.iterateNodesByKind('method');
|
|
113
|
+
yield* queries.iterateNodesByKind('function');
|
|
114
|
+
}
|
|
115
|
+
/** Phase 1: field-backed observer channels (registrar/dispatcher share a store). */
|
|
116
|
+
function fieldChannelEdges(queries, ctx) {
|
|
117
|
+
const registrars = [];
|
|
118
|
+
const dispatchers = [];
|
|
119
|
+
for (const m of methodAndFunctionNodes(queries)) {
|
|
120
|
+
const isReg = REGISTRAR_NAME.test(m.name);
|
|
121
|
+
const isDisp = DISPATCHER_NAME.test(m.name);
|
|
122
|
+
if (!isReg && !isDisp)
|
|
123
|
+
continue;
|
|
124
|
+
const content = ctx.readFile(m.filePath);
|
|
125
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
126
|
+
if (!src)
|
|
127
|
+
continue;
|
|
128
|
+
if (isReg) {
|
|
129
|
+
const f = registrarField(src);
|
|
130
|
+
if (f)
|
|
131
|
+
registrars.push({ node: m, field: f });
|
|
132
|
+
}
|
|
133
|
+
if (isDisp) {
|
|
134
|
+
const f = dispatcherField(src);
|
|
135
|
+
if (f)
|
|
136
|
+
dispatchers.push({ node: m, field: f });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const edges = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const reg of registrars) {
|
|
142
|
+
const chDispatchers = dispatchers.filter((d) => d.node.filePath === reg.node.filePath && d.field === reg.field);
|
|
143
|
+
if (chDispatchers.length === 0)
|
|
144
|
+
continue;
|
|
145
|
+
const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(?:this\\.)?(\\w+)`);
|
|
146
|
+
let added = 0;
|
|
147
|
+
for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) {
|
|
148
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
149
|
+
break;
|
|
150
|
+
if (!e.line)
|
|
151
|
+
continue;
|
|
152
|
+
const caller = queries.getNodeById(e.source);
|
|
153
|
+
if (!caller)
|
|
154
|
+
continue;
|
|
155
|
+
const line = ctx.readFile(caller.filePath)?.split('\n')[e.line - 1];
|
|
156
|
+
const am = line?.match(argRe);
|
|
157
|
+
if (!am)
|
|
158
|
+
continue;
|
|
159
|
+
const fn = ctx.getNodesByName(am[1]).find((n) => n.kind === 'method' || n.kind === 'function');
|
|
160
|
+
if (!fn)
|
|
161
|
+
continue;
|
|
162
|
+
for (const disp of chDispatchers) {
|
|
163
|
+
if (disp.node.id === fn.id)
|
|
164
|
+
continue;
|
|
165
|
+
const key = `${disp.node.id}>${fn.id}`;
|
|
166
|
+
if (seen.has(key))
|
|
167
|
+
continue;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
edges.push({
|
|
170
|
+
source: disp.node.id, target: fn.id, kind: 'calls', line: disp.node.startLine,
|
|
171
|
+
provenance: 'heuristic',
|
|
172
|
+
metadata: {
|
|
173
|
+
synthesizedBy: 'callback', via: reg.node.name, field: reg.field,
|
|
174
|
+
// Where the callback was wired up (`scene.onUpdate(this.triggerRender)`).
|
|
175
|
+
// This is the #1 thing an agent reads/greps to explain the flow — surface
|
|
176
|
+
// it so node/trace/context can show it without a callers() + Read round-trip.
|
|
177
|
+
registeredAt: `${caller.filePath}:${e.line}`,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
added++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return edges;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Closure-collection dispatch: dispatcher iterates a closure-collection property
|
|
188
|
+
* invoking each element; registrar appends a closure to the same-named property.
|
|
189
|
+
* Emits dispatcher → registrar so a flow reaches the registration site (where the
|
|
190
|
+
* appended closure's body — and its callers — live). High-precision: the
|
|
191
|
+
* dispatcher's element-invoke is the gate (a `.forEach` that does NOT invoke its
|
|
192
|
+
* element is ignored), so a repo with no closure-collection dispatch yields zero
|
|
193
|
+
* edges regardless of how many `.append`/`.push` sites it has.
|
|
194
|
+
*
|
|
195
|
+
* Pairs globally by field name (cross-file/class is required — see Alamofire's
|
|
196
|
+
* base-class `Request.didCompleteTask` iterating `validators` appended by the
|
|
197
|
+
* subclass `DataRequest.validate`), bounded by a fan-out cap so a generic field
|
|
198
|
+
* name shared across unrelated classes can't fan out into noise.
|
|
199
|
+
*/
|
|
200
|
+
function closureCollectionEdges(queries, ctx) {
|
|
201
|
+
const dispatchers = new Map(); // field → dispatcher methods + forEach line
|
|
202
|
+
const registrars = new Map(); // field → registrar methods + append line
|
|
203
|
+
const addReg = (field, node, absLine) => {
|
|
204
|
+
if (!field || /^\d+$/.test(field))
|
|
205
|
+
return; // `$0.append` mis-captures the `0`; the write-RE owns that field
|
|
206
|
+
const arr = registrars.get(field) ?? [];
|
|
207
|
+
if (!arr.some((r) => r.node.id === node.id))
|
|
208
|
+
arr.push({ node, line: absLine });
|
|
209
|
+
registrars.set(field, arr);
|
|
210
|
+
};
|
|
211
|
+
for (const m of methodAndFunctionNodes(queries)) {
|
|
212
|
+
const content = ctx.readFile(m.filePath);
|
|
213
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
214
|
+
if (!src)
|
|
215
|
+
continue;
|
|
216
|
+
const hasForEach = src.includes('.forEach');
|
|
217
|
+
const hasAppend = src.includes('.append(') || src.includes('.add(') || src.includes('.push(') || src.includes('.insert(');
|
|
218
|
+
if (!hasForEach && !hasAppend)
|
|
219
|
+
continue;
|
|
220
|
+
const lineAt = (idx) => (m.startLine ?? 1) + src.slice(0, idx).split('\n').length - 1;
|
|
221
|
+
if (hasForEach) {
|
|
222
|
+
CC_DISPATCH_RE.lastIndex = 0;
|
|
223
|
+
let d;
|
|
224
|
+
while ((d = CC_DISPATCH_RE.exec(src))) {
|
|
225
|
+
const arr = dispatchers.get(d[1]) ?? [];
|
|
226
|
+
if (!arr.some((n) => n.node.id === m.id))
|
|
227
|
+
arr.push({ node: m, line: lineAt(d.index) });
|
|
228
|
+
dispatchers.set(d[1], arr);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (hasAppend) {
|
|
232
|
+
CC_APPEND_WRITE_RE.lastIndex = 0;
|
|
233
|
+
let w;
|
|
234
|
+
while ((w = CC_APPEND_WRITE_RE.exec(src)))
|
|
235
|
+
addReg(w[2] || w[1], m, lineAt(w.index)); // nested `$0.streams` else the `.write` receiver
|
|
236
|
+
CC_APPEND_DIRECT_RE.lastIndex = 0;
|
|
237
|
+
let a;
|
|
238
|
+
while ((a = CC_APPEND_DIRECT_RE.exec(src)))
|
|
239
|
+
addReg(a[1], m, lineAt(a.index));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const edges = [];
|
|
243
|
+
const seen = new Set();
|
|
244
|
+
for (const [field, disps] of dispatchers) {
|
|
245
|
+
const regs = registrars.get(field);
|
|
246
|
+
if (!regs || regs.length === 0)
|
|
247
|
+
continue;
|
|
248
|
+
if (disps.length > CC_FANOUT_CAP || regs.length > CC_FANOUT_CAP)
|
|
249
|
+
continue; // generic field — can't pair confidently
|
|
250
|
+
for (const disp of disps)
|
|
251
|
+
for (const reg of regs) {
|
|
252
|
+
if (disp.node.id === reg.node.id)
|
|
253
|
+
continue;
|
|
254
|
+
const key = `${disp.node.id}>${reg.node.id}`;
|
|
255
|
+
if (seen.has(key))
|
|
256
|
+
continue;
|
|
257
|
+
seen.add(key);
|
|
258
|
+
edges.push({
|
|
259
|
+
source: disp.node.id, target: reg.node.id, kind: 'calls', line: disp.line,
|
|
260
|
+
provenance: 'heuristic',
|
|
261
|
+
metadata: { synthesizedBy: 'closure-collection', field, registeredAt: `${reg.node.filePath}:${reg.line}` },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return edges;
|
|
266
|
+
}
|
|
267
|
+
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
268
|
+
function eventEmitterEdges(ctx) {
|
|
269
|
+
const emitsByEvent = new Map(); // event → dispatcher node ids
|
|
270
|
+
const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
|
|
271
|
+
for (const file of ctx.getAllFiles()) {
|
|
272
|
+
const content = ctx.readFile(file);
|
|
273
|
+
if (!content)
|
|
274
|
+
continue;
|
|
275
|
+
const hasEmit = content.includes('.emit(') || content.includes('.fire(') || content.includes('.dispatchEvent(');
|
|
276
|
+
const hasOn = content.includes('.on(') || content.includes('.once(') || content.includes('.addListener(');
|
|
277
|
+
if (!hasEmit && !hasOn)
|
|
278
|
+
continue;
|
|
279
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
280
|
+
const lineOf = (idx) => content.slice(0, idx).split('\n').length;
|
|
281
|
+
if (hasEmit) {
|
|
282
|
+
EMIT_RE.lastIndex = 0;
|
|
283
|
+
let m;
|
|
284
|
+
while ((m = EMIT_RE.exec(content))) {
|
|
285
|
+
const disp = enclosingFn(nodesInFile, lineOf(m.index));
|
|
286
|
+
if (!disp)
|
|
287
|
+
continue;
|
|
288
|
+
const set = emitsByEvent.get(m[1]) ?? new Set();
|
|
289
|
+
set.add(disp.id);
|
|
290
|
+
emitsByEvent.set(m[1], set);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (hasOn) {
|
|
294
|
+
ON_RE.lastIndex = 0;
|
|
295
|
+
let m;
|
|
296
|
+
while ((m = ON_RE.exec(content))) {
|
|
297
|
+
const handlerName = m[2] || m[3];
|
|
298
|
+
if (!handlerName)
|
|
299
|
+
continue;
|
|
300
|
+
const handler = ctx.getNodesByName(handlerName).find((n) => n.kind === 'function' || n.kind === 'method');
|
|
301
|
+
if (!handler)
|
|
302
|
+
continue;
|
|
303
|
+
const map = handlersByEvent.get(m[1]) ?? new Map();
|
|
304
|
+
map.set(handler.id, `${file}:${lineOf(m.index)}`);
|
|
305
|
+
handlersByEvent.set(m[1], map);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const edges = [];
|
|
310
|
+
const seen = new Set();
|
|
311
|
+
for (const [event, dispatchers] of emitsByEvent) {
|
|
312
|
+
const handlers = handlersByEvent.get(event);
|
|
313
|
+
if (!handlers)
|
|
314
|
+
continue;
|
|
315
|
+
// Precision guard: a generic event name with many handlers/dispatchers can't
|
|
316
|
+
// be matched without receiver-type info (Phase 3) — skip rather than over-link.
|
|
317
|
+
if (dispatchers.size > EVENT_FANOUT_CAP || handlers.size > EVENT_FANOUT_CAP)
|
|
318
|
+
continue;
|
|
319
|
+
for (const d of dispatchers)
|
|
320
|
+
for (const [h, registeredAt] of handlers) {
|
|
321
|
+
if (d === h)
|
|
322
|
+
continue;
|
|
323
|
+
const key = `${d}>${h}`;
|
|
324
|
+
if (seen.has(key))
|
|
325
|
+
continue;
|
|
326
|
+
seen.add(key);
|
|
327
|
+
edges.push({ source: d, target: h, kind: 'calls', provenance: 'heuristic', metadata: { synthesizedBy: 'event-emitter', event, registeredAt } });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return edges;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Phase 4: React class-component re-render. `this.setState(...)` re-runs the
|
|
334
|
+
* component's `render()`, but that hop is React-internal — no static edge — so a
|
|
335
|
+
* flow like "mutation → setState → canvas repaint" dead-ends at setState even
|
|
336
|
+
* though `render → getRenderableElements → …` is fully call-connected after it.
|
|
337
|
+
* Bridge it: for each class that has a `render` method, link every sibling method
|
|
338
|
+
* whose body calls `this.setState(` → `render`. The setState gate keeps this to
|
|
339
|
+
* React class components (a non-React class with a `render` method won't call
|
|
340
|
+
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
341
|
+
* accepted — it's reachability-correct, like the callback channels.
|
|
342
|
+
*/
|
|
343
|
+
function reactRenderEdges(queries, ctx) {
|
|
344
|
+
const edges = [];
|
|
345
|
+
const seen = new Set();
|
|
346
|
+
for (const cls of queries.getNodesByKind('class')) {
|
|
347
|
+
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
348
|
+
.map((e) => queries.getNodeById(e.target))
|
|
349
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
350
|
+
const render = children.find((n) => n.name === 'render');
|
|
351
|
+
if (!render)
|
|
352
|
+
continue;
|
|
353
|
+
let added = 0;
|
|
354
|
+
for (const m of children) {
|
|
355
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
356
|
+
break;
|
|
357
|
+
if (m.id === render.id)
|
|
358
|
+
continue;
|
|
359
|
+
const content = ctx.readFile(m.filePath);
|
|
360
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
361
|
+
if (!src || !SETSTATE_RE.test(src))
|
|
362
|
+
continue;
|
|
363
|
+
const key = `${m.id}>${render.id}`;
|
|
364
|
+
if (seen.has(key))
|
|
365
|
+
continue;
|
|
366
|
+
seen.add(key);
|
|
367
|
+
edges.push({
|
|
368
|
+
source: m.id, target: render.id, kind: 'calls', line: m.startLine,
|
|
369
|
+
provenance: 'heuristic',
|
|
370
|
+
metadata: { synthesizedBy: 'react-render', via: 'setState', registeredAt: `${render.filePath}:${render.startLine}` },
|
|
371
|
+
});
|
|
372
|
+
added++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return edges;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Phase 4b: Flutter setState → build (the Dart analog of react-render). In a
|
|
379
|
+
* StatefulWidget's State class, `setState(() {…})` re-runs `build(context)`, but
|
|
380
|
+
* that hop is framework-internal (Flutter calls build), so a flow like
|
|
381
|
+
* "onPressed → _increment → setState → rebuilt UI" dead-ends at setState. Bridge
|
|
382
|
+
* it: for each Dart class with a `build` method, link every sibling method whose
|
|
383
|
+
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
384
|
+
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
385
|
+
*/
|
|
386
|
+
function flutterBuildEdges(queries, ctx) {
|
|
387
|
+
const edges = [];
|
|
388
|
+
const seen = new Set();
|
|
389
|
+
for (const cls of queries.getNodesByKind('class')) {
|
|
390
|
+
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
391
|
+
.map((e) => queries.getNodeById(e.target))
|
|
392
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
393
|
+
const build = children.find((n) => n.name === 'build');
|
|
394
|
+
if (!build || !build.filePath.endsWith('.dart'))
|
|
395
|
+
continue;
|
|
396
|
+
let added = 0;
|
|
397
|
+
for (const m of children) {
|
|
398
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
399
|
+
break;
|
|
400
|
+
if (m.id === build.id)
|
|
401
|
+
continue;
|
|
402
|
+
const content = ctx.readFile(m.filePath);
|
|
403
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
404
|
+
if (!src || !FLUTTER_SETSTATE_RE.test(src))
|
|
405
|
+
continue;
|
|
406
|
+
const key = `${m.id}>${build.id}`;
|
|
407
|
+
if (seen.has(key))
|
|
408
|
+
continue;
|
|
409
|
+
seen.add(key);
|
|
410
|
+
edges.push({
|
|
411
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
412
|
+
provenance: 'heuristic',
|
|
413
|
+
metadata: { synthesizedBy: 'flutter-build', via: 'setState', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
414
|
+
});
|
|
415
|
+
added++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return edges;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
422
|
+
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
423
|
+
* but that hop is a vtable indirection — no static call edge — so a flow stops at
|
|
424
|
+
* the abstract base method. Bridge it like react-render: for each C++ class that
|
|
425
|
+
* `extends` a base, link each base method → the subclass method of the same name
|
|
426
|
+
* (the override), so trace/callees from the interface method reach the
|
|
427
|
+
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
428
|
+
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
429
|
+
*/
|
|
430
|
+
function cppOverrideEdges(queries) {
|
|
431
|
+
const edges = [];
|
|
432
|
+
const seen = new Set();
|
|
433
|
+
const methodsOf = (classId) => queries
|
|
434
|
+
.getOutgoingEdges(classId, ['contains'])
|
|
435
|
+
.map((e) => queries.getNodeById(e.target))
|
|
436
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
437
|
+
for (const cls of queries.getNodesByKind('class')) {
|
|
438
|
+
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
439
|
+
if (subMethods.length === 0)
|
|
440
|
+
continue;
|
|
441
|
+
for (const ext of queries.getOutgoingEdges(cls.id, ['extends'])) {
|
|
442
|
+
const base = queries.getNodeById(ext.target);
|
|
443
|
+
if (!base || base.language !== 'cpp' || base.id === cls.id)
|
|
444
|
+
continue;
|
|
445
|
+
const baseMethods = new Map(methodsOf(base.id).map((m) => [m.name, m]));
|
|
446
|
+
let added = 0;
|
|
447
|
+
for (const m of subMethods) {
|
|
448
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
449
|
+
break;
|
|
450
|
+
const bm = baseMethods.get(m.name);
|
|
451
|
+
if (!bm || bm.id === m.id)
|
|
452
|
+
continue;
|
|
453
|
+
const key = `${bm.id}>${m.id}`;
|
|
454
|
+
if (seen.has(key))
|
|
455
|
+
continue;
|
|
456
|
+
seen.add(key);
|
|
457
|
+
edges.push({
|
|
458
|
+
source: bm.id,
|
|
459
|
+
target: m.id,
|
|
460
|
+
kind: 'calls',
|
|
461
|
+
line: bm.startLine,
|
|
462
|
+
provenance: 'heuristic',
|
|
463
|
+
metadata: { synthesizedBy: 'cpp-override', via: m.name, registeredAt: `${m.filePath}:${m.startLine}` },
|
|
464
|
+
});
|
|
465
|
+
added++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return edges;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Phase 5.5: interface / abstract dispatch (Java, Kotlin). A call through an
|
|
473
|
+
* injected interface (`@Autowired FooService svc; svc.list()`) or an abstract
|
|
474
|
+
* base dispatches at runtime to the implementing class's override — a vtable
|
|
475
|
+
* indirection with no static call edge — so a request→service flow stops at the
|
|
476
|
+
* interface method. Bridge it like cpp-override: for each class that
|
|
477
|
+
* `implements` an interface (or `extends` an abstract base), link each
|
|
478
|
+
* base/interface method → the class's same-name method (the override) so
|
|
479
|
+
* trace/callees reach the implementation. Over-approximation accepted
|
|
480
|
+
* (reachability-correct); capped per class, gated to JVM languages.
|
|
481
|
+
*/
|
|
482
|
+
// Languages whose static `implements`/`extends` edges should bridge an
|
|
483
|
+
// interface (or abstract base) method to the matching concrete-class method.
|
|
484
|
+
// The set is "languages with explicit nominal subtyping and a single class
|
|
485
|
+
// kind that holds methods" — i.e. the shape this loop expects. Swift and
|
|
486
|
+
// Scala fit shape-wise (Swift `protocol`/`class`, Scala `trait`/`class`)
|
|
487
|
+
// and are added below; their concrete-side nodes can be a `struct` (Swift)
|
|
488
|
+
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
489
|
+
const IFACE_OVERRIDE_LANGS = new Set([
|
|
490
|
+
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
491
|
+
]);
|
|
492
|
+
/**
|
|
493
|
+
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
494
|
+
* struct satisfies an interface structurally when its method set covers the
|
|
495
|
+
* interface's. Synthesize the missing `implements` edge (struct → interface) by
|
|
496
|
+
* matching method-NAME sets, so impl-navigation works and the interface-dispatch
|
|
497
|
+
* bridge ({@link interfaceOverrideEdges}, now 'go'-enabled) can link an interface
|
|
498
|
+
* method call to the concrete overrides.
|
|
499
|
+
*
|
|
500
|
+
* Name-only matching (signatures ignored) — over-approximation accepted, in line
|
|
501
|
+
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
502
|
+
* (`any`) are skipped so they don't match every struct.
|
|
503
|
+
*/
|
|
504
|
+
function goImplementsEdges(queries) {
|
|
505
|
+
const edges = [];
|
|
506
|
+
const seen = new Set();
|
|
507
|
+
const methodNameSet = (id) => new Set(queries
|
|
508
|
+
.getOutgoingEdges(id, ['contains'])
|
|
509
|
+
.map((e) => queries.getNodeById(e.target))
|
|
510
|
+
.filter((n) => !!n && n.kind === 'method')
|
|
511
|
+
.map((n) => n.name));
|
|
512
|
+
const goStructs = queries.getNodesByKind('struct').filter((s) => s.language === 'go');
|
|
513
|
+
const structMethods = new Map();
|
|
514
|
+
for (const s of goStructs)
|
|
515
|
+
structMethods.set(s.id, methodNameSet(s.id));
|
|
516
|
+
for (const iface of queries.getNodesByKind('interface')) {
|
|
517
|
+
if (iface.language !== 'go')
|
|
518
|
+
continue;
|
|
519
|
+
const want = methodNameSet(iface.id);
|
|
520
|
+
if (want.size === 0)
|
|
521
|
+
continue; // empty interface (`any`) — would match everything
|
|
522
|
+
let added = 0;
|
|
523
|
+
for (const s of goStructs) {
|
|
524
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
525
|
+
break;
|
|
526
|
+
const have = structMethods.get(s.id);
|
|
527
|
+
if (!have || have.size < want.size)
|
|
528
|
+
continue;
|
|
529
|
+
let all = true;
|
|
530
|
+
for (const m of want) {
|
|
531
|
+
if (!have.has(m)) {
|
|
532
|
+
all = false;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (!all)
|
|
537
|
+
continue;
|
|
538
|
+
const key = `${s.id}>${iface.id}`;
|
|
539
|
+
if (seen.has(key))
|
|
540
|
+
continue;
|
|
541
|
+
seen.add(key);
|
|
542
|
+
edges.push({
|
|
543
|
+
source: s.id,
|
|
544
|
+
target: iface.id,
|
|
545
|
+
kind: 'implements',
|
|
546
|
+
line: s.startLine,
|
|
547
|
+
provenance: 'heuristic',
|
|
548
|
+
metadata: { synthesizedBy: 'go-implements', via: iface.name, registeredAt: `${s.filePath}:${s.startLine}` },
|
|
549
|
+
});
|
|
550
|
+
added++;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return edges;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Cross-file Go method → receiver-type `contains` edges. In Go a type's methods
|
|
557
|
+
* are commonly declared in a different file from the `type` declaration itself
|
|
558
|
+
* (`type User struct{…}` in `user.go`, `func (u *User) Save()` in
|
|
559
|
+
* `user_store.go`). Extraction attaches the struct→method `contains` edge only
|
|
560
|
+
* when the receiver type is in the SAME file — the owner lookup in
|
|
561
|
+
* `tree-sitter.ts` is scoped to the file being parsed — so a cross-file method
|
|
562
|
+
* is left orphaned from its type (it's still `contains`ed by its file, just not
|
|
563
|
+
* its struct). That breaks `codegraph_node` member outlines, any
|
|
564
|
+
* callers/callees/impact traversal that goes through the type's `contains`
|
|
565
|
+
* edges, and the {@link goImplementsEdges} method-set computation (which derives
|
|
566
|
+
* a struct's method set from those same edges, so it under-counts interfaces a
|
|
567
|
+
* cross-file struct satisfies).
|
|
568
|
+
*
|
|
569
|
+
* Go guarantees a method's receiver type is declared in the SAME PACKAGE as the
|
|
570
|
+
* method, and a Go package is a single directory — so this is a deterministic
|
|
571
|
+
* structural link, not a heuristic: find the same-named type in the method's own
|
|
572
|
+
* directory and add the missing `contains` edge (no `provenance: 'heuristic'`,
|
|
573
|
+
* matching the same-file edges extraction already emits). Skips methods that
|
|
574
|
+
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
575
|
+
*/
|
|
576
|
+
function goCrossFileMethodContainsEdges(queries) {
|
|
577
|
+
const edges = [];
|
|
578
|
+
const seen = new Set();
|
|
579
|
+
const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
580
|
+
const dirOf = (p) => {
|
|
581
|
+
const i = p.replace(/\\/g, '/').lastIndexOf('/');
|
|
582
|
+
return i >= 0 ? p.slice(0, i) : '';
|
|
583
|
+
};
|
|
584
|
+
for (const method of queries.getNodesByKind('method')) {
|
|
585
|
+
if (method.language !== 'go')
|
|
586
|
+
continue;
|
|
587
|
+
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
588
|
+
// (extraction sets `${receiverType}::${name}` for receiver methods).
|
|
589
|
+
const qn = method.qualifiedName;
|
|
590
|
+
if (!qn)
|
|
591
|
+
continue;
|
|
592
|
+
const sep = qn.lastIndexOf('::');
|
|
593
|
+
if (sep <= 0)
|
|
594
|
+
continue;
|
|
595
|
+
const receiver = qn.slice(0, sep);
|
|
596
|
+
if (!receiver)
|
|
597
|
+
continue;
|
|
598
|
+
// Already attached to its type (same-file case handled at extraction)?
|
|
599
|
+
const hasTypeParent = queries
|
|
600
|
+
.getIncomingEdges(method.id, ['contains'])
|
|
601
|
+
.some((e) => {
|
|
602
|
+
const src = queries.getNodeById(e.source);
|
|
603
|
+
return src != null && TYPE_KINDS.has(src.kind);
|
|
604
|
+
});
|
|
605
|
+
if (hasTypeParent)
|
|
606
|
+
continue;
|
|
607
|
+
// Find the receiver type in the SAME directory (= same Go package). Go forbids
|
|
608
|
+
// duplicate type names within a package, so a same-name same-dir match is
|
|
609
|
+
// unambiguous; scoping to the directory avoids linking to a same-named type
|
|
610
|
+
// in another package.
|
|
611
|
+
const dir = dirOf(method.filePath);
|
|
612
|
+
const owner = queries
|
|
613
|
+
.getNodesByName(receiver)
|
|
614
|
+
.find((n) => n.language === 'go' && TYPE_KINDS.has(n.kind) && dirOf(n.filePath) === dir);
|
|
615
|
+
if (!owner)
|
|
616
|
+
continue;
|
|
617
|
+
const key = `${owner.id}>${method.id}`;
|
|
618
|
+
if (seen.has(key))
|
|
619
|
+
continue;
|
|
620
|
+
seen.add(key);
|
|
621
|
+
edges.push({ source: owner.id, target: method.id, kind: 'contains', line: method.startLine });
|
|
622
|
+
}
|
|
623
|
+
return edges;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Kotlin Multiplatform `expect`/`actual` linking. A `common` source set declares
|
|
627
|
+
* `expect fun foo()` / `expect class Bar`; each platform source set (jvm, native,
|
|
628
|
+
* js, …) provides an `actual` implementation with the IDENTICAL fully-qualified
|
|
629
|
+
* name in a different file. Callers in common code resolve to the `expect`
|
|
630
|
+
* declaration, so every `actual` impl ends up with zero dependents — invisible to
|
|
631
|
+
* impact/affected even though editing it can break every caller of the API.
|
|
632
|
+
*
|
|
633
|
+
* Synthesize a `calls` edge from the common declaration to each platform `actual`
|
|
634
|
+
* (mirroring the interface-impl bridge: abstract → concrete), so editing a
|
|
635
|
+
* platform impl surfaces the common `expect` and its callers, and the impl file
|
|
636
|
+
* participates in the graph.
|
|
637
|
+
*
|
|
638
|
+
* `expect`/`actual` are captured onto the node's `decorators` list at extraction
|
|
639
|
+
* (kotlin.ts `extractModifiers`). Members of an `expect class` are NOT themselves
|
|
640
|
+
* keyword-marked, so the declaration side is matched as the same-FQN, same-kind
|
|
641
|
+
* node that is NOT marked `actual`. Requiring an `actual`-marked counterpart also
|
|
642
|
+
* gates out plain cross-file overloads (neither side is marked).
|
|
643
|
+
*/
|
|
644
|
+
// Kinds that an `expect`/`actual` pair may legitimately straddle. `expect class`
|
|
645
|
+
// is routinely fulfilled by an `actual typealias` (e.g. `actual typealias
|
|
646
|
+
// CancellationException = …`, `actual typealias SchedulerTask = Task`), so a
|
|
647
|
+
// strict kind match would miss those one-line alias files. Same-FQN + the
|
|
648
|
+
// `actual` marker already gates out unrelated symbols, so widening to the
|
|
649
|
+
// type-like kinds is safe.
|
|
650
|
+
const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_alias']);
|
|
651
|
+
function kmpKindsCompatible(a, b) {
|
|
652
|
+
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
653
|
+
}
|
|
654
|
+
function kotlinExpectActualEdges(queries) {
|
|
655
|
+
const edges = [];
|
|
656
|
+
const seen = new Set();
|
|
657
|
+
const actuals = queries
|
|
658
|
+
.getAllNodes()
|
|
659
|
+
.filter((n) => n.language === 'kotlin' && !!n.decorators?.includes('actual'));
|
|
660
|
+
for (const act of actuals) {
|
|
661
|
+
let added = 0;
|
|
662
|
+
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
663
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
664
|
+
break;
|
|
665
|
+
// The declaration side: same FQN + compatible kind, a different file, NOT
|
|
666
|
+
// itself an `actual` (that would be a sibling platform impl, not the decl).
|
|
667
|
+
if (cand.language !== 'kotlin' || cand.id === act.id)
|
|
668
|
+
continue;
|
|
669
|
+
if (!kmpKindsCompatible(cand.kind, act.kind) || cand.filePath === act.filePath)
|
|
670
|
+
continue;
|
|
671
|
+
if (cand.decorators?.includes('actual'))
|
|
672
|
+
continue;
|
|
673
|
+
const key = `${cand.id}>${act.id}`;
|
|
674
|
+
if (seen.has(key))
|
|
675
|
+
continue;
|
|
676
|
+
seen.add(key);
|
|
677
|
+
edges.push({
|
|
678
|
+
source: cand.id,
|
|
679
|
+
target: act.id,
|
|
680
|
+
kind: 'calls',
|
|
681
|
+
line: cand.startLine,
|
|
682
|
+
provenance: 'heuristic',
|
|
683
|
+
metadata: {
|
|
684
|
+
synthesizedBy: 'kotlin-expect-actual',
|
|
685
|
+
via: act.name,
|
|
686
|
+
registeredAt: `${act.filePath}:${act.startLine}`,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
added++;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return edges;
|
|
693
|
+
}
|
|
694
|
+
function interfaceOverrideEdges(queries) {
|
|
695
|
+
const edges = [];
|
|
696
|
+
const seen = new Set();
|
|
697
|
+
const methodsOf = (classId) => queries
|
|
698
|
+
.getOutgoingEdges(classId, ['contains'])
|
|
699
|
+
.map((e) => queries.getNodeById(e.target))
|
|
700
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
701
|
+
// Concrete-side kinds vary by language: `class` covers Java / Kotlin /
|
|
702
|
+
// C# / TS / Swift-classes / Scala-classes; `struct` covers Swift value
|
|
703
|
+
// types that conform to protocols. Iterate both.
|
|
704
|
+
const concreteKinds = ['class', 'struct'];
|
|
705
|
+
for (const kind of concreteKinds) {
|
|
706
|
+
for (const cls of queries.getNodesByKind(kind)) {
|
|
707
|
+
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
708
|
+
if (implMethods.length === 0)
|
|
709
|
+
continue;
|
|
710
|
+
for (const sup of queries.getOutgoingEdges(cls.id, ['implements', 'extends'])) {
|
|
711
|
+
const base = queries.getNodeById(sup.target);
|
|
712
|
+
if (!base || !IFACE_OVERRIDE_LANGS.has(base.language) || base.id === cls.id)
|
|
713
|
+
continue;
|
|
714
|
+
// Group impl methods by name to handle OVERLOADS: an interface `list()` and
|
|
715
|
+
// `list(params)` are distinct nodes and a call may resolve to either, so
|
|
716
|
+
// link every base overload → every same-name impl overload (keying by name
|
|
717
|
+
// alone would drop all but one and miss the resolved overload).
|
|
718
|
+
const implByName = new Map();
|
|
719
|
+
for (const m of implMethods) {
|
|
720
|
+
const arr = implByName.get(m.name);
|
|
721
|
+
if (arr)
|
|
722
|
+
arr.push(m);
|
|
723
|
+
else
|
|
724
|
+
implByName.set(m.name, [m]);
|
|
725
|
+
}
|
|
726
|
+
let added = 0;
|
|
727
|
+
for (const bm of methodsOf(base.id)) {
|
|
728
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
729
|
+
break;
|
|
730
|
+
for (const m of implByName.get(bm.name) ?? []) {
|
|
731
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
732
|
+
break;
|
|
733
|
+
if (bm.id === m.id)
|
|
734
|
+
continue;
|
|
735
|
+
const key = `${bm.id}>${m.id}`;
|
|
736
|
+
if (seen.has(key))
|
|
737
|
+
continue;
|
|
738
|
+
seen.add(key);
|
|
739
|
+
edges.push({
|
|
740
|
+
source: bm.id,
|
|
741
|
+
target: m.id,
|
|
742
|
+
kind: 'calls',
|
|
743
|
+
line: bm.startLine,
|
|
744
|
+
provenance: 'heuristic',
|
|
745
|
+
metadata: { synthesizedBy: 'interface-impl', via: m.name, registeredAt: `${m.filePath}:${m.startLine}` },
|
|
746
|
+
});
|
|
747
|
+
added++;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return edges;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Go gRPC stub → impl bridge. The protoc-gen-go-grpc codegen emits an
|
|
757
|
+
* `UnimplementedXxxServer` struct in `*_grpc.pb.go` carrying one method
|
|
758
|
+
* per service RPC; the real handler is a hand-written struct in another
|
|
759
|
+
* file (`x/bank/keeper/msg_server.go::msgServer.Send` in cosmos-sdk).
|
|
760
|
+
* Go's structural typing means no `implements` edge exists for our
|
|
761
|
+
* resolver to follow, so `trace("Send","SendCoins")` lands on the
|
|
762
|
+
* empty stub and reports "no path" (validated empirically — the cosmos
|
|
763
|
+
* Q1 r1 trace failure that drove this work).
|
|
764
|
+
*
|
|
765
|
+
* Bridge: for each `UnimplementedXxxServer` whose RPC-method names are
|
|
766
|
+
* a SUBSET of some other Go struct's method names, emit `calls` edges
|
|
767
|
+
* `stub.method → impl.method` (paired by name). Excludes the gRPC
|
|
768
|
+
* internal markers `mustEmbedUnimplementedXxxServer` and
|
|
769
|
+
* `testEmbeddedByValue`, and skips candidate impls that themselves
|
|
770
|
+
* live in a generated file (their `xxxClient` / sibling stubs would
|
|
771
|
+
* otherwise look like impls).
|
|
772
|
+
*
|
|
773
|
+
* Multiple candidates is allowed and capped at MAX_CALLBACKS_PER_CHANNEL —
|
|
774
|
+
* a service often has both a production impl and one or more test
|
|
775
|
+
* mocks; linking to all preserves trace utility without false-favoring.
|
|
776
|
+
*
|
|
777
|
+
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
778
|
+
* stub's source line is the wiring site shown in the trace trail.
|
|
779
|
+
*/
|
|
780
|
+
function goGrpcStubImplEdges(queries) {
|
|
781
|
+
const edges = [];
|
|
782
|
+
const seen = new Set();
|
|
783
|
+
const STUB_RE = /^Unimplemented.*Server$/;
|
|
784
|
+
// gRPC internal-helper methods that appear on every Unimplemented*Server;
|
|
785
|
+
// not part of the service contract, so exclude when computing the RPC-method
|
|
786
|
+
// signature used to match impls.
|
|
787
|
+
const isInternalMarker = (n) => n.startsWith('mustEmbed') || n === 'testEmbeddedByValue';
|
|
788
|
+
// Methods directly contained by each Go struct, name-only. Built once.
|
|
789
|
+
const methodNamesByStruct = new Map();
|
|
790
|
+
const methodNodesByStruct = new Map();
|
|
791
|
+
const goStructs = [];
|
|
792
|
+
for (const s of queries.getNodesByKind('struct')) {
|
|
793
|
+
if (s.language !== 'go')
|
|
794
|
+
continue;
|
|
795
|
+
goStructs.push(s);
|
|
796
|
+
const ms = queries
|
|
797
|
+
.getOutgoingEdges(s.id, ['contains'])
|
|
798
|
+
.map((e) => queries.getNodeById(e.target))
|
|
799
|
+
.filter((n) => !!n && n.kind === 'method');
|
|
800
|
+
methodNodesByStruct.set(s.id, ms);
|
|
801
|
+
methodNamesByStruct.set(s.id, new Set(ms.map((m) => m.name)));
|
|
802
|
+
}
|
|
803
|
+
for (const stub of goStructs) {
|
|
804
|
+
if (!STUB_RE.test(stub.name))
|
|
805
|
+
continue;
|
|
806
|
+
// The stub MUST live in a generated file — that's what tells us this is
|
|
807
|
+
// a protoc-emitted scaffold rather than someone naming a struct
|
|
808
|
+
// `UnimplementedXxxServer` by hand. Without this gate we'd also bridge
|
|
809
|
+
// such hand-written structs and create misleading edges.
|
|
810
|
+
if (!(0, generated_detection_1.isGeneratedFile)(stub.filePath))
|
|
811
|
+
continue;
|
|
812
|
+
const stubMethods = (methodNodesByStruct.get(stub.id) ?? []).filter((m) => !isInternalMarker(m.name));
|
|
813
|
+
if (stubMethods.length === 0)
|
|
814
|
+
continue;
|
|
815
|
+
const stubMethodNames = stubMethods.map((m) => m.name);
|
|
816
|
+
for (const cand of goStructs) {
|
|
817
|
+
if (cand.id === stub.id)
|
|
818
|
+
continue;
|
|
819
|
+
// Skip generated-file candidates — they're siblings (msgClient,
|
|
820
|
+
// UnsafeMsgServer, …) whose method sets coincidentally match.
|
|
821
|
+
if ((0, generated_detection_1.isGeneratedFile)(cand.filePath))
|
|
822
|
+
continue;
|
|
823
|
+
const candNames = methodNamesByStruct.get(cand.id);
|
|
824
|
+
if (!candNames)
|
|
825
|
+
continue;
|
|
826
|
+
// Subset: every RPC method must exist on the candidate by name.
|
|
827
|
+
// Signature-level match would tighten this further, but name-match
|
|
828
|
+
// alone already gives one-to-one pairing in real codebases because
|
|
829
|
+
// gRPC method-name sets are highly distinctive (Send + MultiSend +
|
|
830
|
+
// UpdateParams + SetSendEnabled is unique to bank's MsgServer).
|
|
831
|
+
if (!stubMethodNames.every((n) => candNames.has(n)))
|
|
832
|
+
continue;
|
|
833
|
+
const candMethods = methodNodesByStruct.get(cand.id) ?? [];
|
|
834
|
+
let added = 0;
|
|
835
|
+
for (const sm of stubMethods) {
|
|
836
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
837
|
+
break;
|
|
838
|
+
for (const cm of candMethods) {
|
|
839
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
840
|
+
break;
|
|
841
|
+
if (cm.name !== sm.name)
|
|
842
|
+
continue;
|
|
843
|
+
const key = `${sm.id}>${cm.id}`;
|
|
844
|
+
if (seen.has(key))
|
|
845
|
+
continue;
|
|
846
|
+
seen.add(key);
|
|
847
|
+
edges.push({
|
|
848
|
+
source: sm.id,
|
|
849
|
+
target: cm.id,
|
|
850
|
+
kind: 'calls',
|
|
851
|
+
line: sm.startLine,
|
|
852
|
+
provenance: 'heuristic',
|
|
853
|
+
metadata: {
|
|
854
|
+
synthesizedBy: 'go-grpc-stub-impl',
|
|
855
|
+
via: cm.name,
|
|
856
|
+
registeredAt: `${cm.filePath}:${cm.startLine}`,
|
|
857
|
+
},
|
|
858
|
+
});
|
|
859
|
+
added++;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return edges;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Phase 5: React JSX child rendering. A component that returns `<Child .../>`
|
|
868
|
+
* mounts Child — React calls it — but JSX instantiation isn't a static call edge,
|
|
869
|
+
* so a render tree (App.render → StaticCanvas → renderStaticScene) breaks at the
|
|
870
|
+
* JSX hop. Link parent → each capitalized JSX child it renders. File-oriented
|
|
871
|
+
* (read each JSX file once). Precision gate: the child name must resolve to a
|
|
872
|
+
* component/function/class node — TS generics like `Array<Foo>` resolve to a type
|
|
873
|
+
* (or nothing) and are dropped.
|
|
874
|
+
*/
|
|
875
|
+
function reactJsxChildEdges(ctx) {
|
|
876
|
+
const edges = [];
|
|
877
|
+
const seen = new Set();
|
|
878
|
+
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
879
|
+
for (const file of ctx.getAllFiles()) {
|
|
880
|
+
const content = ctx.readFile(file);
|
|
881
|
+
if (!content || (!content.includes('</') && !content.includes('/>')))
|
|
882
|
+
continue; // JSX-file gate
|
|
883
|
+
const parents = ctx.getNodesInFile(file).filter((n) => PARENT_KINDS.has(n.kind));
|
|
884
|
+
for (const parent of parents) {
|
|
885
|
+
const src = sliceLines(content, parent.startLine, parent.endLine);
|
|
886
|
+
if (!src || (!src.includes('</') && !src.includes('/>')))
|
|
887
|
+
continue;
|
|
888
|
+
const names = new Set();
|
|
889
|
+
JSX_TAG_RE.lastIndex = 0;
|
|
890
|
+
let m;
|
|
891
|
+
while ((m = JSX_TAG_RE.exec(src)))
|
|
892
|
+
names.add(m[1]);
|
|
893
|
+
let added = 0;
|
|
894
|
+
for (const name of names) {
|
|
895
|
+
if (added >= MAX_JSX_CHILDREN)
|
|
896
|
+
break;
|
|
897
|
+
const child = ctx.getNodesByName(name).find((n) => n.kind === 'component' || n.kind === 'function' || n.kind === 'class');
|
|
898
|
+
if (!child || child.id === parent.id)
|
|
899
|
+
continue;
|
|
900
|
+
const key = `${parent.id}>${child.id}`;
|
|
901
|
+
if (seen.has(key))
|
|
902
|
+
continue;
|
|
903
|
+
seen.add(key);
|
|
904
|
+
edges.push({
|
|
905
|
+
source: parent.id, target: child.id, kind: 'calls', line: parent.startLine,
|
|
906
|
+
provenance: 'heuristic',
|
|
907
|
+
metadata: { synthesizedBy: 'jsx-render', via: name },
|
|
908
|
+
});
|
|
909
|
+
added++;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return edges;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Phase 6: Vue SFC templates. The `.vue` extractor only parses `<script>`, so
|
|
917
|
+
* template usage is invisible — child components and event handlers used ONLY in
|
|
918
|
+
* the template have no edge to them. PascalCase children (`<VPNav/>`) are already
|
|
919
|
+
* caught by reactJsxChildEdges (which scans the SFC component node), so this adds
|
|
920
|
+
* the two Vue-specific shapes:
|
|
921
|
+
* - kebab-case children: `<el-button>` → `ElButton` component (renders).
|
|
922
|
+
* - event bindings: `@click="onClick"` / `v-on:submit="save"` → handler method.
|
|
923
|
+
* Scoped to the `<template>` block of `.vue` files; resolution gate (kebab→
|
|
924
|
+
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
925
|
+
* skipped.
|
|
926
|
+
*/
|
|
927
|
+
function vueTemplateEdges(ctx) {
|
|
928
|
+
const edges = [];
|
|
929
|
+
const seen = new Set();
|
|
930
|
+
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
931
|
+
const HANDLER_KINDS = new Set(['method', 'function']);
|
|
932
|
+
// A composable's returned member may be a fn (`function close(){}`) or an
|
|
933
|
+
// arrow assigned to a const (`const close = () => {}`).
|
|
934
|
+
const RETURN_KINDS = new Set(['method', 'function', 'variable', 'constant']);
|
|
935
|
+
// Nuxt auto-imports nested components by a DIRECTORY-PREFIXED name —
|
|
936
|
+
// `components/media/Card.vue` is used as `<MediaCard/>`, not `<Card/>` — but
|
|
937
|
+
// the component node is named by basename (`Card`), so a direct tag match
|
|
938
|
+
// misses it (flat components match by basename and don't need this). Map each
|
|
939
|
+
// nested component's Nuxt name → node so those template usages resolve.
|
|
940
|
+
const nuxtComponents = new Map();
|
|
941
|
+
for (const c of ctx.getNodesByKind('component')) {
|
|
942
|
+
const nn = nuxtComponentName(c.filePath);
|
|
943
|
+
if (nn && !nuxtComponents.has(nn))
|
|
944
|
+
nuxtComponents.set(nn, c);
|
|
945
|
+
}
|
|
946
|
+
for (const file of ctx.getAllFiles()) {
|
|
947
|
+
if (!file.endsWith('.vue'))
|
|
948
|
+
continue;
|
|
949
|
+
const content = ctx.readFile(file);
|
|
950
|
+
const tpl = content && content.match(/<template[^>]*>([\s\S]*)<\/template>/i)?.[1];
|
|
951
|
+
if (!tpl)
|
|
952
|
+
continue;
|
|
953
|
+
const comp = ctx.getNodesInFile(file).find((n) => n.kind === 'component');
|
|
954
|
+
if (!comp)
|
|
955
|
+
continue;
|
|
956
|
+
// Composable-destructure map: alias → { composable, key }. Lets us resolve a
|
|
957
|
+
// template handler that isn't a local function but a destructured composable
|
|
958
|
+
// return (`@click="closeSidebar"` ← `const { close: closeSidebar } = useSidebarControl()`).
|
|
959
|
+
const script = content.match(/<script[^>]*>([\s\S]*?)<\/script>/i)?.[1] ?? '';
|
|
960
|
+
const destructured = new Map();
|
|
961
|
+
VUE_DESTRUCTURE_RE.lastIndex = 0;
|
|
962
|
+
let dm;
|
|
963
|
+
while ((dm = VUE_DESTRUCTURE_RE.exec(script))) {
|
|
964
|
+
if (!/^use[A-Z]/.test(dm[2]))
|
|
965
|
+
continue; // composables / hooks only
|
|
966
|
+
for (const part of dm[1].split(',')) {
|
|
967
|
+
const pm = part.trim().match(/^(\w+)\s*(?::\s*(\w+))?$/); // key | key: alias
|
|
968
|
+
if (pm)
|
|
969
|
+
destructured.set(pm[2] || pm[1], { composable: dm[2], key: pm[1] });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
let added = 0;
|
|
973
|
+
const addEdge = (target, meta) => {
|
|
974
|
+
if (added >= MAX_JSX_CHILDREN || !target || target.id === comp.id)
|
|
975
|
+
return;
|
|
976
|
+
const k = `${comp.id}>${target.id}>${meta.synthesizedBy}`;
|
|
977
|
+
if (seen.has(k))
|
|
978
|
+
return;
|
|
979
|
+
seen.add(k);
|
|
980
|
+
edges.push({ source: comp.id, target: target.id, kind: 'calls', line: comp.startLine, provenance: 'heuristic', metadata: meta });
|
|
981
|
+
added++;
|
|
982
|
+
};
|
|
983
|
+
// Prefer a target in THIS SFC (handlers live in the same file's script) —
|
|
984
|
+
// avoids cross-file mis-match when a name repeats across a monorepo.
|
|
985
|
+
const resolve = (name, kinds) => {
|
|
986
|
+
const matches = ctx.getNodesByName(name).filter((n) => kinds.has(n.kind));
|
|
987
|
+
return matches.find((n) => n.filePath === file) ?? matches[0];
|
|
988
|
+
};
|
|
989
|
+
let m;
|
|
990
|
+
VUE_KEBAB_RE.lastIndex = 0;
|
|
991
|
+
while ((m = VUE_KEBAB_RE.exec(tpl))) {
|
|
992
|
+
const tag = kebabToPascal(m[1]);
|
|
993
|
+
addEdge(resolve(tag, COMPONENT_KINDS) ?? nuxtComponents.get(tag), { synthesizedBy: 'jsx-render', via: m[1] });
|
|
994
|
+
}
|
|
995
|
+
// PascalCase component tags. Try a direct name match first (flat components
|
|
996
|
+
// and explicit registrations), then the Nuxt dir-prefixed auto-import name
|
|
997
|
+
// (`<MediaCard>` → components/media/Card.vue). Built-ins match neither → no edge.
|
|
998
|
+
VUE_PASCAL_RE.lastIndex = 0;
|
|
999
|
+
while ((m = VUE_PASCAL_RE.exec(tpl))) {
|
|
1000
|
+
const tag = m[1];
|
|
1001
|
+
addEdge(resolve(tag, COMPONENT_KINDS) ?? nuxtComponents.get(tag), { synthesizedBy: 'jsx-render', via: tag });
|
|
1002
|
+
}
|
|
1003
|
+
VUE_HANDLER_RE.lastIndex = 0;
|
|
1004
|
+
while ((m = VUE_HANDLER_RE.exec(tpl))) {
|
|
1005
|
+
const event = m[1];
|
|
1006
|
+
const expr = m[2].trim();
|
|
1007
|
+
if (expr.includes('=>') || expr.startsWith('$'))
|
|
1008
|
+
continue; // inline arrow / $emit
|
|
1009
|
+
const name = expr.match(/^([A-Za-z_]\w*)/)?.[1];
|
|
1010
|
+
if (!name)
|
|
1011
|
+
continue;
|
|
1012
|
+
const direct = resolve(name, HANDLER_KINDS);
|
|
1013
|
+
if (direct) {
|
|
1014
|
+
addEdge(direct, { synthesizedBy: 'vue-handler', event });
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
// Composable-destructure handler → resolve to the composable's returned fn.
|
|
1018
|
+
const d = destructured.get(name);
|
|
1019
|
+
if (!d)
|
|
1020
|
+
continue;
|
|
1021
|
+
const composable = resolve(d.composable, HANDLER_KINDS);
|
|
1022
|
+
// Resolve to the SPECIFIC returned member (e.g. `close`) defined in the
|
|
1023
|
+
// composable's file. No fallback to the composable itself — the component
|
|
1024
|
+
// already has a static `useX()` call edge, so that would just be redundant
|
|
1025
|
+
// and less precise.
|
|
1026
|
+
const keyFn = composable
|
|
1027
|
+
? ctx.getNodesByName(d.key).find((n) => RETURN_KINDS.has(n.kind) && n.filePath === composable.filePath)
|
|
1028
|
+
: undefined;
|
|
1029
|
+
if (keyFn)
|
|
1030
|
+
addEdge(keyFn, { synthesizedBy: 'vue-handler', event, via: d.composable });
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return edges;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* React Native cross-language event channel (Phase 3 of the mixed-iOS/RN
|
|
1037
|
+
* bridging effort). Same shape as `eventEmitterEdges` but cross-language:
|
|
1038
|
+
*
|
|
1039
|
+
* Native (ObjC, on RCTEventEmitter subclass):
|
|
1040
|
+
* [self sendEventWithName:@"locationUpdate" body:@{...}];
|
|
1041
|
+
*
|
|
1042
|
+
* Native (Java/Kotlin, via the JS module dispatcher):
|
|
1043
|
+
* emitter.emit("locationUpdate", body);
|
|
1044
|
+
* reactContext.getJSModule(RCTDeviceEventEmitter.class).emit("locationUpdate", body);
|
|
1045
|
+
*
|
|
1046
|
+
* JS (subscriber):
|
|
1047
|
+
* new NativeEventEmitter(NativeModules.Geo).addListener("locationUpdate", handler);
|
|
1048
|
+
* DeviceEventEmitter.addListener("locationUpdate", handler);
|
|
1049
|
+
*
|
|
1050
|
+
* Synthesize: native dispatch site → JS handler, keyed by the literal
|
|
1051
|
+
* event name. Only matches NAMED handlers (the existing `ON_RE` named-
|
|
1052
|
+
* capture form). Inline arrow handlers like `addListener('x', d => …)`
|
|
1053
|
+
* aren't named at extraction time and would need link-through-body
|
|
1054
|
+
* support; matches the deliberate scope of the in-language synthesizer.
|
|
1055
|
+
*
|
|
1056
|
+
* Provenance `'heuristic'`, synthesizedBy `'rn-event-channel'`.
|
|
1057
|
+
*/
|
|
1058
|
+
// ObjC's `[self sendEventWithName:@"X" body:...]` shape (bracket syntax,
|
|
1059
|
+
// `@` string literals).
|
|
1060
|
+
const RN_OBJC_SEND_RE = /\bsendEventWithName\s*:\s*@"([^"]+)"/g;
|
|
1061
|
+
// Swift's `sendEvent(withName: "X", body: ...)` shape — same RCTEventEmitter
|
|
1062
|
+
// method, different call syntax. Both Objective-C and Swift subclass
|
|
1063
|
+
// RCTEventEmitter so this catches the Swift-side equivalent emission sites
|
|
1064
|
+
// (e.g. RNFusedLocation.swift's `sendEvent(withName: "geolocationDidChange",
|
|
1065
|
+
// body: locationData)`).
|
|
1066
|
+
const RN_SWIFT_SEND_RE = /\bsendEvent\s*\(\s*withName\s*:\s*"([^"]+)"/g;
|
|
1067
|
+
// JVM-side emitter calls: `emitter.emit("X", body)`. Matches both Java
|
|
1068
|
+
// and Kotlin syntax because the call form is identical. Restricted to
|
|
1069
|
+
// JVM source files in the consumer so we don't re-process JS emits
|
|
1070
|
+
// (which `eventEmitterEdges` already handles).
|
|
1071
|
+
const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
1072
|
+
// Custom `sendEvent(reactContext, "X", body)` wrapper — extremely common
|
|
1073
|
+
// (react-native-device-info and many libs wrap `DeviceEventManagerModule…emit`
|
|
1074
|
+
// behind a helper whose `.emit(eventName, …)` uses a VARIABLE, so RN_JVM_EMIT_RE
|
|
1075
|
+
// misses it; the literal lives in the wrapper CALL instead). Captures the first
|
|
1076
|
+
// string literal inside a `sendEvent(...)` call. `[^;{}]*?` keeps it on one
|
|
1077
|
+
// statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
|
|
1078
|
+
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
1079
|
+
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
1080
|
+
function rnEventEdges(ctx) {
|
|
1081
|
+
// Native dispatchers (source = the native method whose body sends the
|
|
1082
|
+
// event) and JS handlers (target = the function/method registered as
|
|
1083
|
+
// the listener) keyed by event name.
|
|
1084
|
+
const nativeDispatchersByEvent = new Map();
|
|
1085
|
+
const jsHandlersByEvent = new Map();
|
|
1086
|
+
for (const file of ctx.getAllFiles()) {
|
|
1087
|
+
const content = ctx.readFile(file);
|
|
1088
|
+
if (!content)
|
|
1089
|
+
continue;
|
|
1090
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
1091
|
+
const lineOf = (idx) => content.slice(0, idx).split('\n').length;
|
|
1092
|
+
const addDispatcher = (event, line) => {
|
|
1093
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
1094
|
+
if (!disp)
|
|
1095
|
+
return;
|
|
1096
|
+
const set = nativeDispatchersByEvent.get(event) ?? new Set();
|
|
1097
|
+
set.add(disp.id);
|
|
1098
|
+
nativeDispatchersByEvent.set(event, set);
|
|
1099
|
+
};
|
|
1100
|
+
// ObjC side: `sendEventWithName:@"X"` only fires inside `.m`/`.mm`
|
|
1101
|
+
// files (RCTEventEmitter subclasses).
|
|
1102
|
+
if (file.endsWith('.m') || file.endsWith('.mm')) {
|
|
1103
|
+
RN_OBJC_SEND_RE.lastIndex = 0;
|
|
1104
|
+
let m;
|
|
1105
|
+
while ((m = RN_OBJC_SEND_RE.exec(content))) {
|
|
1106
|
+
if (m[1])
|
|
1107
|
+
addDispatcher(m[1], lineOf(m.index));
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
// Swift side: same RCTEventEmitter method, parens/named-args syntax.
|
|
1111
|
+
if (file.endsWith('.swift')) {
|
|
1112
|
+
RN_SWIFT_SEND_RE.lastIndex = 0;
|
|
1113
|
+
let m;
|
|
1114
|
+
while ((m = RN_SWIFT_SEND_RE.exec(content))) {
|
|
1115
|
+
if (m[1])
|
|
1116
|
+
addDispatcher(m[1], lineOf(m.index));
|
|
1117
|
+
}
|
|
1118
|
+
RN_NATIVE_SENDEVENT_RE.lastIndex = 0;
|
|
1119
|
+
while ((m = RN_NATIVE_SENDEVENT_RE.exec(content))) {
|
|
1120
|
+
if (m[1])
|
|
1121
|
+
addDispatcher(m[1], lineOf(m.index));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// JVM side: `.emit("X", …)` in Java/Kotlin, plus the common
|
|
1125
|
+
// `sendEvent(ctx, "X", body)` wrapper. (We pattern-match anywhere in the
|
|
1126
|
+
// file; the JS in-language path uses a separate emitter object pattern and
|
|
1127
|
+
// is already handled by eventEmitterEdges.)
|
|
1128
|
+
if (file.endsWith('.java') || file.endsWith('.kt')) {
|
|
1129
|
+
let m;
|
|
1130
|
+
RN_JVM_EMIT_RE.lastIndex = 0;
|
|
1131
|
+
while ((m = RN_JVM_EMIT_RE.exec(content))) {
|
|
1132
|
+
if (m[1])
|
|
1133
|
+
addDispatcher(m[1], lineOf(m.index));
|
|
1134
|
+
}
|
|
1135
|
+
RN_NATIVE_SENDEVENT_RE.lastIndex = 0;
|
|
1136
|
+
while ((m = RN_NATIVE_SENDEVENT_RE.exec(content))) {
|
|
1137
|
+
if (m[1])
|
|
1138
|
+
addDispatcher(m[1], lineOf(m.index));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// JS subscribers (.addListener("X", handler)). Restrict to JS-family
|
|
1142
|
+
// files so a native file's `addListener:` (the ObjC method) doesn't
|
|
1143
|
+
// get mistaken for a JS subscription — they're entirely different
|
|
1144
|
+
// things despite sharing a name.
|
|
1145
|
+
if (file.endsWith('.js') ||
|
|
1146
|
+
file.endsWith('.jsx') ||
|
|
1147
|
+
file.endsWith('.ts') ||
|
|
1148
|
+
file.endsWith('.tsx') ||
|
|
1149
|
+
file.endsWith('.mjs') ||
|
|
1150
|
+
file.endsWith('.cjs')) {
|
|
1151
|
+
// Match BOTH the named-handler form (`.addListener('x', fn)`) and
|
|
1152
|
+
// an unnamed-handler form (`.addListener('x', listener)` where
|
|
1153
|
+
// `listener` is a parameter — common in RN wrapper APIs like
|
|
1154
|
+
// RNFirebase's `messaging().onMessageReceived(listener)`). For the
|
|
1155
|
+
// unnamed case we attribute the subscription to the ENCLOSING JS
|
|
1156
|
+
// function (the abstraction layer), giving a reachability-correct
|
|
1157
|
+
// hop even when the actual user-side handler lives one call up.
|
|
1158
|
+
const ADDLISTENER_ANY = /\.(?:on|once|addListener)\(\s*['"]([^'"]+)['"]\s*,\s*([A-Za-z_][\w.]*)/g;
|
|
1159
|
+
ADDLISTENER_ANY.lastIndex = 0;
|
|
1160
|
+
let m;
|
|
1161
|
+
while ((m = ADDLISTENER_ANY.exec(content))) {
|
|
1162
|
+
const event = m[1];
|
|
1163
|
+
const arg = m[2];
|
|
1164
|
+
if (!event || !arg)
|
|
1165
|
+
continue;
|
|
1166
|
+
const bareName = arg.includes('.') ? arg.slice(arg.lastIndexOf('.') + 1) : arg;
|
|
1167
|
+
// Try a named-symbol match first (matches the in-language semantic).
|
|
1168
|
+
const namedHandler = ctx
|
|
1169
|
+
.getNodesByName(bareName)
|
|
1170
|
+
.find((n) => n.kind === 'function' || n.kind === 'method');
|
|
1171
|
+
let targetId = namedHandler?.id ?? null;
|
|
1172
|
+
if (!targetId) {
|
|
1173
|
+
// Fall back to the enclosing function — the subscribe-wrapper
|
|
1174
|
+
// pattern means the event fires THROUGH this function on its
|
|
1175
|
+
// way to user code. Reachability-correct attribution.
|
|
1176
|
+
const enclosing = enclosingFn(nodesInFile, lineOf(m.index));
|
|
1177
|
+
targetId = enclosing?.id ?? null;
|
|
1178
|
+
}
|
|
1179
|
+
if (!targetId) {
|
|
1180
|
+
// Broader fallback for JS object-literal API shape
|
|
1181
|
+
// (`const Foo = { watchX(...) { … addListener(...) … } }`):
|
|
1182
|
+
// method shorthand inside an object literal isn't extracted
|
|
1183
|
+
// as a method node, so enclosingFn returns null. Attribute to
|
|
1184
|
+
// the smallest enclosing `constant` / `variable` node — that's
|
|
1185
|
+
// the API surface a downstream caller would `import` and
|
|
1186
|
+
// invoke. Reachability-correct.
|
|
1187
|
+
const line = lineOf(m.index);
|
|
1188
|
+
let smallest = null;
|
|
1189
|
+
for (const n of nodesInFile) {
|
|
1190
|
+
if (n.kind !== 'constant' && n.kind !== 'variable')
|
|
1191
|
+
continue;
|
|
1192
|
+
const end = n.endLine ?? n.startLine;
|
|
1193
|
+
if (n.startLine <= line && end >= line) {
|
|
1194
|
+
if (!smallest || n.startLine >= smallest.startLine)
|
|
1195
|
+
smallest = n;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
targetId = smallest?.id ?? null;
|
|
1199
|
+
}
|
|
1200
|
+
if (!targetId)
|
|
1201
|
+
continue;
|
|
1202
|
+
const map = jsHandlersByEvent.get(event) ?? new Map();
|
|
1203
|
+
map.set(targetId, `${file}:${lineOf(m.index)}`);
|
|
1204
|
+
jsHandlersByEvent.set(event, map);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const edges = [];
|
|
1209
|
+
const seen = new Set();
|
|
1210
|
+
for (const [event, dispatchers] of nativeDispatchersByEvent) {
|
|
1211
|
+
const handlers = jsHandlersByEvent.get(event);
|
|
1212
|
+
if (!handlers)
|
|
1213
|
+
continue;
|
|
1214
|
+
// Same fan-out guard as the in-language channel: generic event names
|
|
1215
|
+
// (e.g. 'change', 'error', 'data') with many handlers/dispatchers
|
|
1216
|
+
// can't be matched precisely without receiver-type info.
|
|
1217
|
+
if (dispatchers.size > EVENT_FANOUT_CAP || handlers.size > EVENT_FANOUT_CAP)
|
|
1218
|
+
continue;
|
|
1219
|
+
for (const d of dispatchers) {
|
|
1220
|
+
for (const [h, registeredAt] of handlers) {
|
|
1221
|
+
if (d === h)
|
|
1222
|
+
continue;
|
|
1223
|
+
const key = `${d}>${h}`;
|
|
1224
|
+
if (seen.has(key))
|
|
1225
|
+
continue;
|
|
1226
|
+
seen.add(key);
|
|
1227
|
+
edges.push({
|
|
1228
|
+
source: d,
|
|
1229
|
+
target: h,
|
|
1230
|
+
kind: 'calls',
|
|
1231
|
+
provenance: 'heuristic',
|
|
1232
|
+
metadata: { synthesizedBy: 'rn-event-channel', event, registeredAt },
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return edges;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Phase 6 — React Native Fabric/Codegen view component bridge.
|
|
1241
|
+
*
|
|
1242
|
+
* The Fabric framework extractor (`frameworks/fabric.ts`) emits
|
|
1243
|
+
* `component` nodes named after the JS-visible component (e.g.
|
|
1244
|
+
* `RNSScreenStack`) from each `codegenNativeComponent<Props>('Name')`
|
|
1245
|
+
* spec declaration. The native implementation lives in an ObjC++/.mm or
|
|
1246
|
+
* Kotlin/Java class whose name follows one of RN's conventions:
|
|
1247
|
+
*
|
|
1248
|
+
* - Exact: `RNSScreenStack`
|
|
1249
|
+
* - With suffix: `RNSScreenStackView`, `RNSScreenStackViewManager`,
|
|
1250
|
+
* `RNSScreenStackComponentView`, `RNSScreenStackManager`
|
|
1251
|
+
*
|
|
1252
|
+
* This synthesizer walks every Fabric component node and looks for a
|
|
1253
|
+
* native class matching one of those names; when found, emits a
|
|
1254
|
+
* `calls` edge `component → native class` (provenance `'heuristic'`,
|
|
1255
|
+
* `synthesizedBy:'fabric-native-impl'`) so trace from JSX usage of the
|
|
1256
|
+
* component continues into native.
|
|
1257
|
+
*
|
|
1258
|
+
* The convention-based suffix lookup is precise: there's no name
|
|
1259
|
+
* collision in RN view-manager codebases by design (Codegen output would
|
|
1260
|
+
* conflict otherwise).
|
|
1261
|
+
*/
|
|
1262
|
+
const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Manager'];
|
|
1263
|
+
/**
|
|
1264
|
+
* Expo Modules cross-platform pairing. An Expo Module exposes the SAME
|
|
1265
|
+
* JS-visible method (`AsyncFunction("getBatteryLevelAsync")`) from BOTH an iOS
|
|
1266
|
+
* (Swift) and an Android (Kotlin) implementation. A JS callsite name-resolves to
|
|
1267
|
+
* only ONE of them, so the other platform's impl looked like nothing called it
|
|
1268
|
+
* (and editing it showed no blast radius). Link the iOS and Android impls of the
|
|
1269
|
+
* same `<module>.<method>` to each other (both directions), so a JS call that
|
|
1270
|
+
* reaches one platform reaches the other, and editing either surfaces the JS
|
|
1271
|
+
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
1272
|
+
* `<file>::<module>.<method>` by the framework extractor.
|
|
1273
|
+
*/
|
|
1274
|
+
function expoCrossPlatformEdges(queries) {
|
|
1275
|
+
const edges = [];
|
|
1276
|
+
const seen = new Set();
|
|
1277
|
+
const byKey = new Map();
|
|
1278
|
+
for (const m of queries.getNodesByKind('method')) {
|
|
1279
|
+
if (!m.id.startsWith('expo-module:'))
|
|
1280
|
+
continue;
|
|
1281
|
+
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
1282
|
+
if (!key)
|
|
1283
|
+
continue;
|
|
1284
|
+
const arr = byKey.get(key);
|
|
1285
|
+
if (arr)
|
|
1286
|
+
arr.push(m);
|
|
1287
|
+
else
|
|
1288
|
+
byKey.set(key, [m]);
|
|
1289
|
+
}
|
|
1290
|
+
for (const group of byKey.values()) {
|
|
1291
|
+
if (group.length < 2)
|
|
1292
|
+
continue;
|
|
1293
|
+
for (const a of group) {
|
|
1294
|
+
for (const b of group) {
|
|
1295
|
+
if (a.id === b.id || a.language === b.language)
|
|
1296
|
+
continue; // cross-platform only
|
|
1297
|
+
const key = `${a.id}>${b.id}`;
|
|
1298
|
+
if (seen.has(key))
|
|
1299
|
+
continue;
|
|
1300
|
+
seen.add(key);
|
|
1301
|
+
edges.push({
|
|
1302
|
+
source: a.id,
|
|
1303
|
+
target: b.id,
|
|
1304
|
+
kind: 'calls',
|
|
1305
|
+
line: a.startLine,
|
|
1306
|
+
provenance: 'heuristic',
|
|
1307
|
+
metadata: { synthesizedBy: 'expo-cross-platform', via: a.name },
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return edges;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Classic React Native NativeModules cross-platform pairing. A native module
|
|
1316
|
+
* method (`@ReactMethod` on Android, `RCT_EXPORT_METHOD` on iOS) is implemented
|
|
1317
|
+
* on BOTH platforms, but a JS callsite name-resolves to only ONE — so the other
|
|
1318
|
+
* platform's impl looked like nothing called it. A native method that HAS a JS
|
|
1319
|
+
* caller is a confirmed bridge method; link it to the same-named native method
|
|
1320
|
+
* in another language (the other platform's impl) so a JS call reaching one
|
|
1321
|
+
* platform reaches the other, and editing either surfaces the JS caller.
|
|
1322
|
+
*
|
|
1323
|
+
* Names are normalized to the first selector keyword (`getFreeDiskStorage:` →
|
|
1324
|
+
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
1325
|
+
* lines up with the bare Android method name.
|
|
1326
|
+
*/
|
|
1327
|
+
function rnCrossPlatformEdges(queries) {
|
|
1328
|
+
const edges = [];
|
|
1329
|
+
const seen = new Set();
|
|
1330
|
+
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
1331
|
+
const JS = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
|
|
1332
|
+
// RN module INFRASTRUCTURE methods exist on every native module (called by the
|
|
1333
|
+
// RN runtime, not user JS), so pairing them by name would cross-link unrelated
|
|
1334
|
+
// modules in a multi-module repo. Skip them — they aren't user-facing methods.
|
|
1335
|
+
const RN_INFRA = new Set([
|
|
1336
|
+
'addListener', 'removeListeners', 'getConstants', 'constantsToExport', 'getName',
|
|
1337
|
+
'invalidate', 'initialize', 'getDefaultEventTypes', 'supportedEvents',
|
|
1338
|
+
'requiresMainQueueSetup', 'methodQueue',
|
|
1339
|
+
]);
|
|
1340
|
+
const norm = (name) => {
|
|
1341
|
+
const i = name.indexOf(':');
|
|
1342
|
+
return i >= 0 ? name.slice(0, i) : name;
|
|
1343
|
+
};
|
|
1344
|
+
// Index native methods by their JS-visible (normalized) name. Only names with
|
|
1345
|
+
// impls in ≥2 native languages can pair, so the per-method JS-caller check
|
|
1346
|
+
// below only runs for genuine cross-platform candidates.
|
|
1347
|
+
const byName = new Map();
|
|
1348
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1349
|
+
if (!NATIVE.has(m.language))
|
|
1350
|
+
continue;
|
|
1351
|
+
const key = norm(m.name);
|
|
1352
|
+
const arr = byName.get(key);
|
|
1353
|
+
if (arr)
|
|
1354
|
+
arr.push(m);
|
|
1355
|
+
else
|
|
1356
|
+
byName.set(key, [m]);
|
|
1357
|
+
}
|
|
1358
|
+
for (const [groupName, group] of byName) {
|
|
1359
|
+
if (RN_INFRA.has(groupName))
|
|
1360
|
+
continue;
|
|
1361
|
+
const langs = new Set(group.map((m) => m.language));
|
|
1362
|
+
if (langs.size < 2)
|
|
1363
|
+
continue; // single-platform — nothing to pair
|
|
1364
|
+
for (const m of group) {
|
|
1365
|
+
// Is m a bridge method? (a JS-language `calls` edge points at it)
|
|
1366
|
+
const incoming = queries.getIncomingEdges(m.id, ['calls']);
|
|
1367
|
+
if (incoming.length === 0)
|
|
1368
|
+
continue;
|
|
1369
|
+
const sources = queries.getNodesByIds(incoming.map((e) => e.source));
|
|
1370
|
+
const isBridge = incoming.some((e) => {
|
|
1371
|
+
const s = sources.get(e.source);
|
|
1372
|
+
return !!s && JS.has(s.language);
|
|
1373
|
+
});
|
|
1374
|
+
if (!isBridge)
|
|
1375
|
+
continue;
|
|
1376
|
+
// Link to the other-platform impls (both directions).
|
|
1377
|
+
for (const sib of group) {
|
|
1378
|
+
if (sib.id === m.id || sib.language === m.language)
|
|
1379
|
+
continue;
|
|
1380
|
+
for (const [a, b] of [[m, sib], [sib, m]]) {
|
|
1381
|
+
const key = `${a.id}>${b.id}`;
|
|
1382
|
+
if (seen.has(key))
|
|
1383
|
+
continue;
|
|
1384
|
+
seen.add(key);
|
|
1385
|
+
edges.push({
|
|
1386
|
+
source: a.id,
|
|
1387
|
+
target: b.id,
|
|
1388
|
+
kind: 'calls',
|
|
1389
|
+
line: a.startLine,
|
|
1390
|
+
provenance: 'heuristic',
|
|
1391
|
+
metadata: { synthesizedBy: 'rn-cross-platform', via: norm(m.name) },
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return edges;
|
|
1398
|
+
}
|
|
1399
|
+
function fabricNativeImplEdges(ctx) {
|
|
1400
|
+
const edges = [];
|
|
1401
|
+
const seen = new Set();
|
|
1402
|
+
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
1403
|
+
// filter to just those without iterating all `component` nodes.
|
|
1404
|
+
const components = ctx.getNodesByKind('component').filter((n) => n.id.startsWith('fabric-component:'));
|
|
1405
|
+
if (components.length === 0)
|
|
1406
|
+
return edges;
|
|
1407
|
+
// Pre-index native classes by name for O(1) lookup.
|
|
1408
|
+
const nativeClassesByName = new Map();
|
|
1409
|
+
for (const n of ctx.getNodesByKind('class')) {
|
|
1410
|
+
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
|
|
1411
|
+
continue;
|
|
1412
|
+
const arr = nativeClassesByName.get(n.name);
|
|
1413
|
+
if (arr)
|
|
1414
|
+
arr.push(n);
|
|
1415
|
+
else
|
|
1416
|
+
nativeClassesByName.set(n.name, [n]);
|
|
1417
|
+
}
|
|
1418
|
+
for (const component of components) {
|
|
1419
|
+
for (const suffix of FABRIC_NATIVE_SUFFIXES) {
|
|
1420
|
+
const candidate = component.name + suffix;
|
|
1421
|
+
const matches = nativeClassesByName.get(candidate);
|
|
1422
|
+
if (!matches || matches.length === 0)
|
|
1423
|
+
continue;
|
|
1424
|
+
// Link the component node to every matching native class (iOS +
|
|
1425
|
+
// Android each have one).
|
|
1426
|
+
for (const native of matches) {
|
|
1427
|
+
const key = `${component.id}>${native.id}`;
|
|
1428
|
+
if (seen.has(key))
|
|
1429
|
+
continue;
|
|
1430
|
+
seen.add(key);
|
|
1431
|
+
edges.push({
|
|
1432
|
+
source: component.id,
|
|
1433
|
+
target: native.id,
|
|
1434
|
+
kind: 'calls',
|
|
1435
|
+
provenance: 'heuristic',
|
|
1436
|
+
metadata: {
|
|
1437
|
+
synthesizedBy: 'fabric-native-impl',
|
|
1438
|
+
viaSuffix: suffix || '(exact)',
|
|
1439
|
+
componentName: component.name,
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return edges;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* MyBatis: link a Java mapper interface method to the XML statement that holds
|
|
1449
|
+
* its SQL. The XML extractor (`src/extraction/mybatis-extractor.ts`) qualifies
|
|
1450
|
+
* each `<select|insert|update|delete|sql id="X">` as `<namespace>::<id>` where
|
|
1451
|
+
* `<namespace>` is the Java FQN of the mapper interface. A Java method's
|
|
1452
|
+
* qualifiedName ends with `<ClassName>::<methodName>`, so we suffix-match the
|
|
1453
|
+
* last two segments of the XML qualified name to find a unique Java method by
|
|
1454
|
+
* `<ClassName>::<methodName>` (`ClassName` = last dotted segment of the XML
|
|
1455
|
+
* namespace). Cross-mapper `<include refid="other.X">` references go through
|
|
1456
|
+
* the normal qualified-name resolver — only the Java↔XML bridge is synthetic.
|
|
1457
|
+
*
|
|
1458
|
+
* Precision over recall: ambiguous mappers (multiple Java classes with the
|
|
1459
|
+
* same simple name) are dropped. We need-not bridge by package because Java
|
|
1460
|
+
* mapper interfaces are typically uniquely named within a project.
|
|
1461
|
+
*/
|
|
1462
|
+
function mybatisJavaXmlEdges(queries) {
|
|
1463
|
+
const edges = [];
|
|
1464
|
+
const seen = new Set();
|
|
1465
|
+
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
1466
|
+
const javaIndex = new Map();
|
|
1467
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1468
|
+
if (m.language !== 'java' && m.language !== 'kotlin')
|
|
1469
|
+
continue;
|
|
1470
|
+
const parts = m.qualifiedName.split('::');
|
|
1471
|
+
const last = parts[parts.length - 1];
|
|
1472
|
+
const cls = parts[parts.length - 2];
|
|
1473
|
+
if (!last || !cls)
|
|
1474
|
+
continue;
|
|
1475
|
+
const key = `${cls}::${last}`;
|
|
1476
|
+
const arr = javaIndex.get(key);
|
|
1477
|
+
if (arr)
|
|
1478
|
+
arr.push(m);
|
|
1479
|
+
else
|
|
1480
|
+
javaIndex.set(key, [m]);
|
|
1481
|
+
}
|
|
1482
|
+
for (const xml of queries.iterateNodesByKind('method')) {
|
|
1483
|
+
if (xml.language !== 'xml')
|
|
1484
|
+
continue;
|
|
1485
|
+
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
1486
|
+
const colonIdx = xml.qualifiedName.lastIndexOf('::');
|
|
1487
|
+
if (colonIdx < 0)
|
|
1488
|
+
continue;
|
|
1489
|
+
const namespace = xml.qualifiedName.slice(0, colonIdx);
|
|
1490
|
+
const id = xml.qualifiedName.slice(colonIdx + 2);
|
|
1491
|
+
if (!namespace || !id)
|
|
1492
|
+
continue;
|
|
1493
|
+
const dotIdx = namespace.lastIndexOf('.');
|
|
1494
|
+
const className = dotIdx >= 0 ? namespace.slice(dotIdx + 1) : namespace;
|
|
1495
|
+
const candidates = javaIndex.get(`${className}::${id}`);
|
|
1496
|
+
if (!candidates || candidates.length === 0)
|
|
1497
|
+
continue;
|
|
1498
|
+
// Drop ambiguous matches (multiple same-name classes); the user can
|
|
1499
|
+
// disambiguate by adding the package-suffix match in a future enhancement.
|
|
1500
|
+
if (candidates.length > 1)
|
|
1501
|
+
continue;
|
|
1502
|
+
const java = candidates[0];
|
|
1503
|
+
const key = `${java.id}>${xml.id}`;
|
|
1504
|
+
if (seen.has(key))
|
|
1505
|
+
continue;
|
|
1506
|
+
seen.add(key);
|
|
1507
|
+
edges.push({
|
|
1508
|
+
source: java.id,
|
|
1509
|
+
target: xml.id,
|
|
1510
|
+
kind: 'calls',
|
|
1511
|
+
line: java.startLine,
|
|
1512
|
+
provenance: 'heuristic',
|
|
1513
|
+
metadata: {
|
|
1514
|
+
synthesizedBy: 'mybatis-java-xml',
|
|
1515
|
+
via: `${className}.${id}`,
|
|
1516
|
+
registeredAt: `${xml.filePath}:${xml.startLine}`,
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
return edges;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Gin middleware chain. Gin runs its entire handler chain through one dynamic
|
|
1524
|
+
* line in `(*Context).Next`:
|
|
1525
|
+
* for c.index < len(c.handlers) { c.handlers[c.index](c); c.index++ }
|
|
1526
|
+
* `c.handlers` is a `HandlersChain` (`[]HandlerFunc`) assembled at registration
|
|
1527
|
+
* time by `combineHandlers` from the funcs passed to `r.Use(...)` /
|
|
1528
|
+
* `r.GET("/path", h...)` / `r.Handle(...)`. Because the call is a computed index
|
|
1529
|
+
* into a runtime-built slice, tree-sitter resolves `c.handlers[c.index](c)` to
|
|
1530
|
+
* NOTHING — so `callees(Next)` is just the `len()` helper and the flow
|
|
1531
|
+
* `ServeHTTP → handleHTTPRequest → Next` dead-ends at the exact symbol the
|
|
1532
|
+
* "how do requests flow through the middleware chain" question is about. The
|
|
1533
|
+
* agent then re-queries Next and falls back to Read/grep (validated: the gin
|
|
1534
|
+
* WITH-arm rabbit-holed on precisely this dead-end).
|
|
1535
|
+
*
|
|
1536
|
+
* Bridge it: find the chain DISPATCHER (a Go method whose body invokes a
|
|
1537
|
+
* `handlers` slice by index) and link it → every HandlerFunc registered via a
|
|
1538
|
+
* gin registration call, so `callees(Next)` and `trace(ServeHTTP, <handler>)`
|
|
1539
|
+
* connect end-to-end. Named handlers only (`gin.Logger()` → `Logger`,
|
|
1540
|
+
* `authMiddleware`); inline closures are anonymous and skipped. Like
|
|
1541
|
+
* react-render / interface-impl this is a deliberate over-approximation —
|
|
1542
|
+
* reachability-correct (any registered handler CAN run for some route), capped,
|
|
1543
|
+
* and gated on the dispatcher existing so it never runs on non-gin Go repos.
|
|
1544
|
+
* Provenance `heuristic`, `synthesizedBy:'gin-middleware-chain'`; `registeredAt`
|
|
1545
|
+
* is the `.Use`/`.GET` site an agent would otherwise grep for.
|
|
1546
|
+
*/
|
|
1547
|
+
const GIN_DISPATCH_RE = /\.handlers\s*\[[^\]]*\]\s*\(/; // c.handlers[c.index](c)
|
|
1548
|
+
const GIN_REG_RE = /\.(?:Use|GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\s*\(/g;
|
|
1549
|
+
/** Balanced `(...)` body starting at the '(' index; null if unbalanced. */
|
|
1550
|
+
function goBalancedArgs(s, openIdx) {
|
|
1551
|
+
let depth = 0;
|
|
1552
|
+
for (let i = openIdx; i < s.length; i++) {
|
|
1553
|
+
const c = s[i];
|
|
1554
|
+
if (c === '(')
|
|
1555
|
+
depth++;
|
|
1556
|
+
else if (c === ')') {
|
|
1557
|
+
depth--;
|
|
1558
|
+
if (depth === 0)
|
|
1559
|
+
return s.slice(openIdx + 1, i);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
/** Split a top-level comma list, respecting nested () [] {}. */
|
|
1565
|
+
function goSplitArgs(args) {
|
|
1566
|
+
const out = [];
|
|
1567
|
+
let depth = 0, cur = '';
|
|
1568
|
+
for (const c of args) {
|
|
1569
|
+
if (c === '(' || c === '[' || c === '{') {
|
|
1570
|
+
depth++;
|
|
1571
|
+
cur += c;
|
|
1572
|
+
}
|
|
1573
|
+
else if (c === ')' || c === ']' || c === '}') {
|
|
1574
|
+
depth--;
|
|
1575
|
+
cur += c;
|
|
1576
|
+
}
|
|
1577
|
+
else if (c === ',' && depth === 0) {
|
|
1578
|
+
out.push(cur);
|
|
1579
|
+
cur = '';
|
|
1580
|
+
}
|
|
1581
|
+
else
|
|
1582
|
+
cur += c;
|
|
1583
|
+
}
|
|
1584
|
+
if (cur.trim())
|
|
1585
|
+
out.push(cur);
|
|
1586
|
+
return out;
|
|
1587
|
+
}
|
|
1588
|
+
/** Tail ident of a handler arg: `gin.Logger()`→`Logger`, `mw`→`mw`; null for string paths / closures. */
|
|
1589
|
+
function goHandlerIdent(expr) {
|
|
1590
|
+
const cleaned = expr.trim().replace(/\(\s*\)$/, ''); // drop a trailing call ()
|
|
1591
|
+
if (!cleaned || cleaned.startsWith('"') || cleaned.startsWith('`') || cleaned.startsWith('func'))
|
|
1592
|
+
return null;
|
|
1593
|
+
const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
1594
|
+
return m ? m[1] : null;
|
|
1595
|
+
}
|
|
1596
|
+
function ginMiddlewareChainEdges(queries, ctx) {
|
|
1597
|
+
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
1598
|
+
const dispatchers = [];
|
|
1599
|
+
for (const n of queries.iterateNodesByKind('method')) {
|
|
1600
|
+
if (n.language !== 'go')
|
|
1601
|
+
continue;
|
|
1602
|
+
const content = ctx.readFile(n.filePath);
|
|
1603
|
+
const src = content && sliceLines(content, n.startLine, n.endLine);
|
|
1604
|
+
if (src && GIN_DISPATCH_RE.test(src))
|
|
1605
|
+
dispatchers.push(n);
|
|
1606
|
+
}
|
|
1607
|
+
if (dispatchers.length === 0)
|
|
1608
|
+
return []; // not a gin repo — bail
|
|
1609
|
+
// 2. Collect handler identifiers registered via gin registration calls
|
|
1610
|
+
// (.Use / .GET / … / .Handle). String args (paths/methods) and inline
|
|
1611
|
+
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
1612
|
+
const registered = new Map(); // name → registeredAt (file:line)
|
|
1613
|
+
for (const file of ctx.getAllFiles()) {
|
|
1614
|
+
if (!file.endsWith('.go'))
|
|
1615
|
+
continue;
|
|
1616
|
+
const content = ctx.readFile(file);
|
|
1617
|
+
if (!content || (!content.includes('.Use(') && !/\.(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\(/.test(content)))
|
|
1618
|
+
continue;
|
|
1619
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'go');
|
|
1620
|
+
GIN_REG_RE.lastIndex = 0;
|
|
1621
|
+
let m;
|
|
1622
|
+
while ((m = GIN_REG_RE.exec(safe))) {
|
|
1623
|
+
const parenIdx = m.index + m[0].length - 1;
|
|
1624
|
+
const argStr = goBalancedArgs(safe, parenIdx);
|
|
1625
|
+
if (!argStr)
|
|
1626
|
+
continue;
|
|
1627
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
1628
|
+
for (const arg of goSplitArgs(argStr)) {
|
|
1629
|
+
const name = goHandlerIdent(arg);
|
|
1630
|
+
if (name && !registered.has(name))
|
|
1631
|
+
registered.set(name, `${file}:${line}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
if (registered.size === 0)
|
|
1636
|
+
return [];
|
|
1637
|
+
// 3. Link each dispatcher → each registered handler node (dedup, capped).
|
|
1638
|
+
const edges = [];
|
|
1639
|
+
const seen = new Set();
|
|
1640
|
+
for (const disp of dispatchers) {
|
|
1641
|
+
let added = 0;
|
|
1642
|
+
for (const [name, registeredAt] of registered) {
|
|
1643
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
1644
|
+
break;
|
|
1645
|
+
const handler = ctx.getNodesByName(name).find((n) => (n.kind === 'function' || n.kind === 'method') && n.language === 'go');
|
|
1646
|
+
if (!handler || handler.id === disp.id)
|
|
1647
|
+
continue;
|
|
1648
|
+
const key = `${disp.id}>${handler.id}`;
|
|
1649
|
+
if (seen.has(key))
|
|
1650
|
+
continue;
|
|
1651
|
+
seen.add(key);
|
|
1652
|
+
edges.push({
|
|
1653
|
+
source: disp.id, target: handler.id, kind: 'calls', line: disp.startLine,
|
|
1654
|
+
provenance: 'heuristic',
|
|
1655
|
+
metadata: { synthesizedBy: 'gin-middleware-chain', via: name, registeredAt },
|
|
1656
|
+
});
|
|
1657
|
+
added++;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return edges;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Delphi form code-behind: a form unit `UFRMAbout.pas` owns its visual form
|
|
1664
|
+
* definition `UFRMAbout.dfm` (VCL) / `.fmx` (FireMonkey) — paired by basename in
|
|
1665
|
+
* the same directory, wired by the `{$R *.dfm}` directive rather than a `uses`
|
|
1666
|
+
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
1667
|
+
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
1668
|
+
*/
|
|
1669
|
+
function pascalFormEdges(ctx) {
|
|
1670
|
+
const edges = [];
|
|
1671
|
+
const allFiles = new Set(ctx.getAllFiles());
|
|
1672
|
+
for (const file of allFiles) {
|
|
1673
|
+
if (!/\.(dfm|fmx)$/i.test(file))
|
|
1674
|
+
continue;
|
|
1675
|
+
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
1676
|
+
if (!allFiles.has(pasFile))
|
|
1677
|
+
continue;
|
|
1678
|
+
const formNode = ctx.getNodesInFile(file).find((n) => n.kind === 'file');
|
|
1679
|
+
const unitNode = ctx.getNodesInFile(pasFile).find((n) => n.kind === 'file');
|
|
1680
|
+
if (!formNode || !unitNode)
|
|
1681
|
+
continue;
|
|
1682
|
+
edges.push({
|
|
1683
|
+
source: unitNode.id,
|
|
1684
|
+
target: formNode.id,
|
|
1685
|
+
kind: 'references',
|
|
1686
|
+
line: unitNode.startLine,
|
|
1687
|
+
provenance: 'heuristic',
|
|
1688
|
+
metadata: { synthesizedBy: 'pascal-form', registeredAt: pasFile },
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return edges;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* SvelteKit file-convention data flow. A route directory's `+page.svelte` (a
|
|
1695
|
+
* `component` node) receives its `data` from the sibling `+page.server.{ts,js}`
|
|
1696
|
+
* / `+page.{ts,js}` `load` function and posts forms to its `actions` — wired by
|
|
1697
|
+
* the framework BY FILE PATH, with no static import between them. So editing a
|
|
1698
|
+
* `load` shows no impact on the page it feeds, and the page looks like it has no
|
|
1699
|
+
* server-side dependency. Link the page component to its sibling loader's
|
|
1700
|
+
* `load` / `actions` (same for `+layout`). The pairing is path-deterministic
|
|
1701
|
+
* (same directory, matching `+page`/`+layout` prefix), so it's precise — but
|
|
1702
|
+
* it's a framework-convention edge, so provenance stays `heuristic`.
|
|
1703
|
+
*
|
|
1704
|
+
* Direction: page → load, so `getImpactRadius(load)` surfaces the page (editing
|
|
1705
|
+
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
1706
|
+
* its loader.
|
|
1707
|
+
*/
|
|
1708
|
+
function svelteKitLoadEdges(ctx) {
|
|
1709
|
+
const edges = [];
|
|
1710
|
+
const allFiles = new Set(ctx.getAllFiles());
|
|
1711
|
+
const HOOKS = new Set(['load', 'actions']);
|
|
1712
|
+
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
1713
|
+
for (const file of allFiles) {
|
|
1714
|
+
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
1715
|
+
if (!m)
|
|
1716
|
+
continue;
|
|
1717
|
+
const dir = m[1];
|
|
1718
|
+
const prefix = m[2];
|
|
1719
|
+
const page = ctx.getNodesInFile(file).find((n) => n.kind === 'component');
|
|
1720
|
+
if (!page)
|
|
1721
|
+
continue;
|
|
1722
|
+
for (const ext of ['.server.ts', '.server.js', '.ts', '.js']) {
|
|
1723
|
+
const loaderFile = `${dir}${prefix}${ext}`;
|
|
1724
|
+
if (!allFiles.has(loaderFile))
|
|
1725
|
+
continue;
|
|
1726
|
+
for (const hook of ctx.getNodesInFile(loaderFile)) {
|
|
1727
|
+
if (!HOOK_KINDS.has(hook.kind) || !HOOKS.has(hook.name))
|
|
1728
|
+
continue;
|
|
1729
|
+
edges.push({
|
|
1730
|
+
source: page.id,
|
|
1731
|
+
target: hook.id,
|
|
1732
|
+
kind: 'references',
|
|
1733
|
+
line: page.startLine,
|
|
1734
|
+
provenance: 'heuristic',
|
|
1735
|
+
metadata: {
|
|
1736
|
+
synthesizedBy: 'sveltekit-load',
|
|
1737
|
+
via: hook.name,
|
|
1738
|
+
registeredAt: `${loaderFile}:${hook.startLine ?? 0}`,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return edges;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
|
|
1748
|
+
* (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
|
|
1749
|
+
* tree-sitter never extracts it as a function node: `X` is a `constant` whose body's calls are
|
|
1750
|
+
* ORPHANED. The `dispatch(nextThunk(...))` calls that drive a thunk chain forward therefore produce
|
|
1751
|
+
* no edges, so `callees(X)` is empty and a flow `dispatch(X(...)) → X → nextThunk` dead-ends at the
|
|
1752
|
+
* constant (validated on trezor-suite: the signXxxThunk constants had ZERO outgoing edges). Bridge
|
|
1753
|
+
* it: body-scan each thunk constant for `dispatch(Y(...))` and link `X → Y`, so the dispatch chain
|
|
1754
|
+
* connects. High-precision — the `dispatch(` keyword plus `Y` must resolve to a function/constant/
|
|
1755
|
+
* method node; capped; gated on thunk constants existing so it never runs on non-RTK repos.
|
|
1756
|
+
* Cross-file by design (a suite thunk dispatches a wallet-core thunk). Provenance `heuristic`,
|
|
1757
|
+
* `synthesizedBy:'redux-thunk'`; `registeredAt` is the dispatch site.
|
|
1758
|
+
*/
|
|
1759
|
+
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1760
|
+
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1761
|
+
const THUNK_FANOUT_CAP = 24;
|
|
1762
|
+
function reduxThunkEdges(queries, ctx) {
|
|
1763
|
+
const edges = [];
|
|
1764
|
+
const seen = new Set();
|
|
1765
|
+
for (const node of queries.iterateNodesByKind('constant')) {
|
|
1766
|
+
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1767
|
+
// avoids reading every constant's body on a large repo.
|
|
1768
|
+
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
1769
|
+
continue;
|
|
1770
|
+
const content = ctx.readFile(node.filePath);
|
|
1771
|
+
const src = content && sliceLines(content, node.startLine, node.endLine);
|
|
1772
|
+
if (!src)
|
|
1773
|
+
continue;
|
|
1774
|
+
// Thunks are TS/JS-family (same // and /* */ comment syntax); map to a CommentLang.
|
|
1775
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(src, node.language === 'javascript' || node.language === 'jsx' ? 'javascript' : 'typescript');
|
|
1776
|
+
THUNK_DISPATCH_RE.lastIndex = 0;
|
|
1777
|
+
let m;
|
|
1778
|
+
let added = 0;
|
|
1779
|
+
while ((m = THUNK_DISPATCH_RE.exec(safe)) && added < THUNK_FANOUT_CAP) {
|
|
1780
|
+
const name = m[1];
|
|
1781
|
+
if (name === node.name)
|
|
1782
|
+
continue; // self-dispatch (recursive thunk) — skip
|
|
1783
|
+
// Resolve the dispatched name, PREFERRING the thunk/action-creator over a same-named
|
|
1784
|
+
// service function. `dispatch(X(...))` dispatches a thunk or an action-creator (both
|
|
1785
|
+
// `constant`s) — never an unrelated helper that merely shares the name. On octo-call,
|
|
1786
|
+
// `leaveCall` is BOTH a `createAsyncThunk` const AND a service function, and the bare
|
|
1787
|
+
// `.find()` picked the function (wrong). Order: thunk const > other const > same-file
|
|
1788
|
+
// callable > first match. A single candidate (no collision) is unaffected.
|
|
1789
|
+
const cands = ctx
|
|
1790
|
+
.getNodesByName(name)
|
|
1791
|
+
.filter((n) => n.kind === 'constant' || n.kind === 'function' || n.kind === 'method');
|
|
1792
|
+
const target = cands.find((n) => !!n.signature && THUNK_DECL_RE.test(n.signature)) ??
|
|
1793
|
+
cands.find((n) => n.kind === 'constant') ??
|
|
1794
|
+
cands.find((n) => n.filePath === node.filePath) ??
|
|
1795
|
+
cands[0];
|
|
1796
|
+
if (!target || target.id === node.id)
|
|
1797
|
+
continue;
|
|
1798
|
+
const key = `${node.id}>${target.id}`;
|
|
1799
|
+
if (seen.has(key))
|
|
1800
|
+
continue;
|
|
1801
|
+
seen.add(key);
|
|
1802
|
+
const line = node.startLine + safe.slice(0, m.index).split('\n').length - 1;
|
|
1803
|
+
edges.push({
|
|
1804
|
+
source: node.id,
|
|
1805
|
+
target: target.id,
|
|
1806
|
+
kind: 'calls',
|
|
1807
|
+
line,
|
|
1808
|
+
provenance: 'heuristic',
|
|
1809
|
+
metadata: { synthesizedBy: 'redux-thunk', via: name, registeredAt: `${node.filePath}:${line}` },
|
|
1810
|
+
});
|
|
1811
|
+
added++;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return edges;
|
|
1815
|
+
}
|
|
1816
|
+
// ── Object-literal registry dispatch ─────────────────────────────────────────
|
|
1817
|
+
// A command/handler registry maps string keys → handler class/function symbols in an
|
|
1818
|
+
// object literal, then dispatches by a RUNTIME key static parsing can't follow:
|
|
1819
|
+
// this.commands = { [Cmd.ADD]: AddObjectCommand, ... } // registration
|
|
1820
|
+
// new this.commands[command](args).execute() // dynamic dispatch
|
|
1821
|
+
// Bridge it like gin-middleware-chain: link each dispatching function → each registered
|
|
1822
|
+
// handler's callable entry (a class's execute/run/handle/… method — preferring the method
|
|
1823
|
+
// chained at the dispatch site — or the function value). Scoped to a registry + dispatch in
|
|
1824
|
+
// the SAME file (the cross-file barrel-namespace variant, e.g. trezor's getMethod, is
|
|
1825
|
+
// deferred). Gated on a real object literal with ≥2 entries that RESOLVE to callables (a
|
|
1826
|
+
// `{ width: 5 }` literal resolves to nothing → no edges); fan-out capped.
|
|
1827
|
+
const REGISTRY_ASSIGN_RE = /(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)|((?:this\.)?[A-Za-z_$][\w$]*))\s*=\s*\{/g;
|
|
1828
|
+
const REGISTRY_DISPATCH_RE = /(?:\bnew\s+)?((?:this\.)?[A-Za-z_$][\w$]*)\s*\[\s*([A-Za-z_$][\w$.]*)\s*\]\s*(?:\(|\.[A-Za-z_$])/g;
|
|
1829
|
+
const REGISTRY_MIN_ENTRIES = 2;
|
|
1830
|
+
const REGISTRY_FANOUT_CAP = 40;
|
|
1831
|
+
const REGISTRY_CLASS_ENTRY = new Set(['execute', 'run', 'handle', 'perform', 'process', 'call', 'apply', 'dispatch']);
|
|
1832
|
+
const REGISTRY_JS_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
|
|
1833
|
+
/** From the index of an opening `{`, return the brace-balanced body up to its matching `}`. */
|
|
1834
|
+
function braceBody(src, openIdx) {
|
|
1835
|
+
let depth = 0;
|
|
1836
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
1837
|
+
if (src[i] === '{')
|
|
1838
|
+
depth++;
|
|
1839
|
+
else if (src[i] === '}' && --depth === 0)
|
|
1840
|
+
return src.slice(openIdx + 1, i);
|
|
1841
|
+
}
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
/** Top-level `key: Identifier` entries of an object-literal body. DEPTH-AWARE: only depth-0
|
|
1845
|
+
* segments are considered, so method-shorthand bodies (`number(a,b){…}`), arrow values
|
|
1846
|
+
* (`x: () => …`), and nested objects (`x: { … }`) don't leak their inner `k: v` pairs as
|
|
1847
|
+
* bogus handlers. The per-segment anchor (`^… key: Ident …$`) keeps only pure identifier
|
|
1848
|
+
* values — a data value (`x: 5`), call, or arrow fails to match. */
|
|
1849
|
+
function registryEntryNames(body) {
|
|
1850
|
+
const segs = [];
|
|
1851
|
+
let depth = 0;
|
|
1852
|
+
let start = 0;
|
|
1853
|
+
for (let i = 0; i < body.length; i++) {
|
|
1854
|
+
const c = body[i];
|
|
1855
|
+
if (c === '{' || c === '(' || c === '[')
|
|
1856
|
+
depth++;
|
|
1857
|
+
else if (c === '}' || c === ')' || c === ']')
|
|
1858
|
+
depth--;
|
|
1859
|
+
else if (c === ',' && depth === 0) {
|
|
1860
|
+
segs.push(body.slice(start, i));
|
|
1861
|
+
start = i + 1;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
segs.push(body.slice(start));
|
|
1865
|
+
const names = [];
|
|
1866
|
+
for (const seg of segs) {
|
|
1867
|
+
const m = /^\s*(?:\[[^\]]+\]|['"]?[\w$]+['"]?)\s*:\s*([A-Za-z_$][\w$]*)\s*$/.exec(seg);
|
|
1868
|
+
if (m && m[1].length >= 3 && !names.includes(m[1]))
|
|
1869
|
+
names.push(m[1]);
|
|
1870
|
+
}
|
|
1871
|
+
return names;
|
|
1872
|
+
}
|
|
1873
|
+
/** Resolve a registered handler name to its callable entry: a function value, or a class's
|
|
1874
|
+
* `execute`-like method (preferring the method chained at the dispatch site), else the class. */
|
|
1875
|
+
function resolveRegistryHandler(ctx, name, chained) {
|
|
1876
|
+
const cands = ctx.getNodesByName(name);
|
|
1877
|
+
const fn = cands.find((n) => n.kind === 'function');
|
|
1878
|
+
if (fn)
|
|
1879
|
+
return fn;
|
|
1880
|
+
const cls = cands.find((n) => n.kind === 'class' || n.kind === 'struct');
|
|
1881
|
+
if (cls) {
|
|
1882
|
+
const methods = ctx
|
|
1883
|
+
.getNodesInFile(cls.filePath)
|
|
1884
|
+
.filter((n) => n.kind === 'method' && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine));
|
|
1885
|
+
const want = chained && REGISTRY_CLASS_ENTRY.has(chained) ? chained : null;
|
|
1886
|
+
const entry = (want && methods.find((m) => m.name === want)) ||
|
|
1887
|
+
methods.find((m) => REGISTRY_CLASS_ENTRY.has(m.name)) ||
|
|
1888
|
+
methods.find((m) => m.name === 'constructor');
|
|
1889
|
+
return entry ?? cls;
|
|
1890
|
+
}
|
|
1891
|
+
// Require a CALLABLE target — a registry dispatched as `reg[k](…)` invokes a function/
|
|
1892
|
+
// method, never a data `constant` (dropping it removes false positives like a `{ x: URL }`
|
|
1893
|
+
// entry resolving to the global URL constant).
|
|
1894
|
+
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1895
|
+
}
|
|
1896
|
+
function objectRegistryEdges(ctx) {
|
|
1897
|
+
const edges = [];
|
|
1898
|
+
const seen = new Set();
|
|
1899
|
+
for (const file of ctx.getAllFiles()) {
|
|
1900
|
+
if (!REGISTRY_JS_EXT.test(file))
|
|
1901
|
+
continue;
|
|
1902
|
+
const content = ctx.readFile(file);
|
|
1903
|
+
// Cheap pre-filter: a computed member access BY NAME (`ident[ident`) — the dispatch shape.
|
|
1904
|
+
if (!content || !/[\w$]\s*\[\s*[A-Za-z_$]/.test(content))
|
|
1905
|
+
continue;
|
|
1906
|
+
// Skip minified/generated bundles (draco, three.min, base64…): their pervasive `h[x](...)`
|
|
1907
|
+
// calls + single-letter `{a:b}` literals are a false-positive minefield. Average line
|
|
1908
|
+
// length is the reliable tell — real source ~30–80, minified in the hundreds/thousands.
|
|
1909
|
+
const newlines = (content.match(/\n/g)?.length ?? 0) + 1;
|
|
1910
|
+
if (content.length / newlines > 200)
|
|
1911
|
+
continue;
|
|
1912
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
1913
|
+
// 1. Dispatch sites: `(new )?<ref>[<ident-key>]` followed by a call or a chained method.
|
|
1914
|
+
// A quoted-string key (`['save']`) does NOT match — that's a static access, not dispatch.
|
|
1915
|
+
REGISTRY_DISPATCH_RE.lastIndex = 0;
|
|
1916
|
+
const dispatches = [];
|
|
1917
|
+
let dm;
|
|
1918
|
+
while ((dm = REGISTRY_DISPATCH_RE.exec(safe))) {
|
|
1919
|
+
const win = safe.slice(dm.index, dm.index + 160);
|
|
1920
|
+
const cm = /\]\s*\([^)]*\)\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win) || /\]\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win);
|
|
1921
|
+
dispatches.push({ ref: dm[1], line: safe.slice(0, dm.index).split('\n').length, chained: cm ? cm[1] : null });
|
|
1922
|
+
}
|
|
1923
|
+
if (!dispatches.length)
|
|
1924
|
+
continue;
|
|
1925
|
+
// Normalize a leading `this.` so a class FIELD-INITIALIZER registry (`commands = {…}`)
|
|
1926
|
+
// matches a `this.commands[k]` dispatch, not just the constructor form `this.commands = {…}`.
|
|
1927
|
+
const norm = (r) => r.replace(/^this\./, '');
|
|
1928
|
+
const refs = new Set(dispatches.map((d) => norm(d.ref)));
|
|
1929
|
+
// 2. Registries: an object literal assigned to a dispatched ref, ≥2 entries resolving to callables.
|
|
1930
|
+
REGISTRY_ASSIGN_RE.lastIndex = 0;
|
|
1931
|
+
const registries = new Map();
|
|
1932
|
+
let am;
|
|
1933
|
+
while ((am = REGISTRY_ASSIGN_RE.exec(safe))) {
|
|
1934
|
+
const lhs = norm(am[1] ?? am[2]);
|
|
1935
|
+
if (!refs.has(lhs) || registries.has(lhs))
|
|
1936
|
+
continue;
|
|
1937
|
+
const body = braceBody(safe, am.index + am[0].length - 1);
|
|
1938
|
+
if (!body)
|
|
1939
|
+
continue;
|
|
1940
|
+
const names = registryEntryNames(body); // depth-0 `key: Identifier` entries only
|
|
1941
|
+
if (names.length >= REGISTRY_MIN_ENTRIES) {
|
|
1942
|
+
registries.set(lhs, { names, line: safe.slice(0, am.index).split('\n').length });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (!registries.size)
|
|
1946
|
+
continue;
|
|
1947
|
+
// 3. Link each dispatcher → each registered handler's callable entry.
|
|
1948
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
1949
|
+
for (const d of dispatches) {
|
|
1950
|
+
const reg = registries.get(norm(d.ref));
|
|
1951
|
+
if (!reg)
|
|
1952
|
+
continue;
|
|
1953
|
+
const disp = enclosingFn(nodesInFile, d.line);
|
|
1954
|
+
if (!disp)
|
|
1955
|
+
continue;
|
|
1956
|
+
let added = 0;
|
|
1957
|
+
for (const name of reg.names) {
|
|
1958
|
+
if (added >= REGISTRY_FANOUT_CAP)
|
|
1959
|
+
break;
|
|
1960
|
+
const target = resolveRegistryHandler(ctx, name, d.chained);
|
|
1961
|
+
if (!target || target.id === disp.id)
|
|
1962
|
+
continue;
|
|
1963
|
+
const key = `${disp.id}>${target.id}`;
|
|
1964
|
+
if (seen.has(key))
|
|
1965
|
+
continue;
|
|
1966
|
+
seen.add(key);
|
|
1967
|
+
edges.push({
|
|
1968
|
+
source: disp.id,
|
|
1969
|
+
target: target.id,
|
|
1970
|
+
kind: 'calls',
|
|
1971
|
+
line: d.line,
|
|
1972
|
+
provenance: 'heuristic',
|
|
1973
|
+
metadata: { synthesizedBy: 'object-registry', via: name, registeredAt: `${file}:${reg.line}` },
|
|
1974
|
+
});
|
|
1975
|
+
added++;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return edges;
|
|
1980
|
+
}
|
|
1981
|
+
// ── RTK Query generated-hook → endpoint ──────────────────────────────────────
|
|
1982
|
+
// RTK Query generates one `useGetXQuery`/`useUpdateYMutation` hook per endpoint
|
|
1983
|
+
// (`createApi({ endpoints: b => ({ getX: b.query(...) }) })`). Components call the
|
|
1984
|
+
// hook; the fetch logic lives in the endpoint's queryFn. The hook↔endpoint link is
|
|
1985
|
+
// pure NAMING CONVENTION (no static edge): strip `use` + the optional `Lazy`
|
|
1986
|
+
// variant + the `Query|Mutation` suffix, lowercase the head → the endpoint key.
|
|
1987
|
+
// Both are extracted as function nodes (the hook from its `export const {…}=api`
|
|
1988
|
+
// binding, carrying a sentinel signature; the endpoint from the createApi object),
|
|
1989
|
+
// so bridging hook→endpoint connects `component → useGetXQuery → getX → queryFn`.
|
|
1990
|
+
// Gated on the extraction sentinel so it only ever fires on genuinely-generated
|
|
1991
|
+
// hooks (never a hand-written `useFooQuery`), and on a SAME-FILE endpoint (RTK
|
|
1992
|
+
// colocates the hooks and their api in one module) — 0 on any non-RTK repo.
|
|
1993
|
+
const RTK_HOOK_DERIVE_RE = /^use([A-Z][A-Za-z0-9]*?)(?:Query|Mutation)$/;
|
|
1994
|
+
// MUST match the signature set in tree-sitter.ts `extractRtkHookBindings`.
|
|
1995
|
+
const RTK_GENERATED_HOOK_SIGNATURE = '= RTK Query generated hook';
|
|
1996
|
+
/** Derive the endpoint key from a generated-hook name (`useLazyGetRecordsQuery`
|
|
1997
|
+
* → `getRecords`), or null if it doesn't fit the convention. */
|
|
1998
|
+
function rtkEndpointNameFromHook(hook) {
|
|
1999
|
+
const m = RTK_HOOK_DERIVE_RE.exec(hook);
|
|
2000
|
+
if (!m)
|
|
2001
|
+
return null;
|
|
2002
|
+
let mid = m[1];
|
|
2003
|
+
if (mid.startsWith('Lazy'))
|
|
2004
|
+
mid = mid.slice(4); // useLazyGetX → getX (same endpoint)
|
|
2005
|
+
if (!mid)
|
|
2006
|
+
return null;
|
|
2007
|
+
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2008
|
+
}
|
|
2009
|
+
function rtkQueryEdges(queries, ctx) {
|
|
2010
|
+
const edges = [];
|
|
2011
|
+
const seen = new Set();
|
|
2012
|
+
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2013
|
+
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2014
|
+
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2015
|
+
continue;
|
|
2016
|
+
const endpointName = rtkEndpointNameFromHook(hook.name);
|
|
2017
|
+
if (!endpointName)
|
|
2018
|
+
continue;
|
|
2019
|
+
// The endpoint is a same-file function by the derived name (RTK colocates the
|
|
2020
|
+
// api definition and its generated-hook exports in one module).
|
|
2021
|
+
const target = ctx
|
|
2022
|
+
.getNodesByName(endpointName)
|
|
2023
|
+
.find((n) => n.kind === 'function' && n.filePath === hook.filePath);
|
|
2024
|
+
if (!target || target.id === hook.id)
|
|
2025
|
+
continue;
|
|
2026
|
+
const key = `${hook.id}>${target.id}`;
|
|
2027
|
+
if (seen.has(key))
|
|
2028
|
+
continue;
|
|
2029
|
+
seen.add(key);
|
|
2030
|
+
edges.push({
|
|
2031
|
+
source: hook.id,
|
|
2032
|
+
target: target.id,
|
|
2033
|
+
kind: 'calls',
|
|
2034
|
+
line: hook.startLine,
|
|
2035
|
+
provenance: 'heuristic',
|
|
2036
|
+
metadata: { synthesizedBy: 'rtk-query', via: endpointName, registeredAt: `${hook.filePath}:${hook.startLine}` },
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
return edges;
|
|
2040
|
+
}
|
|
2041
|
+
// ── Pinia useStore().action() dispatch bridge ────────────────────────────────
|
|
2042
|
+
// A Pinia store factory `export const useXStore = defineStore(...)` exposes its
|
|
2043
|
+
// actions as methods on the store instance; a consumer does `const s = useXStore()`
|
|
2044
|
+
// then `s.action()`. The call is a method-on-instance with no static edge to the
|
|
2045
|
+
// action (which lives in the store's module). Bridge it: map each factory → its
|
|
2046
|
+
// file, bind `const <var> = useXStore()` per consumer file, and link the enclosing
|
|
2047
|
+
// function → the `<var>.method()` action node IN THE STORE'S FILE. The same-store-
|
|
2048
|
+
// file gate keeps it precise (a Pinia built-in like `$patch` or an unrelated
|
|
2049
|
+
// same-named method resolves to nothing). Covers both the options and setup store
|
|
2050
|
+
// forms uniformly (the action is a function node in the store file either way).
|
|
2051
|
+
const PINIA_CONSUMER_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs|vue)$/;
|
|
2052
|
+
const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g;
|
|
2053
|
+
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2054
|
+
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2055
|
+
const PINIA_FANOUT_CAP = 80;
|
|
2056
|
+
function piniaStoreEdges(ctx) {
|
|
2057
|
+
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2058
|
+
const factoryFile = new Map();
|
|
2059
|
+
for (const file of ctx.getAllFiles()) {
|
|
2060
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2061
|
+
continue;
|
|
2062
|
+
const content = ctx.readFile(file);
|
|
2063
|
+
if (!content || !content.includes('defineStore'))
|
|
2064
|
+
continue;
|
|
2065
|
+
PINIA_FACTORY_RE.lastIndex = 0;
|
|
2066
|
+
let m;
|
|
2067
|
+
while ((m = PINIA_FACTORY_RE.exec(content)))
|
|
2068
|
+
factoryFile.set(m[1], file);
|
|
2069
|
+
}
|
|
2070
|
+
if (!factoryFile.size)
|
|
2071
|
+
return [];
|
|
2072
|
+
const edges = [];
|
|
2073
|
+
const seen = new Set();
|
|
2074
|
+
for (const file of ctx.getAllFiles()) {
|
|
2075
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2076
|
+
continue;
|
|
2077
|
+
const content = ctx.readFile(file);
|
|
2078
|
+
if (!content || !content.includes('Store'))
|
|
2079
|
+
continue;
|
|
2080
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
2081
|
+
// 2. Bind store vars in this file: `const <var> = <known-factory>(...)`.
|
|
2082
|
+
const varStore = new Map();
|
|
2083
|
+
PINIA_BIND_RE.lastIndex = 0;
|
|
2084
|
+
let bm;
|
|
2085
|
+
while ((bm = PINIA_BIND_RE.exec(safe))) {
|
|
2086
|
+
const sf = factoryFile.get(bm[2]);
|
|
2087
|
+
if (sf)
|
|
2088
|
+
varStore.set(bm[1], sf);
|
|
2089
|
+
}
|
|
2090
|
+
if (!varStore.size)
|
|
2091
|
+
continue;
|
|
2092
|
+
// 3. Link `<var>.<method>(` → the action function node in the store's file.
|
|
2093
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2094
|
+
const fallbackDispatcher = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level setup
|
|
2095
|
+
PINIA_CALL_RE.lastIndex = 0;
|
|
2096
|
+
let cm;
|
|
2097
|
+
let added = 0;
|
|
2098
|
+
while ((cm = PINIA_CALL_RE.exec(safe)) && added < PINIA_FANOUT_CAP) {
|
|
2099
|
+
const storeFile = varStore.get(cm[1]);
|
|
2100
|
+
if (!storeFile)
|
|
2101
|
+
continue;
|
|
2102
|
+
const method = cm[2];
|
|
2103
|
+
const line = safe.slice(0, cm.index).split('\n').length;
|
|
2104
|
+
const disp = enclosingFn(nodesInFile, line) ?? fallbackDispatcher;
|
|
2105
|
+
if (!disp)
|
|
2106
|
+
continue;
|
|
2107
|
+
const target = ctx
|
|
2108
|
+
.getNodesByName(method)
|
|
2109
|
+
.find((n) => n.kind === 'function' && n.filePath === storeFile);
|
|
2110
|
+
if (!target || target.id === disp.id)
|
|
2111
|
+
continue;
|
|
2112
|
+
const key = `${disp.id}>${target.id}`;
|
|
2113
|
+
if (seen.has(key))
|
|
2114
|
+
continue;
|
|
2115
|
+
seen.add(key);
|
|
2116
|
+
edges.push({
|
|
2117
|
+
source: disp.id,
|
|
2118
|
+
target: target.id,
|
|
2119
|
+
kind: 'calls',
|
|
2120
|
+
line,
|
|
2121
|
+
provenance: 'heuristic',
|
|
2122
|
+
metadata: { synthesizedBy: 'pinia-store', via: method, registeredAt: `${file}:${line}` },
|
|
2123
|
+
});
|
|
2124
|
+
added++;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return edges;
|
|
2128
|
+
}
|
|
2129
|
+
// ── Vuex string-keyed dispatch / commit bridge ───────────────────────────────
|
|
2130
|
+
// Vuex dispatches actions/mutations by a runtime STRING key: `dispatch('user/login')`
|
|
2131
|
+
// / `commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')`. The action
|
|
2132
|
+
// & mutation definitions are object-literal methods in store module files (now
|
|
2133
|
+
// extracted as function nodes). Bridge the string key to its node: the LAST `/`
|
|
2134
|
+
// segment is the action/mutation name; the preceding segment is the namespace
|
|
2135
|
+
// (≈ the store module's file). Resolve the name to a function node IN A STORE FILE
|
|
2136
|
+
// (the store-file gate excludes a same-named `api/` helper — `getInfo`/`login`
|
|
2137
|
+
// commonly collide), disambiguated by the namespace appearing in the path (or, for
|
|
2138
|
+
// a root key, the same file — Vuex's local-module `commit('M')` inside an action).
|
|
2139
|
+
const VUEX_DISPATCH_RE = /\b(?:dispatch|commit)\s*\(\s*['"]([A-Za-z][\w/]*)['"]/g;
|
|
2140
|
+
const VUEX_STORE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
|
|
2141
|
+
const VUEX_FANOUT_CAP = 120;
|
|
2142
|
+
/** A path segment (dir or filename stem) equals `seg` — `…/modules/user.js` has
|
|
2143
|
+
* the segment `user` for namespace `user`. */
|
|
2144
|
+
function pathHasSegment(filePath, seg) {
|
|
2145
|
+
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2146
|
+
}
|
|
2147
|
+
function vuexDispatchEdges(ctx) {
|
|
2148
|
+
const storeFileCache = new Map();
|
|
2149
|
+
const isStoreFile = (file) => {
|
|
2150
|
+
let v = storeFileCache.get(file);
|
|
2151
|
+
if (v === undefined) {
|
|
2152
|
+
const c = ctx.readFile(file);
|
|
2153
|
+
const seen = new Set();
|
|
2154
|
+
if (c) {
|
|
2155
|
+
VUEX_STORE_SIGNAL.lastIndex = 0;
|
|
2156
|
+
let sm;
|
|
2157
|
+
while ((sm = VUEX_STORE_SIGNAL.exec(c))) {
|
|
2158
|
+
seen.add(sm[0]);
|
|
2159
|
+
if (seen.size >= 2)
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
v = seen.size >= 2;
|
|
2164
|
+
storeFileCache.set(file, v);
|
|
2165
|
+
}
|
|
2166
|
+
return v;
|
|
2167
|
+
};
|
|
2168
|
+
const resolve = (key, dispatchFile) => {
|
|
2169
|
+
const segs = key.split('/');
|
|
2170
|
+
const action = segs[segs.length - 1];
|
|
2171
|
+
const cands = ctx.getNodesByName(action).filter((n) => n.kind === 'function' && isStoreFile(n.filePath));
|
|
2172
|
+
if (!cands.length)
|
|
2173
|
+
return null;
|
|
2174
|
+
if (segs.length > 1) {
|
|
2175
|
+
const mod = segs[segs.length - 2]; // immediate namespace ≈ the module file
|
|
2176
|
+
return cands.find((c) => pathHasSegment(c.filePath, mod)) ?? (cands.length === 1 ? cands[0] : null);
|
|
2177
|
+
}
|
|
2178
|
+
// Root key: a local `commit('M')` inside an action targets the same module file;
|
|
2179
|
+
// otherwise accept only an unambiguous single store-wide match.
|
|
2180
|
+
return cands.find((c) => c.filePath === dispatchFile) ?? (cands.length === 1 ? cands[0] : null);
|
|
2181
|
+
};
|
|
2182
|
+
const edges = [];
|
|
2183
|
+
const seen = new Set();
|
|
2184
|
+
for (const file of ctx.getAllFiles()) {
|
|
2185
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2186
|
+
continue;
|
|
2187
|
+
const content = ctx.readFile(file);
|
|
2188
|
+
if (!content || (!content.includes('dispatch(') && !content.includes('commit(')))
|
|
2189
|
+
continue;
|
|
2190
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
2191
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2192
|
+
const fallback = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level
|
|
2193
|
+
VUEX_DISPATCH_RE.lastIndex = 0;
|
|
2194
|
+
let m;
|
|
2195
|
+
let added = 0;
|
|
2196
|
+
while ((m = VUEX_DISPATCH_RE.exec(safe)) && added < VUEX_FANOUT_CAP) {
|
|
2197
|
+
const key = m[1];
|
|
2198
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2199
|
+
const disp = enclosingFn(nodesInFile, line) ?? fallback;
|
|
2200
|
+
if (!disp)
|
|
2201
|
+
continue;
|
|
2202
|
+
const target = resolve(key, file);
|
|
2203
|
+
if (!target || target.id === disp.id)
|
|
2204
|
+
continue;
|
|
2205
|
+
const edgeKey = `${disp.id}>${target.id}`;
|
|
2206
|
+
if (seen.has(edgeKey))
|
|
2207
|
+
continue;
|
|
2208
|
+
seen.add(edgeKey);
|
|
2209
|
+
edges.push({
|
|
2210
|
+
source: disp.id,
|
|
2211
|
+
target: target.id,
|
|
2212
|
+
kind: 'calls',
|
|
2213
|
+
line,
|
|
2214
|
+
provenance: 'heuristic',
|
|
2215
|
+
metadata: { synthesizedBy: 'vuex-dispatch', via: key, registeredAt: `${file}:${line}` },
|
|
2216
|
+
});
|
|
2217
|
+
added++;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return edges;
|
|
2221
|
+
}
|
|
2222
|
+
// ── Celery task dispatch (Python) ─────────────────────────────────────────────
|
|
2223
|
+
// Celery decouples a task's call site from its body through async dispatch:
|
|
2224
|
+
// # tasks.py
|
|
2225
|
+
// @shared_task # also @app.task / @celery_app.task / @<app>.task / @task
|
|
2226
|
+
// def process(account_ids): ...
|
|
2227
|
+
// # views.py — a DIFFERENT module
|
|
2228
|
+
// process.apply_async(kwargs={...}) # or process.delay(...) — dynamic, no static edge
|
|
2229
|
+
// Bridge it: link the enclosing function/method at each `.delay(`/`.apply_async(` site → the
|
|
2230
|
+
// task function body. Precision rests on the DECORATOR gate — the dispatched name must resolve
|
|
2231
|
+
// to a Python function carrying a celery task decorator (read from the source lines above its
|
|
2232
|
+
// `def`, since the def's own startLine excludes the decorator). A `.delay()` on a non-task
|
|
2233
|
+
// object resolves to no task node → no edge, so a Celery-free repo yields 0. Same-file /
|
|
2234
|
+
// unique-candidate disambiguation like vuex. (Canvas forms — `group(t).delay()`, `t.s()`/`.si()`
|
|
2235
|
+
// — have no single identifier before `.delay`/`.apply_async`, so they're skipped, not mis-bridged.)
|
|
2236
|
+
const CELERY_DISPATCH_RE = /\b([A-Za-z_]\w*)\s*\.\s*(?:delay|apply_async)\s*\(/g;
|
|
2237
|
+
// A task decorator: bare `@shared_task`/`@task` or attribute `@app.task`/`@celery_app.task`,
|
|
2238
|
+
// each optionally called with args. `\b`-bounded and `@`-anchored so `@mytask`, or a symbol
|
|
2239
|
+
// merely named `task`, can't match. No `/g`, so `.test()` is stateless across reuse.
|
|
2240
|
+
const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)\b/;
|
|
2241
|
+
const CELERY_PY_EXT = /\.py$/;
|
|
2242
|
+
const CELERY_FANOUT_CAP = 80;
|
|
2243
|
+
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2244
|
+
function celeryDispatchEdges(ctx) {
|
|
2245
|
+
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2246
|
+
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2247
|
+
// receivers, so the candidate set stays small.
|
|
2248
|
+
const taskCache = new Map();
|
|
2249
|
+
const isCeleryTask = (node) => {
|
|
2250
|
+
let v = taskCache.get(node.id);
|
|
2251
|
+
if (v !== undefined)
|
|
2252
|
+
return v;
|
|
2253
|
+
v = false;
|
|
2254
|
+
if (node.kind === 'function' && CELERY_PY_EXT.test(node.filePath)) {
|
|
2255
|
+
const content = ctx.readFile(node.filePath);
|
|
2256
|
+
if (content) {
|
|
2257
|
+
const lines = content.split('\n');
|
|
2258
|
+
// startLine is the `def` line (decorators sit ABOVE it). Walk upward, stopping at the
|
|
2259
|
+
// previous declaration so a non-task def can never inherit the prior def's decorator.
|
|
2260
|
+
const stop = Math.max(0, node.startLine - 1 - CELERY_DECORATOR_LOOKBACK);
|
|
2261
|
+
for (let i = node.startLine - 2; i >= stop; i--) {
|
|
2262
|
+
const t = (lines[i] ?? '').trim();
|
|
2263
|
+
if (/^(?:async\s+def|def|class)\b/.test(t))
|
|
2264
|
+
break; // previous decl → stop
|
|
2265
|
+
if (CELERY_TASK_DECORATOR_RE.test(t)) {
|
|
2266
|
+
v = true;
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
taskCache.set(node.id, v);
|
|
2273
|
+
return v;
|
|
2274
|
+
};
|
|
2275
|
+
const resolve = (name, dispatchFile) => {
|
|
2276
|
+
const cands = ctx.getNodesByName(name).filter((n) => n.kind === 'function' && isCeleryTask(n));
|
|
2277
|
+
if (!cands.length)
|
|
2278
|
+
return null;
|
|
2279
|
+
if (cands.length === 1)
|
|
2280
|
+
return cands[0];
|
|
2281
|
+
// Cross-module name collision: prefer a task defined in the dispatching file, else bail
|
|
2282
|
+
// (ambiguous — precision over recall, like vuex's root-key resolution).
|
|
2283
|
+
return cands.find((c) => c.filePath === dispatchFile) ?? null;
|
|
2284
|
+
};
|
|
2285
|
+
const edges = [];
|
|
2286
|
+
const seen = new Set();
|
|
2287
|
+
for (const file of ctx.getAllFiles()) {
|
|
2288
|
+
if (!CELERY_PY_EXT.test(file))
|
|
2289
|
+
continue;
|
|
2290
|
+
const content = ctx.readFile(file);
|
|
2291
|
+
if (!content || (!content.includes('.delay(') && !content.includes('.apply_async(')))
|
|
2292
|
+
continue;
|
|
2293
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'python');
|
|
2294
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2295
|
+
CELERY_DISPATCH_RE.lastIndex = 0;
|
|
2296
|
+
let m;
|
|
2297
|
+
let added = 0;
|
|
2298
|
+
while ((m = CELERY_DISPATCH_RE.exec(safe)) && added < CELERY_FANOUT_CAP) {
|
|
2299
|
+
const name = m[1];
|
|
2300
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2301
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2302
|
+
if (!disp)
|
|
2303
|
+
continue; // module-level dispatch — no source symbol to attribute
|
|
2304
|
+
const target = resolve(name, file);
|
|
2305
|
+
if (!target || target.id === disp.id)
|
|
2306
|
+
continue;
|
|
2307
|
+
const key = `${disp.id}>${target.id}`;
|
|
2308
|
+
if (seen.has(key))
|
|
2309
|
+
continue;
|
|
2310
|
+
seen.add(key);
|
|
2311
|
+
edges.push({
|
|
2312
|
+
source: disp.id,
|
|
2313
|
+
target: target.id,
|
|
2314
|
+
kind: 'calls',
|
|
2315
|
+
line,
|
|
2316
|
+
provenance: 'heuristic',
|
|
2317
|
+
metadata: { synthesizedBy: 'celery-dispatch', via: name, registeredAt: `${file}:${line}` },
|
|
2318
|
+
});
|
|
2319
|
+
added++;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return edges;
|
|
2323
|
+
}
|
|
2324
|
+
// ── Spring application events (Java) ──────────────────────────────────────────
|
|
2325
|
+
// Spring decouples an event PUBLISHER from its LISTENER(s) through the application
|
|
2326
|
+
// event bus, linked by the EVENT TYPE (not a name):
|
|
2327
|
+
// // SomeService.java
|
|
2328
|
+
// eventPublisher.publishEvent(new PasswordChangedEvent(this, username)); // publish
|
|
2329
|
+
// // RememberMeTokenRevoker.java — a DIFFERENT file
|
|
2330
|
+
// @EventListener(PasswordChangedEvent.class) // listen
|
|
2331
|
+
// public void onPasswordChanged(PasswordChangedEvent event) { ... }
|
|
2332
|
+
// Bridge it: link the enclosing method at each `publishEvent(new XEvent(...))` site →
|
|
2333
|
+
// every listener method of XEvent. Listeners are `@EventListener` / `@TransactionalEventListener`
|
|
2334
|
+
// methods (event type = the first param type, or the `@EventListener(X.class)` value form) and
|
|
2335
|
+
// the older `class … implements ApplicationListener<X> { void onApplicationEvent(X e) }`. Keyed
|
|
2336
|
+
// by exact type name, usually cross-file. A repo with no `@EventListener`/`publishEvent` yields 0.
|
|
2337
|
+
// (Java method nodes INCLUDE their leading annotations in the range — startLine is the first
|
|
2338
|
+
// `@…` line — so the annotation block is scanned DOWNWARD from startLine, bounded to consecutive
|
|
2339
|
+
// `@`-lines so it can't bleed into an adjacent method.)
|
|
2340
|
+
const SPRING_PUBLISH_RE = /\.publishEvent\s*\(\s*new\s+([A-Z][A-Za-z0-9_]*)/g;
|
|
2341
|
+
const SPRING_LISTENER_ANNO_RE = /@(?:EventListener|TransactionalEventListener)\b/;
|
|
2342
|
+
const SPRING_ANNO_TYPE_RE = /@(?:EventListener|TransactionalEventListener)\s*\(\s*([A-Z][A-Za-z0-9_]*)\.class/;
|
|
2343
|
+
const SPRING_APP_LISTENER_RE = /\bApplicationListener\s*</;
|
|
2344
|
+
const SPRING_JAVA_EXT = /\.java$/;
|
|
2345
|
+
const SPRING_FANOUT_CAP = 80;
|
|
2346
|
+
/** The first parameter's type from a Java method `signature` (`"void (XEvent e)"` → `XEvent`).
|
|
2347
|
+
* Skips a leading `final`/`@Anno`, strips generics, and requires a PascalCase class name (event
|
|
2348
|
+
* types are classes) — so a no-arg or primitive-param method yields null. */
|
|
2349
|
+
function springFirstParamType(sig) {
|
|
2350
|
+
if (!sig)
|
|
2351
|
+
return null;
|
|
2352
|
+
const open = sig.indexOf('(');
|
|
2353
|
+
if (open < 0)
|
|
2354
|
+
return null;
|
|
2355
|
+
const close = sig.indexOf(')', open);
|
|
2356
|
+
const inner = sig.slice(open + 1, close < 0 ? sig.length : close).trim();
|
|
2357
|
+
if (!inner)
|
|
2358
|
+
return null;
|
|
2359
|
+
const first = inner.split(',')[0].trim();
|
|
2360
|
+
const toks = first.split(/\s+/).filter((t) => t && t !== 'final' && !t.startsWith('@'));
|
|
2361
|
+
if (toks.length < 2)
|
|
2362
|
+
return null; // need `Type name`
|
|
2363
|
+
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2364
|
+
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2365
|
+
}
|
|
2366
|
+
function springEventEdges(ctx) {
|
|
2367
|
+
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2368
|
+
const listeners = new Map();
|
|
2369
|
+
for (const file of ctx.getAllFiles()) {
|
|
2370
|
+
if (!SPRING_JAVA_EXT.test(file))
|
|
2371
|
+
continue;
|
|
2372
|
+
const content = ctx.readFile(file);
|
|
2373
|
+
if (!content)
|
|
2374
|
+
continue;
|
|
2375
|
+
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2376
|
+
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2377
|
+
if (!hasAnno && !hasAppListener)
|
|
2378
|
+
continue;
|
|
2379
|
+
const lines = content.split('\n');
|
|
2380
|
+
for (const node of ctx.getNodesInFile(file)) {
|
|
2381
|
+
if (node.kind !== 'method')
|
|
2382
|
+
continue;
|
|
2383
|
+
// Collect this method's own leading annotation block (consecutive `@`-lines from startLine).
|
|
2384
|
+
const annoLines = [];
|
|
2385
|
+
for (let i = node.startLine - 1; i < lines.length && i < node.startLine + 7; i++) {
|
|
2386
|
+
const t = (lines[i] ?? '').trim();
|
|
2387
|
+
if (!t.startsWith('@'))
|
|
2388
|
+
break; // reached the declaration → stop (no bleed into next method)
|
|
2389
|
+
annoLines.push(t);
|
|
2390
|
+
}
|
|
2391
|
+
const head = annoLines.join('\n');
|
|
2392
|
+
const annotated = hasAnno && SPRING_LISTENER_ANNO_RE.test(head);
|
|
2393
|
+
const isAppListener = hasAppListener && node.name === 'onApplicationEvent';
|
|
2394
|
+
if (!annotated && !isAppListener)
|
|
2395
|
+
continue;
|
|
2396
|
+
let type = springFirstParamType(node.signature);
|
|
2397
|
+
if (!type && annotated) {
|
|
2398
|
+
const m = SPRING_ANNO_TYPE_RE.exec(head);
|
|
2399
|
+
if (m)
|
|
2400
|
+
type = m[1];
|
|
2401
|
+
}
|
|
2402
|
+
if (!type)
|
|
2403
|
+
continue;
|
|
2404
|
+
let arr = listeners.get(type);
|
|
2405
|
+
if (!arr) {
|
|
2406
|
+
arr = [];
|
|
2407
|
+
listeners.set(type, arr);
|
|
2408
|
+
}
|
|
2409
|
+
arr.push(node);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (!listeners.size)
|
|
2413
|
+
return [];
|
|
2414
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
|
|
2415
|
+
const edges = [];
|
|
2416
|
+
const seen = new Set();
|
|
2417
|
+
for (const file of ctx.getAllFiles()) {
|
|
2418
|
+
if (!SPRING_JAVA_EXT.test(file))
|
|
2419
|
+
continue;
|
|
2420
|
+
const content = ctx.readFile(file);
|
|
2421
|
+
if (!content || !content.includes('.publishEvent('))
|
|
2422
|
+
continue;
|
|
2423
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'java');
|
|
2424
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2425
|
+
SPRING_PUBLISH_RE.lastIndex = 0;
|
|
2426
|
+
let m;
|
|
2427
|
+
let added = 0;
|
|
2428
|
+
while ((m = SPRING_PUBLISH_RE.exec(safe)) && added < SPRING_FANOUT_CAP) {
|
|
2429
|
+
const targets = listeners.get(m[1]);
|
|
2430
|
+
if (!targets || !targets.length)
|
|
2431
|
+
continue;
|
|
2432
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2433
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2434
|
+
if (!disp)
|
|
2435
|
+
continue;
|
|
2436
|
+
for (const target of targets) {
|
|
2437
|
+
if (target.id === disp.id)
|
|
2438
|
+
continue;
|
|
2439
|
+
const key = `${disp.id}>${target.id}`;
|
|
2440
|
+
if (seen.has(key))
|
|
2441
|
+
continue;
|
|
2442
|
+
seen.add(key);
|
|
2443
|
+
edges.push({
|
|
2444
|
+
source: disp.id,
|
|
2445
|
+
target: target.id,
|
|
2446
|
+
kind: 'calls',
|
|
2447
|
+
line,
|
|
2448
|
+
provenance: 'heuristic',
|
|
2449
|
+
metadata: { synthesizedBy: 'spring-event', via: m[1], registeredAt: `${file}:${line}` },
|
|
2450
|
+
});
|
|
2451
|
+
added++;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return edges;
|
|
2456
|
+
}
|
|
2457
|
+
// ── MediatR request/notification dispatch (C#/.NET) ───────────────────────────
|
|
2458
|
+
// MediatR decouples a Send/Publish call site from its Handle method through a mediator,
|
|
2459
|
+
// linked by the request/notification TYPE (the IRequestHandler<T,…> generic):
|
|
2460
|
+
// // CancelOrderCommandHandler.cs — the handler
|
|
2461
|
+
// public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool> {
|
|
2462
|
+
// public async Task<bool> Handle(CancelOrderCommand request, CancellationToken ct) { … }
|
|
2463
|
+
// // some controller — the dispatch (usually a DIFFERENT file)
|
|
2464
|
+
// var command = new CancelOrderCommand(orderId); await _mediator.Send(command);
|
|
2465
|
+
// Bridge it: link the enclosing method at each mediator `.Send(x)`/`.Publish(x)` site → the
|
|
2466
|
+
// `Handle` method of the handler for x's type. The sent type is resolved from the argument —
|
|
2467
|
+
// inline `new X(…)`, a local `var v = new X(…)`, or a parameter/local declared `X v` — bounded
|
|
2468
|
+
// to the enclosing method. Precision rests on TWO gates: the receiver must be mediator-ish
|
|
2469
|
+
// (`mediator`/`sender`/`publisher`, so MAUI `MessagingCenter.Send` is ignored) AND the resolved
|
|
2470
|
+
// type must be a known handler request type (so a same-named non-request DTO is never bridged).
|
|
2471
|
+
// C# has no `signature` on method nodes, so the handler's request type is read from the class
|
|
2472
|
+
// base-list source (`: IRequestHandler<X,…>`), not a param signature.
|
|
2473
|
+
const MEDIATR_HANDLER_BASE_RE = /(?:IRequestHandler|INotificationHandler)\s*<\s*([A-Za-z_]\w*)/;
|
|
2474
|
+
const MEDIATR_DISPATCH_RE = /([A-Za-z_][\w.]*)\s*\.\s*(?:Send|Publish)\s*\(\s*(new\s+[A-Z]\w*|[A-Za-z_]\w*)/g;
|
|
2475
|
+
const MEDIATR_RECEIVER_RE = /(?:mediator|sender|publisher)/i;
|
|
2476
|
+
const MEDIATR_CS_EXT = /\.cs$/;
|
|
2477
|
+
const MEDIATR_FANOUT_CAP = 80;
|
|
2478
|
+
const MEDIATR_HANDLER_DECL_LOOKAHEAD = 4; // lines from a class startLine to find a wrapped base list
|
|
2479
|
+
/** The type sent at a MediatR `.Send(arg)`/`.Publish(arg)` site: an inline `new X(…)`, else
|
|
2480
|
+
* `arg` as an identifier resolved within the enclosing method — a `… arg = new X(…)` assignment
|
|
2481
|
+
* (wins), or a parameter/local declared `X arg`. Returns null when the type can't be seen. */
|
|
2482
|
+
function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
2483
|
+
const inl = /^new\s+([A-Z]\w*)/.exec(arg);
|
|
2484
|
+
if (inl)
|
|
2485
|
+
return inl[1];
|
|
2486
|
+
if (!/^[A-Za-z_]\w*$/.test(arg))
|
|
2487
|
+
return null;
|
|
2488
|
+
const assignRe = new RegExp(`\\b${arg}\\b\\s*=\\s*new\\s+([A-Z]\\w*)`);
|
|
2489
|
+
const declRe = new RegExp(`\\b([A-Z]\\w*)\\b\\s+${arg}\\b`);
|
|
2490
|
+
let declType = null;
|
|
2491
|
+
for (let i = Math.max(0, methodStart - 1); i < dispatchLine && i < lines.length; i++) {
|
|
2492
|
+
const ln = lines[i] ?? '';
|
|
2493
|
+
const a = assignRe.exec(ln);
|
|
2494
|
+
if (a)
|
|
2495
|
+
return a[1]; // an explicit `arg = new X` is the most specific — take it
|
|
2496
|
+
if (!declType) {
|
|
2497
|
+
const d = declRe.exec(ln);
|
|
2498
|
+
if (d)
|
|
2499
|
+
declType = d[1]; // a `X arg` declaration — remember, but keep scanning for an assignment
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return declType;
|
|
2503
|
+
}
|
|
2504
|
+
function mediatrDispatchEdges(ctx) {
|
|
2505
|
+
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2506
|
+
const handlers = new Map();
|
|
2507
|
+
for (const file of ctx.getAllFiles()) {
|
|
2508
|
+
if (!MEDIATR_CS_EXT.test(file))
|
|
2509
|
+
continue;
|
|
2510
|
+
const content = ctx.readFile(file);
|
|
2511
|
+
if (!content || (!content.includes('IRequestHandler<') && !content.includes('INotificationHandler<')))
|
|
2512
|
+
continue;
|
|
2513
|
+
const lines = content.split('\n');
|
|
2514
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2515
|
+
for (const cls of nodesInFile) {
|
|
2516
|
+
if (cls.kind !== 'class')
|
|
2517
|
+
continue;
|
|
2518
|
+
const decl = lines.slice(cls.startLine - 1, cls.startLine - 1 + MEDIATR_HANDLER_DECL_LOOKAHEAD).join('\n');
|
|
2519
|
+
const m = MEDIATR_HANDLER_BASE_RE.exec(decl);
|
|
2520
|
+
if (!m)
|
|
2521
|
+
continue;
|
|
2522
|
+
const type = m[1];
|
|
2523
|
+
const end = cls.endLine ?? cls.startLine;
|
|
2524
|
+
const handle = nodesInFile.find((n) => n.kind === 'method' && n.name === 'Handle' && n.startLine >= cls.startLine && n.startLine <= end);
|
|
2525
|
+
if (!handle)
|
|
2526
|
+
continue;
|
|
2527
|
+
let arr = handlers.get(type);
|
|
2528
|
+
if (!arr) {
|
|
2529
|
+
arr = [];
|
|
2530
|
+
handlers.set(type, arr);
|
|
2531
|
+
}
|
|
2532
|
+
arr.push(handle);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (!handlers.size)
|
|
2536
|
+
return [];
|
|
2537
|
+
// Pass 2 — link each mediator-ish .Send(x)/.Publish(x) → the handler of x's type.
|
|
2538
|
+
const edges = [];
|
|
2539
|
+
const seen = new Set();
|
|
2540
|
+
for (const file of ctx.getAllFiles()) {
|
|
2541
|
+
if (!MEDIATR_CS_EXT.test(file))
|
|
2542
|
+
continue;
|
|
2543
|
+
const content = ctx.readFile(file);
|
|
2544
|
+
if (!content || (!content.includes('.Send(') && !content.includes('.Publish(')))
|
|
2545
|
+
continue;
|
|
2546
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'csharp');
|
|
2547
|
+
const safeLines = safe.split('\n');
|
|
2548
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2549
|
+
MEDIATR_DISPATCH_RE.lastIndex = 0;
|
|
2550
|
+
let m;
|
|
2551
|
+
let added = 0;
|
|
2552
|
+
while ((m = MEDIATR_DISPATCH_RE.exec(safe)) && added < MEDIATR_FANOUT_CAP) {
|
|
2553
|
+
if (!MEDIATR_RECEIVER_RE.test(m[1]))
|
|
2554
|
+
continue; // not a mediator (MessagingCenter, HttpClient, …)
|
|
2555
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2556
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2557
|
+
if (!disp)
|
|
2558
|
+
continue;
|
|
2559
|
+
const type = resolveMediatrArgType(m[2], safeLines, disp.startLine, line);
|
|
2560
|
+
if (!type)
|
|
2561
|
+
continue;
|
|
2562
|
+
const targets = handlers.get(type);
|
|
2563
|
+
if (!targets)
|
|
2564
|
+
continue;
|
|
2565
|
+
for (const target of targets) {
|
|
2566
|
+
if (target.id === disp.id)
|
|
2567
|
+
continue;
|
|
2568
|
+
const key = `${disp.id}>${target.id}`;
|
|
2569
|
+
if (seen.has(key))
|
|
2570
|
+
continue;
|
|
2571
|
+
seen.add(key);
|
|
2572
|
+
edges.push({
|
|
2573
|
+
source: disp.id,
|
|
2574
|
+
target: target.id,
|
|
2575
|
+
kind: 'calls',
|
|
2576
|
+
line,
|
|
2577
|
+
provenance: 'heuristic',
|
|
2578
|
+
metadata: { synthesizedBy: 'mediatr-dispatch', via: type, registeredAt: `${file}:${line}` },
|
|
2579
|
+
});
|
|
2580
|
+
added++;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
return edges;
|
|
2585
|
+
}
|
|
2586
|
+
// ── Sidekiq job dispatch (Ruby) ───────────────────────────────────────────────
|
|
2587
|
+
// Sidekiq decouples a job's enqueue site from the worker's `perform`, linked by the WORKER
|
|
2588
|
+
// CLASS NAME:
|
|
2589
|
+
// # app/workers/destroy_user_worker.rb
|
|
2590
|
+
// class DestroyUserWorker
|
|
2591
|
+
// include Sidekiq::Worker # or Sidekiq::Job (the modern alias)
|
|
2592
|
+
// def perform(user_id) … end
|
|
2593
|
+
// # app/services/… — a DIFFERENT file
|
|
2594
|
+
// DestroyUserWorker.perform_async(user.id) # also .perform_in(t, …) / .perform_at(t, …)
|
|
2595
|
+
// Bridge it: link the enclosing method at each `Worker.perform_async/_in/_at(…)` site → that
|
|
2596
|
+
// worker's instance `perform`. Name-keyed (like Celery): the receiver class must be a Sidekiq
|
|
2597
|
+
// worker — gated by reading `include Sidekiq::Job|Worker` from the class body, since that mixin
|
|
2598
|
+
// is an external gem module that forms no resolvable edge. ActiveJob's `perform_later`/`_now` is
|
|
2599
|
+
// a different shape and deliberately not matched, so an ActiveJob-only app yields 0.
|
|
2600
|
+
const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\s*perform_(?:async|in|at)\b/g;
|
|
2601
|
+
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2602
|
+
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2603
|
+
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2604
|
+
function sidekiqDispatchEdges(ctx) {
|
|
2605
|
+
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2606
|
+
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2607
|
+
const performCache = new Map();
|
|
2608
|
+
const performOf = (cls) => {
|
|
2609
|
+
let v = performCache.get(cls.id);
|
|
2610
|
+
if (v !== undefined)
|
|
2611
|
+
return v;
|
|
2612
|
+
v = null;
|
|
2613
|
+
const content = ctx.readFile(cls.filePath);
|
|
2614
|
+
if (content) {
|
|
2615
|
+
const end = cls.endLine ?? cls.startLine;
|
|
2616
|
+
const body = content.split('\n').slice(cls.startLine - 1, end).join('\n');
|
|
2617
|
+
if (SIDEKIQ_WORKER_RE.test(body)) {
|
|
2618
|
+
v = ctx.getNodesInFile(cls.filePath).find((n) => n.kind === 'method' && n.name === 'perform' && n.startLine >= cls.startLine && n.startLine <= end) ?? null;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
performCache.set(cls.id, v);
|
|
2622
|
+
return v;
|
|
2623
|
+
};
|
|
2624
|
+
// Resolve a (possibly namespaced) worker reference to its `perform`. A namespaced ref is
|
|
2625
|
+
// matched by EXACT qualified name first, so same-named workers in different namespaces
|
|
2626
|
+
// (forem has four `SendEmailNotificationWorker`s) resolve to the right one; an unqualified
|
|
2627
|
+
// ref falls back to the simple name and links only when a single worker bears it — an
|
|
2628
|
+
// ambiguous collision bails (precision over recall).
|
|
2629
|
+
const resolve = (ref) => {
|
|
2630
|
+
if (ref.includes('::')) {
|
|
2631
|
+
const q = ctx.getNodesByQualifiedName(ref).find((n) => n.kind === 'class' && performOf(n));
|
|
2632
|
+
if (q)
|
|
2633
|
+
return performOf(q);
|
|
2634
|
+
}
|
|
2635
|
+
const workers = ctx.getNodesByName(ref.split('::').pop()).filter((n) => n.kind === 'class' && performOf(n));
|
|
2636
|
+
return workers.length === 1 ? performOf(workers[0]) : null;
|
|
2637
|
+
};
|
|
2638
|
+
const edges = [];
|
|
2639
|
+
const seen = new Set();
|
|
2640
|
+
for (const file of ctx.getAllFiles()) {
|
|
2641
|
+
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2642
|
+
continue;
|
|
2643
|
+
const content = ctx.readFile(file);
|
|
2644
|
+
if (!content || !/\.perform_(?:async|in|at)\b/.test(content))
|
|
2645
|
+
continue;
|
|
2646
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'ruby');
|
|
2647
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2648
|
+
SIDEKIQ_DISPATCH_RE.lastIndex = 0;
|
|
2649
|
+
let m;
|
|
2650
|
+
let added = 0;
|
|
2651
|
+
while ((m = SIDEKIQ_DISPATCH_RE.exec(safe)) && added < SIDEKIQ_FANOUT_CAP) {
|
|
2652
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2653
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2654
|
+
if (!disp)
|
|
2655
|
+
continue;
|
|
2656
|
+
const target = resolve(m[1]);
|
|
2657
|
+
if (!target || target.id === disp.id)
|
|
2658
|
+
continue;
|
|
2659
|
+
const key = `${disp.id}>${target.id}`;
|
|
2660
|
+
if (seen.has(key))
|
|
2661
|
+
continue;
|
|
2662
|
+
seen.add(key);
|
|
2663
|
+
edges.push({
|
|
2664
|
+
source: disp.id,
|
|
2665
|
+
target: target.id,
|
|
2666
|
+
kind: 'calls',
|
|
2667
|
+
line,
|
|
2668
|
+
provenance: 'heuristic',
|
|
2669
|
+
metadata: { synthesizedBy: 'sidekiq-dispatch', via: m[1], registeredAt: `${file}:${line}` },
|
|
2670
|
+
});
|
|
2671
|
+
added++;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return edges;
|
|
2675
|
+
}
|
|
2676
|
+
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2677
|
+
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2678
|
+
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
2679
|
+
// class UpdateLastfmNowPlaying { public function handle(PlaybackStarted $event) { … } }
|
|
2680
|
+
// // a controller / service — a DIFFERENT file
|
|
2681
|
+
// event(new PlaybackStarted($song, $user));
|
|
2682
|
+
// Bridge it: link the enclosing method at each `event(new XEvent(...))` site → every listener's
|
|
2683
|
+
// `handle` for XEvent. Listeners come from TWO registration mechanisms (both real, both needed):
|
|
2684
|
+
// (A) auto-discovery — a `handle(EventType $e)` typed first param (also splits a union A|B);
|
|
2685
|
+
// (B) the `protected $listen = [ XEvent::class => [Listener::class, …] ]` map in an
|
|
2686
|
+
// EventServiceProvider, which also covers a listener whose `handle()` is UNTYPED.
|
|
2687
|
+
// Only `event(new X)` is matched — queued JOBS dispatch via `::dispatch()` and their `handle()`
|
|
2688
|
+
// takes an injected service, never an event type, so jobs are excluded by construction.
|
|
2689
|
+
const LARAVEL_DISPATCH_RE = /\bevent\s*\(\s*new\s+\\?([A-Za-z_][\w\\]*)/g;
|
|
2690
|
+
const LARAVEL_PHP_EXT = /\.php$/;
|
|
2691
|
+
const LARAVEL_FANOUT_CAP = 200;
|
|
2692
|
+
// A `$listen` entry: `Event::class => [Listener::class, …]`, key/values as `::class` or strings.
|
|
2693
|
+
const LISTEN_ENTRY_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")\s*=>\s*\[([^\]]*)\]/g;
|
|
2694
|
+
const LISTEN_CLASS_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")/g;
|
|
2695
|
+
/** Short class name from a PHP reference: `\App\Events\Foo` / `App\Events::Foo` → `Foo`. */
|
|
2696
|
+
function phpSimpleName(s) {
|
|
2697
|
+
return s.replace(/^\\/, '').split('\\').pop().split('::').pop().trim();
|
|
2698
|
+
}
|
|
2699
|
+
/** The first-parameter class type(s) of a `handle(...)` declaration — union-split, short-named,
|
|
2700
|
+
* primitives dropped. `handle(A|B $e)` → [A, B]; `handle(string $x)` / `handle()` → []. */
|
|
2701
|
+
function laravelHandleEventTypes(decl) {
|
|
2702
|
+
const m = /function\s+handle\s*\(\s*(?:\.\.\.\s*)?(\??[A-Za-z_\\][\w\\|]*)\s+&?\s*(?:\.\.\.\s*)?\$/.exec(decl);
|
|
2703
|
+
if (!m)
|
|
2704
|
+
return [];
|
|
2705
|
+
return m[1]
|
|
2706
|
+
.replace(/^\?/, '')
|
|
2707
|
+
.split('|')
|
|
2708
|
+
.map((t) => phpSimpleName(t))
|
|
2709
|
+
.filter((t) => /^[A-Z]\w*$/.test(t));
|
|
2710
|
+
}
|
|
2711
|
+
/** From an opening `[`, the bracket-balanced body up to its matching `]`. */
|
|
2712
|
+
function phpArrayBody(src, openIdx) {
|
|
2713
|
+
let depth = 0;
|
|
2714
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
2715
|
+
if (src[i] === '[')
|
|
2716
|
+
depth++;
|
|
2717
|
+
else if (src[i] === ']' && --depth === 0)
|
|
2718
|
+
return src.slice(openIdx + 1, i);
|
|
2719
|
+
}
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
function laravelEventEdges(ctx) {
|
|
2723
|
+
// event short name → its listener `handle` methods (deduped by node id).
|
|
2724
|
+
const listeners = new Map();
|
|
2725
|
+
const add = (event, handle) => {
|
|
2726
|
+
let m = listeners.get(event);
|
|
2727
|
+
if (!m) {
|
|
2728
|
+
m = new Map();
|
|
2729
|
+
listeners.set(event, m);
|
|
2730
|
+
}
|
|
2731
|
+
m.set(handle.id, handle);
|
|
2732
|
+
};
|
|
2733
|
+
const handleOf = (cls) => ctx
|
|
2734
|
+
.getNodesInFile(cls.filePath)
|
|
2735
|
+
.find((n) => n.kind === 'method' && n.name === 'handle'
|
|
2736
|
+
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2737
|
+
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2738
|
+
for (const file of ctx.getAllFiles()) {
|
|
2739
|
+
if (!LARAVEL_PHP_EXT.test(file))
|
|
2740
|
+
continue;
|
|
2741
|
+
const content = ctx.readFile(file);
|
|
2742
|
+
if (!content)
|
|
2743
|
+
continue;
|
|
2744
|
+
// (A) typed listener handles — node-driven, so a commented-out method can't leak in.
|
|
2745
|
+
if (content.includes('function handle')) {
|
|
2746
|
+
const lines = content.split('\n');
|
|
2747
|
+
for (const node of ctx.getNodesInFile(file)) {
|
|
2748
|
+
if (node.kind !== 'method' || node.name !== 'handle')
|
|
2749
|
+
continue;
|
|
2750
|
+
const decl = lines.slice(node.startLine - 1, node.startLine + 2).join('\n');
|
|
2751
|
+
for (const ev of laravelHandleEventTypes(decl))
|
|
2752
|
+
add(ev, node);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
// (B) the EventServiceProvider `$listen` map — parsed from comment-stripped source so a
|
|
2756
|
+
// fully-commented map (firefly's, on auto-discovery) contributes nothing.
|
|
2757
|
+
if (content.includes('$listen')) {
|
|
2758
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
|
|
2759
|
+
const decl = safe.search(/\$listen\s*=\s*\[/);
|
|
2760
|
+
const body = decl >= 0 ? phpArrayBody(safe, safe.indexOf('[', decl)) : null;
|
|
2761
|
+
if (body) {
|
|
2762
|
+
LISTEN_ENTRY_RE.lastIndex = 0;
|
|
2763
|
+
let em;
|
|
2764
|
+
while ((em = LISTEN_ENTRY_RE.exec(body))) {
|
|
2765
|
+
const event = phpSimpleName(em[1] ?? em[2] ?? em[3] ?? '');
|
|
2766
|
+
LISTEN_CLASS_RE.lastIndex = 0;
|
|
2767
|
+
let lm;
|
|
2768
|
+
while ((lm = LISTEN_CLASS_RE.exec(em[4]))) {
|
|
2769
|
+
const ln = phpSimpleName(lm[1] ?? lm[2] ?? lm[3] ?? '');
|
|
2770
|
+
const cls = ctx.getNodesByName(ln).find((n) => n.kind === 'class' && handleOf(n));
|
|
2771
|
+
if (cls)
|
|
2772
|
+
add(event, handleOf(cls));
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (!listeners.size)
|
|
2779
|
+
return [];
|
|
2780
|
+
// Pass 2 — link each event(new X(...)) site → every listener of X.
|
|
2781
|
+
const edges = [];
|
|
2782
|
+
const seen = new Set();
|
|
2783
|
+
for (const file of ctx.getAllFiles()) {
|
|
2784
|
+
if (!LARAVEL_PHP_EXT.test(file))
|
|
2785
|
+
continue;
|
|
2786
|
+
const content = ctx.readFile(file);
|
|
2787
|
+
if (!content || !content.includes('event('))
|
|
2788
|
+
continue;
|
|
2789
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
|
|
2790
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2791
|
+
LARAVEL_DISPATCH_RE.lastIndex = 0;
|
|
2792
|
+
let m;
|
|
2793
|
+
let added = 0;
|
|
2794
|
+
while ((m = LARAVEL_DISPATCH_RE.exec(safe)) && added < LARAVEL_FANOUT_CAP) {
|
|
2795
|
+
const targets = listeners.get(phpSimpleName(m[1]));
|
|
2796
|
+
if (!targets)
|
|
2797
|
+
continue;
|
|
2798
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2799
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2800
|
+
if (!disp)
|
|
2801
|
+
continue;
|
|
2802
|
+
for (const target of targets.values()) {
|
|
2803
|
+
if (target.id === disp.id)
|
|
2804
|
+
continue;
|
|
2805
|
+
const key = `${disp.id}>${target.id}`;
|
|
2806
|
+
if (seen.has(key))
|
|
2807
|
+
continue;
|
|
2808
|
+
seen.add(key);
|
|
2809
|
+
edges.push({
|
|
2810
|
+
source: disp.id,
|
|
2811
|
+
target: target.id,
|
|
2812
|
+
kind: 'calls',
|
|
2813
|
+
line,
|
|
2814
|
+
provenance: 'heuristic',
|
|
2815
|
+
metadata: { synthesizedBy: 'laravel-event', via: phpSimpleName(m[1]), registeredAt: `${file}:${line}` },
|
|
2816
|
+
});
|
|
2817
|
+
added++;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return edges;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
2825
|
+
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
|
2826
|
+
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
|
2827
|
+
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
|
2828
|
+
* generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch +
|
|
2829
|
+
* Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener +
|
|
2830
|
+
* MediatR Send/Publish → IRequestHandler/INotificationHandler +
|
|
2831
|
+
* Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
|
|
2832
|
+
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
2833
|
+
*/
|
|
2834
|
+
function synthesizeCallbackEdges(queries, ctx) {
|
|
2835
|
+
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
2836
|
+
// FIRST: a method declared in a different file from its receiver type is
|
|
2837
|
+
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
2838
|
+
// struct's method set from its `contains` edges — so without this it would
|
|
2839
|
+
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
2840
|
+
const goMethodContains = goCrossFileMethodContainsEdges(queries);
|
|
2841
|
+
if (goMethodContains.length > 0)
|
|
2842
|
+
queries.insertEdges(goMethodContains);
|
|
2843
|
+
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
2844
|
+
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
2845
|
+
// Go has none statically. (Other languages already have static implements
|
|
2846
|
+
// edges from extraction, so they don't need this pre-pass.)
|
|
2847
|
+
const goImpl = goImplementsEdges(queries);
|
|
2848
|
+
if (goImpl.length > 0)
|
|
2849
|
+
queries.insertEdges(goImpl);
|
|
2850
|
+
const fieldEdges = fieldChannelEdges(queries, ctx);
|
|
2851
|
+
const closureCollEdges = closureCollectionEdges(queries, ctx);
|
|
2852
|
+
const emitterEdges = eventEmitterEdges(ctx);
|
|
2853
|
+
const renderEdges = reactRenderEdges(queries, ctx);
|
|
2854
|
+
const jsxEdges = reactJsxChildEdges(ctx);
|
|
2855
|
+
const vueEdges = vueTemplateEdges(ctx);
|
|
2856
|
+
const svelteKitEdges = svelteKitLoadEdges(ctx);
|
|
2857
|
+
const pascalEdges = pascalFormEdges(ctx);
|
|
2858
|
+
const flutterEdges = flutterBuildEdges(queries, ctx);
|
|
2859
|
+
const cppEdges = cppOverrideEdges(queries);
|
|
2860
|
+
const ifaceEdges = interfaceOverrideEdges(queries);
|
|
2861
|
+
const kotlinExpectActual = kotlinExpectActualEdges(queries);
|
|
2862
|
+
const goGrpcEdges = goGrpcStubImplEdges(queries);
|
|
2863
|
+
const rnEventEdgesList = rnEventEdges(ctx);
|
|
2864
|
+
const fabricNativeEdges = fabricNativeImplEdges(ctx);
|
|
2865
|
+
const expoXPlatEdges = expoCrossPlatformEdges(queries);
|
|
2866
|
+
const rnXPlatEdges = rnCrossPlatformEdges(queries);
|
|
2867
|
+
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
|
2868
|
+
const ginEdges = ginMiddlewareChainEdges(queries, ctx);
|
|
2869
|
+
const thunkEdges = reduxThunkEdges(queries, ctx);
|
|
2870
|
+
const registryEdges = objectRegistryEdges(ctx);
|
|
2871
|
+
const rtkEdges = rtkQueryEdges(queries, ctx);
|
|
2872
|
+
const piniaEdges = piniaStoreEdges(ctx);
|
|
2873
|
+
const vuexEdges = vuexDispatchEdges(ctx);
|
|
2874
|
+
const celeryEdges = celeryDispatchEdges(ctx);
|
|
2875
|
+
const springEdges = springEventEdges(ctx);
|
|
2876
|
+
const mediatrEdges = mediatrDispatchEdges(ctx);
|
|
2877
|
+
const sidekiqEdges = sidekiqDispatchEdges(ctx);
|
|
2878
|
+
const laravelEdges = laravelEventEdges(ctx);
|
|
2879
|
+
const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
|
|
2880
|
+
const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
|
|
2881
|
+
const merged = [];
|
|
2882
|
+
const seen = new Set();
|
|
2883
|
+
for (const e of [
|
|
2884
|
+
...fieldEdges,
|
|
2885
|
+
...closureCollEdges,
|
|
2886
|
+
...emitterEdges,
|
|
2887
|
+
...renderEdges,
|
|
2888
|
+
...jsxEdges,
|
|
2889
|
+
...vueEdges,
|
|
2890
|
+
...svelteKitEdges,
|
|
2891
|
+
...pascalEdges,
|
|
2892
|
+
...flutterEdges,
|
|
2893
|
+
...cppEdges,
|
|
2894
|
+
...ifaceEdges,
|
|
2895
|
+
...kotlinExpectActual,
|
|
2896
|
+
...goGrpcEdges,
|
|
2897
|
+
...rnEventEdgesList,
|
|
2898
|
+
...fabricNativeEdges,
|
|
2899
|
+
...expoXPlatEdges,
|
|
2900
|
+
...rnXPlatEdges,
|
|
2901
|
+
...mybatisEdges,
|
|
2902
|
+
...ginEdges,
|
|
2903
|
+
...thunkEdges,
|
|
2904
|
+
...registryEdges,
|
|
2905
|
+
...rtkEdges,
|
|
2906
|
+
...piniaEdges,
|
|
2907
|
+
...vuexEdges,
|
|
2908
|
+
...celeryEdges,
|
|
2909
|
+
...springEdges,
|
|
2910
|
+
...mediatrEdges,
|
|
2911
|
+
...sidekiqEdges,
|
|
2912
|
+
...laravelEdges,
|
|
2913
|
+
...cFnPtrEdges,
|
|
2914
|
+
...goframeEdges,
|
|
2915
|
+
]) {
|
|
2916
|
+
const key = `${e.source}>${e.target}`;
|
|
2917
|
+
if (seen.has(key))
|
|
2918
|
+
continue;
|
|
2919
|
+
seen.add(key);
|
|
2920
|
+
merged.push(e);
|
|
2921
|
+
}
|
|
2922
|
+
if (merged.length > 0)
|
|
2923
|
+
queries.insertEdges(merged);
|
|
2924
|
+
return merged.length + goImpl.length + goMethodContains.length;
|
|
2925
|
+
}
|
|
2926
|
+
//# sourceMappingURL=callback-synthesizer.js.map
|