homegraph 0.9.10 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +243 -193
- 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/homegraph.d.ts +6 -6
- package/dist/bin/homegraph.js +532 -109
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/bin/node-version-check.d.ts +4 -4
- package/dist/bin/node-version-check.js +12 -12
- package/dist/bin/uninstall.d.ts +1 -1
- package/dist/bin/uninstall.js +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +8 -1
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +22 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +48 -2
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +26 -17
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +21 -1
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +180 -137
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +152 -151
- package/dist/db/sqlite-adapter.d.ts +1 -1
- package/dist/db/sqlite-adapter.js +3 -3
- package/dist/directory.d.ts +87 -6
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +319 -57
- package/dist/directory.js.map +1 -1
- package/dist/errors.d.ts +15 -15
- package/dist/errors.js +20 -20
- 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/context.d.ts +1 -1
- package/dist/extraction/context.js +1 -1
- package/dist/extraction/extraction-version.d.ts +2 -2
- package/dist/extraction/extraction-version.d.ts.map +1 -1
- package/dist/extraction/extraction-version.js +2 -2
- package/dist/extraction/extraction-version.js.map +1 -1
- 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.map +1 -1
- package/dist/extraction/generated-detection.js +3 -0
- package/dist/extraction/generated-detection.js.map +1 -1
- package/dist/extraction/grammars.d.ts +15 -5
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +44 -10
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +37 -0
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +562 -70
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts-viewtree.d.ts +22 -0
- package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -0
- package/dist/extraction/languages/arkts-viewtree.js +133 -0
- package/dist/extraction/languages/arkts-viewtree.js.map +1 -0
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +211 -20
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +104 -2
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/csharp.d.ts.map +1 -1
- package/dist/extraction/languages/csharp.js +58 -3
- package/dist/extraction/languages/csharp.js.map +1 -1
- package/dist/extraction/languages/dart.d.ts.map +1 -1
- package/dist/extraction/languages/dart.js +183 -1
- package/dist/extraction/languages/dart.js.map +1 -1
- package/dist/extraction/languages/go.d.ts.map +1 -1
- package/dist/extraction/languages/go.js +37 -0
- package/dist/extraction/languages/go.js.map +1 -1
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +2 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/java.d.ts.map +1 -1
- package/dist/extraction/languages/java.js +241 -0
- package/dist/extraction/languages/java.js.map +1 -1
- package/dist/extraction/languages/javascript.d.ts.map +1 -1
- package/dist/extraction/languages/javascript.js +16 -0
- package/dist/extraction/languages/javascript.js.map +1 -1
- package/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/dist/extraction/languages/kotlin.js +94 -0
- package/dist/extraction/languages/kotlin.js.map +1 -1
- package/dist/extraction/languages/objc.d.ts.map +1 -1
- package/dist/extraction/languages/objc.js +42 -0
- package/dist/extraction/languages/objc.js.map +1 -1
- package/dist/extraction/languages/pascal.d.ts.map +1 -1
- package/dist/extraction/languages/pascal.js +11 -0
- package/dist/extraction/languages/pascal.js.map +1 -1
- package/dist/extraction/languages/php.d.ts.map +1 -1
- package/dist/extraction/languages/php.js +77 -1
- package/dist/extraction/languages/php.js.map +1 -1
- 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/rust.d.ts.map +1 -1
- package/dist/extraction/languages/rust.js +29 -0
- package/dist/extraction/languages/rust.js.map +1 -1
- package/dist/extraction/languages/scala.d.ts.map +1 -1
- package/dist/extraction/languages/scala.js +44 -9
- package/dist/extraction/languages/scala.js.map +1 -1
- package/dist/extraction/languages/swift.d.ts.map +1 -1
- package/dist/extraction/languages/swift.js +61 -0
- package/dist/extraction/languages/swift.js.map +1 -1
- package/dist/extraction/languages/typescript.d.ts +13 -0
- package/dist/extraction/languages/typescript.d.ts.map +1 -1
- package/dist/extraction/languages/typescript.js +38 -0
- package/dist/extraction/languages/typescript.js.map +1 -1
- package/dist/extraction/liquid-extractor.d.ts.map +1 -1
- package/dist/extraction/liquid-extractor.js +4 -1
- package/dist/extraction/liquid-extractor.js.map +1 -1
- package/dist/extraction/parse-worker.js +4 -1
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/svelte-extractor.d.ts.map +1 -1
- package/dist/extraction/svelte-extractor.js +6 -3
- package/dist/extraction/svelte-extractor.js.map +1 -1
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +59 -10
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +29 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +192 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +1553 -45
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/vue-extractor.d.ts.map +1 -1
- package/dist/extraction/vue-extractor.js +6 -3
- package/dist/extraction/vue-extractor.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-c_sharp.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/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +11 -2
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +64 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +137 -41
- package/dist/index.js.map +1 -1
- package/dist/installer/config-writer.d.ts +1 -1
- package/dist/installer/config-writer.js +1 -1
- package/dist/installer/index.d.ts +4 -4
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +100 -73
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/instructions-template.d.ts +34 -11
- package/dist/installer/instructions-template.d.ts.map +1 -1
- package/dist/installer/instructions-template.js +44 -12
- package/dist/installer/instructions-template.js.map +1 -1
- package/dist/installer/targets/antigravity.d.ts +6 -6
- package/dist/installer/targets/antigravity.js +14 -14
- package/dist/installer/targets/claude.d.ts +19 -13
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +97 -29
- package/dist/installer/targets/claude.js.map +1 -1
- package/dist/installer/targets/codebuddy.d.ts +17 -0
- package/dist/installer/targets/codebuddy.d.ts.map +1 -0
- package/dist/installer/targets/codebuddy.js +150 -0
- package/dist/installer/targets/codebuddy.js.map +1 -0
- package/dist/installer/targets/codex.js +5 -7
- package/dist/installer/targets/codex.js.map +1 -1
- package/dist/installer/targets/cursor.d.ts +1 -1
- package/dist/installer/targets/cursor.js +7 -7
- package/dist/installer/targets/deveco.d.ts +23 -0
- package/dist/installer/targets/deveco.d.ts.map +1 -0
- package/dist/installer/targets/deveco.js +217 -0
- package/dist/installer/targets/deveco.js.map +1 -0
- package/dist/installer/targets/gemini.js +5 -7
- package/dist/installer/targets/gemini.js.map +1 -1
- package/dist/installer/targets/hermes.js +15 -15
- package/dist/installer/targets/kiro.d.ts +1 -1
- package/dist/installer/targets/kiro.js +4 -4
- package/dist/installer/targets/opencode.d.ts +9 -1
- package/dist/installer/targets/opencode.d.ts.map +1 -1
- package/dist/installer/targets/opencode.js +92 -41
- package/dist/installer/targets/opencode.js.map +1 -1
- package/dist/installer/targets/registry.d.ts.map +1 -1
- package/dist/installer/targets/registry.js +4 -0
- package/dist/installer/targets/registry.js.map +1 -1
- package/dist/installer/targets/shared.d.ts +28 -4
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +32 -15
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/installer/targets/types.d.ts +9 -2
- package/dist/installer/targets/types.d.ts.map +1 -1
- 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 +1 -1
- package/dist/mcp/daemon-paths.js +3 -3
- 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.map +1 -1
- package/dist/mcp/daemon.js +27 -6
- package/dist/mcp/daemon.js.map +1 -1
- 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 +4 -4
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +32 -24
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts +8 -7
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +39 -18
- package/dist/mcp/index.js.map +1 -1
- 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/proxy.d.ts +6 -0
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +75 -18
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +22 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +91 -59
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +3 -1
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +43 -7
- package/dist/mcp/session.js.map +1 -1
- 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 +137 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1271 -219
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +20 -4
- package/dist/mcp/transport.js.map +1 -1
- package/dist/mcp/version.d.ts +1 -1
- package/dist/mcp/version.js +2 -2
- 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 +6 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1197 -1
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/frameworks/arkts-entry.d.ts +12 -0
- package/dist/resolution/frameworks/arkts-entry.d.ts.map +1 -0
- package/dist/resolution/frameworks/arkts-entry.js +214 -0
- package/dist/resolution/frameworks/arkts-entry.js.map +1 -0
- package/dist/resolution/frameworks/arkts-napi.d.ts +3 -0
- package/dist/resolution/frameworks/arkts-napi.d.ts.map +1 -0
- package/dist/resolution/frameworks/arkts-napi.js +166 -0
- package/dist/resolution/frameworks/arkts-napi.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/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 +3 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +17 -1
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.js +6 -1
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/dist/resolution/frameworks/react.js +32 -63
- package/dist/resolution/frameworks/react.js.map +1 -1
- package/dist/resolution/frameworks/svelte.js +6 -2
- package/dist/resolution/frameworks/svelte.js.map +1 -1
- package/dist/resolution/frameworks/vue.js +24 -27
- package/dist/resolution/frameworks/vue.js.map +1 -1
- 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 +10 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +125 -2
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +37 -0
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +306 -3
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +41 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +519 -14
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +2 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +20 -3
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/search/query-utils.d.ts +17 -1
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +79 -10
- package/dist/search/query-utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts +4 -4
- package/dist/sync/git-hooks.js +5 -5
- package/dist/sync/watcher.d.ts +69 -2
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +226 -21
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts +3 -3
- package/dist/sync/worktree.js +5 -5
- 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/types.d.ts +18 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/types.js.map +1 -1
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +46 -10
- package/dist/upgrade/index.js.map +1 -1
- package/dist/utils.d.ts +46 -27
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +87 -53
- package/dist/utils.js.map +1 -1
- package/package.json +57 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- 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 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- 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 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -0
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -67
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +246 -246
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +400 -0
- package/scripts/qa_eval/agent_runner.py +526 -0
- package/scripts/qa_eval/data/.gitignore +4 -0
- package/scripts/qa_eval/data/test-set.jsonl +22 -0
- package/scripts/qa_eval/eval_metrics.py +233 -0
- package/scripts/qa_eval/external_agent.py +577 -0
- package/scripts/qa_eval/llm_config.py +92 -0
- package/scripts/qa_eval/memory_monitor.py +132 -0
- package/scripts/qa_eval/my_answer_accuracy.py +187 -0
- package/scripts/qa_eval/requirements.txt +2 -0
- package/scripts/qa_eval/run_pipeline.py +702 -0
- package/scripts/qa_eval/stats_efficiency.py +279 -0
- package/scripts/qa_eval/stats_scores.py +207 -0
- package/dist/bin/codegraph.d.ts +0 -26
- package/dist/bin/codegraph.d.ts.map +0 -1
- package/dist/bin/codegraph.js +0 -1572
- package/dist/bin/codegraph.js.map +0 -1
- package/dist/extraction/arkanalyzer-extractor.d.ts +0 -6
- package/dist/extraction/arkanalyzer-extractor.d.ts.map +0 -1
- package/dist/extraction/arkanalyzer-extractor.js +0 -11
- package/dist/extraction/arkanalyzer-extractor.js.map +0 -1
- package/dist/extraction/arkanalyzer-project.d.ts +0 -20
- package/dist/extraction/arkanalyzer-project.d.ts.map +0 -1
- package/dist/extraction/arkanalyzer-project.js +0 -596
- package/dist/extraction/arkanalyzer-project.js.map +0 -1
|
@@ -43,10 +43,13 @@ exports.extractFromSource = extractFromSource;
|
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const grammars_1 = require("./grammars");
|
|
45
45
|
const tree_sitter_helpers_1 = require("./tree-sitter-helpers");
|
|
46
|
+
const function_ref_1 = require("./function-ref");
|
|
47
|
+
const generated_detection_1 = require("./generated-detection");
|
|
46
48
|
const languages_1 = require("./languages");
|
|
47
49
|
const liquid_extractor_1 = require("./liquid-extractor");
|
|
48
50
|
const razor_extractor_1 = require("./razor-extractor");
|
|
49
51
|
const svelte_extractor_1 = require("./svelte-extractor");
|
|
52
|
+
const astro_extractor_1 = require("./astro-extractor");
|
|
50
53
|
const dfm_extractor_1 = require("./dfm-extractor");
|
|
51
54
|
const vue_extractor_1 = require("./vue-extractor");
|
|
52
55
|
const mybatis_extractor_1 = require("./mybatis-extractor");
|
|
@@ -55,6 +58,25 @@ const frameworks_1 = require("../resolution/frameworks");
|
|
|
55
58
|
// Re-export for backward compatibility
|
|
56
59
|
var tree_sitter_helpers_2 = require("./tree-sitter-helpers");
|
|
57
60
|
Object.defineProperty(exports, "generateNodeId", { enumerable: true, get: function () { return tree_sitter_helpers_2.generateNodeId; } });
|
|
61
|
+
/**
|
|
62
|
+
* RTK Query generated-hook naming convention: `use` + PascalCase endpoint (with
|
|
63
|
+
* an optional `Lazy` variant prefix) + `Query`/`Mutation`. Matches the hook
|
|
64
|
+
* bindings to extract from an `export const {...} = api` destructuring. Kept in
|
|
65
|
+
* sync with the same convention in `callback-synthesizer.ts` (the synth side).
|
|
66
|
+
*/
|
|
67
|
+
const RTK_HOOK_NAME_RE = /^use[A-Z][A-Za-z0-9]*(?:Query|Mutation)$/;
|
|
68
|
+
/** React HOC callees whose result is itself a component — a PascalCase const
|
|
69
|
+
* initialized with one of these is a component, not a constant (#841). */
|
|
70
|
+
const REACT_COMPONENT_HOCS = new Set(['forwardRef', 'memo', 'React.forwardRef', 'React.memo']);
|
|
71
|
+
/** Vue store collections whose object-literal members are the symbols an agent
|
|
72
|
+
* looks for. Extracted as function nodes so `actions`/`mutations`/`getters` are
|
|
73
|
+
* findable + readable (the foundation under any later dispatch-bridge synth). */
|
|
74
|
+
const VUE_STORE_COLLECTION_NAMES = new Set(['actions', 'mutations', 'getters']);
|
|
75
|
+
/** Store-definition callees whose config object carries those collections. */
|
|
76
|
+
const VUE_STORE_FACTORY_CALLEES = new Set(['defineStore', 'createStore']);
|
|
77
|
+
/** Distinct signals that a file is a Vuex/Pinia store (≥2 ⇒ treat a bare
|
|
78
|
+
* `const actions = {…}` as a store collection — see looksLikeVueStoreFile). */
|
|
79
|
+
const VUE_STORE_FILE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
|
|
58
80
|
/**
|
|
59
81
|
* Extract the name from a node based on language
|
|
60
82
|
*/
|
|
@@ -162,6 +184,77 @@ function scalaBaseTypeName(node, source) {
|
|
|
162
184
|
}
|
|
163
185
|
}
|
|
164
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Resolve the declared identifier inside a C declarator. A `declaration`'s
|
|
189
|
+
* `declarator` field nests the name through `init_declarator` (with value),
|
|
190
|
+
* `pointer_declarator`/`array_declarator`/`parenthesized_declarator`
|
|
191
|
+
* wrappers (each via their own `declarator` field) down to an `identifier`.
|
|
192
|
+
* A `function_declarator` means the declaration is a function prototype (or a
|
|
193
|
+
* function-pointer var) — return null so it isn't extracted as a variable.
|
|
194
|
+
*/
|
|
195
|
+
function cDeclaratorIdentifier(node) {
|
|
196
|
+
let cur = node;
|
|
197
|
+
let guard = 0;
|
|
198
|
+
while (cur && guard++ < 12) {
|
|
199
|
+
switch (cur.type) {
|
|
200
|
+
case 'identifier':
|
|
201
|
+
return cur;
|
|
202
|
+
case 'function_declarator':
|
|
203
|
+
return null;
|
|
204
|
+
case 'init_declarator':
|
|
205
|
+
case 'pointer_declarator':
|
|
206
|
+
case 'array_declarator':
|
|
207
|
+
case 'parenthesized_declarator':
|
|
208
|
+
cur = (0, tree_sitter_helpers_1.getChildByField)(cur, 'declarator');
|
|
209
|
+
break;
|
|
210
|
+
default:
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
/** First `simple_identifier` in `node`'s subtree (breadth-ish, first-found).
|
|
217
|
+
* Swift's property name nests as `property_declaration → <name> pattern →
|
|
218
|
+
* bound_identifier → simple_identifier`; this resolves it (and the bound name of
|
|
219
|
+
* a Kotlin/Swift property declarator for the shadow prune). For a tuple pattern
|
|
220
|
+
* (`let (a, b)`) it returns the first — acceptable, those are rare for consts. */
|
|
221
|
+
function firstSimpleIdentifier(node) {
|
|
222
|
+
const stack = node ? [node] : [];
|
|
223
|
+
let guard = 0;
|
|
224
|
+
while (stack.length > 0 && guard++ < 40) {
|
|
225
|
+
const n = stack.shift();
|
|
226
|
+
if (n.type === 'simple_identifier')
|
|
227
|
+
return n;
|
|
228
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
229
|
+
const c = n.namedChild(i);
|
|
230
|
+
if (c)
|
|
231
|
+
stack.push(c);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
/** Swift property facts: the bound name, whether it's a `let`, and whether it's
|
|
237
|
+
* a *computed* property (a getter block, no stored value — never a constant). */
|
|
238
|
+
function swiftPropertyInfo(node, source) {
|
|
239
|
+
const pattern = (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ??
|
|
240
|
+
node.namedChildren.find((c) => c.type === 'value_binding_pattern' || c.type === 'pattern') ??
|
|
241
|
+
null;
|
|
242
|
+
const binding = node.namedChildren.find((c) => c.type === 'value_binding_pattern');
|
|
243
|
+
const isLet = binding != null && (0, tree_sitter_helpers_1.getNodeText)(binding, source).trimStart().startsWith('let');
|
|
244
|
+
const isComputed = node.namedChildren.some((c) => c.type === 'computed_property' || c.type === 'protocol_property_requirements');
|
|
245
|
+
return { nameNode: firstSimpleIdentifier(pattern), isLet, isComputed };
|
|
246
|
+
}
|
|
247
|
+
/** True when `node` is (transitively) inside a C function body — i.e. a local,
|
|
248
|
+
* not a file/namespace-scope declaration. Walks the parent chain to the root. */
|
|
249
|
+
function hasFunctionAncestor(node) {
|
|
250
|
+
let p = node.parent;
|
|
251
|
+
while (p) {
|
|
252
|
+
if (p.type === 'function_definition')
|
|
253
|
+
return true;
|
|
254
|
+
p = p.parent;
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
165
258
|
/**
|
|
166
259
|
* PHP type-position wrapper node kinds (a type-hint is `named_type`,
|
|
167
260
|
* `?Foo` is `optional_type`, `A|B` is `union_type`, `A&B` is
|
|
@@ -228,15 +321,32 @@ class TreeSitterExtractor {
|
|
|
228
321
|
nodes = [];
|
|
229
322
|
edges = [];
|
|
230
323
|
unresolvedReferences = [];
|
|
324
|
+
// Value-reference edges (default ON; set HOMEGRAPH_VALUE_REFS=0 to disable; see flushValueRefs).
|
|
325
|
+
// Same-file reads of file-scope const/var symbols → `references` edges so impact analysis catches
|
|
326
|
+
// value consumers ("change this constant/table, affect its readers").
|
|
327
|
+
static VALUE_REF_LANGS = new Set(['typescript', 'javascript', 'tsx', 'go', 'python', 'rust', 'ruby', 'c', 'java', 'csharp', 'php', 'scala', 'kotlin', 'swift', 'dart', 'pascal']);
|
|
328
|
+
static MAX_VALUE_REF_NODES = 20_000;
|
|
329
|
+
valueRefsEnabled = process.env.HOMEGRAPH_VALUE_REFS !== '0';
|
|
330
|
+
fileScopeValues = new Map();
|
|
331
|
+
fileScopeValueCounts = new Map(); // file-scope nodes per name (conditional-def detection)
|
|
332
|
+
valueRefScopes = [];
|
|
231
333
|
errors = [];
|
|
232
334
|
extractor = null;
|
|
233
335
|
nodeStack = []; // Stack of parent node IDs
|
|
234
336
|
methodIndex = null; // lookup key → node ID for Pascal defProc lookup
|
|
337
|
+
// Function-as-value capture (#756): per-language spec + candidates collected
|
|
338
|
+
// during the walk, gated & flushed into unresolvedReferences at end-of-file
|
|
339
|
+
// (see flushFnRefCandidates).
|
|
340
|
+
fnRefSpec;
|
|
341
|
+
fnRefCandidates = [];
|
|
342
|
+
// Memoized "is this a Vue store file" verdict (per-extractor = per-file).
|
|
343
|
+
vueStoreFile = null;
|
|
235
344
|
constructor(filePath, source, language) {
|
|
236
345
|
this.filePath = filePath;
|
|
237
346
|
this.source = source;
|
|
238
347
|
this.language = language || (0, grammars_1.detectLanguage)(filePath, source);
|
|
239
348
|
this.extractor = languages_1.EXTRACTORS[this.language] || null;
|
|
349
|
+
this.fnRefSpec = function_ref_1.FN_REF_SPECS[this.language];
|
|
240
350
|
}
|
|
241
351
|
/**
|
|
242
352
|
* Parse and extract from the source code
|
|
@@ -315,6 +425,10 @@ class TreeSitterExtractor {
|
|
|
315
425
|
if (packageNodeId)
|
|
316
426
|
this.nodeStack.push(packageNodeId);
|
|
317
427
|
this.visitNode(this.tree.rootNode);
|
|
428
|
+
// Gate + flush function-as-value candidates (#756) while the file's
|
|
429
|
+
// nodes and import refs are complete and the file node is still pushed.
|
|
430
|
+
this.flushFnRefCandidates();
|
|
431
|
+
this.flushValueRefs();
|
|
318
432
|
if (packageNodeId)
|
|
319
433
|
this.nodeStack.pop();
|
|
320
434
|
this.nodeStack.pop();
|
|
@@ -352,6 +466,364 @@ class TreeSitterExtractor {
|
|
|
352
466
|
durationMs: Date.now() - startTime,
|
|
353
467
|
};
|
|
354
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Function-as-value capture (#756): if this node is one of the language's
|
|
471
|
+
* value-position containers (call arguments, assignment RHS, struct/object
|
|
472
|
+
* initializer, array/table literal), collect candidate function names from
|
|
473
|
+
* it. Candidates are gated & flushed at end-of-file (flushFnRefCandidates).
|
|
474
|
+
*/
|
|
475
|
+
maybeCaptureFnRefs(node, nodeType) {
|
|
476
|
+
const spec = this.fnRefSpec;
|
|
477
|
+
if (!spec)
|
|
478
|
+
return;
|
|
479
|
+
const rule = spec.dispatch.get(nodeType);
|
|
480
|
+
if (!rule || this.nodeStack.length === 0)
|
|
481
|
+
return;
|
|
482
|
+
const fromNodeId = this.nodeStack[this.nodeStack.length - 1];
|
|
483
|
+
if (!fromNodeId)
|
|
484
|
+
return;
|
|
485
|
+
for (const cand of (0, function_ref_1.captureFnRefCandidates)(node, rule, spec, this.source)) {
|
|
486
|
+
this.fnRefCandidates.push({ ...cand, fromNodeId });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Candidates-only scan of a subtree the main walkers won't traverse
|
|
491
|
+
* (top-level variable initializers). No extraction side effects. Halts at
|
|
492
|
+
* nested function definitions: their bodies are walked — and their
|
|
493
|
+
* candidates attributed — by extractFunction's own body walk.
|
|
494
|
+
*/
|
|
495
|
+
scanFnRefSubtree(node, depth) {
|
|
496
|
+
if (!this.fnRefSpec || depth > 12)
|
|
497
|
+
return;
|
|
498
|
+
const nodeType = node.type;
|
|
499
|
+
if (depth > 0 && (this.extractor?.functionTypes.includes(nodeType) ||
|
|
500
|
+
nodeType === 'arrow_function' ||
|
|
501
|
+
nodeType === 'function_expression' ||
|
|
502
|
+
nodeType === 'lambda_literal' ||
|
|
503
|
+
nodeType === 'lambda_expression')) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
507
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
508
|
+
const child = node.namedChild(i);
|
|
509
|
+
if (child)
|
|
510
|
+
this.scanFnRefSubtree(child, depth + 1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Gate captured function-as-value candidates and push survivors as
|
|
515
|
+
* `function_ref` unresolved references.
|
|
516
|
+
*
|
|
517
|
+
* The gate bounds volume and protects precision: a candidate survives only
|
|
518
|
+
* if its name matches a function/method DEFINED IN THIS FILE or a name this
|
|
519
|
+
* file imports/references. Everything else (locals, params, fields passed
|
|
520
|
+
* as arguments) is dropped before it ever reaches the database. Resolution
|
|
521
|
+
* then matches survivors against function/method nodes only
|
|
522
|
+
* (matchFunctionRef) and emits `references` edges — which callers/impact
|
|
523
|
+
* already traverse.
|
|
524
|
+
*
|
|
525
|
+
* Known v1 limit, deliberate: a C/C++ callback registered in a DIFFERENT
|
|
526
|
+
* translation unit than its definition (extern, no symbol imports to match)
|
|
527
|
+
* is not captured. Same-file registration — the dominant C pattern (static
|
|
528
|
+
* callback + same-file ops struct) — is.
|
|
529
|
+
*/
|
|
530
|
+
flushFnRefCandidates() {
|
|
531
|
+
if (this.fnRefCandidates.length === 0)
|
|
532
|
+
return;
|
|
533
|
+
const candidates = this.fnRefCandidates;
|
|
534
|
+
this.fnRefCandidates = [];
|
|
535
|
+
// Generated/minified files (vendored jquery.min.js and friends): their
|
|
536
|
+
// function-as-value edges are noise — single-letter minified symbols
|
|
537
|
+
// resolve everywhere. Same policy as the callback synthesizer.
|
|
538
|
+
if ((0, generated_detection_1.isGeneratedFile)(this.filePath))
|
|
539
|
+
return;
|
|
540
|
+
const definedHere = new Set();
|
|
541
|
+
for (const n of this.nodes) {
|
|
542
|
+
if (n.kind === 'function' || n.kind === 'method')
|
|
543
|
+
definedHere.add(n.name);
|
|
544
|
+
}
|
|
545
|
+
// Import-binding names only (all binding emitters push kind 'imports').
|
|
546
|
+
// Deliberately NOT 'references': those carry type-annotation and
|
|
547
|
+
// interface-member names, which let local variables that share a type
|
|
548
|
+
// member's name slip through the gate (excalidraw A/B finding). A dotted
|
|
549
|
+
// import (JVM `import com.example.OtherClass`) also contributes its LAST
|
|
550
|
+
// segment — the simple name Java/Kotlin code uses in `OtherClass::method`
|
|
551
|
+
// references.
|
|
552
|
+
const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
553
|
+
// JVM imports are dotted (`com.example.OtherClass`); PHP `use` imports
|
|
554
|
+
// are backslashed (`App\Services\Mailer`). Both contribute their last
|
|
555
|
+
// segment — the simple name code uses to reference them.
|
|
556
|
+
const QUALIFIED_IMPORT = /^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
|
557
|
+
const importedNames = new Set();
|
|
558
|
+
for (const r of this.unresolvedReferences) {
|
|
559
|
+
if (r.referenceKind !== 'imports')
|
|
560
|
+
continue;
|
|
561
|
+
if (SIMPLE_NAME.test(r.referenceName)) {
|
|
562
|
+
importedNames.add(r.referenceName);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const qualified = r.referenceName.match(QUALIFIED_IMPORT);
|
|
566
|
+
if (qualified)
|
|
567
|
+
importedNames.add(qualified[1]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const ungated = this.fnRefSpec?.ungatedModes;
|
|
571
|
+
const addressOfOnly = this.fnRefSpec?.addressOfOnly === true;
|
|
572
|
+
const seen = new Set();
|
|
573
|
+
for (const c of candidates) {
|
|
574
|
+
const atFileScope = c.fromNodeId.startsWith('file:');
|
|
575
|
+
// C++ (addressOfOnly): a BARE identifier qualifies only inside a
|
|
576
|
+
// file-scope initializer table. Everywhere else — args, assignments,
|
|
577
|
+
// local braced-init lists like `{begin, size}` — only explicit `&`
|
|
578
|
+
// forms count (fmt A/B finding: generic names `begin`/`out`/`size`
|
|
579
|
+
// collide with locals and members).
|
|
580
|
+
if (addressOfOnly &&
|
|
581
|
+
!c.explicitRef &&
|
|
582
|
+
!(atFileScope && (c.mode === 'value' || c.mode === 'list'))) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
// Gate policy by candidate shape:
|
|
586
|
+
// - `this.<member>`: ALWAYS flush — the member may be inherited from a
|
|
587
|
+
// class in another file (definedHere can't see it), volume is
|
|
588
|
+
// naturally bounded by real `this.X` expressions, and resolution is
|
|
589
|
+
// strictly class-scoped (own members or the validated supertype
|
|
590
|
+
// pass), so nothing fuzzy can leak.
|
|
591
|
+
// - `Scope::member` (C++ member-pointers, Java/Kotlin type-qualified
|
|
592
|
+
// method refs, PHP `'Cls::m'`): ALWAYS flush — the explicit-ref
|
|
593
|
+
// syntax is self-selecting, the referenced type often needs NO
|
|
594
|
+
// import (Java/Kotlin same-package, Kotlin companions), and
|
|
595
|
+
// resolution is scope-suffix-anchored + unique-or-drop, so a
|
|
596
|
+
// same-named member on another class can't match.
|
|
597
|
+
// - C-family file-scope initializers skip the gate entirely
|
|
598
|
+
// (constant-expression context — see FnRefSpec.ungatedModes).
|
|
599
|
+
// - everything else: name ∈ same-file functions/methods ∪ imports.
|
|
600
|
+
if (!c.name.startsWith('this.') && !c.name.includes('::')) {
|
|
601
|
+
const skipGate = (ungated?.has(c.mode) === true && atFileScope) ||
|
|
602
|
+
c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
|
|
603
|
+
if (!skipGate && !definedHere.has(c.name) && !importedNames.has(c.name)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const key = `${c.fromNodeId}|${c.name}`;
|
|
608
|
+
if (seen.has(key))
|
|
609
|
+
continue;
|
|
610
|
+
seen.add(key);
|
|
611
|
+
this.unresolvedReferences.push({
|
|
612
|
+
fromNodeId: c.fromNodeId,
|
|
613
|
+
referenceName: c.name,
|
|
614
|
+
referenceKind: 'function_ref',
|
|
615
|
+
line: c.line,
|
|
616
|
+
column: c.column,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Record value-reference bookkeeping as nodes are created: file-scope const/var symbols with
|
|
622
|
+
* distinctive names become reference targets; function/method/const/var symbols become reader
|
|
623
|
+
* scopes whose bodies flushValueRefs scans.
|
|
624
|
+
*/
|
|
625
|
+
captureValueRefScope(kind, name, id, node) {
|
|
626
|
+
// Pascal targets `constant` only: its extractor emits function PARAMETERS
|
|
627
|
+
// (`Dest: TBufferWriter`) and class fields (`declField`) as `variable` at the
|
|
628
|
+
// enclosing scope, which would otherwise become noisy targets (a param name
|
|
629
|
+
// shared across many procs collapses to one file-wide target). Genuine
|
|
630
|
+
// Pascal shared values are `const` (`constant`), so restrict to that. (Unit
|
|
631
|
+
// `var` globals are the rare cost; the parameter/field noise dominates.)
|
|
632
|
+
const targetKindOk = this.language === 'pascal' ? kind === 'constant' : kind === 'constant' || kind === 'variable';
|
|
633
|
+
if (targetKindOk && name.length >= 3 && /[A-Z_]/.test(name)) {
|
|
634
|
+
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
635
|
+
// file-scope OR class/module/struct/enum-scope constants are targets.
|
|
636
|
+
// Class/module scope matters for languages (Ruby) that keep nearly all
|
|
637
|
+
// constants inside a class or module; struct/enum scope matters for Swift,
|
|
638
|
+
// which namespaces shared constants in `struct`/`enum` (`enum Constants {
|
|
639
|
+
// static let X }`). Readers are same-file methods of that type.
|
|
640
|
+
if (parentId &&
|
|
641
|
+
(parentId.startsWith('file:') || parentId.startsWith('class:') ||
|
|
642
|
+
parentId.startsWith('module:') || parentId.startsWith('struct:') ||
|
|
643
|
+
parentId.startsWith('enum:'))) {
|
|
644
|
+
this.fileScopeValues.set(name, id);
|
|
645
|
+
// How many target nodes carry this name. A conditional def
|
|
646
|
+
// (`try: X = a; except: X = b`) makes >1 — distinct from a local shadow,
|
|
647
|
+
// which adds a binding the prune must catch (see flushValueRefs).
|
|
648
|
+
this.fileScopeValueCounts.set(name, (this.fileScopeValueCounts.get(name) ?? 0) + 1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (kind === 'function' || kind === 'method' || kind === 'constant' || kind === 'variable') {
|
|
652
|
+
this.valueRefScopes.push({ id, node, name });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Emit same-file `references` edges from a symbol to the file-scope const/var it reads (TS/JS).
|
|
657
|
+
* The engine doesn't edge const→consumer, so impact analysis misses "change this table, affect
|
|
658
|
+
* its readers" (the ReScript-PR false positive). Same-file only (resolution is unambiguous),
|
|
659
|
+
* distinctive target names only (dodges the local-shadowing precision trap documented on
|
|
660
|
+
* function_ref), deduped per (reader, target). Default on (HOMEGRAPH_VALUE_REFS=0 disables) +
|
|
661
|
+
* additive. Shadowed targets are pruned — see below.
|
|
662
|
+
*/
|
|
663
|
+
flushValueRefs() {
|
|
664
|
+
const scopes = this.valueRefScopes;
|
|
665
|
+
const targets = this.fileScopeValues;
|
|
666
|
+
const fileScopeCounts = this.fileScopeValueCounts;
|
|
667
|
+
this.valueRefScopes = [];
|
|
668
|
+
this.fileScopeValues = new Map();
|
|
669
|
+
this.fileScopeValueCounts = new Map();
|
|
670
|
+
if (!this.valueRefsEnabled || !TreeSitterExtractor.VALUE_REF_LANGS.has(this.language))
|
|
671
|
+
return;
|
|
672
|
+
if (targets.size === 0 || scopes.length === 0 || (0, generated_detection_1.isGeneratedFile)(this.filePath))
|
|
673
|
+
return;
|
|
674
|
+
// Prune SHADOWED targets. A target re-bound in an INNER scope (a
|
|
675
|
+
// bundled/Emscripten `const Module` re-declared as a nested `var Module`; a
|
|
676
|
+
// Go package `const Timeout` shadowed by a local `Timeout := …`; a Python
|
|
677
|
+
// module `CONFIG` shadowed by a local `CONFIG = …`) resolves to the inner
|
|
678
|
+
// binding for nested readers, so a file-scope edge is a false positive.
|
|
679
|
+
// Inner re-bindings aren't graph nodes, so detect them at the syntax level:
|
|
680
|
+
// count every declarator of the name across the tree and compare against how
|
|
681
|
+
// many FILE-SCOPE nodes carry it. A real shadow makes (declarators >
|
|
682
|
+
// file-scope nodes) — the excess is the local binding. A conditional
|
|
683
|
+
// module-level def (`try: X = a; except: X = b`) makes them EQUAL (both
|
|
684
|
+
// declarators are file-scope nodes), so it's correctly kept. Complements the
|
|
685
|
+
// path-based isGeneratedFile() check, which can't catch content-minified
|
|
686
|
+
// bundles.
|
|
687
|
+
//
|
|
688
|
+
// Declarator node types are per-grammar; a file only contains its own
|
|
689
|
+
// language's nodes, so matching all of them in one switch is safe.
|
|
690
|
+
if (this.tree) {
|
|
691
|
+
const declCounts = new Map();
|
|
692
|
+
const bump = (nameNode) => {
|
|
693
|
+
// `simple_identifier` is Kotlin's name node (a property declarator's name).
|
|
694
|
+
if (nameNode && (nameNode.type === 'identifier' || nameNode.type === 'simple_identifier')) {
|
|
695
|
+
const nm = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
696
|
+
if (targets.has(nm))
|
|
697
|
+
declCounts.set(nm, (declCounts.get(nm) ?? 0) + 1);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
const dstack = [this.tree.rootNode];
|
|
701
|
+
let dvisited = 0;
|
|
702
|
+
while (dstack.length > 0 && dvisited < TreeSitterExtractor.MAX_VALUE_REF_NODES) {
|
|
703
|
+
const n = dstack.pop();
|
|
704
|
+
dvisited++;
|
|
705
|
+
switch (n.type) {
|
|
706
|
+
case 'variable_declarator': // TS/JS/tsx
|
|
707
|
+
case 'const_spec': // Go `const X = …`
|
|
708
|
+
case 'var_spec': // Go `var X = …`
|
|
709
|
+
bump(n.namedChild(0));
|
|
710
|
+
break;
|
|
711
|
+
case 'const_item': // Rust `const X: T = …`
|
|
712
|
+
case 'static_item': // Rust `static X: T = …`
|
|
713
|
+
bump((0, tree_sitter_helpers_1.getChildByField)(n, 'name'));
|
|
714
|
+
break;
|
|
715
|
+
case 'let_declaration': // Rust `let x = …` (locals — the shadow source)
|
|
716
|
+
case 'short_var_declaration': // Go `x, Y := …`
|
|
717
|
+
case 'assignment': { // Python `X = …` / `X: T = …` / `A, B = …`
|
|
718
|
+
const left = (0, tree_sitter_helpers_1.getChildByField)(n, 'left') ?? (0, tree_sitter_helpers_1.getChildByField)(n, 'pattern') ?? n.namedChild(0);
|
|
719
|
+
if (left?.type === 'identifier')
|
|
720
|
+
bump(left);
|
|
721
|
+
else if (left)
|
|
722
|
+
for (const c of left.namedChildren)
|
|
723
|
+
bump(c);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case 'init_declarator': // C `T X = …` (file-scope const AND the local that shadows it)
|
|
727
|
+
bump(cDeclaratorIdentifier(n));
|
|
728
|
+
break;
|
|
729
|
+
case 'val_definition': // Scala `val X = …` (object/top-level const AND a method-local that shadows it)
|
|
730
|
+
case 'var_definition': { // Scala `var X = …`
|
|
731
|
+
const pat = (0, tree_sitter_helpers_1.getChildByField)(n, 'pattern');
|
|
732
|
+
if (pat?.type === 'identifier')
|
|
733
|
+
bump(pat);
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case 'static_final_declaration': // Dart top-level/`static` `const`/`final` (the target itself)
|
|
737
|
+
case 'initialized_identifier': // Dart instance field / `var`
|
|
738
|
+
case 'initialized_variable_definition': { // Dart a method-local `const`/`final`/`var` that shadows a const
|
|
739
|
+
const id = n.namedChildren.find((c) => c.type === 'identifier');
|
|
740
|
+
if (id)
|
|
741
|
+
bump(id);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case 'declConst': // Pascal unit/class `const` (the target itself) AND a function-local `const` that shadows it
|
|
745
|
+
case 'declVar': { // Pascal a function-local `var` that shadows a const
|
|
746
|
+
bump((0, tree_sitter_helpers_1.getChildByField)(n, 'name'));
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case 'property_declaration': { // Kotlin / Swift `val`/`let X = …` (object/static const AND a method-local that shadows it)
|
|
750
|
+
// Kotlin: variable_declaration → simple_identifier; Swift: a `pattern`
|
|
751
|
+
// (`<name>` field) → simple_identifier. Resolve either shape.
|
|
752
|
+
const vd = n.namedChildren.find((c) => c.type === 'variable_declaration');
|
|
753
|
+
const id = vd
|
|
754
|
+
? vd.namedChildren.find((c) => c.type === 'simple_identifier')
|
|
755
|
+
: firstSimpleIdentifier((0, tree_sitter_helpers_1.getChildByField)(n, 'name') ??
|
|
756
|
+
n.namedChildren.find((c) => c.type === 'value_binding_pattern' || c.type === 'pattern') ??
|
|
757
|
+
null);
|
|
758
|
+
if (id)
|
|
759
|
+
bump(id);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
764
|
+
const c = n.namedChild(i);
|
|
765
|
+
if (c)
|
|
766
|
+
dstack.push(c);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const [nm, c] of declCounts)
|
|
770
|
+
if (c > (fileScopeCounts.get(nm) ?? 1))
|
|
771
|
+
targets.delete(nm);
|
|
772
|
+
if (targets.size === 0)
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
for (const scope of scopes) {
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
const stack = [scope.node];
|
|
778
|
+
// Dart and Pascal attach a function/method BODY as a *next sibling* of the
|
|
779
|
+
// signature node that is stored as the reader scope (Dart `method_signature`
|
|
780
|
+
// ← `function_body`; Pascal `declProc` ← `block`, both under a `defProc`),
|
|
781
|
+
// not as a child — so the scope subtree is just the signature and the reads
|
|
782
|
+
// live in the sibling. Pull it in. (A body as a next sibling of the scope
|
|
783
|
+
// node is unique to Dart/Pascal among the value-ref languages — every other
|
|
784
|
+
// grammar nests the body inside the function node — so this is inert
|
|
785
|
+
// elsewhere.)
|
|
786
|
+
const sib = scope.node.nextNamedSibling;
|
|
787
|
+
if (sib && (sib.type === 'function_body' || sib.type === 'block'))
|
|
788
|
+
stack.push(sib);
|
|
789
|
+
let visited = 0;
|
|
790
|
+
while (stack.length > 0 && visited < TreeSitterExtractor.MAX_VALUE_REF_NODES) {
|
|
791
|
+
const n = stack.pop();
|
|
792
|
+
visited++;
|
|
793
|
+
// `constant` covers Ruby, where both a constant's definition and its
|
|
794
|
+
// references are `constant`-typed nodes, not `identifier`. `name` covers
|
|
795
|
+
// PHP, where a constant reference — bare `MAX_ITEMS` or the const half of
|
|
796
|
+
// `self::MAX_ITEMS` / `Foo::MAX_ITEMS` — is a `name` node (a `$var` local
|
|
797
|
+
// is a `variable_name`, a different namespace, so it can never shadow a
|
|
798
|
+
// bare constant — no prune wiring needed). `simple_identifier` covers
|
|
799
|
+
// Kotlin, whose every name reference (a const read included) is that
|
|
800
|
+
// node type. Safe across languages: a file only holds its own grammar's
|
|
801
|
+
// nodes; `name` is PHP-only and `simple_identifier` is Kotlin-only here.
|
|
802
|
+
if (n.type === 'identifier' || n.type === 'constant' ||
|
|
803
|
+
n.type === 'name' || n.type === 'simple_identifier') {
|
|
804
|
+
const refName = (0, tree_sitter_helpers_1.getNodeText)(n, this.source);
|
|
805
|
+
const targetId = targets.get(refName);
|
|
806
|
+
// Skip self and same-name targets: a symbol referencing a file-scope
|
|
807
|
+
// sibling of its own name (the two halves of a conditional `try: X=…;
|
|
808
|
+
// except: X=…`) is never a meaningful value read.
|
|
809
|
+
if (targetId && targetId !== scope.id && refName !== scope.name && !seen.has(targetId)) {
|
|
810
|
+
seen.add(targetId);
|
|
811
|
+
this.edges.push({
|
|
812
|
+
source: scope.id,
|
|
813
|
+
target: targetId,
|
|
814
|
+
kind: 'references',
|
|
815
|
+
metadata: { valueRef: true },
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
820
|
+
const c = n.namedChild(i);
|
|
821
|
+
if (c)
|
|
822
|
+
stack.push(c);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
355
827
|
/**
|
|
356
828
|
* Visit a node and extract information
|
|
357
829
|
*/
|
|
@@ -364,8 +836,14 @@ class TreeSitterExtractor {
|
|
|
364
836
|
if (this.extractor.visitNode) {
|
|
365
837
|
const ctx = this.makeExtractorContext();
|
|
366
838
|
const handled = this.extractor.visitNode(node, ctx);
|
|
367
|
-
if (handled)
|
|
839
|
+
if (handled) {
|
|
840
|
+
// The hook consumed this subtree, so the walkers below never descend
|
|
841
|
+
// into it — scan it for function-as-value candidates (#756). Scala's
|
|
842
|
+
// hook handles val/var definitions (`val table = Seq(targetCb)`), for
|
|
843
|
+
// example. The scan is capture-only and halts at nested functions.
|
|
844
|
+
this.scanFnRefSubtree(node, 0);
|
|
368
845
|
return;
|
|
846
|
+
}
|
|
369
847
|
}
|
|
370
848
|
// Pascal-specific AST handling
|
|
371
849
|
if (this.language === 'pascal') {
|
|
@@ -373,6 +851,10 @@ class TreeSitterExtractor {
|
|
|
373
851
|
if (skipChildren)
|
|
374
852
|
return;
|
|
375
853
|
}
|
|
854
|
+
// Function-as-value capture (#756) — independent of the dispatch ladder
|
|
855
|
+
// below (the captured container types have no other handler there), so it
|
|
856
|
+
// can never shadow or be shadowed by an extraction branch.
|
|
857
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
376
858
|
// Check for function declarations
|
|
377
859
|
// For Python/Ruby, function_definition inside a class should be treated as method
|
|
378
860
|
if (this.extractor.functionTypes.includes(nodeType)) {
|
|
@@ -414,8 +896,31 @@ class TreeSitterExtractor {
|
|
|
414
896
|
}
|
|
415
897
|
// Check for method declarations (only if not already handled by functionTypes)
|
|
416
898
|
else if (this.extractor.methodTypes.includes(nodeType)) {
|
|
417
|
-
|
|
418
|
-
|
|
899
|
+
// TS/JS class fields parse as a methodTypes node; only function-valued
|
|
900
|
+
// fields are methods — a plain field (`public fonts: Fonts;`) is a
|
|
901
|
+
// property (#808). classifyMethodNode is absent for other languages.
|
|
902
|
+
if (this.extractor.classifyMethodNode?.(node) === 'property') {
|
|
903
|
+
const propNode = this.extractProperty(node);
|
|
904
|
+
// Walk the initializer so its calls/instantiations attribute to the
|
|
905
|
+
// property (`history = createHistory()` → history calls
|
|
906
|
+
// createHistory). The old field-as-method path never walked these
|
|
907
|
+
// (resolveBody only resolves function bodies), so this is additive.
|
|
908
|
+
const valueNode = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
|
|
909
|
+
if (propNode && valueNode) {
|
|
910
|
+
this.nodeStack.push(propNode.id);
|
|
911
|
+
this.visitFunctionBody(valueNode, '');
|
|
912
|
+
this.nodeStack.pop();
|
|
913
|
+
}
|
|
914
|
+
// A field initializer can also register callbacks
|
|
915
|
+
// (`static handlers = { click: onClick }`) — scan it for
|
|
916
|
+
// function-as-value candidates (capture-only, halts at functions).
|
|
917
|
+
this.scanFnRefSubtree(node, 0);
|
|
918
|
+
skipChildren = true;
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
this.extractMethod(node);
|
|
922
|
+
skipChildren = true; // extractMethod visits children via visitFunctionBody
|
|
923
|
+
}
|
|
419
924
|
}
|
|
420
925
|
// Check for interface/protocol/trait declarations
|
|
421
926
|
else if (this.extractor.interfaceTypes.includes(nodeType)) {
|
|
@@ -441,17 +946,38 @@ class TreeSitterExtractor {
|
|
|
441
946
|
// Check for class properties (e.g. C# property_declaration)
|
|
442
947
|
else if (this.extractor.propertyTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
|
443
948
|
this.extractProperty(node);
|
|
949
|
+
// Property initializers aren't walked — scan for function-as-value
|
|
950
|
+
// candidates (#756): Scala `val table = Seq(targetCb)` in an object,
|
|
951
|
+
// Kotlin `val cb = ::handler` class properties.
|
|
952
|
+
this.scanFnRefSubtree(node, 0);
|
|
444
953
|
skipChildren = true;
|
|
445
954
|
}
|
|
446
955
|
// Check for class fields (e.g. Java field_declaration, C# field_declaration)
|
|
447
956
|
else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) {
|
|
448
957
|
this.extractField(node);
|
|
958
|
+
// Field initializers aren't walked — scan for function-as-value
|
|
959
|
+
// candidates (#756): Java `List<IntConsumer> table = List.of(Main::cb)`,
|
|
960
|
+
// C# `List<Action<int>> table = new() { TargetCb }`.
|
|
961
|
+
this.scanFnRefSubtree(node, 0);
|
|
449
962
|
skipChildren = true;
|
|
450
963
|
}
|
|
451
964
|
// Check for variable declarations (const, let, var, etc.)
|
|
452
|
-
// Only extract top-level variables (not inside functions/methods)
|
|
453
|
-
|
|
965
|
+
// Only extract top-level variables (not inside functions/methods) — plus
|
|
966
|
+
// class/module-scope CONSTANTS, which Ruby (and other const-in-class
|
|
967
|
+
// languages) keep almost exclusively inside a class/module. A Ruby `CONST =
|
|
968
|
+
// …` has a `constant`-typed LHS; other languages don't put one here, so this
|
|
969
|
+
// is effectively Ruby-only and doesn't disturb their class-internal locals.
|
|
970
|
+
else if (this.extractor.variableTypes.includes(nodeType) &&
|
|
971
|
+
(!this.isInsideClassLikeNode() || this.isClassScopeConstantAssignment(node))) {
|
|
454
972
|
this.extractVariable(node);
|
|
973
|
+
// extractVariable doesn't walk every initializer shape (object literals
|
|
974
|
+
// are deliberately skipped; Python/Ruby don't walk at all), so scan the
|
|
975
|
+
// declaration subtree for function-as-value candidates — `const routes =
|
|
976
|
+
// { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts
|
|
977
|
+
// at nested function definitions (their bodies are walked — and
|
|
978
|
+
// attributed — separately) and flush-time dedup absorbs any overlap with
|
|
979
|
+
// initializers extractVariable DOES walk.
|
|
980
|
+
this.scanFnRefSubtree(node, 0);
|
|
455
981
|
skipChildren = true; // extractVariable handles children
|
|
456
982
|
}
|
|
457
983
|
// Swift stored properties inside a type. Swift instance properties aren't
|
|
@@ -464,6 +990,20 @@ class TreeSitterExtractor {
|
|
|
464
990
|
nodeType === 'property_declaration' &&
|
|
465
991
|
this.isInsideClassLikeNode()) {
|
|
466
992
|
const ownerId = this.nodeStack[this.nodeStack.length - 1];
|
|
993
|
+
// A `static let`/`static var` member is a SHARED constant of the type
|
|
994
|
+
// (Swift's `static`-namespacing idiom, esp. in `enum`/`struct`) — extract
|
|
995
|
+
// it as `constant`/`variable` so value-reference edges can target it. An
|
|
996
|
+
// instance stored property stays a `field` (per-instance; Swift instance
|
|
997
|
+
// properties otherwise aren't own nodes — that's unchanged). A *computed*
|
|
998
|
+
// property (getter, no stored value) is never a constant — skip the node.
|
|
999
|
+
const { nameNode, isLet, isComputed } = swiftPropertyInfo(node, this.source);
|
|
1000
|
+
if (nameNode && !isComputed) {
|
|
1001
|
+
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
|
1002
|
+
this.createNode(isStatic ? (isLet ? 'constant' : 'variable') : 'field', (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source), node, {
|
|
1003
|
+
visibility: this.extractor.getVisibility?.(node),
|
|
1004
|
+
isStatic,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
467
1007
|
if (ownerId) {
|
|
468
1008
|
this.extractDecoratorsFor(node, ownerId);
|
|
469
1009
|
this.extractVariableTypeAnnotation(node, ownerId);
|
|
@@ -521,6 +1061,22 @@ class TreeSitterExtractor {
|
|
|
521
1061
|
if (parentId)
|
|
522
1062
|
this.emitReExportRefs(node, parentId);
|
|
523
1063
|
}
|
|
1064
|
+
// Vuex MODULE default export — `export default { namespaced, actions: {…},
|
|
1065
|
+
// mutations: {…} }` (the canonical Vuex module shape). Object-literal methods
|
|
1066
|
+
// aren't otherwise extracted, so scan the config's actions/mutations/getters
|
|
1067
|
+
// collections and extract their methods as nodes. Store-file gated (the
|
|
1068
|
+
// ≥2-signal heuristic) so a plain default-exported object is untouched; skip
|
|
1069
|
+
// the subtree afterward (the collection methods are now handled).
|
|
1070
|
+
else if (nodeType === 'export_statement' &&
|
|
1071
|
+
(this.language === 'typescript' || this.language === 'tsx' ||
|
|
1072
|
+
this.language === 'javascript' || this.language === 'jsx') &&
|
|
1073
|
+
this.looksLikeVueStoreFile()) {
|
|
1074
|
+
const exported = (0, tree_sitter_helpers_1.getChildByField)(node, 'value');
|
|
1075
|
+
if (exported && (exported.type === 'object' || exported.type === 'object_expression')) {
|
|
1076
|
+
this.extractStoreCollectionMethods(exported);
|
|
1077
|
+
skipChildren = true;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
524
1080
|
// Check for function calls
|
|
525
1081
|
else if (this.extractor.callTypes.includes(nodeType)) {
|
|
526
1082
|
this.extractCall(node);
|
|
@@ -632,6 +1188,8 @@ class TreeSitterExtractor {
|
|
|
632
1188
|
});
|
|
633
1189
|
}
|
|
634
1190
|
}
|
|
1191
|
+
if (this.valueRefsEnabled)
|
|
1192
|
+
this.captureValueRefScope(kind, name, id, node);
|
|
635
1193
|
return newNode;
|
|
636
1194
|
}
|
|
637
1195
|
/**
|
|
@@ -727,6 +1285,18 @@ class TreeSitterExtractor {
|
|
|
727
1285
|
parentNode.kind === 'enum' ||
|
|
728
1286
|
parentNode.kind === 'module');
|
|
729
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Ruby `CONST = …` assignment whose LHS is a `constant` node — a class/module
|
|
1290
|
+
* (or top-level) constant worth extracting as a symbol even inside a class.
|
|
1291
|
+
* Other languages don't give an assignment a `constant`-typed LHS, so this
|
|
1292
|
+
* gate is effectively Ruby-only.
|
|
1293
|
+
*/
|
|
1294
|
+
isClassScopeConstantAssignment(node) {
|
|
1295
|
+
if (node.type !== 'assignment')
|
|
1296
|
+
return false;
|
|
1297
|
+
const left = (0, tree_sitter_helpers_1.getChildByField)(node, 'left') ?? node.namedChild(0);
|
|
1298
|
+
return left?.type === 'constant';
|
|
1299
|
+
}
|
|
730
1300
|
/**
|
|
731
1301
|
* Extract a function
|
|
732
1302
|
*/
|
|
@@ -788,6 +1358,7 @@ class TreeSitterExtractor {
|
|
|
788
1358
|
const isExported = this.extractor.isExported?.(node, this.source);
|
|
789
1359
|
const isAsync = this.extractor.isAsync?.(node);
|
|
790
1360
|
const isStatic = this.extractor.isStatic?.(node);
|
|
1361
|
+
const returnType = this.extractor.getReturnType?.(node, this.source);
|
|
791
1362
|
const funcNode = this.createNode('function', name, node, {
|
|
792
1363
|
docstring,
|
|
793
1364
|
signature,
|
|
@@ -795,6 +1366,7 @@ class TreeSitterExtractor {
|
|
|
795
1366
|
isExported,
|
|
796
1367
|
isAsync,
|
|
797
1368
|
isStatic,
|
|
1369
|
+
returnType,
|
|
798
1370
|
});
|
|
799
1371
|
if (!funcNode)
|
|
800
1372
|
return;
|
|
@@ -813,6 +1385,70 @@ class TreeSitterExtractor {
|
|
|
813
1385
|
}
|
|
814
1386
|
this.nodeStack.pop();
|
|
815
1387
|
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Detect a React component declared via an HOC wrapper whose result is itself a
|
|
1390
|
+
* component: `forwardRef(...)`, `memo(...)`, `React.forwardRef/memo(...)`, and
|
|
1391
|
+
* styled-components / emotion `styled.tag\`…\`` / `styled(Base)\`…\``. These
|
|
1392
|
+
* initializers are a call / tagged-template (not a bare arrow), so the const is
|
|
1393
|
+
* otherwise classified `constant` — and a constant is skipped by both the
|
|
1394
|
+
* JSX-render edge synthesizer and component resolution, so `<Button/>` usages
|
|
1395
|
+
* get no edge and callers/impact silently return empty (#841).
|
|
1396
|
+
*
|
|
1397
|
+
* Returns `{ inner }` — the inline render function to extract as the component
|
|
1398
|
+
* body, or `null` when the wrapper has no inline function (`memo(Imported)`,
|
|
1399
|
+
* `styled.button\`…\``) and only a bodyless component node is minted — or
|
|
1400
|
+
* `undefined` when this initializer is not a recognized component wrapper.
|
|
1401
|
+
*/
|
|
1402
|
+
reactComponentHoc(valueNode) {
|
|
1403
|
+
if (valueNode.type !== 'call_expression')
|
|
1404
|
+
return undefined;
|
|
1405
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(valueNode, 'function');
|
|
1406
|
+
if (!callee)
|
|
1407
|
+
return undefined;
|
|
1408
|
+
const calleeText = (0, tree_sitter_helpers_1.getNodeText)(callee, this.source);
|
|
1409
|
+
// styled-components / emotion: `styled.button\`…\`` / `styled(Base)\`…\``.
|
|
1410
|
+
// tree-sitter models these tagged templates as a call_expression whose callee
|
|
1411
|
+
// is the `styled.x` / `styled(Base)` tag (\b avoids matching `styledFoo`).
|
|
1412
|
+
// No inline render fn — the argument is the CSS template.
|
|
1413
|
+
if (/^styled\b/.test(calleeText))
|
|
1414
|
+
return { inner: null };
|
|
1415
|
+
// React HOCs: `forwardRef`/`memo`/`React.forwardRef`/`React.memo`.
|
|
1416
|
+
if (!REACT_COMPONENT_HOCS.has(calleeText))
|
|
1417
|
+
return undefined;
|
|
1418
|
+
// The first arrow / function-expression argument is the render fn (if inline;
|
|
1419
|
+
// `memo(Imported)` passes a bare identifier and has none).
|
|
1420
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(valueNode, 'arguments');
|
|
1421
|
+
let inner = null;
|
|
1422
|
+
if (args) {
|
|
1423
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
1424
|
+
const a = args.namedChild(i);
|
|
1425
|
+
if (a && (a.type === 'arrow_function' || a.type === 'function_expression')) {
|
|
1426
|
+
inner = a;
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return { inner };
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Emit a `component` node for an HOC-wrapped React component declaration (see
|
|
1435
|
+
* reactComponentHoc). Named by the declarator (`Button`) and located at it so
|
|
1436
|
+
* the node range spans the body. When the wrapper has an inline render
|
|
1437
|
+
* function, its body is walked so the component's callees (hooks, helpers) are
|
|
1438
|
+
* captured under the component node — matching how a plain
|
|
1439
|
+
* `const Foo = () => …` arrow component already behaves.
|
|
1440
|
+
*/
|
|
1441
|
+
extractReactComponentNode(name, declarator, innerFn, extra) {
|
|
1442
|
+
const compNode = this.createNode('component', name, declarator, extra);
|
|
1443
|
+
if (!compNode || !innerFn || !this.extractor)
|
|
1444
|
+
return;
|
|
1445
|
+
this.nodeStack.push(compNode.id);
|
|
1446
|
+
const body = this.extractor.resolveBody?.(innerFn, this.extractor.bodyField)
|
|
1447
|
+
?? (0, tree_sitter_helpers_1.getChildByField)(innerFn, this.extractor.bodyField);
|
|
1448
|
+
if (body)
|
|
1449
|
+
this.visitFunctionBody(body, compNode.id);
|
|
1450
|
+
this.nodeStack.pop();
|
|
1451
|
+
}
|
|
816
1452
|
/**
|
|
817
1453
|
* Extract a class
|
|
818
1454
|
*/
|
|
@@ -849,6 +1485,12 @@ class TreeSitterExtractor {
|
|
|
849
1485
|
this.visitNode(child);
|
|
850
1486
|
}
|
|
851
1487
|
}
|
|
1488
|
+
// Synthesize compile-time-generated members (Lombok accessors, #912). Runs
|
|
1489
|
+
// after the body so the hook can dedup against hand-written members, and
|
|
1490
|
+
// while the class is still on the stack so containment/QNs attach.
|
|
1491
|
+
if (this.extractor.synthesizeMembers) {
|
|
1492
|
+
this.extractor.synthesizeMembers(node, this.makeExtractorContext());
|
|
1493
|
+
}
|
|
852
1494
|
this.nodeStack.pop();
|
|
853
1495
|
}
|
|
854
1496
|
/**
|
|
@@ -894,12 +1536,14 @@ class TreeSitterExtractor {
|
|
|
894
1536
|
const visibility = this.extractor.getVisibility?.(node);
|
|
895
1537
|
const isAsync = this.extractor.isAsync?.(node);
|
|
896
1538
|
const isStatic = this.extractor.isStatic?.(node);
|
|
1539
|
+
const returnType = this.extractor.getReturnType?.(node, this.source);
|
|
897
1540
|
const extraProps = {
|
|
898
1541
|
docstring,
|
|
899
1542
|
signature,
|
|
900
1543
|
visibility,
|
|
901
1544
|
isAsync,
|
|
902
1545
|
isStatic,
|
|
1546
|
+
returnType,
|
|
903
1547
|
};
|
|
904
1548
|
if (receiverType) {
|
|
905
1549
|
extraProps.qualifiedName = `${receiverType}::${name}`;
|
|
@@ -973,8 +1617,10 @@ class TreeSitterExtractor {
|
|
|
973
1617
|
if (!this.extractor)
|
|
974
1618
|
return;
|
|
975
1619
|
// Skip forward declarations and type references (no body = not a definition)
|
|
1620
|
+
// — EXCEPT C# positional records (`record struct M(decimal Amount);`),
|
|
1621
|
+
// complete definitions with no body block. (#831)
|
|
976
1622
|
const body = (0, tree_sitter_helpers_1.getChildByField)(node, this.extractor.bodyField);
|
|
977
|
-
if (!body)
|
|
1623
|
+
if (!body && node.type !== 'record_declaration')
|
|
978
1624
|
return;
|
|
979
1625
|
const name = extractName(node, this.source, this.extractor);
|
|
980
1626
|
const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
|
|
@@ -992,15 +1638,18 @@ class TreeSitterExtractor {
|
|
|
992
1638
|
// C# primary-constructor parameter dependencies (`struct P(int x)`, and
|
|
993
1639
|
// `record struct M(decimal Amount)` which the grammar nests here).
|
|
994
1640
|
this.extractCsharpPrimaryCtorParamRefs(node, structNode.id);
|
|
995
|
-
// Push to stack for field extraction
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1641
|
+
// Push to stack for field extraction (bodiless positional records have
|
|
1642
|
+
// no members to visit)
|
|
1643
|
+
if (body) {
|
|
1644
|
+
this.nodeStack.push(structNode.id);
|
|
1645
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
1646
|
+
const child = body.namedChild(i);
|
|
1647
|
+
if (child) {
|
|
1648
|
+
this.visitNode(child);
|
|
1649
|
+
}
|
|
1001
1650
|
}
|
|
1651
|
+
this.nodeStack.pop();
|
|
1002
1652
|
}
|
|
1003
|
-
this.nodeStack.pop();
|
|
1004
1653
|
}
|
|
1005
1654
|
/**
|
|
1006
1655
|
* Extract an enum
|
|
@@ -1073,22 +1722,35 @@ class TreeSitterExtractor {
|
|
|
1073
1722
|
*/
|
|
1074
1723
|
extractProperty(node) {
|
|
1075
1724
|
if (!this.extractor)
|
|
1076
|
-
return;
|
|
1725
|
+
return null;
|
|
1077
1726
|
const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
|
|
1078
1727
|
const visibility = this.extractor.getVisibility?.(node);
|
|
1079
1728
|
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
|
1080
1729
|
const hookName = this.extractor.extractPropertyName?.(node, this.source);
|
|
1730
|
+
// JS `field_definition` names its key the `property` field (TS uses
|
|
1731
|
+
// `name`) — try both before the generic identifier scan (#808).
|
|
1081
1732
|
const nameNode = hookName
|
|
1082
1733
|
? null
|
|
1083
|
-
: (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
|
|
1734
|
+
: (0, tree_sitter_helpers_1.getChildByField)(node, 'name') ||
|
|
1735
|
+
(0, tree_sitter_helpers_1.getChildByField)(node, 'property') ||
|
|
1736
|
+
node.namedChildren.find(c => c.type === 'identifier');
|
|
1084
1737
|
const name = hookName ?? (nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : null);
|
|
1085
1738
|
if (!name)
|
|
1086
|
-
return;
|
|
1087
|
-
// Get property type
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1739
|
+
return null;
|
|
1740
|
+
// Get property type. TS/JS field definitions carry an explicit `type`
|
|
1741
|
+
// field (a `type_annotation`); their other named children are the name
|
|
1742
|
+
// and the initializer VALUE, which the generic finder below would
|
|
1743
|
+
// wrongly pick — so fields use the type field only (#808). Other
|
|
1744
|
+
// languages (C# property_declaration) keep the generic scan.
|
|
1745
|
+
const isTsJsField = node.type === 'public_field_definition' || node.type === 'field_definition';
|
|
1746
|
+
const typeNode = isTsJsField
|
|
1747
|
+
? (0, tree_sitter_helpers_1.getChildByField)(node, 'type')
|
|
1748
|
+
: node.namedChildren.find(c => c.type !== 'modifier' && c.type !== 'modifiers'
|
|
1749
|
+
&& c.type !== 'identifier' && c.type !== 'accessor_list'
|
|
1750
|
+
&& c.type !== 'accessors' && c.type !== 'equals_value_clause');
|
|
1751
|
+
const typeText = typeNode
|
|
1752
|
+
? (0, tree_sitter_helpers_1.getNodeText)(typeNode, this.source).replace(/^:\s*/, '')
|
|
1753
|
+
: undefined;
|
|
1092
1754
|
const signature = typeText ? `${typeText} ${name}` : name;
|
|
1093
1755
|
const propNode = this.createNode('property', name, node, {
|
|
1094
1756
|
docstring,
|
|
@@ -1105,6 +1767,7 @@ class TreeSitterExtractor {
|
|
|
1105
1767
|
// `type_annotation` children; the C# branch walks the `type` field.
|
|
1106
1768
|
this.extractTypeAnnotations(node, propNode.id);
|
|
1107
1769
|
}
|
|
1770
|
+
return propNode;
|
|
1108
1771
|
}
|
|
1109
1772
|
/**
|
|
1110
1773
|
* Extract a class field declaration (e.g. Java field_declaration, C# field_declaration).
|
|
@@ -1116,6 +1779,15 @@ class TreeSitterExtractor {
|
|
|
1116
1779
|
const docstring = (0, tree_sitter_helpers_1.getPrecedingDocstring)(node, this.source);
|
|
1117
1780
|
const visibility = this.extractor.getVisibility?.(node);
|
|
1118
1781
|
const isStatic = this.extractor.isStatic?.(node) ?? false;
|
|
1782
|
+
// A class field that is actually a CONSTANT (Java `static final`, C# `const`
|
|
1783
|
+
// / `static readonly`) is extracted as `constant` kind, not `field`, so
|
|
1784
|
+
// value-reference edges treat it as a target (the gate accepts
|
|
1785
|
+
// constant/variable, not field). Scoped to languages whose `isConst`
|
|
1786
|
+
// predicate is field-shaped — other languages' fields stay `field`.
|
|
1787
|
+
const fieldKind = (this.language === 'java' || this.language === 'csharp') &&
|
|
1788
|
+
(this.extractor.isConst?.(node) ?? false)
|
|
1789
|
+
? 'constant'
|
|
1790
|
+
: 'field';
|
|
1119
1791
|
// Java field_declaration: "private final String name = value;" → variable_declarator(s) are direct children
|
|
1120
1792
|
// C# field_declaration: wraps in variable_declaration → variable_declarator(s)
|
|
1121
1793
|
let declarators = node.namedChildren.filter(c => c.type === 'variable_declarator');
|
|
@@ -1168,7 +1840,7 @@ class TreeSitterExtractor {
|
|
|
1168
1840
|
continue;
|
|
1169
1841
|
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
1170
1842
|
const signature = typeText ? `${typeText} ${name}` : name;
|
|
1171
|
-
const fieldNode = this.createNode(
|
|
1843
|
+
const fieldNode = this.createNode(fieldKind, name, decl, {
|
|
1172
1844
|
docstring,
|
|
1173
1845
|
signature,
|
|
1174
1846
|
visibility,
|
|
@@ -1193,7 +1865,7 @@ class TreeSitterExtractor {
|
|
|
1193
1865
|
|| node.namedChildren.find(c => c.type === 'identifier');
|
|
1194
1866
|
if (nameNode) {
|
|
1195
1867
|
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
1196
|
-
this.createNode(
|
|
1868
|
+
this.createNode(fieldKind, name, node, {
|
|
1197
1869
|
docstring,
|
|
1198
1870
|
visibility,
|
|
1199
1871
|
isStatic,
|
|
@@ -1309,6 +1981,317 @@ class TreeSitterExtractor {
|
|
|
1309
1981
|
}
|
|
1310
1982
|
return null;
|
|
1311
1983
|
}
|
|
1984
|
+
/**
|
|
1985
|
+
* RTK Query: from a `createApi({ ..., endpoints: build => ({...}) })` or a
|
|
1986
|
+
* `baseApi.injectEndpoints({ endpoints: build => ({...}) })` call initializer,
|
|
1987
|
+
* return the object literal of endpoint definitions (the object the `endpoints`
|
|
1988
|
+
* arrow returns). Returns null for any other call — the common case — so this
|
|
1989
|
+
* stays cheap and silent. Keyed on the RTK entry-point names (`createApi` /
|
|
1990
|
+
* `injectEndpoints`) like the framework extractors key on their library APIs.
|
|
1991
|
+
*/
|
|
1992
|
+
findRtkEndpointsObject(callNode) {
|
|
1993
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function');
|
|
1994
|
+
if (!callee)
|
|
1995
|
+
return null;
|
|
1996
|
+
const calleeName = callee.type === 'identifier'
|
|
1997
|
+
? (0, tree_sitter_helpers_1.getNodeText)(callee, this.source)
|
|
1998
|
+
: callee.type === 'member_expression'
|
|
1999
|
+
? (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source)
|
|
2000
|
+
: '';
|
|
2001
|
+
if (calleeName !== 'createApi' && calleeName !== 'injectEndpoints')
|
|
2002
|
+
return null;
|
|
2003
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
|
|
2004
|
+
if (!args)
|
|
2005
|
+
return null;
|
|
2006
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2007
|
+
const arg = args.namedChild(i);
|
|
2008
|
+
if (arg?.type !== 'object' && arg?.type !== 'object_expression')
|
|
2009
|
+
continue;
|
|
2010
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2011
|
+
const member = arg.namedChild(j);
|
|
2012
|
+
// Two equally-common spellings: `endpoints: build => ({...})` (pair with an
|
|
2013
|
+
// arrow value) and `endpoints(build) { return {...} }` (method shorthand).
|
|
2014
|
+
if (member?.type === 'pair') {
|
|
2015
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
2016
|
+
if (!key || (0, tree_sitter_helpers_1.getNodeText)(key, this.source) !== 'endpoints')
|
|
2017
|
+
continue;
|
|
2018
|
+
const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2019
|
+
if (value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
|
2020
|
+
return this.functionReturnedObject(value);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
else if (member?.type === 'method_definition') {
|
|
2024
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'name');
|
|
2025
|
+
if (!key || (0, tree_sitter_helpers_1.getNodeText)(key, this.source) !== 'endpoints')
|
|
2026
|
+
continue;
|
|
2027
|
+
return this.functionReturnedObject(member);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Extract each RTK Query endpoint (`getX: build.query({...})` / `build.mutation`)
|
|
2035
|
+
* as a function node named by the endpoint key, spanning its primary handler
|
|
2036
|
+
* (the `queryFn`/`query` arrow) so the fetch logic's calls attribute to the
|
|
2037
|
+
* endpoint. Without this an endpoint exists only as an object-literal property —
|
|
2038
|
+
* never a node — so the generated `useXQuery` hook can't be bridged to it.
|
|
2039
|
+
*/
|
|
2040
|
+
extractRtkEndpoints(obj) {
|
|
2041
|
+
for (let i = 0; i < obj.namedChildCount; i++) {
|
|
2042
|
+
const member = obj.namedChild(i);
|
|
2043
|
+
if (member?.type !== 'pair')
|
|
2044
|
+
continue;
|
|
2045
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
2046
|
+
const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2047
|
+
if (!key || value?.type !== 'call_expression')
|
|
2048
|
+
continue;
|
|
2049
|
+
// The value must be a builder dispatch `<builder>.query|mutation(...)`.
|
|
2050
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(value, 'function');
|
|
2051
|
+
if (callee?.type !== 'member_expression')
|
|
2052
|
+
continue;
|
|
2053
|
+
const method = (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source);
|
|
2054
|
+
if (method !== 'query' && method !== 'mutation' && method !== 'infiniteQuery')
|
|
2055
|
+
continue;
|
|
2056
|
+
const handler = this.rtkEndpointHandler(value);
|
|
2057
|
+
if (handler) {
|
|
2058
|
+
this.extractFunction(handler, this.objectKeyName(key));
|
|
2059
|
+
}
|
|
2060
|
+
else {
|
|
2061
|
+
// Factory / config-only handler (`queryFn: makeQueryFn(url)`): no function
|
|
2062
|
+
// literal to name. Mint a bare endpoint node spanning the builder call so
|
|
2063
|
+
// the generated hook still bridges to it, and walk the call so its handler
|
|
2064
|
+
// factory (and any inline transform) is captured as an outgoing edge.
|
|
2065
|
+
const epNode = this.createNode('function', this.objectKeyName(key), value, {
|
|
2066
|
+
signature: (0, tree_sitter_helpers_1.getNodeText)(value, this.source).slice(0, 80),
|
|
2067
|
+
});
|
|
2068
|
+
if (epNode) {
|
|
2069
|
+
this.nodeStack.push(epNode.id);
|
|
2070
|
+
this.visitFunctionBody(value, epNode.id);
|
|
2071
|
+
this.nodeStack.pop();
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* The primary handler arrow of a `build.query({ queryFn|query: (…) => … })`
|
|
2078
|
+
* endpoint — prefers `queryFn`, then `query`, else the first function-valued
|
|
2079
|
+
* property. Returns null when the endpoint is config-only (no handler arrow).
|
|
2080
|
+
*/
|
|
2081
|
+
rtkEndpointHandler(callNode) {
|
|
2082
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
|
|
2083
|
+
if (!args)
|
|
2084
|
+
return null;
|
|
2085
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2086
|
+
const arg = args.namedChild(i);
|
|
2087
|
+
if (arg?.type !== 'object' && arg?.type !== 'object_expression')
|
|
2088
|
+
continue;
|
|
2089
|
+
let queryFn = null;
|
|
2090
|
+
let query = null;
|
|
2091
|
+
let firstFn = null;
|
|
2092
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2093
|
+
const member = arg.namedChild(j);
|
|
2094
|
+
// The handler may be `queryFn: () => …` / `query: () => …` (pair) or the
|
|
2095
|
+
// method-shorthand `query(arg) { … }` / `queryFn(arg) { … }`.
|
|
2096
|
+
let fn = null;
|
|
2097
|
+
let kn = '';
|
|
2098
|
+
if (member?.type === 'pair') {
|
|
2099
|
+
const v = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2100
|
+
if (v?.type === 'arrow_function' || v?.type === 'function_expression') {
|
|
2101
|
+
fn = v;
|
|
2102
|
+
const k = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
2103
|
+
kn = k ? (0, tree_sitter_helpers_1.getNodeText)(k, this.source) : '';
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
else if (member?.type === 'method_definition') {
|
|
2107
|
+
fn = member;
|
|
2108
|
+
const k = (0, tree_sitter_helpers_1.getChildByField)(member, 'name');
|
|
2109
|
+
kn = k ? (0, tree_sitter_helpers_1.getNodeText)(k, this.source) : '';
|
|
2110
|
+
}
|
|
2111
|
+
if (!fn)
|
|
2112
|
+
continue;
|
|
2113
|
+
if (kn === 'queryFn')
|
|
2114
|
+
queryFn = fn;
|
|
2115
|
+
else if (kn === 'query')
|
|
2116
|
+
query = fn;
|
|
2117
|
+
if (!firstFn)
|
|
2118
|
+
firstFn = fn;
|
|
2119
|
+
}
|
|
2120
|
+
if (queryFn)
|
|
2121
|
+
return queryFn;
|
|
2122
|
+
if (query)
|
|
2123
|
+
return query;
|
|
2124
|
+
if (firstFn)
|
|
2125
|
+
return firstFn;
|
|
2126
|
+
}
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* RTK Query generated-hook bindings. `export const { useGetXQuery,
|
|
2131
|
+
* useUpdateYMutation } = someApi` destructures the hooks RTK generates per
|
|
2132
|
+
* endpoint off a createApi result. They are real exported symbols that
|
|
2133
|
+
* components import, but destructured bindings aren't otherwise extracted —
|
|
2134
|
+
* mint a function node per binding matching the RTK hook convention so the hook
|
|
2135
|
+
* resolves and the synthesizer can bridge it to its endpoint. Gated tight by the
|
|
2136
|
+
* caller (object-pattern off a bare identifier) + the name convention here, so
|
|
2137
|
+
* ordinary destructures stay unextracted.
|
|
2138
|
+
*/
|
|
2139
|
+
extractRtkHookBindings(pattern, isExported) {
|
|
2140
|
+
for (let i = 0; i < pattern.namedChildCount; i++) {
|
|
2141
|
+
const binding = pattern.namedChild(i);
|
|
2142
|
+
if (binding?.type !== 'shorthand_property_identifier_pattern')
|
|
2143
|
+
continue;
|
|
2144
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(binding, this.source);
|
|
2145
|
+
if (!RTK_HOOK_NAME_RE.test(name))
|
|
2146
|
+
continue;
|
|
2147
|
+
this.createNode('function', name, binding, {
|
|
2148
|
+
isExported,
|
|
2149
|
+
signature: '= RTK Query generated hook',
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/** Cheap per-file heuristic: the file carries ≥2 distinct Vue-store signals
|
|
2154
|
+
* (defineStore/createStore/Vuex, or the actions/mutations/getters/namespaced
|
|
2155
|
+
* vocabulary). Gates the non-exported `const actions = {…}` Vuex-module form so
|
|
2156
|
+
* a stray `const actions` in unrelated code is never mistaken for a store. */
|
|
2157
|
+
looksLikeVueStoreFile() {
|
|
2158
|
+
if (this.vueStoreFile !== null)
|
|
2159
|
+
return this.vueStoreFile;
|
|
2160
|
+
const seen = new Set();
|
|
2161
|
+
VUE_STORE_FILE_SIGNAL.lastIndex = 0;
|
|
2162
|
+
let m;
|
|
2163
|
+
while ((m = VUE_STORE_FILE_SIGNAL.exec(this.source))) {
|
|
2164
|
+
seen.add(m[0]);
|
|
2165
|
+
if (seen.size >= 2)
|
|
2166
|
+
break;
|
|
2167
|
+
}
|
|
2168
|
+
this.vueStoreFile = seen.size >= 2;
|
|
2169
|
+
return this.vueStoreFile;
|
|
2170
|
+
}
|
|
2171
|
+
/** True if an object literal has ≥1 inline function member (`key: () => …` /
|
|
2172
|
+
* `method(){}`) — distinguishes an inline action map (zustand/SvelteKit form
|
|
2173
|
+
* actions) from a Pinia SETUP store's all-shorthand `return { foo, bar }`
|
|
2174
|
+
* (whose functions are body-local consts, walked normally instead). */
|
|
2175
|
+
objectHasInlineFunctions(obj) {
|
|
2176
|
+
for (let i = 0; i < obj.namedChildCount; i++) {
|
|
2177
|
+
const member = obj.namedChild(i);
|
|
2178
|
+
if (member?.type === 'method_definition')
|
|
2179
|
+
return true;
|
|
2180
|
+
if (member?.type === 'pair') {
|
|
2181
|
+
const v = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2182
|
+
if (v?.type === 'arrow_function' || v?.type === 'function_expression')
|
|
2183
|
+
return true;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
/** Vue store action/mutation/getter collections defined INLINE in a store call:
|
|
2189
|
+
* `defineStore({ actions: {…}, getters: {…} })` (Pinia options form),
|
|
2190
|
+
* `defineStore('id', { actions: {…} })`, `createStore({ mutations: {…} })`,
|
|
2191
|
+
* `new Vuex.Store({ actions: {…} })`. Returns the object literals under those
|
|
2192
|
+
* keys so their methods become nodes. Gated on the store-factory callee. */
|
|
2193
|
+
findVueStoreCollectionObjects(callNode) {
|
|
2194
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function') ?? (0, tree_sitter_helpers_1.getChildByField)(callNode, 'constructor');
|
|
2195
|
+
if (!callee)
|
|
2196
|
+
return [];
|
|
2197
|
+
const calleeName = callee.type === 'identifier'
|
|
2198
|
+
? (0, tree_sitter_helpers_1.getNodeText)(callee, this.source)
|
|
2199
|
+
: callee.type === 'member_expression'
|
|
2200
|
+
? (0, tree_sitter_helpers_1.getNodeText)((0, tree_sitter_helpers_1.getChildByField)(callee, 'property') ?? callee, this.source)
|
|
2201
|
+
: '';
|
|
2202
|
+
if (!VUE_STORE_FACTORY_CALLEES.has(calleeName) && calleeName !== 'Store')
|
|
2203
|
+
return [];
|
|
2204
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
|
|
2205
|
+
if (!args)
|
|
2206
|
+
return [];
|
|
2207
|
+
const objects = [];
|
|
2208
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2209
|
+
const arg = args.namedChild(i);
|
|
2210
|
+
if (arg?.type !== 'object' && arg?.type !== 'object_expression')
|
|
2211
|
+
continue;
|
|
2212
|
+
for (let j = 0; j < arg.namedChildCount; j++) {
|
|
2213
|
+
const member = arg.namedChild(j);
|
|
2214
|
+
if (member?.type !== 'pair')
|
|
2215
|
+
continue;
|
|
2216
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
2217
|
+
if (!key || !VUE_STORE_COLLECTION_NAMES.has((0, tree_sitter_helpers_1.getNodeText)(key, this.source)))
|
|
2218
|
+
continue;
|
|
2219
|
+
const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2220
|
+
if (value && (value.type === 'object' || value.type === 'object_expression')) {
|
|
2221
|
+
objects.push(value);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return objects;
|
|
2226
|
+
}
|
|
2227
|
+
/** Extract the methods of a store-config object's `actions`/`mutations`/`getters`
|
|
2228
|
+
* properties. Used for the canonical Vuex MODULE shape `export default {
|
|
2229
|
+
* namespaced, actions: {…}, mutations: {…} }` — object-literal methods aren't
|
|
2230
|
+
* otherwise extracted, so the actions/mutations would never be nodes. */
|
|
2231
|
+
extractStoreCollectionMethods(configObj) {
|
|
2232
|
+
for (let j = 0; j < configObj.namedChildCount; j++) {
|
|
2233
|
+
const member = configObj.namedChild(j);
|
|
2234
|
+
if (member?.type !== 'pair')
|
|
2235
|
+
continue;
|
|
2236
|
+
const key = (0, tree_sitter_helpers_1.getChildByField)(member, 'key');
|
|
2237
|
+
if (!key || !VUE_STORE_COLLECTION_NAMES.has((0, tree_sitter_helpers_1.getNodeText)(key, this.source)))
|
|
2238
|
+
continue;
|
|
2239
|
+
const value = (0, tree_sitter_helpers_1.getChildByField)(member, 'value');
|
|
2240
|
+
if (value && (value.type === 'object' || value.type === 'object_expression')) {
|
|
2241
|
+
this.extractObjectLiteralFunctions(value);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
/** The SETUP function of a Pinia setup store (`defineStore('id', () => {…})`)
|
|
2246
|
+
* — an arrow/function arg with a block body. Returns null for the options form
|
|
2247
|
+
* (`defineStore({…})`) and for any non-defineStore call. The setup body's local
|
|
2248
|
+
* function consts are the store's actions; the generic body walk doesn't reach
|
|
2249
|
+
* them (nested functions are separate scopes), so they're extracted explicitly. */
|
|
2250
|
+
findPiniaSetupFn(callNode) {
|
|
2251
|
+
const callee = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'function');
|
|
2252
|
+
if (!callee || callee.type !== 'identifier' || (0, tree_sitter_helpers_1.getNodeText)(callee, this.source) !== 'defineStore')
|
|
2253
|
+
return null;
|
|
2254
|
+
const args = (0, tree_sitter_helpers_1.getChildByField)(callNode, 'arguments');
|
|
2255
|
+
if (!args)
|
|
2256
|
+
return null;
|
|
2257
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
2258
|
+
const arg = args.namedChild(i);
|
|
2259
|
+
if (arg?.type !== 'arrow_function' && arg?.type !== 'function_expression')
|
|
2260
|
+
continue;
|
|
2261
|
+
const body = (0, tree_sitter_helpers_1.getChildByField)(arg, 'body');
|
|
2262
|
+
if (body?.type === 'statement_block')
|
|
2263
|
+
return arg; // block body ⇒ setup form
|
|
2264
|
+
}
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2267
|
+
/** Extract a Pinia setup store's actions: the body-local `const foo = () => …`
|
|
2268
|
+
* / `function foo(){}` declarations, named by the binding. (State refs and other
|
|
2269
|
+
* consts are left to the normal value-extraction; only the functions matter as
|
|
2270
|
+
* the store's callable surface.) */
|
|
2271
|
+
extractPiniaSetupBody(setupFn) {
|
|
2272
|
+
const body = (0, tree_sitter_helpers_1.getChildByField)(setupFn, 'body');
|
|
2273
|
+
if (!body || body.type !== 'statement_block')
|
|
2274
|
+
return;
|
|
2275
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
2276
|
+
const stmt = body.namedChild(i);
|
|
2277
|
+
if (!stmt)
|
|
2278
|
+
continue;
|
|
2279
|
+
if (stmt.type === 'function_declaration') {
|
|
2280
|
+
this.extractFunction(stmt);
|
|
2281
|
+
}
|
|
2282
|
+
else if (this.extractor.variableTypes.includes(stmt.type)) {
|
|
2283
|
+
for (let j = 0; j < stmt.namedChildCount; j++) {
|
|
2284
|
+
const decl = stmt.namedChild(j);
|
|
2285
|
+
if (decl?.type !== 'variable_declarator')
|
|
2286
|
+
continue;
|
|
2287
|
+
const v = (0, tree_sitter_helpers_1.getChildByField)(decl, 'value');
|
|
2288
|
+
if (v?.type === 'arrow_function' || v?.type === 'function_expression') {
|
|
2289
|
+
this.extractFunction(v); // name resolved from the parent declarator
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
1312
2295
|
/**
|
|
1313
2296
|
* Extract a variable declaration (const, let, var, etc.)
|
|
1314
2297
|
*
|
|
@@ -1338,8 +2321,15 @@ class TreeSitterExtractor {
|
|
|
1338
2321
|
const valueNode = (0, tree_sitter_helpers_1.getChildByField)(child, 'value');
|
|
1339
2322
|
if (nameNode) {
|
|
1340
2323
|
// Skip destructured patterns (e.g., `let { x, y } = $props()` in Svelte)
|
|
1341
|
-
// These produce ugly multi-line names like "{ class: className }"
|
|
2324
|
+
// These produce ugly multi-line names like "{ class: className }".
|
|
2325
|
+
// EXCEPT `export const { useGetXQuery } = someApi` — the RTK Query
|
|
2326
|
+
// generated hooks: real exported symbols destructured off a createApi
|
|
2327
|
+
// result. Mint a node per binding matching the hook convention (gated
|
|
2328
|
+
// on a bare-identifier RHS so ordinary destructures stay skipped).
|
|
1342
2329
|
if (nameNode.type === 'object_pattern' || nameNode.type === 'array_pattern') {
|
|
2330
|
+
if (nameNode.type === 'object_pattern' && valueNode?.type === 'identifier') {
|
|
2331
|
+
this.extractRtkHookBindings(nameNode, isExported);
|
|
2332
|
+
}
|
|
1343
2333
|
continue;
|
|
1344
2334
|
}
|
|
1345
2335
|
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
@@ -1351,6 +2341,25 @@ class TreeSitterExtractor {
|
|
|
1351
2341
|
// Capture first 100 chars of initializer for context (stored in signature for searchability)
|
|
1352
2342
|
const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
|
|
1353
2343
|
const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
|
|
2344
|
+
// React HOC-wrapped components (`forwardRef`/`memo`/`styled`) — see
|
|
2345
|
+
// reactComponentHoc. The initializer is a call / tagged-template (not
|
|
2346
|
+
// a bare arrow), so without this the const is a plain `constant`,
|
|
2347
|
+
// which the JSX-render synthesizer and component resolution both skip
|
|
2348
|
+
// → `<Button/>` usages get no edge and callers/impact return empty
|
|
2349
|
+
// (the whole shadcn/ui design-system pattern, #841). PascalCase-gated
|
|
2350
|
+
// to the component naming convention so a memoization util
|
|
2351
|
+
// (`const cache = memo(fn)`) stays a constant.
|
|
2352
|
+
if (valueNode && /^[A-Z]/.test(name)) {
|
|
2353
|
+
const hoc = this.reactComponentHoc(valueNode);
|
|
2354
|
+
if (hoc) {
|
|
2355
|
+
this.extractReactComponentNode(name, child, hoc.inner, {
|
|
2356
|
+
docstring,
|
|
2357
|
+
signature: initSignature,
|
|
2358
|
+
isExported,
|
|
2359
|
+
});
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
1354
2363
|
const varNode = this.createNode(kind, name, child, {
|
|
1355
2364
|
docstring,
|
|
1356
2365
|
signature: initSignature,
|
|
@@ -1381,21 +2390,65 @@ class TreeSitterExtractor {
|
|
|
1381
2390
|
: valueNode?.type === 'call_expression'
|
|
1382
2391
|
? this.findInitializerReturnedObject(valueNode)
|
|
1383
2392
|
: null;
|
|
1384
|
-
|
|
2393
|
+
// Only treat as an inline object-of-functions when the object actually
|
|
2394
|
+
// HAS inline functions. A Pinia SETUP store `defineStore('id', () => {
|
|
2395
|
+
// const foo = …; return { foo } })` returns an ALL-SHORTHAND object
|
|
2396
|
+
// whose functions are body-local consts — it must fall through to a
|
|
2397
|
+
// normal body walk (extracting those consts), not be skipped here.
|
|
2398
|
+
const hasInlineFns = !!objectOfFns && this.objectHasInlineFunctions(objectOfFns);
|
|
2399
|
+
const extractObjectMethods = isExported && !!objectOfFns && hasInlineFns;
|
|
2400
|
+
// RTK Query: `createApi`/`injectEndpoints` define endpoints as
|
|
2401
|
+
// object-literal properties whose values are `build.query/mutation(...)`
|
|
2402
|
+
// calls — nested under an `endpoints` arrow, so neither the
|
|
2403
|
+
// object-of-functions path above nor the normal walk extracts them.
|
|
2404
|
+
// Extract each endpoint as a function node (named by its key), and skip
|
|
2405
|
+
// walking the createApi call body (its handler arrows are extracted
|
|
2406
|
+
// individually below, exactly like the store-factory case).
|
|
2407
|
+
const rtkEndpoints = valueNode?.type === 'call_expression' ? this.findRtkEndpointsObject(valueNode) : null;
|
|
2408
|
+
// Pinia SETUP store: `defineStore('id', () => { const foo = …; return {…} })`.
|
|
2409
|
+
// Its actions are body-local consts the generic walk can't reach.
|
|
2410
|
+
const piniaSetup = valueNode?.type === 'call_expression' ? this.findPiniaSetupFn(valueNode) : null;
|
|
2411
|
+
// Vue store collections — make `actions`/`mutations`/`getters` findable
|
|
2412
|
+
// function nodes (the foundation under any later dispatch-bridge synth).
|
|
2413
|
+
// Two positions: INLINE in a store call (`defineStore({ actions: {…} })`
|
|
2414
|
+
// / `createStore` / `new Vuex.Store`), and the non-exported Vuex-MODULE
|
|
2415
|
+
// form (`const actions = {…}` at a store file's top level, wired via a
|
|
2416
|
+
// `export default { actions }`). The Pinia SETUP form is handled by the
|
|
2417
|
+
// body walk above (its actions are local consts).
|
|
2418
|
+
const storeCollections = [];
|
|
2419
|
+
if (valueNode?.type === 'call_expression' || valueNode?.type === 'new_expression') {
|
|
2420
|
+
storeCollections.push(...this.findVueStoreCollectionObjects(valueNode));
|
|
2421
|
+
}
|
|
2422
|
+
if (objectOfFns && !extractObjectMethods &&
|
|
2423
|
+
VUE_STORE_COLLECTION_NAMES.has(name) && this.looksLikeVueStoreFile()) {
|
|
2424
|
+
storeCollections.push(objectOfFns);
|
|
2425
|
+
}
|
|
1385
2426
|
// Visit the initializer body for calls — EXCEPT object literals (their
|
|
1386
2427
|
// function-valued properties are extracted below) and the store-factory
|
|
1387
|
-
// call whose
|
|
1388
|
-
// the whole call would re-visit those
|
|
1389
|
-
// their inner calls to the file
|
|
2428
|
+
// / createApi / store-collection call whose nested objects we extract
|
|
2429
|
+
// method-by-method below (walking the whole call would re-visit those
|
|
2430
|
+
// method arrows and mis-attribute their inner calls to the file scope).
|
|
1390
2431
|
if (valueNode &&
|
|
1391
2432
|
valueNode.type !== 'object' &&
|
|
1392
2433
|
valueNode.type !== 'object_expression' &&
|
|
1393
|
-
!(extractObjectMethods && valueNode.type === 'call_expression')
|
|
2434
|
+
!(extractObjectMethods && valueNode.type === 'call_expression') &&
|
|
2435
|
+
!rtkEndpoints &&
|
|
2436
|
+
!piniaSetup &&
|
|
2437
|
+
storeCollections.length === 0) {
|
|
1394
2438
|
this.visitFunctionBody(valueNode, '');
|
|
1395
2439
|
}
|
|
1396
2440
|
if (extractObjectMethods && objectOfFns) {
|
|
1397
2441
|
this.extractObjectLiteralFunctions(objectOfFns);
|
|
1398
2442
|
}
|
|
2443
|
+
if (rtkEndpoints) {
|
|
2444
|
+
this.extractRtkEndpoints(rtkEndpoints);
|
|
2445
|
+
}
|
|
2446
|
+
if (piniaSetup) {
|
|
2447
|
+
this.extractPiniaSetupBody(piniaSetup);
|
|
2448
|
+
}
|
|
2449
|
+
for (const coll of storeCollections) {
|
|
2450
|
+
this.extractObjectLiteralFunctions(coll);
|
|
2451
|
+
}
|
|
1399
2452
|
}
|
|
1400
2453
|
}
|
|
1401
2454
|
}
|
|
@@ -1404,7 +2457,9 @@ class TreeSitterExtractor {
|
|
|
1404
2457
|
// Python/Ruby assignment: left = right
|
|
1405
2458
|
const left = (0, tree_sitter_helpers_1.getChildByField)(node, 'left') || node.namedChild(0);
|
|
1406
2459
|
const right = (0, tree_sitter_helpers_1.getChildByField)(node, 'right') || node.namedChild(1);
|
|
1407
|
-
|
|
2460
|
+
// Ruby constant assignments (`MAX = 3`) have a `constant`-typed LHS, not
|
|
2461
|
+
// `identifier`; without this they were never extracted as symbols at all.
|
|
2462
|
+
if (left && (left.type === 'identifier' || left.type === 'constant')) {
|
|
1408
2463
|
const name = (0, tree_sitter_helpers_1.getNodeText)(left, this.source);
|
|
1409
2464
|
// Skip if name starts with lowercase and looks like a function call result
|
|
1410
2465
|
// Python constants are usually UPPER_CASE
|
|
@@ -1422,12 +2477,13 @@ class TreeSitterExtractor {
|
|
|
1422
2477
|
const specs = node.namedChildren.filter(c => c.type === 'var_spec' || c.type === 'const_spec');
|
|
1423
2478
|
for (const spec of specs) {
|
|
1424
2479
|
const nameNode = spec.namedChild(0);
|
|
2480
|
+
let varNode = null;
|
|
1425
2481
|
if (nameNode && nameNode.type === 'identifier') {
|
|
1426
2482
|
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
1427
2483
|
const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null;
|
|
1428
2484
|
const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
|
|
1429
2485
|
const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined;
|
|
1430
|
-
this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
|
|
2486
|
+
varNode = this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, {
|
|
1431
2487
|
docstring,
|
|
1432
2488
|
signature: initSignature,
|
|
1433
2489
|
});
|
|
@@ -1437,9 +2493,18 @@ class TreeSitterExtractor {
|
|
|
1437
2493
|
// implementations) or `var c = pkg.New()` are extracted as
|
|
1438
2494
|
// instantiates/calls dependencies — the body walker only covers
|
|
1439
2495
|
// initializers inside functions, not these top-level declarations.
|
|
2496
|
+
// Scope the walk to the declared symbol so a call inside an anonymous
|
|
2497
|
+
// func_literal initializer — a cobra `RunE: func(){…}` handler, a
|
|
2498
|
+
// goroutine or callback closure — attributes to the var instead of
|
|
2499
|
+
// leaking to the file node (which reads as "no caller"), issue #693.
|
|
1440
2500
|
const valueField = (0, tree_sitter_helpers_1.getChildByField)(spec, 'value');
|
|
1441
|
-
if (valueField)
|
|
1442
|
-
|
|
2501
|
+
if (valueField) {
|
|
2502
|
+
if (varNode)
|
|
2503
|
+
this.nodeStack.push(varNode.id);
|
|
2504
|
+
this.visitFunctionBody(valueField, varNode?.id ?? '');
|
|
2505
|
+
if (varNode)
|
|
2506
|
+
this.nodeStack.pop();
|
|
2507
|
+
}
|
|
1443
2508
|
}
|
|
1444
2509
|
// Handle short_var_declaration (:=)
|
|
1445
2510
|
if (node.type === 'short_var_declaration') {
|
|
@@ -1481,6 +2546,65 @@ class TreeSitterExtractor {
|
|
|
1481
2546
|
this.createNode(kind, name, nameNode, { docstring, signature: initSignature, isExported });
|
|
1482
2547
|
});
|
|
1483
2548
|
}
|
|
2549
|
+
else if (this.language === 'c') {
|
|
2550
|
+
// C: a `declaration` node's name nests inside the `declarator` field —
|
|
2551
|
+
// `init_declarator` (with value) or bare/pointer/array declarators (no
|
|
2552
|
+
// value); a `function_declarator` is a prototype, not a variable. The
|
|
2553
|
+
// generic fallback below only finds a *direct* identifier child, which C
|
|
2554
|
+
// never has, so file-scope consts/globals went unextracted entirely (and
|
|
2555
|
+
// so had no impact-radius edges). Only file-scope declarations are tracked
|
|
2556
|
+
// — locals inside a function body are skipped (a `static const` table read
|
|
2557
|
+
// by same-file functions is the value the impact graph wants, not every
|
|
2558
|
+
// block-local). C allows several declarators per declaration
|
|
2559
|
+
// (`int a = 1, b = 2;`), so iterate them.
|
|
2560
|
+
if (!hasFunctionAncestor(node)) {
|
|
2561
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2562
|
+
const child = node.namedChild(i);
|
|
2563
|
+
if (!child)
|
|
2564
|
+
continue;
|
|
2565
|
+
// Accept only `init_declarator` (has a value) and pointer/array
|
|
2566
|
+
// declarators. A *bare* `identifier` declarator is deliberately
|
|
2567
|
+
// skipped: an unknown leading macro (`CURL_EXTERN`, `XXH_PUBLIC_API`)
|
|
2568
|
+
// makes tree-sitter-c misparse a prototype `MACRO RetType fn(args);`
|
|
2569
|
+
// as a declaration whose "variable" is the bare return-type
|
|
2570
|
+
// identifier, splitting `fn(args)` off as a bogus expression — minting
|
|
2571
|
+
// a spurious type-named global for every macro-prefixed prototype in a
|
|
2572
|
+
// header. Those misparses are always bare identifiers; real
|
|
2573
|
+
// consts/tables always carry an initializer. The only legit loss is
|
|
2574
|
+
// uninitialized scalar globals (`static int g;`).
|
|
2575
|
+
if (child.type !== 'init_declarator' &&
|
|
2576
|
+
child.type !== 'pointer_declarator' &&
|
|
2577
|
+
child.type !== 'array_declarator') {
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
const nameNode = cDeclaratorIdentifier(child);
|
|
2581
|
+
if (!nameNode)
|
|
2582
|
+
continue;
|
|
2583
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source);
|
|
2584
|
+
if (!name)
|
|
2585
|
+
continue;
|
|
2586
|
+
const valueNode = child.type === 'init_declarator' ? (0, tree_sitter_helpers_1.getChildByField)(child, 'value') : null;
|
|
2587
|
+
const initValue = valueNode ? (0, tree_sitter_helpers_1.getNodeText)(valueNode, this.source).slice(0, 100) : undefined;
|
|
2588
|
+
const initSignature = initValue
|
|
2589
|
+
? `= ${initValue}${initValue.length >= 100 ? '...' : ''}`
|
|
2590
|
+
: undefined;
|
|
2591
|
+
this.createNode(kind, name, child, { docstring, signature: initSignature, isExported });
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
else if (this.language === 'swift') {
|
|
2596
|
+
// Swift top-level property (`let X = …` / `var Y = …`). The name nests in
|
|
2597
|
+
// a `pattern`, which the generic fallback can't read, so top-level Swift
|
|
2598
|
+
// constants/globals went unextracted. A top-level `let`→`constant`,
|
|
2599
|
+
// `var`→`variable`; a computed property (getter, no value) is skipped.
|
|
2600
|
+
const { nameNode, isLet, isComputed } = swiftPropertyInfo(node, this.source);
|
|
2601
|
+
if (nameNode && !isComputed) {
|
|
2602
|
+
this.createNode(isLet ? 'constant' : 'variable', (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source), node, {
|
|
2603
|
+
docstring,
|
|
2604
|
+
isExported,
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
1484
2608
|
else {
|
|
1485
2609
|
// Generic fallback for other languages
|
|
1486
2610
|
// Try to find identifier children
|
|
@@ -1603,6 +2727,9 @@ class TreeSitterExtractor {
|
|
|
1603
2727
|
// an unrelated class method picked by path-proximity (#359).
|
|
1604
2728
|
if (this.language === 'typescript' || this.language === 'tsx') {
|
|
1605
2729
|
this.extractTsTypeAliasMembers(value, typeAliasNode);
|
|
2730
|
+
// `type List = [ Service<'name', Req, Resp>, … ]` — surface each
|
|
2731
|
+
// entry's string-literal name as a searchable member (issue #634).
|
|
2732
|
+
this.extractTsTupleContractNames(value, typeAliasNode);
|
|
1606
2733
|
}
|
|
1607
2734
|
}
|
|
1608
2735
|
}
|
|
@@ -1689,6 +2816,82 @@ class TreeSitterExtractor {
|
|
|
1689
2816
|
}
|
|
1690
2817
|
this.nodeStack.pop();
|
|
1691
2818
|
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Surface the string-literal "names" of a TypeScript service/contract
|
|
2821
|
+
* registry written as a tuple of generic instantiations:
|
|
2822
|
+
*
|
|
2823
|
+
* type MyServiceList = [
|
|
2824
|
+
* Service<'query_apply_record', Req, Resp>,
|
|
2825
|
+
* Service<'apply_confirm', Req, Resp>,
|
|
2826
|
+
* ];
|
|
2827
|
+
*
|
|
2828
|
+
* Each `Service<'name', …>` tags an entry with a string-literal name that a
|
|
2829
|
+
* dynamic factory (`createService<MyServiceList>()`) turns into a callable
|
|
2830
|
+
* property (`api.query_apply_record(…)`). Static extraction otherwise never
|
|
2831
|
+
* sees that name — it's a type argument, not a declaration — so
|
|
2832
|
+
* `homegraph query query_apply_record` returned nothing (issue #634). We emit
|
|
2833
|
+
* each name as a `method` node under the type alias (qualifiedName
|
|
2834
|
+
* `MyServiceList::query_apply_record`) so it's searchable and resolvable as a
|
|
2835
|
+
* symbol. (A call through the proxy, `api.query_apply_record(…)`, still
|
|
2836
|
+
* resolves to the imported `api` binding — the receiver's type isn't known —
|
|
2837
|
+
* so this fixes discoverability, not the per-method call edge.)
|
|
2838
|
+
*
|
|
2839
|
+
* Scope is deliberately narrow to avoid noise: only a string literal that is
|
|
2840
|
+
* a DIRECT type argument of a `generic_type` that is itself a DIRECT element
|
|
2841
|
+
* of a `tuple_type`. This excludes utility types (`Pick`/`Omit`/`Record` are
|
|
2842
|
+
* never written as tuples) and string args nested deeper
|
|
2843
|
+
* (`Service<'a', Pick<U, 'id'>>` yields only `a`, never `id`). Names must be
|
|
2844
|
+
* valid identifiers, which also rules out route paths / arbitrary strings.
|
|
2845
|
+
*/
|
|
2846
|
+
extractTsTupleContractNames(value, typeAliasNode) {
|
|
2847
|
+
const tuples = [];
|
|
2848
|
+
const collectTuples = (n, depth) => {
|
|
2849
|
+
if (depth > 6)
|
|
2850
|
+
return; // a type expression is shallow; cap defensively
|
|
2851
|
+
if (n.type === 'tuple_type')
|
|
2852
|
+
tuples.push(n);
|
|
2853
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
2854
|
+
const c = n.namedChild(i);
|
|
2855
|
+
if (c)
|
|
2856
|
+
collectTuples(c, depth + 1);
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2859
|
+
collectTuples(value, 0);
|
|
2860
|
+
if (tuples.length === 0)
|
|
2861
|
+
return;
|
|
2862
|
+
this.nodeStack.push(typeAliasNode.id);
|
|
2863
|
+
for (const tuple of tuples) {
|
|
2864
|
+
for (let i = 0; i < tuple.namedChildCount; i++) {
|
|
2865
|
+
const entry = tuple.namedChild(i);
|
|
2866
|
+
if (!entry || entry.type !== 'generic_type')
|
|
2867
|
+
continue;
|
|
2868
|
+
const typeArgs = (0, tree_sitter_helpers_1.getChildByField)(entry, 'type_arguments');
|
|
2869
|
+
if (!typeArgs)
|
|
2870
|
+
continue;
|
|
2871
|
+
for (let j = 0; j < typeArgs.namedChildCount; j++) {
|
|
2872
|
+
const arg = typeArgs.namedChild(j);
|
|
2873
|
+
if (!arg || arg.type !== 'literal_type')
|
|
2874
|
+
continue;
|
|
2875
|
+
// literal_type wraps the actual literal; only a string is a name.
|
|
2876
|
+
const strNode = arg.namedChild(0);
|
|
2877
|
+
if (!strNode || strNode.type !== 'string')
|
|
2878
|
+
continue;
|
|
2879
|
+
const name = (0, tree_sitter_helpers_1.getNodeText)(strNode, this.source)
|
|
2880
|
+
.trim()
|
|
2881
|
+
.replace(/^['"`]/, '')
|
|
2882
|
+
.replace(/['"`]$/, '');
|
|
2883
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
|
|
2884
|
+
continue;
|
|
2885
|
+
const signature = (0, tree_sitter_helpers_1.getNodeText)(entry, this.source).replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
2886
|
+
this.createNode('method', name, entry, {
|
|
2887
|
+
signature,
|
|
2888
|
+
qualifiedName: `${typeAliasNode.name}::${name}`,
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
this.nodeStack.pop();
|
|
2894
|
+
}
|
|
1692
2895
|
/**
|
|
1693
2896
|
* `foo: () => T` → property_signature whose type_annotation contains a
|
|
1694
2897
|
* `function_type`. Treat that as a method-shaped contract member, since
|
|
@@ -2202,6 +3405,57 @@ class TreeSitterExtractor {
|
|
|
2202
3405
|
// single-dot receiver regex fails. Pull out the immediate field after `this.`
|
|
2203
3406
|
// so the receiver is the field name (`userbo`), which the resolver can then
|
|
2204
3407
|
// look up in the enclosing class's field declarations.
|
|
3408
|
+
// PHP static-factory fluent chain: `Cls::for($x)->method()` — the receiver
|
|
3409
|
+
// is itself a static call, so resolution must infer the method's class
|
|
3410
|
+
// from what `Cls::for` RETURNS (its `: self` / `: static` / `: Type`),
|
|
3411
|
+
// #608 (mirrors the C++ chain fix in #645). Encode `<Cls::factory>().<method>`;
|
|
3412
|
+
// the `().` marker lets the PHP resolver split it. The receiver text
|
|
3413
|
+
// (`Cls::for('x')`) carries the args, so without this it degrades to an
|
|
3414
|
+
// unresolvable string and the call edge is dropped.
|
|
3415
|
+
if (methodName && this.language === 'php' && objectField.type === 'scoped_call_expression') {
|
|
3416
|
+
const innerScope = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'scope');
|
|
3417
|
+
const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
|
|
3418
|
+
if (innerScope && innerName) {
|
|
3419
|
+
calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerScope, this.source)}::${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
|
|
3420
|
+
}
|
|
3421
|
+
else {
|
|
3422
|
+
calleeName = methodName;
|
|
3423
|
+
}
|
|
3424
|
+
if (calleeName) {
|
|
3425
|
+
this.unresolvedReferences.push({
|
|
3426
|
+
fromNodeId: callerId,
|
|
3427
|
+
referenceName: calleeName,
|
|
3428
|
+
referenceKind: 'calls',
|
|
3429
|
+
line: node.startPosition.row + 1,
|
|
3430
|
+
column: node.startPosition.column,
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
// Java static-factory / fluent chain: `Foo.getInstance().bar()` — the
|
|
3436
|
+
// receiver is itself a method call, so resolution must infer bar's class
|
|
3437
|
+
// from what `Foo.getInstance` RETURNS (its declared return type), the
|
|
3438
|
+
// #645/#608 mechanism. Encode `<inner-receiver>.<inner-method>().<method>`;
|
|
3439
|
+
// the `().` marker lets the Java chain resolver split it, and normalizing to
|
|
3440
|
+
// empty parens drops any factory args (`Foo.create(cfg).bar()`) that would
|
|
3441
|
+
// otherwise leave a `(cfg)` in the receiver text and break the split.
|
|
3442
|
+
if (methodName &&
|
|
3443
|
+
this.language === 'java' &&
|
|
3444
|
+
objectField.type === 'method_invocation') {
|
|
3445
|
+
const innerObj = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
|
|
3446
|
+
const innerName = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'name');
|
|
3447
|
+
if (innerObj && innerName) {
|
|
3448
|
+
calleeName = `${(0, tree_sitter_helpers_1.getNodeText)(innerObj, this.source)}.${(0, tree_sitter_helpers_1.getNodeText)(innerName, this.source)}().${methodName}`;
|
|
3449
|
+
this.unresolvedReferences.push({
|
|
3450
|
+
fromNodeId: callerId,
|
|
3451
|
+
referenceName: calleeName,
|
|
3452
|
+
referenceKind: 'calls',
|
|
3453
|
+
line: node.startPosition.row + 1,
|
|
3454
|
+
column: node.startPosition.column,
|
|
3455
|
+
});
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
2205
3459
|
let receiverName;
|
|
2206
3460
|
if (objectField.type === 'field_access') {
|
|
2207
3461
|
const inner = (0, tree_sitter_helpers_1.getChildByField)(objectField, 'object');
|
|
@@ -2290,6 +3544,39 @@ class TreeSitterExtractor {
|
|
|
2290
3544
|
calleeName = methodName;
|
|
2291
3545
|
}
|
|
2292
3546
|
}
|
|
3547
|
+
else if (receiverField && receiverField.type === 'message_expression' && /^\w+$/.test(methodName)) {
|
|
3548
|
+
// Chained message send `[[Foo create] doIt]` — the receiver is itself a
|
|
3549
|
+
// class message. Recover the inner `Class.selector` and encode
|
|
3550
|
+
// `Class.selector().doIt` so resolution infers doIt's class from what
|
|
3551
|
+
// `Class.selector` RETURNS (#645/#608). Only a CLASS-factory chain
|
|
3552
|
+
// (capitalized inner receiver); a unary outer selector is required
|
|
3553
|
+
// because the chain resolver's method part is `\w+` (no `:`). An
|
|
3554
|
+
// instance chain (`[[obj foo] bar]`, lowercase inner) stays bare.
|
|
3555
|
+
const innerRecv = (0, tree_sitter_helpers_1.getChildByField)(receiverField, 'receiver');
|
|
3556
|
+
const innerRecvName = innerRecv ? (0, tree_sitter_helpers_1.getNodeText)(innerRecv, this.source) : '';
|
|
3557
|
+
if (innerRecv?.type === 'identifier' && /^[A-Z]/.test(innerRecvName)) {
|
|
3558
|
+
const innerKw = [];
|
|
3559
|
+
for (let i = 0; i < receiverField.namedChildCount; i++) {
|
|
3560
|
+
if (receiverField.fieldNameForNamedChild(i) === 'method') {
|
|
3561
|
+
const kw = receiverField.namedChild(i);
|
|
3562
|
+
if (kw)
|
|
3563
|
+
innerKw.push((0, tree_sitter_helpers_1.getNodeText)(kw, this.source));
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
let innerColon = false;
|
|
3567
|
+
for (let i = 0; i < receiverField.childCount; i++) {
|
|
3568
|
+
if (receiverField.child(i)?.type === ':') {
|
|
3569
|
+
innerColon = true;
|
|
3570
|
+
break;
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
const innerSelector = innerColon ? innerKw.map((k) => `${k}:`).join('') : innerKw[0];
|
|
3574
|
+
calleeName = innerSelector ? `${innerRecvName}.${innerSelector}().${methodName}` : methodName;
|
|
3575
|
+
}
|
|
3576
|
+
else {
|
|
3577
|
+
calleeName = methodName;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
2293
3580
|
else {
|
|
2294
3581
|
calleeName = methodName;
|
|
2295
3582
|
}
|
|
@@ -2334,6 +3621,67 @@ class TreeSitterExtractor {
|
|
|
2334
3621
|
calleeName = methodName;
|
|
2335
3622
|
}
|
|
2336
3623
|
}
|
|
3624
|
+
else if ((this.language === 'cpp' ||
|
|
3625
|
+
this.language === 'c' ||
|
|
3626
|
+
this.language === 'kotlin' ||
|
|
3627
|
+
this.language === 'swift' ||
|
|
3628
|
+
this.language === 'rust' ||
|
|
3629
|
+
this.language === 'go' ||
|
|
3630
|
+
this.language === 'scala') &&
|
|
3631
|
+
receiver &&
|
|
3632
|
+
receiver.type === 'call_expression') {
|
|
3633
|
+
// Receiver that is itself a call — `Foo::instance().bar()`,
|
|
3634
|
+
// `openSession()->run()`, `mgr.view().render()` (C/C++),
|
|
3635
|
+
// `Foo.getInstance().bar()` (Kotlin) / `Foo.make().draw()` (Swift),
|
|
3636
|
+
// `Foo::new().bar()` (Rust), or `New().Method()` (Go). Keep the inner
|
|
3637
|
+
// call so resolution can infer bar()'s class from what the inner call
|
|
3638
|
+
// RETURNS (#645/#608). Encode as `<innerCallee>().<method>`; the `().`
|
|
3639
|
+
// marker never appears in an ordinary ref, so the resolver can detect
|
|
3640
|
+
// and split it. Other languages keep the bare-name behavior below.
|
|
3641
|
+
let innerCallee;
|
|
3642
|
+
let reencode;
|
|
3643
|
+
if (this.language === 'kotlin' || this.language === 'swift') {
|
|
3644
|
+
// tree-sitter-kotlin/swift expose the inner callee as the
|
|
3645
|
+
// call_expression's first named child (a navigation_expression
|
|
3646
|
+
// `Foo.getInstance`, or a bare identifier for a free/constructor call).
|
|
3647
|
+
const innerNav = receiver.namedChild(0);
|
|
3648
|
+
innerCallee = innerNav ? (0, tree_sitter_helpers_1.getNodeText)(innerNav, this.source).replace(/\s+/g, '') : '';
|
|
3649
|
+
// Only re-encode a CLASS / companion-factory / constructor chain,
|
|
3650
|
+
// whose receiver chain starts with a capitalized type
|
|
3651
|
+
// (`Foo.getInstance().bar()`, `Foo().bar()`). An instance chain
|
|
3652
|
+
// (`list.filter{}.map{}`) has a lowercase receiver whose type we
|
|
3653
|
+
// can't recover here — re-encoding it would only drop the edge (no
|
|
3654
|
+
// chain resolution, no bare-name fallback), regressing recall in
|
|
3655
|
+
// fluent codebases. Leave those to the bare-name path.
|
|
3656
|
+
reencode = /^[A-Z]/.test(innerCallee);
|
|
3657
|
+
}
|
|
3658
|
+
else {
|
|
3659
|
+
const innerFn = (0, tree_sitter_helpers_1.getChildByField)(receiver, 'function');
|
|
3660
|
+
innerCallee = innerFn
|
|
3661
|
+
? (0, tree_sitter_helpers_1.getNodeText)(innerFn, this.source).replace(/->/g, '.').replace(/\s+/g, '')
|
|
3662
|
+
: '';
|
|
3663
|
+
// Rust: only re-encode an associated-function chain
|
|
3664
|
+
// (`Foo::new().bar()`), whose inner callee is a path/`scoped_identifier`.
|
|
3665
|
+
// Go: only a bare package-level factory chain (`New().Method()`),
|
|
3666
|
+
// whose inner callee is an `identifier`. An instance chain
|
|
3667
|
+
// (`x.foo().bar()` Rust, `obj.Method().Other()` Go) keeps bare-name —
|
|
3668
|
+
// the resolver can't recover a variable's type, so re-encoding would
|
|
3669
|
+
// only drop the edge. C/C++ re-encode any inner.
|
|
3670
|
+
if (this.language === 'rust')
|
|
3671
|
+
reencode = innerFn?.type === 'scoped_identifier';
|
|
3672
|
+
else if (this.language === 'go')
|
|
3673
|
+
reencode = innerFn?.type === 'identifier';
|
|
3674
|
+
// Scala: only a companion-factory / case-class-apply chain whose
|
|
3675
|
+
// receiver chain starts with a capitalized type (`Foo.create().bar()`,
|
|
3676
|
+
// `Foo(args).bar()`). An instance chain (`list.map().filter()`) has a
|
|
3677
|
+
// lowercase receiver whose type we can't recover — leave it bare.
|
|
3678
|
+
else if (this.language === 'scala')
|
|
3679
|
+
reencode = /^[A-Z]/.test(innerCallee);
|
|
3680
|
+
else
|
|
3681
|
+
reencode = !!innerCallee;
|
|
3682
|
+
}
|
|
3683
|
+
calleeName = reencode ? `${innerCallee}().${methodName}` : methodName;
|
|
3684
|
+
}
|
|
2337
3685
|
else {
|
|
2338
3686
|
calleeName = methodName;
|
|
2339
3687
|
}
|
|
@@ -2343,6 +3691,24 @@ class TreeSitterExtractor {
|
|
|
2343
3691
|
// Scoped call: Module::function()
|
|
2344
3692
|
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
2345
3693
|
}
|
|
3694
|
+
else if (this.language === 'csharp' && func.type === 'member_access_expression') {
|
|
3695
|
+
// C# member call `recv.Method(...)`. When the receiver is itself a call
|
|
3696
|
+
// — a chained factory `Foo.Create(args).Bar()` — encode `inner().Bar`
|
|
3697
|
+
// with normalized empty parens so resolution can infer Bar's class from
|
|
3698
|
+
// what `Foo.Create` RETURNS (#645/#608). A non-call receiver keeps the
|
|
3699
|
+
// full member-access text (the existing `recv.Method` behavior).
|
|
3700
|
+
const recv = (0, tree_sitter_helpers_1.getChildByField)(func, 'expression');
|
|
3701
|
+
const nameNode = (0, tree_sitter_helpers_1.getChildByField)(func, 'name');
|
|
3702
|
+
const methodName = nameNode ? (0, tree_sitter_helpers_1.getNodeText)(nameNode, this.source) : '';
|
|
3703
|
+
if (recv && recv.type === 'invocation_expression' && methodName) {
|
|
3704
|
+
const innerFunc = (0, tree_sitter_helpers_1.getChildByField)(recv, 'function');
|
|
3705
|
+
const innerCallee = innerFunc ? (0, tree_sitter_helpers_1.getNodeText)(innerFunc, this.source).replace(/\s+/g, '') : '';
|
|
3706
|
+
calleeName = innerCallee ? `${innerCallee}().${methodName}` : methodName;
|
|
3707
|
+
}
|
|
3708
|
+
else {
|
|
3709
|
+
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
2346
3712
|
else {
|
|
2347
3713
|
calleeName = (0, tree_sitter_helpers_1.getNodeText)(func, this.source);
|
|
2348
3714
|
}
|
|
@@ -2796,6 +4162,9 @@ class TreeSitterExtractor {
|
|
|
2796
4162
|
return;
|
|
2797
4163
|
const visitForCallsAndStructure = (node) => {
|
|
2798
4164
|
const nodeType = node.type;
|
|
4165
|
+
// Function-as-value capture (#756) — function bodies are walked here,
|
|
4166
|
+
// not in visitNode, so the capture hook must fire in both walkers.
|
|
4167
|
+
this.maybeCaptureFnRefs(node, nodeType);
|
|
2799
4168
|
// Rocket route-registration macros (`routes![…]` / `catchers![…]`): the
|
|
2800
4169
|
// handler paths live in a raw token tree the call walker can't see.
|
|
2801
4170
|
if (nodeType === 'macro_invocation')
|
|
@@ -3926,9 +5295,29 @@ class TreeSitterExtractor {
|
|
|
3926
5295
|
}
|
|
3927
5296
|
}
|
|
3928
5297
|
}
|
|
3929
|
-
|
|
3930
|
-
this.methodIndex.get(shortNameKey)
|
|
3931
|
-
|
|
5298
|
+
let parentId = this.methodIndex.get(fullNameKey) ||
|
|
5299
|
+
this.methodIndex.get(shortNameKey);
|
|
5300
|
+
// No existing node? This is an implementation-only **free** procedure/function
|
|
5301
|
+
// (`procedure Helper; begin … end;` with no interface declaration and not a
|
|
5302
|
+
// class method). Create a function node so its body's calls attribute to it,
|
|
5303
|
+
// not to the enclosing file/module. A method (`TClass.Method`, a dotted name)
|
|
5304
|
+
// always has a node from its class declaration, so this only fires for free
|
|
5305
|
+
// routines — and the methodIndex lookup above already covers interface-declared
|
|
5306
|
+
// free routines, so there's no duplicate.
|
|
5307
|
+
if (!parentId && !fullName.includes('.')) {
|
|
5308
|
+
const fnNode = this.createNode('function', fullName, declProc, {
|
|
5309
|
+
signature: this.extractor?.getSignature?.(declProc, this.source),
|
|
5310
|
+
visibility: this.extractor?.getVisibility?.(declProc),
|
|
5311
|
+
});
|
|
5312
|
+
if (fnNode) {
|
|
5313
|
+
parentId = fnNode.id;
|
|
5314
|
+
this.methodIndex.set(fullNameKey, fnNode.id);
|
|
5315
|
+
if (!this.methodIndex.has(shortNameKey))
|
|
5316
|
+
this.methodIndex.set(shortNameKey, fnNode.id);
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
if (!parentId)
|
|
5320
|
+
parentId = this.nodeStack[this.nodeStack.length - 1];
|
|
3932
5321
|
if (!parentId)
|
|
3933
5322
|
return;
|
|
3934
5323
|
// Visit the block for calls
|
|
@@ -3954,10 +5343,41 @@ class TreeSitterExtractor {
|
|
|
3954
5343
|
return;
|
|
3955
5344
|
let calleeName = '';
|
|
3956
5345
|
if (firstChild.type === 'exprDot') {
|
|
3957
|
-
//
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
5346
|
+
// Chained static-factory call: `TFoo.GetInstance().DoIt()` — the exprDot's
|
|
5347
|
+
// receiver is itself an `exprCall`, so the bare identifier list would
|
|
5348
|
+
// collapse to just `DoIt` and mis-resolve to a same-named method on an
|
|
5349
|
+
// unrelated class. Encode `TFoo.GetInstance().DoIt` so resolution infers
|
|
5350
|
+
// DoIt's class from what `TFoo.GetInstance` RETURNS (#645/#608). Only a
|
|
5351
|
+
// capitalized class-factory chain; a unary outer method.
|
|
5352
|
+
const innerCall = firstChild.namedChildren.find((c) => c.type === 'exprCall');
|
|
5353
|
+
const outerId = firstChild.namedChildren.filter((c) => c.type === 'identifier').pop();
|
|
5354
|
+
const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
|
|
5355
|
+
if (innerCall && method && /^\w+$/.test(method)) {
|
|
5356
|
+
const innerFirst = innerCall.namedChild(0);
|
|
5357
|
+
let innerCallee = '';
|
|
5358
|
+
if (innerFirst?.type === 'exprDot') {
|
|
5359
|
+
innerCallee = innerFirst.namedChildren
|
|
5360
|
+
.filter((c) => c.type === 'identifier')
|
|
5361
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
5362
|
+
.join('.');
|
|
5363
|
+
}
|
|
5364
|
+
else if (innerFirst?.type === 'identifier') {
|
|
5365
|
+
innerCallee = (0, tree_sitter_helpers_1.getNodeText)(innerFirst, this.source);
|
|
5366
|
+
}
|
|
5367
|
+
// Gate on the Delphi type-naming convention — `TFoo` classes / `IFoo`
|
|
5368
|
+
// interfaces — so a class-factory chain re-encodes but a capitalized
|
|
5369
|
+
// VARIABLE/parameter chain (Pascal capitalizes locals too: `Curve.X().Y()`,
|
|
5370
|
+
// `Self.X().Y()`) stays bare and keeps its existing bare-name resolution.
|
|
5371
|
+
calleeName = innerCallee && /^[TI][A-Z]/.test(innerCallee)
|
|
5372
|
+
? `${innerCallee}().${method}`
|
|
5373
|
+
: method;
|
|
5374
|
+
}
|
|
5375
|
+
else {
|
|
5376
|
+
// Qualified call: Obj.Method(...)
|
|
5377
|
+
const identifiers = firstChild.namedChildren.filter((c) => c.type === 'identifier');
|
|
5378
|
+
if (identifiers.length > 0) {
|
|
5379
|
+
calleeName = identifiers.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source)).join('.');
|
|
5380
|
+
}
|
|
3961
5381
|
}
|
|
3962
5382
|
}
|
|
3963
5383
|
else if (firstChild.type === 'identifier') {
|
|
@@ -3978,6 +5398,72 @@ class TreeSitterExtractor {
|
|
|
3978
5398
|
this.visitPascalBlock(args);
|
|
3979
5399
|
}
|
|
3980
5400
|
}
|
|
5401
|
+
/**
|
|
5402
|
+
* Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
|
|
5403
|
+
* `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
|
|
5404
|
+
* parses as a bare `exprDot` (not an `exprCall`). A bare `exprDot` is
|
|
5405
|
+
* syntactically identical to a field/property access, so this is only ever
|
|
5406
|
+
* called for a STATEMENT-level exprDot (caller-gated): a bare `Obj.Field;`
|
|
5407
|
+
* statement is a no-op, so a statement-level dot expression is a call. (An
|
|
5408
|
+
* exprDot in assignment LHS/RHS or a condition is left alone — there it really
|
|
5409
|
+
* can be a field/property read.)
|
|
5410
|
+
*/
|
|
5411
|
+
extractPascalParenlessCall(node) {
|
|
5412
|
+
if (this.nodeStack.length === 0)
|
|
5413
|
+
return;
|
|
5414
|
+
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
|
5415
|
+
if (!callerId)
|
|
5416
|
+
return;
|
|
5417
|
+
const receiver = node.namedChild(0);
|
|
5418
|
+
const outerId = node.namedChildren.filter((c) => c.type === 'identifier').pop();
|
|
5419
|
+
const method = outerId ? (0, tree_sitter_helpers_1.getNodeText)(outerId, this.source) : '';
|
|
5420
|
+
if (!method)
|
|
5421
|
+
return;
|
|
5422
|
+
let calleeName = '';
|
|
5423
|
+
// Chained: the receiver is itself a call — a paren-less `TFoo.GetInstance` (an
|
|
5424
|
+
// inner exprDot) or a paren'd `TFoo.GetInstance()` (an exprCall). Encode the
|
|
5425
|
+
// chain `TFoo.GetInstance().DoIt` so resolution infers DoIt's class from what
|
|
5426
|
+
// the factory RETURNS (#645/#608), gated on the Delphi `TFoo`/`IFoo` type
|
|
5427
|
+
// convention; a capitalized VARIABLE chain stays a bare method name.
|
|
5428
|
+
if ((receiver?.type === 'exprDot' || receiver?.type === 'exprCall') && /^\w+$/.test(method)) {
|
|
5429
|
+
const innerCalleeNode = receiver.type === 'exprCall' ? receiver.namedChild(0) : receiver;
|
|
5430
|
+
const innerCallee = !innerCalleeNode
|
|
5431
|
+
? ''
|
|
5432
|
+
: innerCalleeNode.type === 'identifier'
|
|
5433
|
+
? (0, tree_sitter_helpers_1.getNodeText)(innerCalleeNode, this.source)
|
|
5434
|
+
: innerCalleeNode.namedChildren
|
|
5435
|
+
.filter((c) => c.type === 'identifier')
|
|
5436
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
5437
|
+
.join('.');
|
|
5438
|
+
if (innerCallee && /^[TI][A-Z]/.test(innerCallee)) {
|
|
5439
|
+
calleeName = `${innerCallee}().${method}`;
|
|
5440
|
+
// The T/I-prefixed inner is itself a real call — record it too.
|
|
5441
|
+
if (receiver.type === 'exprCall')
|
|
5442
|
+
this.extractPascalCall(receiver);
|
|
5443
|
+
else
|
|
5444
|
+
this.extractPascalParenlessCall(receiver);
|
|
5445
|
+
}
|
|
5446
|
+
else {
|
|
5447
|
+
calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
else {
|
|
5451
|
+
// Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
|
|
5452
|
+
calleeName = node.namedChildren
|
|
5453
|
+
.filter((c) => c.type === 'identifier')
|
|
5454
|
+
.map((id) => (0, tree_sitter_helpers_1.getNodeText)(id, this.source))
|
|
5455
|
+
.join('.');
|
|
5456
|
+
}
|
|
5457
|
+
if (calleeName) {
|
|
5458
|
+
this.unresolvedReferences.push({
|
|
5459
|
+
fromNodeId: callerId,
|
|
5460
|
+
referenceName: calleeName,
|
|
5461
|
+
referenceKind: 'calls',
|
|
5462
|
+
line: node.startPosition.row + 1,
|
|
5463
|
+
column: node.startPosition.column,
|
|
5464
|
+
});
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
3981
5467
|
/**
|
|
3982
5468
|
* Recursively visit a Pascal block/statement tree for call expressions
|
|
3983
5469
|
*/
|
|
@@ -3986,15 +5472,32 @@ class TreeSitterExtractor {
|
|
|
3986
5472
|
const child = node.namedChild(i);
|
|
3987
5473
|
if (!child)
|
|
3988
5474
|
continue;
|
|
5475
|
+
// Function-as-value capture (#756): Pascal bodies are walked here, not
|
|
5476
|
+
// in visitNode/visitForCallsAndStructure, so the capture hook fires here
|
|
5477
|
+
// — assignment RHS is the Delphi event-wiring idiom (`OnFire := Handler`).
|
|
5478
|
+
this.maybeCaptureFnRefs(child, child.type);
|
|
3989
5479
|
if (child.type === 'exprCall') {
|
|
3990
5480
|
this.extractPascalCall(child);
|
|
5481
|
+
// The walker doesn't descend into a call's arguments — dispatch the
|
|
5482
|
+
// argument container directly (`RegisterHandler(TargetCb)` / `(@Cb)`).
|
|
5483
|
+
const args = child.namedChildren.find((c) => c.type === 'exprArgs');
|
|
5484
|
+
if (args)
|
|
5485
|
+
this.maybeCaptureFnRefs(args, 'exprArgs');
|
|
3991
5486
|
}
|
|
3992
5487
|
else if (child.type === 'exprDot') {
|
|
3993
|
-
//
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
5488
|
+
// A STATEMENT-level bare exprDot is a paren-less call (`Obj.Free;`,
|
|
5489
|
+
// `TFoo.GetInstance.DoIt;`). Anywhere else (assignment side, condition,
|
|
5490
|
+
// expression) a bare exprDot is ambiguous with a field/property access,
|
|
5491
|
+
// so there we only descend for paren'd inner calls.
|
|
5492
|
+
if (node.type === 'statement') {
|
|
5493
|
+
this.extractPascalParenlessCall(child);
|
|
5494
|
+
}
|
|
5495
|
+
else {
|
|
5496
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
5497
|
+
const grandchild = child.namedChild(j);
|
|
5498
|
+
if (grandchild?.type === 'exprCall') {
|
|
5499
|
+
this.extractPascalCall(grandchild);
|
|
5500
|
+
}
|
|
3998
5501
|
}
|
|
3999
5502
|
}
|
|
4000
5503
|
}
|
|
@@ -4026,6 +5529,11 @@ function extractFromSource(filePath, source, language, frameworkNames) {
|
|
|
4026
5529
|
const extractor = new vue_extractor_1.VueExtractor(filePath, source);
|
|
4027
5530
|
result = extractor.extract();
|
|
4028
5531
|
}
|
|
5532
|
+
else if (detectedLanguage === 'astro') {
|
|
5533
|
+
// Use custom extractor for Astro (frontmatter + template delegation)
|
|
5534
|
+
const extractor = new astro_extractor_1.AstroExtractor(filePath, source);
|
|
5535
|
+
result = extractor.extract();
|
|
5536
|
+
}
|
|
4029
5537
|
else if (detectedLanguage === 'liquid') {
|
|
4030
5538
|
// Use custom extractor for Liquid
|
|
4031
5539
|
const extractor = new liquid_extractor_1.LiquidExtractor(filePath, source);
|