@quolu/lattice 0.69.5 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +5 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +746 -323
- package/sensor/dist/bin/viewer-gate.d.ts +17 -0
- package/sensor/dist/bin/viewer-gate.d.ts.map +1 -0
- package/sensor/dist/bin/viewer-gate.js +31 -0
- package/sensor/dist/bin/viewer-gate.js.map +1 -0
- package/sensor/dist/context/formatter.d.ts +7 -1
- package/sensor/dist/context/formatter.d.ts.map +1 -1
- package/sensor/dist/context/formatter.js +12 -6
- package/sensor/dist/context/formatter.js.map +1 -1
- package/sensor/dist/context/index.d.ts +12 -0
- package/sensor/dist/context/index.d.ts.map +1 -1
- package/sensor/dist/context/index.js +103 -16
- package/sensor/dist/context/index.js.map +1 -1
- package/sensor/dist/db/index.d.ts +23 -2
- package/sensor/dist/db/index.d.ts.map +1 -1
- package/sensor/dist/db/index.js +109 -31
- package/sensor/dist/db/index.js.map +1 -1
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +68 -1
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +583 -6
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +1425 -57
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +27 -4
- package/sensor/dist/db/sqlite-adapter.d.ts +2 -0
- package/sensor/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/sensor/dist/db/sqlite-adapter.js +56 -7
- package/sensor/dist/db/sqlite-adapter.js.map +1 -1
- package/sensor/dist/db/synthesis-stage.d.ts +12 -0
- package/sensor/dist/db/synthesis-stage.d.ts.map +1 -0
- package/sensor/dist/db/synthesis-stage.js +93 -0
- package/sensor/dist/db/synthesis-stage.js.map +1 -0
- package/sensor/dist/db/wal-valve.d.ts +32 -12
- package/sensor/dist/db/wal-valve.d.ts.map +1 -1
- package/sensor/dist/db/wal-valve.js +65 -22
- package/sensor/dist/db/wal-valve.js.map +1 -1
- package/sensor/dist/db/wsl-shared-index.d.ts +53 -0
- package/sensor/dist/db/wsl-shared-index.d.ts.map +1 -0
- package/sensor/dist/db/wsl-shared-index.js +121 -0
- package/sensor/dist/db/wsl-shared-index.js.map +1 -0
- package/sensor/dist/directory.d.ts +126 -2
- package/sensor/dist/directory.d.ts.map +1 -1
- package/sensor/dist/directory.js +285 -16
- package/sensor/dist/directory.js.map +1 -1
- package/sensor/dist/errors.d.ts +14 -0
- package/sensor/dist/errors.d.ts.map +1 -1
- package/sensor/dist/errors.js +16 -1
- package/sensor/dist/errors.js.map +1 -1
- package/sensor/dist/extraction/cfml-extractor.d.ts +1 -0
- package/sensor/dist/extraction/cfml-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/cfml-extractor.js +15 -3
- package/sensor/dist/extraction/cfml-extractor.js.map +1 -1
- package/sensor/dist/extraction/dfm-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/dfm-extractor.js +14 -10
- package/sensor/dist/extraction/dfm-extractor.js.map +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
- package/sensor/dist/extraction/extraction-version.js +3 -1
- package/sensor/dist/extraction/extraction-version.js.map +1 -1
- package/sensor/dist/extraction/function-ref.d.ts +6 -4
- package/sensor/dist/extraction/function-ref.d.ts.map +1 -1
- package/sensor/dist/extraction/function-ref.js +35 -14
- package/sensor/dist/extraction/function-ref.js.map +1 -1
- package/sensor/dist/extraction/generated-detection.d.ts +48 -12
- package/sensor/dist/extraction/generated-detection.d.ts.map +1 -1
- package/sensor/dist/extraction/generated-detection.js +176 -11
- package/sensor/dist/extraction/generated-detection.js.map +1 -1
- package/sensor/dist/extraction/grammars.d.ts +38 -0
- package/sensor/dist/extraction/grammars.d.ts.map +1 -1
- package/sensor/dist/extraction/grammars.js +115 -3
- package/sensor/dist/extraction/grammars.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +218 -14
- package/sensor/dist/extraction/index.d.ts.map +1 -1
- package/sensor/dist/extraction/index.js +887 -175
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.d.ts +58 -0
- package/sensor/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.js +310 -19
- package/sensor/dist/extraction/languages/c-cpp.js.map +1 -1
- package/sensor/dist/extraction/languages/dart.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/dart.js +8 -3
- package/sensor/dist/extraction/languages/dart.js.map +1 -1
- package/sensor/dist/extraction/languages/erlang.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/erlang.js +58 -22
- package/sensor/dist/extraction/languages/erlang.js.map +1 -1
- package/sensor/dist/extraction/languages/javascript.js +1 -1
- package/sensor/dist/extraction/languages/javascript.js.map +1 -1
- package/sensor/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/kotlin.js +169 -36
- package/sensor/dist/extraction/languages/kotlin.js.map +1 -1
- package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/lua.js +4 -1
- package/sensor/dist/extraction/languages/lua.js.map +1 -1
- package/sensor/dist/extraction/languages/objc.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/objc.js +4 -0
- package/sensor/dist/extraction/languages/objc.js.map +1 -1
- package/sensor/dist/extraction/languages/python.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/python.js +71 -0
- package/sensor/dist/extraction/languages/python.js.map +1 -1
- package/sensor/dist/extraction/languages/rust.d.ts +20 -0
- package/sensor/dist/extraction/languages/rust.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/rust.js +50 -20
- package/sensor/dist/extraction/languages/rust.js.map +1 -1
- package/sensor/dist/extraction/languages/scala.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/scala.js +18 -3
- package/sensor/dist/extraction/languages/scala.js.map +1 -1
- package/sensor/dist/extraction/languages/swift.d.ts +15 -0
- package/sensor/dist/extraction/languages/swift.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/swift.js +50 -0
- package/sensor/dist/extraction/languages/swift.js.map +1 -1
- package/sensor/dist/extraction/languages/typescript.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/typescript.js +9 -2
- package/sensor/dist/extraction/languages/typescript.js.map +1 -1
- package/sensor/dist/extraction/liquid-extractor.d.ts +18 -0
- package/sensor/dist/extraction/liquid-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/liquid-extractor.js +98 -22
- package/sensor/dist/extraction/liquid-extractor.js.map +1 -1
- package/sensor/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/mybatis-extractor.js +6 -4
- package/sensor/dist/extraction/mybatis-extractor.js.map +1 -1
- package/sensor/dist/extraction/parse-pool.d.ts +7 -0
- package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
- package/sensor/dist/extraction/parse-pool.js +20 -1
- package/sensor/dist/extraction/parse-pool.js.map +1 -1
- package/sensor/dist/extraction/syntax-tokens.d.ts +103 -0
- package/sensor/dist/extraction/syntax-tokens.d.ts.map +1 -0
- package/sensor/dist/extraction/syntax-tokens.js +381 -0
- package/sensor/dist/extraction/syntax-tokens.js.map +1 -0
- package/sensor/dist/extraction/tree-sitter-helpers.d.ts +5 -0
- package/sensor/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter-helpers.js +17 -0
- package/sensor/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/sensor/dist/extraction/tree-sitter-types.d.ts +31 -3
- package/sensor/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts +101 -18
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +928 -134
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/sensor/dist/file-limits.d.ts +40 -0
- package/sensor/dist/file-limits.d.ts.map +1 -0
- package/sensor/dist/file-limits.js +147 -0
- package/sensor/dist/file-limits.js.map +1 -0
- package/sensor/dist/graph/branch-guards.d.ts +265 -0
- package/sensor/dist/graph/branch-guards.d.ts.map +1 -0
- package/sensor/dist/graph/branch-guards.js +2325 -0
- package/sensor/dist/graph/branch-guards.js.map +1 -0
- package/sensor/dist/graph/dead-code.d.ts +251 -0
- package/sensor/dist/graph/dead-code.d.ts.map +1 -0
- package/sensor/dist/graph/dead-code.js +776 -0
- package/sensor/dist/graph/dead-code.js.map +1 -0
- package/sensor/dist/graph/dynamic-boundary-report.d.ts +121 -0
- package/sensor/dist/graph/dynamic-boundary-report.d.ts.map +1 -0
- package/sensor/dist/graph/dynamic-boundary-report.js +283 -0
- package/sensor/dist/graph/dynamic-boundary-report.js.map +1 -0
- package/sensor/dist/graph/index.d.ts +4 -0
- package/sensor/dist/graph/index.d.ts.map +1 -1
- package/sensor/dist/graph/index.js +16 -1
- package/sensor/dist/graph/index.js.map +1 -1
- package/sensor/dist/graph/named-symbol-flow.d.ts +125 -0
- package/sensor/dist/graph/named-symbol-flow.d.ts.map +1 -0
- package/sensor/dist/graph/named-symbol-flow.js +552 -0
- package/sensor/dist/graph/named-symbol-flow.js.map +1 -0
- package/sensor/dist/graph/queries.d.ts.map +1 -1
- package/sensor/dist/graph/queries.js +2 -0
- package/sensor/dist/graph/queries.js.map +1 -1
- package/sensor/dist/graph/symbol-lookup.d.ts +83 -0
- package/sensor/dist/graph/symbol-lookup.d.ts.map +1 -0
- package/sensor/dist/graph/symbol-lookup.js +181 -0
- package/sensor/dist/graph/symbol-lookup.js.map +1 -0
- package/sensor/dist/graph/traversal.d.ts +9 -0
- package/sensor/dist/graph/traversal.d.ts.map +1 -1
- package/sensor/dist/graph/traversal.js +62 -36
- package/sensor/dist/graph/traversal.js.map +1 -1
- package/sensor/dist/graph/type-hierarchy.d.ts +155 -0
- package/sensor/dist/graph/type-hierarchy.d.ts.map +1 -0
- package/sensor/dist/graph/type-hierarchy.js +382 -0
- package/sensor/dist/graph/type-hierarchy.js.map +1 -0
- package/sensor/dist/index.d.ts +272 -10
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +510 -34
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/mcp/answer-freshness.d.ts +10 -0
- package/sensor/dist/mcp/answer-freshness.d.ts.map +1 -0
- package/sensor/dist/mcp/answer-freshness.js +68 -0
- package/sensor/dist/mcp/answer-freshness.js.map +1 -0
- package/sensor/dist/mcp/daemon-manager.d.ts +1 -1
- package/sensor/dist/mcp/daemon-manager.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-manager.js +17 -1
- package/sensor/dist/mcp/daemon-manager.js.map +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +8 -0
- package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-paths.js +65 -2
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/daemon-registry.d.ts +17 -4
- package/sensor/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-registry.js +133 -23
- package/sensor/dist/mcp/daemon-registry.js.map +1 -1
- package/sensor/dist/mcp/daemon.d.ts +26 -13
- package/sensor/dist/mcp/daemon.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon.js +126 -32
- package/sensor/dist/mcp/daemon.js.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.d.ts +1 -10
- package/sensor/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.js +46 -42
- package/sensor/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/sensor/dist/mcp/engine.d.ts +31 -9
- package/sensor/dist/mcp/engine.d.ts.map +1 -1
- package/sensor/dist/mcp/engine.js +190 -79
- package/sensor/dist/mcp/engine.js.map +1 -1
- package/sensor/dist/mcp/explore-dedup.d.ts +140 -0
- package/sensor/dist/mcp/explore-dedup.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-dedup.js +239 -0
- package/sensor/dist/mcp/explore-dedup.js.map +1 -0
- package/sensor/dist/mcp/explore-diagnostics.d.ts +289 -0
- package/sensor/dist/mcp/explore-diagnostics.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-diagnostics.js +558 -0
- package/sensor/dist/mcp/explore-diagnostics.js.map +1 -0
- package/sensor/dist/mcp/explore-session-state.d.ts +217 -0
- package/sensor/dist/mcp/explore-session-state.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-session-state.js +322 -0
- package/sensor/dist/mcp/explore-session-state.js.map +1 -0
- package/sensor/dist/mcp/index-freshness-worker.d.ts +2 -0
- package/sensor/dist/mcp/index-freshness-worker.d.ts.map +1 -0
- package/sensor/dist/mcp/index-freshness-worker.js +30 -0
- package/sensor/dist/mcp/index-freshness-worker.js.map +1 -0
- package/sensor/dist/mcp/index-freshness.d.ts +7 -0
- package/sensor/dist/mcp/index-freshness.d.ts.map +1 -0
- package/sensor/dist/mcp/index-freshness.js +94 -0
- package/sensor/dist/mcp/index-freshness.js.map +1 -0
- package/sensor/dist/mcp/index.d.ts +3 -1
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +109 -12
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.d.ts +4 -1
- package/sensor/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.js +11 -2
- package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
- package/sensor/dist/mcp/project-lifecycle.d.ts +9 -0
- package/sensor/dist/mcp/project-lifecycle.d.ts.map +1 -0
- package/sensor/dist/mcp/project-lifecycle.js +182 -0
- package/sensor/dist/mcp/project-lifecycle.js.map +1 -0
- package/sensor/dist/mcp/proxy.d.ts.map +1 -1
- package/sensor/dist/mcp/proxy.js +15 -1
- package/sensor/dist/mcp/proxy.js.map +1 -1
- package/sensor/dist/mcp/query-pool.d.ts +2 -2
- package/sensor/dist/mcp/query-pool.d.ts.map +1 -1
- package/sensor/dist/mcp/query-pool.js +9 -7
- package/sensor/dist/mcp/query-pool.js.map +1 -1
- package/sensor/dist/mcp/query-worker.js +2 -2
- package/sensor/dist/mcp/query-worker.js.map +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts +2 -2
- package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
- package/sensor/dist/mcp/server-instructions.js +13 -6
- package/sensor/dist/mcp/server-instructions.js.map +1 -1
- package/sensor/dist/mcp/session.d.ts +16 -0
- package/sensor/dist/mcp/session.d.ts.map +1 -1
- package/sensor/dist/mcp/session.js +36 -13
- package/sensor/dist/mcp/session.js.map +1 -1
- package/sensor/dist/mcp/startup-handshake.d.ts +3 -1
- package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
- package/sensor/dist/mcp/startup-handshake.js +10 -2
- package/sensor/dist/mcp/startup-handshake.js.map +1 -1
- package/sensor/dist/mcp/tools.d.ts +496 -38
- package/sensor/dist/mcp/tools.d.ts.map +1 -1
- package/sensor/dist/mcp/tools.js +4021 -772
- package/sensor/dist/mcp/tools.js.map +1 -1
- package/sensor/dist/mcp/writer-lock.d.ts +61 -0
- package/sensor/dist/mcp/writer-lock.d.ts.map +1 -0
- package/sensor/dist/mcp/writer-lock.js +260 -0
- package/sensor/dist/mcp/writer-lock.js.map +1 -0
- package/sensor/dist/project-config.d.ts +23 -0
- package/sensor/dist/project-config.d.ts.map +1 -1
- package/sensor/dist/project-config.js +39 -2
- package/sensor/dist/project-config.js.map +1 -1
- package/sensor/dist/resolution/alias-binding.d.ts +56 -0
- package/sensor/dist/resolution/alias-binding.d.ts.map +1 -0
- package/sensor/dist/resolution/alias-binding.js +127 -0
- package/sensor/dist/resolution/alias-binding.js.map +1 -0
- package/sensor/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/c-fnptr-synthesizer.js +21 -19
- package/sensor/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts +23 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.js +356 -155
- package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/cpp-constructor.d.ts +4 -0
- package/sensor/dist/resolution/cpp-constructor.d.ts.map +1 -0
- package/sensor/dist/resolution/cpp-constructor.js +123 -0
- package/sensor/dist/resolution/cpp-constructor.js.map +1 -0
- package/sensor/dist/resolution/cpp-macro-visibility.d.ts +12 -0
- package/sensor/dist/resolution/cpp-macro-visibility.d.ts.map +1 -0
- package/sensor/dist/resolution/cpp-macro-visibility.js +342 -0
- package/sensor/dist/resolution/cpp-macro-visibility.js.map +1 -0
- package/sensor/dist/resolution/expo-router-synthesizer.d.ts +32 -0
- package/sensor/dist/resolution/expo-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/expo-router-synthesizer.js +189 -0
- package/sensor/dist/resolution/expo-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/frameworks/csharp.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/csharp.js +96 -2
- package/sensor/dist/resolution/frameworks/csharp.js.map +1 -1
- package/sensor/dist/resolution/frameworks/expo-router.d.ts +189 -0
- package/sensor/dist/resolution/frameworks/expo-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/expo-router.js +785 -0
- package/sensor/dist/resolution/frameworks/expo-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/express.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/express.js +251 -16
- package/sensor/dist/resolution/frameworks/express.js.map +1 -1
- package/sensor/dist/resolution/frameworks/index.d.ts +5 -0
- package/sensor/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/index.js +29 -1
- package/sensor/dist/resolution/frameworks/index.js.map +1 -1
- package/sensor/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/java.js +79 -47
- package/sensor/dist/resolution/frameworks/java.js.map +1 -1
- package/sensor/dist/resolution/frameworks/laravel.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/laravel.js +294 -4
- package/sensor/dist/resolution/frameworks/laravel.js.map +1 -1
- package/sensor/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/nestjs.js +6 -14
- package/sensor/dist/resolution/frameworks/nestjs.js.map +1 -1
- package/sensor/dist/resolution/frameworks/nextjs.d.ts +76 -0
- package/sensor/dist/resolution/frameworks/nextjs.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/nextjs.js +328 -0
- package/sensor/dist/resolution/frameworks/nextjs.js.map +1 -0
- package/sensor/dist/resolution/frameworks/object-literal.d.ts +32 -0
- package/sensor/dist/resolution/frameworks/object-literal.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/object-literal.js +139 -0
- package/sensor/dist/resolution/frameworks/object-literal.js.map +1 -0
- package/sensor/dist/resolution/frameworks/package-deps.d.ts +14 -0
- package/sensor/dist/resolution/frameworks/package-deps.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/package-deps.js +78 -0
- package/sensor/dist/resolution/frameworks/package-deps.js.map +1 -0
- package/sensor/dist/resolution/frameworks/python.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/python.js +166 -0
- package/sensor/dist/resolution/frameworks/python.js.map +1 -1
- package/sensor/dist/resolution/frameworks/react-native.d.ts +26 -0
- package/sensor/dist/resolution/frameworks/react-native.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/react-native.js +153 -14
- package/sensor/dist/resolution/frameworks/react-native.js.map +1 -1
- package/sensor/dist/resolution/frameworks/react-router.d.ts +46 -0
- package/sensor/dist/resolution/frameworks/react-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/react-router.js +187 -0
- package/sensor/dist/resolution/frameworks/react-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/react.d.ts +2 -1
- package/sensor/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/react.js +188 -139
- package/sensor/dist/resolution/frameworks/react.js.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js +6 -1
- package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
- package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts +36 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.js +156 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/swift.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/swift.js +140 -99
- package/sensor/dist/resolution/frameworks/swift.js.map +1 -1
- package/sensor/dist/resolution/frameworks/tanstack-router.d.ts +84 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.js +437 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/vue-router.d.ts +82 -0
- package/sensor/dist/resolution/frameworks/vue-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/vue-router.js +377 -0
- package/sensor/dist/resolution/frameworks/vue-router.js.map +1 -0
- package/sensor/dist/resolution/import-resolver.d.ts +38 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +394 -28
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts +89 -25
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +573 -148
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/js-builtins.d.ts +13 -0
- package/sensor/dist/resolution/js-builtins.d.ts.map +1 -0
- package/sensor/dist/resolution/js-builtins.js +46 -0
- package/sensor/dist/resolution/js-builtins.js.map +1 -0
- package/sensor/dist/resolution/name-matcher.d.ts +99 -22
- package/sensor/dist/resolution/name-matcher.d.ts.map +1 -1
- package/sensor/dist/resolution/name-matcher.js +2220 -86
- package/sensor/dist/resolution/name-matcher.js.map +1 -1
- package/sensor/dist/resolution/next-router-synthesizer.d.ts +27 -0
- package/sensor/dist/resolution/next-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/next-router-synthesizer.js +113 -0
- package/sensor/dist/resolution/next-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/path-aliases.d.ts +6 -4
- package/sensor/dist/resolution/path-aliases.d.ts.map +1 -1
- package/sensor/dist/resolution/path-aliases.js +125 -17
- package/sensor/dist/resolution/path-aliases.js.map +1 -1
- package/sensor/dist/resolution/react-router-synthesizer.d.ts +33 -0
- package/sensor/dist/resolution/react-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/react-router-synthesizer.js +126 -0
- package/sensor/dist/resolution/react-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/resolver-pool.d.ts +2 -0
- package/sensor/dist/resolution/resolver-pool.d.ts.map +1 -1
- package/sensor/dist/resolution/resolver-pool.js +4 -0
- package/sensor/dist/resolution/resolver-pool.js.map +1 -1
- package/sensor/dist/resolution/strip-comments.d.ts +6 -0
- package/sensor/dist/resolution/strip-comments.d.ts.map +1 -1
- package/sensor/dist/resolution/strip-comments.js +58 -0
- package/sensor/dist/resolution/strip-comments.js.map +1 -1
- package/sensor/dist/resolution/sveltekit-synthesizer.d.ts +58 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.js +173 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/swift-type-visibility.d.ts +52 -0
- package/sensor/dist/resolution/swift-type-visibility.d.ts.map +1 -0
- package/sensor/dist/resolution/swift-type-visibility.js +328 -0
- package/sensor/dist/resolution/swift-type-visibility.js.map +1 -0
- package/sensor/dist/resolution/synth-utils.d.ts +26 -0
- package/sensor/dist/resolution/synth-utils.d.ts.map +1 -0
- package/sensor/dist/resolution/synth-utils.js +75 -0
- package/sensor/dist/resolution/synth-utils.js.map +1 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts +31 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.js +114 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/tier-synthesizer.d.ts +54 -0
- package/sensor/dist/resolution/tier-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/tier-synthesizer.js +1294 -0
- package/sensor/dist/resolution/tier-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/types.d.ts +78 -2
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/resolution/types.js +64 -0
- package/sensor/dist/resolution/types.js.map +1 -1
- package/sensor/dist/resolution/vue-router-synthesizer.d.ts +30 -0
- package/sensor/dist/resolution/vue-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/vue-router-synthesizer.js +115 -0
- package/sensor/dist/resolution/vue-router-synthesizer.js.map +1 -0
- package/sensor/dist/search/identifier-segments.d.ts +10 -0
- package/sensor/dist/search/identifier-segments.d.ts.map +1 -1
- package/sensor/dist/search/identifier-segments.js +29 -0
- package/sensor/dist/search/identifier-segments.js.map +1 -1
- package/sensor/dist/search/query-paths.d.ts +106 -0
- package/sensor/dist/search/query-paths.d.ts.map +1 -0
- package/sensor/dist/search/query-paths.js +513 -0
- package/sensor/dist/search/query-paths.js.map +1 -0
- package/sensor/dist/search/query-utils.d.ts +22 -2
- package/sensor/dist/search/query-utils.d.ts.map +1 -1
- package/sensor/dist/search/query-utils.js +56 -16
- package/sensor/dist/search/query-utils.js.map +1 -1
- package/sensor/dist/sync/watch-policy.d.ts +14 -0
- package/sensor/dist/sync/watch-policy.d.ts.map +1 -1
- package/sensor/dist/sync/watch-policy.js +19 -0
- package/sensor/dist/sync/watch-policy.js.map +1 -1
- package/sensor/dist/sync/watcher.d.ts +74 -35
- package/sensor/dist/sync/watcher.d.ts.map +1 -1
- package/sensor/dist/sync/watcher.js +281 -99
- package/sensor/dist/sync/watcher.js.map +1 -1
- package/sensor/dist/telemetry/index.d.ts +4 -2
- package/sensor/dist/telemetry/index.d.ts.map +1 -1
- package/sensor/dist/telemetry/index.js +92 -41
- package/sensor/dist/telemetry/index.js.map +1 -1
- package/sensor/dist/types.d.ts +18 -2
- package/sensor/dist/types.d.ts.map +1 -1
- package/sensor/dist/types.js +2 -0
- package/sensor/dist/types.js.map +1 -1
- package/sensor/dist/ui-server/api/deadcode.d.ts +84 -0
- package/sensor/dist/ui-server/api/deadcode.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/deadcode.js +136 -0
- package/sensor/dist/ui-server/api/deadcode.js.map +1 -0
- package/sensor/dist/ui-server/api/effects.d.ts +94 -0
- package/sensor/dist/ui-server/api/effects.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/effects.js +527 -0
- package/sensor/dist/ui-server/api/effects.js.map +1 -0
- package/sensor/dist/ui-server/api/entrypoints.d.ts +92 -0
- package/sensor/dist/ui-server/api/entrypoints.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/entrypoints.js +361 -0
- package/sensor/dist/ui-server/api/entrypoints.js.map +1 -0
- package/sensor/dist/ui-server/api/events.d.ts +183 -0
- package/sensor/dist/ui-server/api/events.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/events.js +430 -0
- package/sensor/dist/ui-server/api/events.js.map +1 -0
- package/sensor/dist/ui-server/api/file.d.ts +66 -0
- package/sensor/dist/ui-server/api/file.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/file.js +235 -0
- package/sensor/dist/ui-server/api/file.js.map +1 -0
- package/sensor/dist/ui-server/api/filecode.d.ts +126 -0
- package/sensor/dist/ui-server/api/filecode.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/filecode.js +205 -0
- package/sensor/dist/ui-server/api/filecode.js.map +1 -0
- package/sensor/dist/ui-server/api/flow.d.ts +226 -0
- package/sensor/dist/ui-server/api/flow.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/flow.js +612 -0
- package/sensor/dist/ui-server/api/flow.js.map +1 -0
- package/sensor/dist/ui-server/api/hierarchy.d.ts +81 -0
- package/sensor/dist/ui-server/api/hierarchy.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/hierarchy.js +92 -0
- package/sensor/dist/ui-server/api/hierarchy.js.map +1 -0
- package/sensor/dist/ui-server/api/index.d.ts +90 -0
- package/sensor/dist/ui-server/api/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/index.js +332 -0
- package/sensor/dist/ui-server/api/index.js.map +1 -0
- package/sensor/dist/ui-server/api/map.d.ts +256 -0
- package/sensor/dist/ui-server/api/map.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/map.js +760 -0
- package/sensor/dist/ui-server/api/map.js.map +1 -0
- package/sensor/dist/ui-server/api/node.d.ts +81 -0
- package/sensor/dist/ui-server/api/node.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/node.js +388 -0
- package/sensor/dist/ui-server/api/node.js.map +1 -0
- package/sensor/dist/ui-server/api/nodes.d.ts +25 -0
- package/sensor/dist/ui-server/api/nodes.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/nodes.js +45 -0
- package/sensor/dist/ui-server/api/nodes.js.map +1 -0
- package/sensor/dist/ui-server/api/program.d.ts +139 -0
- package/sensor/dist/ui-server/api/program.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/program.js +315 -0
- package/sensor/dist/ui-server/api/program.js.map +1 -0
- package/sensor/dist/ui-server/api/respond.d.ts +76 -0
- package/sensor/dist/ui-server/api/respond.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/respond.js +186 -0
- package/sensor/dist/ui-server/api/respond.js.map +1 -0
- package/sensor/dist/ui-server/api/route-roots.d.ts +39 -0
- package/sensor/dist/ui-server/api/route-roots.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/route-roots.js +96 -0
- package/sensor/dist/ui-server/api/route-roots.js.map +1 -0
- package/sensor/dist/ui-server/api/routes.d.ts +59 -0
- package/sensor/dist/ui-server/api/routes.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/routes.js +108 -0
- package/sensor/dist/ui-server/api/routes.js.map +1 -0
- package/sensor/dist/ui-server/api/screens.d.ts +117 -0
- package/sensor/dist/ui-server/api/screens.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/screens.js +611 -0
- package/sensor/dist/ui-server/api/screens.js.map +1 -0
- package/sensor/dist/ui-server/api/search.d.ts +29 -0
- package/sensor/dist/ui-server/api/search.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/search.js +232 -0
- package/sensor/dist/ui-server/api/search.js.map +1 -0
- package/sensor/dist/ui-server/api/session.d.ts +52 -0
- package/sensor/dist/ui-server/api/session.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/session.js +196 -0
- package/sensor/dist/ui-server/api/session.js.map +1 -0
- package/sensor/dist/ui-server/api/source.d.ts +200 -0
- package/sensor/dist/ui-server/api/source.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/source.js +422 -0
- package/sensor/dist/ui-server/api/source.js.map +1 -0
- package/sensor/dist/ui-server/api/stats.d.ts +27 -0
- package/sensor/dist/ui-server/api/stats.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/stats.js +164 -0
- package/sensor/dist/ui-server/api/stats.js.map +1 -0
- package/sensor/dist/ui-server/api/steps.d.ts +274 -0
- package/sensor/dist/ui-server/api/steps.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/steps.js +1544 -0
- package/sensor/dist/ui-server/api/steps.js.map +1 -0
- package/sensor/dist/ui-server/api/trail-store.d.ts +142 -0
- package/sensor/dist/ui-server/api/trail-store.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/trail-store.js +325 -0
- package/sensor/dist/ui-server/api/trail-store.js.map +1 -0
- package/sensor/dist/ui-server/api/trails.d.ts +163 -0
- package/sensor/dist/ui-server/api/trails.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/trails.js +369 -0
- package/sensor/dist/ui-server/api/trails.js.map +1 -0
- package/sensor/dist/ui-server/api/when.d.ts +133 -0
- package/sensor/dist/ui-server/api/when.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/when.js +275 -0
- package/sensor/dist/ui-server/api/when.js.map +1 -0
- package/sensor/dist/ui-server/api/wire.d.ts +179 -0
- package/sensor/dist/ui-server/api/wire.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/wire.js +231 -0
- package/sensor/dist/ui-server/api/wire.js.map +1 -0
- package/sensor/dist/ui-server/assets.d.ts +40 -0
- package/sensor/dist/ui-server/assets.d.ts.map +1 -0
- package/sensor/dist/ui-server/assets.js +110 -0
- package/sensor/dist/ui-server/assets.js.map +1 -0
- package/sensor/dist/ui-server/constants.d.ts +32 -0
- package/sensor/dist/ui-server/constants.d.ts.map +1 -0
- package/sensor/dist/ui-server/constants.js +35 -0
- package/sensor/dist/ui-server/constants.js.map +1 -0
- package/sensor/dist/ui-server/highlight/index.d.ts +113 -0
- package/sensor/dist/ui-server/highlight/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/highlight/index.js +307 -0
- package/sensor/dist/ui-server/highlight/index.js.map +1 -0
- package/sensor/dist/ui-server/highlight/languages.d.ts +36 -0
- package/sensor/dist/ui-server/highlight/languages.d.ts.map +1 -0
- package/sensor/dist/ui-server/highlight/languages.js +50 -0
- package/sensor/dist/ui-server/highlight/languages.js.map +1 -0
- package/sensor/dist/ui-server/index.d.ts +92 -0
- package/sensor/dist/ui-server/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/index.js +399 -0
- package/sensor/dist/ui-server/index.js.map +1 -0
- package/sensor/dist/ui-server/open-browser.d.ts +31 -0
- package/sensor/dist/ui-server/open-browser.d.ts.map +1 -0
- package/sensor/dist/ui-server/open-browser.js +79 -0
- package/sensor/dist/ui-server/open-browser.js.map +1 -0
- package/sensor/dist/ui-server/security.d.ts +127 -0
- package/sensor/dist/ui-server/security.d.ts.map +1 -0
- package/sensor/dist/ui-server/security.js +332 -0
- package/sensor/dist/ui-server/security.js.map +1 -0
- package/sensor/dist/ui-server/static.d.ts +45 -0
- package/sensor/dist/ui-server/static.d.ts.map +1 -0
- package/sensor/dist/ui-server/static.js +165 -0
- package/sensor/dist/ui-server/static.js.map +1 -0
- package/sensor/dist/utils.d.ts +14 -2
- package/sensor/dist/utils.d.ts.map +1 -1
- package/sensor/dist/utils.js +25 -10
- package/sensor/dist/utils.js.map +1 -1
- package/src/cli-help.mjs +4 -2
- package/src/sensor-adapter.mjs +5 -1
- package/src/todo-cli.mjs +88 -11
|
@@ -4,15 +4,52 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Handles symbol name matching for reference resolution.
|
|
6
6
|
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
7
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
41
|
exports.matchByFilePath = matchByFilePath;
|
|
9
42
|
exports.sameLanguageFamily = sameLanguageFamily;
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
43
|
+
exports.crossesCodeBoundary = crossesCodeBoundary;
|
|
44
|
+
exports.gateLanguageMatch = gateLanguageMatch;
|
|
12
45
|
exports.matchFunctionRef = matchFunctionRef;
|
|
46
|
+
exports.isVisibleAcrossFiles = isVisibleAcrossFiles;
|
|
13
47
|
exports.matchByExactName = matchByExactName;
|
|
14
48
|
exports.matchByQualifiedName = matchByQualifiedName;
|
|
15
49
|
exports.preferCallSiteFile = preferCallSiteFile;
|
|
50
|
+
exports.resolveObjectLiteralMember = resolveObjectLiteralMember;
|
|
51
|
+
exports.objectLiteralMemberBinding = objectLiteralMemberBinding;
|
|
52
|
+
exports.resolveObjectLiteralBinding = resolveObjectLiteralBinding;
|
|
16
53
|
exports.resolveMethodOnType = resolveMethodOnType;
|
|
17
54
|
exports.matchCppCallChain = matchCppCallChain;
|
|
18
55
|
exports.matchScopedCallChain = matchScopedCallChain;
|
|
@@ -21,9 +58,16 @@ exports.normalizeInferredTypeName = normalizeInferredTypeName;
|
|
|
21
58
|
exports.clearNameMatcherMemos = clearNameMatcherMemos;
|
|
22
59
|
exports.localReceiverTypePatterns = localReceiverTypePatterns;
|
|
23
60
|
exports.matchMethodCall = matchMethodCall;
|
|
61
|
+
exports.rustFieldTypeName = rustFieldTypeName;
|
|
62
|
+
exports.matchJsStoreBindingCall = matchJsStoreBindingCall;
|
|
63
|
+
exports.isUnresolvedJsMemberCall = isUnresolvedJsMemberCall;
|
|
24
64
|
exports.matchFuzzy = matchFuzzy;
|
|
25
65
|
exports.dumpNameMatcherProfile = dumpNameMatcherProfile;
|
|
26
66
|
exports.matchReference = matchReference;
|
|
67
|
+
const path = __importStar(require("path"));
|
|
68
|
+
const types_1 = require("./types");
|
|
69
|
+
const strip_comments_1 = require("./strip-comments");
|
|
70
|
+
const js_builtins_1 = require("./js-builtins");
|
|
27
71
|
/**
|
|
28
72
|
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
|
|
29
73
|
* score. A name defined more times than this is "ubiquitous" — a method/symbol
|
|
@@ -143,14 +187,16 @@ function pickClosestFileNode(candidates, ref) {
|
|
|
143
187
|
*/
|
|
144
188
|
const LANGUAGE_FAMILY = {
|
|
145
189
|
java: 'jvm', kotlin: 'jvm', scala: 'jvm',
|
|
146
|
-
swift: '
|
|
190
|
+
swift: 'native', objc: 'native',
|
|
147
191
|
// ArkTS is a TS superset — every HarmonyOS project mixes `.ets` UI with
|
|
148
192
|
// `.ts` logic modules, so refs must cross freely between them.
|
|
149
193
|
typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web', arkts: 'web',
|
|
150
|
-
c: '
|
|
194
|
+
c: 'native', cpp: 'native',
|
|
151
195
|
// Razor/Blazor markup names C# types — same family so `@model Foo` /
|
|
152
196
|
// `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
|
|
153
|
-
csharp: 'dotnet', razor: 'dotnet',
|
|
197
|
+
csharp: 'dotnet', razor: 'dotnet', vbnet: 'dotnet',
|
|
198
|
+
svelte: 'web', vue: 'web', astro: 'web',
|
|
199
|
+
cfml: 'cfml', cfscript: 'cfml',
|
|
154
200
|
};
|
|
155
201
|
function sameLanguageFamily(a, b) {
|
|
156
202
|
if (a === b)
|
|
@@ -158,62 +204,324 @@ function sameLanguageFamily(a, b) {
|
|
|
158
204
|
const fa = LANGUAGE_FAMILY[a];
|
|
159
205
|
return fa !== undefined && fa === LANGUAGE_FAMILY[b];
|
|
160
206
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
function
|
|
169
|
-
return
|
|
207
|
+
/** Config/markup transitions stay open; every other code language has a family. */
|
|
208
|
+
const CODE_FAMILY = {
|
|
209
|
+
...LANGUAGE_FAMILY,
|
|
210
|
+
python: 'python', go: 'go', rust: 'rust', php: 'php', ruby: 'ruby', dart: 'dart',
|
|
211
|
+
lua: 'lua', luau: 'lua', r: 'r', erlang: 'erlang', pascal: 'pascal', solidity: 'solidity',
|
|
212
|
+
nix: 'nix', cobol: 'cobol',
|
|
213
|
+
};
|
|
214
|
+
function crossesCodeBoundary(a, b) {
|
|
215
|
+
return CODE_FAMILY[a] !== undefined && CODE_FAMILY[b] !== undefined &&
|
|
216
|
+
CODE_FAMILY[a] !== CODE_FAMILY[b];
|
|
170
217
|
}
|
|
171
218
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* React` matching a Swift `import React`, a C++ `#include "X.h"` matching a
|
|
175
|
-
* same-named ObjC header on another platform). The both-*known* test is
|
|
176
|
-
* deliberately weaker than {@link sameLanguageFamily}'s negation: a
|
|
177
|
-
* single-file-component language that carries its own tag (`vue`/`svelte`)
|
|
178
|
-
* importing a `.ts` module, or any singleton-family language (php/go/ruby/…),
|
|
179
|
-
* returns `false` here and is left alone.
|
|
219
|
+
* Cross-family name matches need a framework export or an actual ABI boundary,
|
|
220
|
+
* not merely a native caller. ABI evidence is scoped to the named free function.
|
|
180
221
|
*/
|
|
181
|
-
function
|
|
182
|
-
|
|
222
|
+
function hasBridgeEvidence(candidate, ref, context) {
|
|
223
|
+
if (ref.referenceKind !== 'calls')
|
|
224
|
+
return false;
|
|
225
|
+
// Expo's extractor creates explicit JS exports, resolved by the ordinary
|
|
226
|
+
// name matcher rather than a framework resolve() branch.
|
|
227
|
+
if (CODE_FAMILY[ref.language] === 'web' && candidate.id.startsWith('expo-module:') &&
|
|
228
|
+
candidate.isExported && (candidate.language === 'swift' || candidate.language === 'kotlin'))
|
|
229
|
+
return true;
|
|
230
|
+
if (candidate.kind !== 'function')
|
|
231
|
+
return false;
|
|
232
|
+
const name = candidate.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
233
|
+
if (CODE_FAMILY[ref.language] === 'native') {
|
|
234
|
+
const source = context.readFile(candidate.filePath);
|
|
235
|
+
if (!source)
|
|
236
|
+
return false;
|
|
237
|
+
if (candidate.language === 'go') {
|
|
238
|
+
const declaration = source.split('\n').slice(Math.max(0, candidate.startLine - 2), candidate.endLine).join('\n');
|
|
239
|
+
return /\bimport\s+(?:\(\s*)?"C"/.test((0, strip_comments_1.stripCommentsForRegex)(source, 'go')) &&
|
|
240
|
+
new RegExp('^//export ' + name + '\\r?\\nfunc ' + name + '\\s*\\(', 'm').test(declaration);
|
|
241
|
+
}
|
|
242
|
+
if (candidate.language === 'rust') {
|
|
243
|
+
const declaration = source.split('\n').slice(candidate.startLine - 1, candidate.endLine).join('\n');
|
|
244
|
+
return new RegExp('\\bpub\\s+extern\\s+"C"\\s+fn\\s+' + name + '\\b')
|
|
245
|
+
.test((0, strip_comments_1.stripCommentsForRegex)(declaration, 'rust'));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (candidate.language === 'c' || candidate.language === 'cpp') {
|
|
249
|
+
const source = context.readFile(ref.filePath);
|
|
250
|
+
if (!source)
|
|
251
|
+
return false;
|
|
252
|
+
if (ref.language === 'go') {
|
|
253
|
+
return /\bimport\s+(?:\(\s*)?"C"/.test((0, strip_comments_1.stripCommentsForRegex)(source, 'go')) &&
|
|
254
|
+
ref.referenceName === 'C.' + candidate.name;
|
|
255
|
+
}
|
|
256
|
+
if (ref.language === 'rust') {
|
|
257
|
+
return new RegExp('extern\\s+"C"\\s*\\{[^}]*\\bfn\\s+' + name + '\\s*\\(')
|
|
258
|
+
.test((0, strip_comments_1.stripCommentsForRegex)(source, 'rust'));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
183
262
|
}
|
|
184
263
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* - `imports` (import binding): an `import`/`#include` never crosses two
|
|
191
|
-
* KNOWN families (TS `import React` ↮ Swift `import React`). Weaker
|
|
192
|
-
* both-known filter so `.vue`/`.svelte` (own tag) importing `.ts` survives.
|
|
264
|
+
* Per-context memo: node id → its language, for gateLanguageMatch. Matches
|
|
265
|
+
* land on ~5 refs per target on vscode, and each check otherwise fetched the
|
|
266
|
+
* whole node (a point read + row decode past the query layer's small cache)
|
|
267
|
+
* only to read one field. Nodes are fixed within a resolution pass; the memo
|
|
268
|
+
* drops with clearNameMatcherMemos.
|
|
193
269
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
270
|
+
const TARGET_LANGUAGE = new WeakMap();
|
|
271
|
+
/** Reject the chosen result without shrinking a pool or trying a replacement. */
|
|
272
|
+
function gateLanguageMatch(result, ref, context) {
|
|
273
|
+
if (!result)
|
|
274
|
+
return result;
|
|
275
|
+
// No code family on the reference's side: no target can cross a boundary.
|
|
276
|
+
if (CODE_FAMILY[ref.language] === undefined)
|
|
277
|
+
return result;
|
|
278
|
+
if (context.getNodeById) {
|
|
279
|
+
let languages = TARGET_LANGUAGE.get(context);
|
|
280
|
+
if (!languages) {
|
|
281
|
+
languages = new Map();
|
|
282
|
+
TARGET_LANGUAGE.set(context, languages);
|
|
283
|
+
}
|
|
284
|
+
let language = languages.get(result.targetNodeId);
|
|
285
|
+
if (language === undefined) {
|
|
286
|
+
const node = context.getNodeById(result.targetNodeId);
|
|
287
|
+
if (node) {
|
|
288
|
+
language = node.language;
|
|
289
|
+
if (languages.size >= 400_000)
|
|
290
|
+
languages.clear();
|
|
291
|
+
languages.set(result.targetNodeId, language);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (language !== undefined) {
|
|
295
|
+
if (!crossesCodeBoundary(ref.language, language))
|
|
296
|
+
return result;
|
|
297
|
+
const target = context.getNodeById(result.targetNodeId);
|
|
298
|
+
return target && !hasBridgeEvidence(target, ref, context) ? null : result;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const target = context.getNodeById?.(result.targetNodeId) ??
|
|
302
|
+
context.getNodesByName(ref.referenceName).find((n) => n.id === result.targetNodeId);
|
|
303
|
+
if (target && crossesCodeBoundary(ref.language, target.language) &&
|
|
304
|
+
!hasBridgeEvidence(target, ref, context))
|
|
305
|
+
return null;
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
/** Member values retain their receiver; never break ties by file order (#1820). */
|
|
309
|
+
function matchMemberFunctionRef(ref, context) {
|
|
310
|
+
const dot = ref.referenceName.lastIndexOf('.');
|
|
311
|
+
const receiver = ref.referenceName.slice(0, dot);
|
|
312
|
+
const member = ref.referenceName.slice(dot + 1);
|
|
313
|
+
const result = (nodes, confidence = 0.9) => {
|
|
314
|
+
const pool = nodes.filter(n => sameLanguageFamily(n.language, ref.language));
|
|
315
|
+
const target = pool.length === 1 ? pool[0] : undefined;
|
|
316
|
+
return target && (target.kind === 'function' || target.kind === 'method') &&
|
|
317
|
+
target.id !== ref.fromNodeId && !isPythonProperty(target, context)
|
|
318
|
+
? { original: ref, targetNodeId: target.id, confidence, resolvedBy: 'function-ref' }
|
|
319
|
+
: null;
|
|
320
|
+
};
|
|
321
|
+
const imports = context.getImportMappings(ref.filePath, ref.language);
|
|
322
|
+
// An import is authoritative even when it points outside the project.
|
|
323
|
+
if (imports.some(i => i.localName === receiver.split('.')[0])) {
|
|
324
|
+
if (ref.language === 'python') {
|
|
325
|
+
const cls = pythonRefClass(receiver, ref, context);
|
|
326
|
+
if (cls)
|
|
327
|
+
return result(pythonMembers(cls, member, ref, context));
|
|
328
|
+
}
|
|
329
|
+
const imported = context.resolveImport?.(ref);
|
|
330
|
+
const node = imported && context.getNodeById?.(imported.targetNodeId);
|
|
331
|
+
return node ? result(context.getNodesByQualifiedName(node.qualifiedName).filter(n => n.filePath === node.filePath)) : null;
|
|
332
|
+
}
|
|
333
|
+
if (ref.language === 'go') {
|
|
334
|
+
if (receiver.includes('.'))
|
|
335
|
+
return matchGoFieldChainCall(receiver, member, ref, context);
|
|
336
|
+
const type = inferLocalReceiverType(receiver, ref, context);
|
|
337
|
+
if (type)
|
|
338
|
+
return resolveMethodOnType(type, member, ref, context, 0.9, 'function-ref');
|
|
339
|
+
const types = context.getNodesByName(receiver).filter(n => n.language === 'go' && (n.kind === 'struct' || n.kind === 'interface'));
|
|
340
|
+
if (types.length)
|
|
341
|
+
return types.length === 1 ? resolveMethodOnType(receiver, member, ref, context, 0.9, 'function-ref') : null;
|
|
197
342
|
}
|
|
198
|
-
|
|
199
|
-
|
|
343
|
+
else {
|
|
344
|
+
const owner = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'class' && n.startLine <= ref.line && n.endLine >= ref.line)
|
|
345
|
+
.sort((a, b) => b.startLine - a.startLine)[0];
|
|
346
|
+
let type = null;
|
|
347
|
+
if (receiver === 'self' || receiver === 'cls') {
|
|
348
|
+
return owner ? result(pythonMembers(owner, member, ref, context)) : null;
|
|
349
|
+
}
|
|
350
|
+
if (/^(self|cls)\.\w+$/.test(receiver)) {
|
|
351
|
+
if (!owner)
|
|
352
|
+
return null;
|
|
353
|
+
type = pythonFieldType(receiver, owner, ref, context);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
type = pythonLocalType(receiver, ref, context);
|
|
357
|
+
}
|
|
358
|
+
// A type name used directly (`Store.fetch`) is scoped just like an annotation.
|
|
359
|
+
if (!type && /^[A-Z]\w*$/.test(receiver))
|
|
360
|
+
type = receiver;
|
|
361
|
+
if (type && type !== 'object' && type !== 'Any') {
|
|
362
|
+
const cls = pythonRefClass(type, ref, context);
|
|
363
|
+
if (!cls)
|
|
364
|
+
return null;
|
|
365
|
+
const members = pythonMembers(cls, member, ref, context);
|
|
366
|
+
if (members.length)
|
|
367
|
+
return result(members);
|
|
368
|
+
// A base-typed field can hold a subclass-only method (the reported case).
|
|
369
|
+
// Keep only descendants of THAT base; unrelated same-name methods cannot win.
|
|
370
|
+
const candidates = context.getNodesByName(member).filter(n => n.kind === 'method' && n.language === 'python');
|
|
371
|
+
const descendants = candidates.filter(n => {
|
|
372
|
+
const parent = context.getNodesInFile(n.filePath).find(c => c.kind === 'class' && n.qualifiedName === `${c.qualifiedName}::${member}`);
|
|
373
|
+
return parent && pythonDerivesFrom(parent, cls, ref, context);
|
|
374
|
+
});
|
|
375
|
+
return result(descendants, 0.8);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Unknown receivers retain the old unique-or-drop discipline, across ALL
|
|
379
|
+
// files. Tests and abstract-looking bodies are candidates too.
|
|
380
|
+
return result(context.getNodesByName(member), 0.8);
|
|
381
|
+
}
|
|
382
|
+
function pythonRefClass(name, ref, context) {
|
|
383
|
+
const imports = context.getImportMappings(ref.filePath, 'python');
|
|
384
|
+
if (imports.some(i => i.localName === name.split('.')[0])) {
|
|
385
|
+
const hit = context.resolveImport?.({ ...ref, referenceName: name, referenceKind: 'references' });
|
|
386
|
+
const node = hit && context.getNodeById?.(hit.targetNodeId);
|
|
387
|
+
return node?.kind === 'class' && context.getNodesByQualifiedName(node.qualifiedName)
|
|
388
|
+
.filter(n => n.kind === 'class' && n.filePath === node.filePath).length === 1 ? node : null;
|
|
200
389
|
}
|
|
201
|
-
|
|
390
|
+
const classes = context.getNodesByName(name).filter(n => n.kind === 'class' && n.filePath === ref.filePath);
|
|
391
|
+
return classes.length === 1 ? classes[0] : null;
|
|
392
|
+
}
|
|
393
|
+
function pythonBases(cls, ref, context) {
|
|
394
|
+
const line = context.getFileLines?.(cls.filePath)?.[cls.startLine - 1]
|
|
395
|
+
?? context.readFile(cls.filePath)?.split('\n')[cls.startLine - 1] ?? '';
|
|
396
|
+
const bases = line.match(/^\s*class\s+\w+\s*\(([^)]*)\)/)?.[1];
|
|
397
|
+
return (bases?.split(',') ?? []).flatMap(name => {
|
|
398
|
+
const base = pythonRefClass(name.trim(), { ...ref, filePath: cls.filePath }, context);
|
|
399
|
+
return base ? [base] : [];
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function pythonDerivesFrom(cls, base, ref, context, seen = new Set()) {
|
|
403
|
+
if (seen.has(cls.id) || seen.size >= 16)
|
|
404
|
+
return false;
|
|
405
|
+
seen.add(cls.id);
|
|
406
|
+
return pythonBases(cls, ref, context).some(p => p.id === base.id || pythonDerivesFrom(p, base, ref, context, seen));
|
|
407
|
+
}
|
|
408
|
+
function pythonMembers(cls, member, ref, context, seen = new Set()) {
|
|
409
|
+
if (seen.has(cls.id) || seen.size >= 16)
|
|
410
|
+
return [];
|
|
411
|
+
seen.add(cls.id);
|
|
412
|
+
// Instance assignments also shadow methods, even though they are not nodes.
|
|
413
|
+
const body = pythonMemberLines(cls.filePath, context).slice(cls.startLine - 1, cls.endLine).join('\n');
|
|
414
|
+
if (new RegExp(`^\\s*(?:(?:self|cls)\\.)?${member}\\s*(?:=|:)`, 'm').test(body))
|
|
415
|
+
return [cls];
|
|
416
|
+
const own = context.getNodesByQualifiedName(`${cls.qualifiedName}::${member}`).filter(n => n.filePath === cls.filePath);
|
|
417
|
+
if (own.length)
|
|
418
|
+
return own;
|
|
419
|
+
return [...new Map(pythonBases(cls, ref, context).flatMap(p => pythonMembers(p, member, ref, context, seen)).map(n => [n.id, n])).values()];
|
|
420
|
+
}
|
|
421
|
+
function isPythonProperty(node, context) {
|
|
422
|
+
if (node.language !== 'python' || node.kind !== 'method')
|
|
423
|
+
return false;
|
|
424
|
+
const lines = context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split('\n') ?? [];
|
|
425
|
+
for (let i = node.startLine - 2; i >= 0 && lines[i].trim().startsWith('@'); i--) {
|
|
426
|
+
if (/^\s*@(?:property|(?:functools\.)?cached_property)\s*$/.test(lines[i]))
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
const PYTHON_MEMBER_LINES = new WeakMap();
|
|
432
|
+
function pythonMemberLines(filePath, context) {
|
|
433
|
+
let files = PYTHON_MEMBER_LINES.get(context);
|
|
434
|
+
if (!files) {
|
|
435
|
+
files = new Map();
|
|
436
|
+
PYTHON_MEMBER_LINES.set(context, files);
|
|
437
|
+
}
|
|
438
|
+
let lines = files.get(filePath);
|
|
439
|
+
if (!lines) {
|
|
440
|
+
lines = (0, strip_comments_1.stripCommentsForRegex)(context.readFile(filePath) ?? '', 'python').split('\n');
|
|
441
|
+
files.set(filePath, lines);
|
|
442
|
+
}
|
|
443
|
+
return lines;
|
|
444
|
+
}
|
|
445
|
+
function pythonLocalType(receiver, ref, context) {
|
|
446
|
+
if (!/^\w+$/.test(receiver))
|
|
447
|
+
return null;
|
|
448
|
+
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
449
|
+
const lines = pythonMemberLines(ref.filePath, context);
|
|
450
|
+
const declaration = new RegExp(`^\\s*${receiver}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
|
|
451
|
+
const annotation = new RegExp(`^\\s*${receiver}\\s*:\\s*["']?([\\w.]+)`);
|
|
452
|
+
for (let i = ref.line - 1; i >= (caller?.startLine ?? 1) - 1; i--) {
|
|
453
|
+
const line = lines[i] ?? '';
|
|
454
|
+
const assigned = line.match(declaration);
|
|
455
|
+
if (assigned)
|
|
456
|
+
return assigned[1] ?? assigned[2].match(/^([A-Z][\w.]*)\s*\(/)?.[1] ?? '<unknown>';
|
|
457
|
+
const declared = line.match(annotation)?.[1];
|
|
458
|
+
if (declared)
|
|
459
|
+
return declared;
|
|
460
|
+
}
|
|
461
|
+
return caller?.signature?.match(new RegExp(`\\b${receiver}\\s*:\\s*["']?([\\w.]+)`))?.[1] ?? null;
|
|
462
|
+
}
|
|
463
|
+
/** Read a field's own annotation/initializer, or a constructor parameter assigned to it. */
|
|
464
|
+
function pythonFieldType(receiver, owner, ref, context) {
|
|
465
|
+
const lines = pythonMemberLines(ref.filePath, context);
|
|
466
|
+
const field = receiver.split('.')[1];
|
|
467
|
+
const assignment = new RegExp(`^\\s*(?:self|cls)\\.${field}\\s*(?::\\s*["']?([\\w.]+)["']?)?\\s*=\\s*(.*)$`);
|
|
468
|
+
const annotation = new RegExp(`^\\s*(?:(?:self|cls)\\.)?${field}\\s*:\\s*["']?([\\w.]+)`);
|
|
469
|
+
const methods = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'method' &&
|
|
470
|
+
n.qualifiedName.startsWith(`${owner.qualifiedName}::`));
|
|
471
|
+
const types = new Set();
|
|
472
|
+
for (let i = owner.startLine; i < owner.endLine; i++) {
|
|
473
|
+
const method = methods.find(n => n.startLine <= i + 1 && n.endLine >= i + 1);
|
|
474
|
+
if (method && method.name !== '__init__' && method.id !== ref.fromNodeId)
|
|
475
|
+
continue;
|
|
476
|
+
if (method?.id === ref.fromNodeId && i + 1 > ref.line)
|
|
477
|
+
continue;
|
|
478
|
+
const line = lines[i] ?? '';
|
|
479
|
+
const declared = !method || /^\s*(?:self|cls)\./.test(line) ? line.match(annotation)?.[1] : undefined;
|
|
480
|
+
if (declared)
|
|
481
|
+
types.add(declared);
|
|
482
|
+
const assigned = line.match(assignment);
|
|
483
|
+
if (!assigned)
|
|
484
|
+
continue;
|
|
485
|
+
if (assigned[1]) {
|
|
486
|
+
types.add(assigned[1]);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const constructor = assigned[2].match(/^([A-Z][\w.]*)\s*\(/)?.[1];
|
|
490
|
+
if (constructor) {
|
|
491
|
+
types.add(constructor);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const param = assigned[2].trim();
|
|
495
|
+
if (method && /^\w+$/.test(param)) {
|
|
496
|
+
const signature = method.signature ?? '';
|
|
497
|
+
const type = signature.match(new RegExp(`\\b${param}\\s*:\\s*["']?([\\w.]+)`))?.[1];
|
|
498
|
+
if (type)
|
|
499
|
+
types.add(type);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
types.add('<unknown>');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Conflicting assignments are known-but-ambiguous, never a name-only fallback.
|
|
506
|
+
return types.size === 1 ? [...types][0] : types.size > 1 ? '<ambiguous>' : null;
|
|
202
507
|
}
|
|
203
508
|
/**
|
|
204
509
|
* Resolve a function-as-value reference (#756) — a function name used as a
|
|
205
510
|
* callback/function-pointer value (`register(handler)`, `o->cb = handler`,
|
|
206
511
|
* `{ .cb = handler }`, `signal(SIGINT, handler)`). The ONLY strategy allowed
|
|
207
512
|
* for `function_ref` refs: exact name, function/method targets only, same
|
|
208
|
-
* language family, same-file first, and cross-file
|
|
209
|
-
*
|
|
210
|
-
* edge is worse than none.
|
|
513
|
+
* language family, same-file first for bare names, and unique-only cross-file.
|
|
514
|
+
* Member values use receiver/type/import scope before a unique-name fallback.
|
|
515
|
+
* A wrong callback edge is worse than none.
|
|
211
516
|
*/
|
|
212
517
|
function matchFunctionRef(ref, context) {
|
|
213
518
|
// `this.<member>` refs are resolved ONLY by the class-scoped resolver in
|
|
214
519
|
// resolveOne (resolveThisMemberFnRef) — never by name matching here.
|
|
215
520
|
if (ref.referenceName.startsWith('this.'))
|
|
216
521
|
return null;
|
|
522
|
+
if ((ref.language === 'python' || ref.language === 'go') && ref.referenceName.includes('.')) {
|
|
523
|
+
return matchMemberFunctionRef(ref, context);
|
|
524
|
+
}
|
|
217
525
|
// In JS/TS/Python a bare identifier can never be a method value (methods
|
|
218
526
|
// are only reachable through a receiver — `this.m` / `self.m` /
|
|
219
527
|
// `Cls.m`), so bare fn-refs match FUNCTIONS only. This also sidesteps the
|
|
@@ -343,6 +651,8 @@ function matchFunctionRef(ref, context) {
|
|
|
343
651
|
}
|
|
344
652
|
return null;
|
|
345
653
|
}
|
|
654
|
+
/** Languages with no nested named functions: nesting in the graph is never a scope. */
|
|
655
|
+
const NO_NESTED_FUNCTIONS = new Set(['c', 'cpp']);
|
|
346
656
|
/**
|
|
347
657
|
* A function nested inside another FUNCTION is only callable from within its
|
|
348
658
|
* container — Python, JS/TS, and every closure language scope it lexically.
|
|
@@ -357,6 +667,15 @@ function matchFunctionRef(ref, context) {
|
|
|
357
667
|
function isLexicallyReachable(candidate, ref, context) {
|
|
358
668
|
if (candidate.kind !== 'function')
|
|
359
669
|
return true;
|
|
670
|
+
// C and C++ have no nested named functions, so a function the graph shows
|
|
671
|
+
// inside another is an extraction artifact, not a scope: tree-sitter-c
|
|
672
|
+
// cannot parse a macro call whose arguments are designated initializers
|
|
673
|
+
// (betaflight's `RESET_CONFIG(pidProfile_t, pidProfile, .pid = {…})`), and
|
|
674
|
+
// its error recovery runs the enclosing function_definition to the end of
|
|
675
|
+
// the file, nesting every function after it. Trusting that nesting rejected
|
|
676
|
+
// 117 real calls into pid.c on that tree; the functions are reachable.
|
|
677
|
+
if (NO_NESTED_FUNCTIONS.has(candidate.language))
|
|
678
|
+
return true;
|
|
360
679
|
const qn = candidate.qualifiedName;
|
|
361
680
|
if (!qn || !qn.includes('::'))
|
|
362
681
|
return true;
|
|
@@ -372,6 +691,503 @@ function isLexicallyReachable(candidate, ref, context) {
|
|
|
372
691
|
return (ref.filePath === candidate.filePath &&
|
|
373
692
|
containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine));
|
|
374
693
|
}
|
|
694
|
+
/** Languages whose module boundary is `import`/`export` (or CommonJS). */
|
|
695
|
+
const ESM_FAMILY = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
|
|
696
|
+
/**
|
|
697
|
+
* A line-initial `import` statement — the marker that a JS/TS file is a MODULE
|
|
698
|
+
* rather than a classic script. Line-anchored and followed by a name, brace,
|
|
699
|
+
* star or quote, so a dynamic `import(` and the word inside a comment or string
|
|
700
|
+
* do not match.
|
|
701
|
+
*/
|
|
702
|
+
const HAS_IMPORT_STATEMENT = /^[ \t]*import[\s{*'"]/m;
|
|
703
|
+
/**
|
|
704
|
+
* Anything the file could offer another file, in every form the extractor's own
|
|
705
|
+
* `isExported` flag misses. `^export` covers the declaration and later forms
|
|
706
|
+
* (`export const`, `export { x }`, `export default x`, `export *`); the
|
|
707
|
+
* CommonJS shapes cover files that never use ESM syntax at all, in both the dot
|
|
708
|
+
* and the bracket form; and `declare global` contributes names to every file
|
|
709
|
+
* whether or not the module exports anything of its own. Kept as a source test
|
|
710
|
+
* rather than a node scan precisely because `isExported` is set only from an
|
|
711
|
+
* `export_statement` ancestor, so `const x = …; export { x }` and
|
|
712
|
+
* `module.exports = { x }` both read as unexported on the node.
|
|
713
|
+
*/
|
|
714
|
+
const HAS_ESM_EXPORT = /^[ \t]*export[\s{*]|^[ \t]*declare\s+global\b/m;
|
|
715
|
+
const HAS_CJS_EXPORT = /\bmodule\.exports\b|\bexports\s*[.[]/;
|
|
716
|
+
/**
|
|
717
|
+
* Per-context memo of "this file is a module that exports nothing", asked once
|
|
718
|
+
* per candidate FILE rather than once per reference. Derived from file source,
|
|
719
|
+
* so it drops with the context's file caches — clearNameMatcherMemos deletes it
|
|
720
|
+
* alongside INFER_SCAN_STATES.
|
|
721
|
+
*/
|
|
722
|
+
const SEALED_MODULES = new WeakMap();
|
|
723
|
+
/**
|
|
724
|
+
* Whether `filePath` is a JS/TS module that exports NOTHING — an import
|
|
725
|
+
* statement present, no export of any form. No reference from another file can
|
|
726
|
+
* reach any binding in such a file, so every one of its symbols is a false
|
|
727
|
+
* candidate for a cross-file name match.
|
|
728
|
+
*
|
|
729
|
+
* This is the general case behind a package name capturing a same-named local:
|
|
730
|
+
* on `vitejs/vite`, 157 cross-file `imports` refs — every `import { defineConfig
|
|
731
|
+
* } from 'vite'` in the playground and the create-vite templates — resolved onto
|
|
732
|
+
* `playground/ssr-html/test-stacktrace.js::vite`, which is `const vite = await
|
|
733
|
+
* createServer(…)` at module scope in a file with zero exports. The existing
|
|
734
|
+
* guards cannot see it: `isLexicallyReachable` returns early for any candidate
|
|
735
|
+
* that is not a `function`, and the bare-import guard correctly declines because
|
|
736
|
+
* `vite` IS a workspace member, so the specifier really is project-local. What
|
|
737
|
+
* is wrong is only which node the name lands on.
|
|
738
|
+
*
|
|
739
|
+
* Deliberately narrow on three axes, because each is a class this would
|
|
740
|
+
* otherwise resolve wrongly in the opposite direction:
|
|
741
|
+
*
|
|
742
|
+
* - **A classic script is exempt.** Requiring an `import` statement means a
|
|
743
|
+
* non-module `.js` file — concatenated globals, a browser `<script>` — keeps
|
|
744
|
+
* its cross-file matches, where a top-level binding genuinely is reachable.
|
|
745
|
+
* - **CommonJS is exempt.** `module.exports` and `exports.x` are matched as
|
|
746
|
+
* exports, so a CJS file is never sealed.
|
|
747
|
+
* - **Other languages are exempt.** Go, Python, Java and the rest have no
|
|
748
|
+
* equivalent boundary, and several extractors hardcode `isExported`.
|
|
749
|
+
*/
|
|
750
|
+
function isSealedModule(filePath, context) {
|
|
751
|
+
let memo = SEALED_MODULES.get(context);
|
|
752
|
+
if (!memo) {
|
|
753
|
+
memo = new Map();
|
|
754
|
+
SEALED_MODULES.set(context, memo);
|
|
755
|
+
}
|
|
756
|
+
const hit = memo.get(filePath);
|
|
757
|
+
if (hit !== undefined)
|
|
758
|
+
return hit;
|
|
759
|
+
// CommonJS assignments can execute inside template interpolations, which the
|
|
760
|
+
// masker blanks. Keep the conservative raw-source exemption for those forms.
|
|
761
|
+
// Cheapest disqualifiers first: nearly every module exports a node (asked
|
|
762
|
+
// without reading or decoding the file), and masking the source is only
|
|
763
|
+
// needed to rule out the ones that don't. (The masker only blanks text, so
|
|
764
|
+
// no `import` in the source means none in code.)
|
|
765
|
+
const exportsNode = context.fileHasExportedNode
|
|
766
|
+
? context.fileHasExportedNode(filePath)
|
|
767
|
+
: context.getNodesInFile(filePath).some((n) => n.isExported);
|
|
768
|
+
const source = exportsNode ? null : context.readFile(filePath);
|
|
769
|
+
const sealed = !exportsNode && source !== null && source.includes('import') &&
|
|
770
|
+
!HAS_CJS_EXPORT.test(source) &&
|
|
771
|
+
(() => {
|
|
772
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(source, 'typescript'));
|
|
773
|
+
return HAS_IMPORT_STATEMENT.test(code) && !HAS_ESM_EXPORT.test(code);
|
|
774
|
+
})();
|
|
775
|
+
memo.set(filePath, sealed);
|
|
776
|
+
return sealed;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Whether `candidate` can be named by a reference in `ref`'s file at all.
|
|
780
|
+
* Both name-based strategies validate their chosen candidate. Removing an
|
|
781
|
+
* unreachable candidate before ranking can promote an unrelated runner-up;
|
|
782
|
+
* rejecting the chosen target must leave the reference unresolved instead.
|
|
783
|
+
*/
|
|
784
|
+
function isCrossFileReachable(candidate, ref, context) {
|
|
785
|
+
if (ref.language !== 'markdown' && candidate.language === 'markdown')
|
|
786
|
+
return false;
|
|
787
|
+
if (ref.referenceKind === 'calls' && ESM_FAMILY.has(candidate.language) &&
|
|
788
|
+
(candidate.kind === 'constant' || candidate.kind === 'variable') &&
|
|
789
|
+
/^=\s*require\s*\(\s*(['"])[^'"]+\.json\1\s*\)\s*;?\s*$/.test(candidate.signature ?? ''))
|
|
790
|
+
return false;
|
|
791
|
+
return (candidate.filePath === ref.filePath ||
|
|
792
|
+
!ESM_FAMILY.has(candidate.language) ||
|
|
793
|
+
!isSealedModule(candidate.filePath, context));
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Languages in which `visibility: 'private'` on a definition means no other
|
|
797
|
+
* FILE can name it: a Kotlin `private fun` is file- or class-local, and the
|
|
798
|
+
* same holds for Java, C#, Swift, Scala, Dart and PHP members.
|
|
799
|
+
*/
|
|
800
|
+
const PRIVATE_IS_FILE_LOCAL = new Set(['kotlin', 'java', 'csharp', 'swift', 'scala', 'dart', 'php']);
|
|
801
|
+
/** Per-context memo: node id → "this C/C++ function is declared `static`". */
|
|
802
|
+
const C_STATIC_MEMO = new WeakMap();
|
|
803
|
+
/**
|
|
804
|
+
* A C/C++ file that IS a translation unit. A `static` defined here is local
|
|
805
|
+
* to it. A `static` (typically `static inline`) in a header is a different
|
|
806
|
+
* thing: the header is textually included, so the function exists in every
|
|
807
|
+
* unit that includes it and is callable from each — MAVLink's generated
|
|
808
|
+
* `mavlink_msg_*.h` are nothing but such functions, 4,306 real calls on one
|
|
809
|
+
* betaflight tree.
|
|
810
|
+
*/
|
|
811
|
+
const C_SOURCE_EXT = /\.(c|cc|cpp|cxx|c\+\+|m|mm)$/i;
|
|
812
|
+
/**
|
|
813
|
+
* Whether a C/C++ function definition carries the `static` storage class —
|
|
814
|
+
* read from its first source line(s), since the extractor records no storage
|
|
815
|
+
* class and the kernel arm would need the same field. `static` on the line
|
|
816
|
+
* above the name (`static void\nfoo(void)`) is the common alternative layout.
|
|
817
|
+
*/
|
|
818
|
+
function isStaticCFunction(candidate, context) {
|
|
819
|
+
let memo = C_STATIC_MEMO.get(context);
|
|
820
|
+
if (!memo) {
|
|
821
|
+
memo = new Map();
|
|
822
|
+
C_STATIC_MEMO.set(context, memo);
|
|
823
|
+
}
|
|
824
|
+
const hit = memo.get(candidate.id);
|
|
825
|
+
if (hit !== undefined)
|
|
826
|
+
return hit;
|
|
827
|
+
const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split('\n') ?? [];
|
|
828
|
+
const head = [lines[candidate.startLine - 2] ?? '', lines[candidate.startLine - 1] ?? ''].join('\n');
|
|
829
|
+
const isStatic = /(^|[\s;}])static\s/.test(head);
|
|
830
|
+
memo.set(candidate.id, isStatic);
|
|
831
|
+
return isStatic;
|
|
832
|
+
}
|
|
833
|
+
/** Per-context memo: node id → "this Rust method implements a trait". */
|
|
834
|
+
const RUST_TRAIT_IMPL_MEMO = new WeakMap();
|
|
835
|
+
/**
|
|
836
|
+
* Whether a Rust method sits in an `impl Trait for Type` block. Such a method
|
|
837
|
+
* carries no `pub` — the trait decides its visibility — so the extractor
|
|
838
|
+
* records it as private; it is reachable wherever the trait is. Read from the
|
|
839
|
+
* nearest enclosing `impl` header above the method, memoised per node.
|
|
840
|
+
*/
|
|
841
|
+
function isRustTraitImplMethod(candidate, context) {
|
|
842
|
+
if (candidate.kind !== 'method')
|
|
843
|
+
return false;
|
|
844
|
+
let memo = RUST_TRAIT_IMPL_MEMO.get(context);
|
|
845
|
+
if (!memo) {
|
|
846
|
+
memo = new Map();
|
|
847
|
+
RUST_TRAIT_IMPL_MEMO.set(context, memo);
|
|
848
|
+
}
|
|
849
|
+
const hit = memo.get(candidate.id);
|
|
850
|
+
if (hit !== undefined)
|
|
851
|
+
return hit;
|
|
852
|
+
const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split('\n') ?? [];
|
|
853
|
+
let isTrait = false;
|
|
854
|
+
for (let i = candidate.startLine - 2; i >= 0; i--) {
|
|
855
|
+
const line = lines[i] ?? '';
|
|
856
|
+
if (/^\s*(pub(\([^)]*\))?\s+)?(unsafe\s+)?impl\b/.test(line)) {
|
|
857
|
+
isTrait = /\sfor\s/.test(line.replace(/\/\/.*$/, ''));
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
// A top-level item above the method means it was not inside an impl.
|
|
861
|
+
if (/^(pub(\([^)]*\))?\s+)?(fn|struct|enum|mod|trait|const|static|type)\b/.test(line))
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
memo.set(candidate.id, isTrait);
|
|
865
|
+
return isTrait;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* The directory a Rust file's private items are visible from: the file's own
|
|
869
|
+
* module subtree. `src/net.rs` and `src/net/mod.rs` own `src/net/`; a crate
|
|
870
|
+
* root (`lib.rs` / `main.rs`) owns its directory. A child module reaches its
|
|
871
|
+
* ancestors' private items (`super::`), a sibling or another crate never does.
|
|
872
|
+
*/
|
|
873
|
+
function rustModuleDir(filePath) {
|
|
874
|
+
const base = path.posix.basename(filePath);
|
|
875
|
+
const dir = path.posix.dirname(filePath);
|
|
876
|
+
if (base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs')
|
|
877
|
+
return dir;
|
|
878
|
+
return path.posix.join(dir, base.replace(/\.rs$/, ''));
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Whether `candidate` can be NAMED from a reference in `ref`'s file at all,
|
|
882
|
+
* given what its language says about the definition's visibility. A
|
|
883
|
+
* definition the language makes file-local is not a candidate for a
|
|
884
|
+
* cross-file name match, however well the names agree:
|
|
885
|
+
*
|
|
886
|
+
* - **C / C++**: a `static` function defined in a SOURCE file is local to
|
|
887
|
+
* that translation unit; one in a header is part of every unit that
|
|
888
|
+
* includes it and stays visible. On a 2,109-file betaflight tree 145
|
|
889
|
+
* cross-file calls resolved onto a `static` in another `.c` (#1730) —
|
|
890
|
+
* `usbd_get_descriptor` onto the `static get_device_descriptor` of
|
|
891
|
+
* whichever USB class file ranked first.
|
|
892
|
+
* - **Kotlin, Java, C#, Swift, Scala, Dart, PHP**: `private` is class- or
|
|
893
|
+
* file-local. An Android `editor.apply()` resolved onto an unrelated class's
|
|
894
|
+
* `private fun apply`.
|
|
895
|
+
* - **Go**: an unexported (lowercase) identifier is package-local, and a
|
|
896
|
+
* package is a directory. Judged by the name's case: the extractor's
|
|
897
|
+
* `isExported` is unset for every Go method.
|
|
898
|
+
* - **Rust**: a non-`pub` item is visible to its module and that module's
|
|
899
|
+
* descendants, never to a sibling module or another crate — `.count()` on
|
|
900
|
+
* an iterator resolved onto a `fn count` in a different crate. A method in
|
|
901
|
+
* an `impl Trait for Type` block has the trait's visibility, not `private`.
|
|
902
|
+
* - **JS / TS / ArkTS**: a binding in a module that exports nothing (an
|
|
903
|
+
* `import` present, no `export` / CommonJS / `declare global`) is sealed —
|
|
904
|
+
* the vite playground's `const vite = await createServer(…)` took 157
|
|
905
|
+
* `import { defineConfig } from 'vite'` edges (#1719). Classic scripts,
|
|
906
|
+
* CommonJS, later `export { … }`, and ambient globals stay visible.
|
|
907
|
+
*
|
|
908
|
+
* Same-file candidates are always visible. Applied by ReferenceResolver to
|
|
909
|
+
* the target the whole name-matching pipeline settled on, so a rejection ends
|
|
910
|
+
* the reference unresolved: declining inside matchByExactName instead let the
|
|
911
|
+
* ref fall through to matchFuzzy, which then committed to a same-language
|
|
912
|
+
* namesake the ranking had passed over — eight such edges on one tree, all
|
|
913
|
+
* onto a local `const fail = …` arrow the graph does not hold. matchFuzzy
|
|
914
|
+
* checks its own survivor as well, since nothing runs after it.
|
|
915
|
+
*/
|
|
916
|
+
function isVisibleAcrossFiles(candidate, ref, context) {
|
|
917
|
+
if (candidate.filePath === ref.filePath)
|
|
918
|
+
return true;
|
|
919
|
+
const lang = candidate.language;
|
|
920
|
+
if (lang === 'c' || lang === 'cpp') {
|
|
921
|
+
return (candidate.kind !== 'function' ||
|
|
922
|
+
!C_SOURCE_EXT.test(candidate.filePath) ||
|
|
923
|
+
!isStaticCFunction(candidate, context));
|
|
924
|
+
}
|
|
925
|
+
if (lang === 'go') {
|
|
926
|
+
// By the name's first letter, not the extractor's flag: the flag is unset
|
|
927
|
+
// for every Go method, exported or not.
|
|
928
|
+
return /^[A-Z]/.test(candidate.name) || path.posix.dirname(candidate.filePath) === path.posix.dirname(ref.filePath);
|
|
929
|
+
}
|
|
930
|
+
if (lang === 'rust') {
|
|
931
|
+
if (candidate.visibility !== 'private')
|
|
932
|
+
return true;
|
|
933
|
+
if (isRustTraitImplMethod(candidate, context))
|
|
934
|
+
return true;
|
|
935
|
+
const owner = rustModuleDir(candidate.filePath);
|
|
936
|
+
return ref.filePath.startsWith(owner + '/');
|
|
937
|
+
}
|
|
938
|
+
if (PRIVATE_IS_FILE_LOCAL.has(lang))
|
|
939
|
+
return candidate.visibility !== 'private';
|
|
940
|
+
// JS/TS/ArkTS sealed modules + markdown/JSON call-target guards (#1719).
|
|
941
|
+
// Same predicate matchByExactName / matchFuzzy apply to their survivors so a
|
|
942
|
+
// rejection here cannot fall through to a promoted runner-up.
|
|
943
|
+
return isCrossFileReachable(candidate, ref, context);
|
|
944
|
+
}
|
|
945
|
+
const JS_FAMILY = new Set(['typescript', 'tsx', 'javascript', 'jsx']);
|
|
946
|
+
/**
|
|
947
|
+
* Whether a JS/TS `calls` ref is a RECEIVER-LESS call — `serialize(x)`, not
|
|
948
|
+
* `this.serialize(x)` / `obj.serialize(x)`. The extractor emits `this.m()`
|
|
949
|
+
* and `super.m()` under the bare method name, so the receiver is read back
|
|
950
|
+
* from the call site's own line: the text at the ref's column is the call
|
|
951
|
+
* expression, and it starts with the name itself only when nothing precedes
|
|
952
|
+
* it. In JS/TS a bare call can never bind to a class method (methods need a
|
|
953
|
+
* receiver), so a `method` node is not a candidate for it (#1714) — the
|
|
954
|
+
* enclosing method itself least of all, which the same-file proximity term
|
|
955
|
+
* used to pick over the module-scope function the call actually means.
|
|
956
|
+
*/
|
|
957
|
+
function isBareJsCall(ref, context) {
|
|
958
|
+
return JS_FAMILY.has(ref.language) && isReceiverLessCall(ref, context);
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Whether a Go `calls` ref is receiver-less — `relogin(ctx)`, not
|
|
962
|
+
* `l.relogin(ctx)`. A Go method is only reachable through a value or a method
|
|
963
|
+
* expression, so a bare call (a func parameter, a local func value, a
|
|
964
|
+
* package-level function) is never a method, in its own package or in one the
|
|
965
|
+
* file does not import (#1857). Read from the source line like the JS/TS
|
|
966
|
+
* check, because `pkg.Factory().Method()` reaches the resolver as a bare
|
|
967
|
+
* `Method` ref too.
|
|
968
|
+
*/
|
|
969
|
+
function isBareGoCall(ref, context) {
|
|
970
|
+
return ref.language === 'go' && isReceiverLessCall(ref, context);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Whether a PHP `calls` ref is a bare function call — `redirect($url)`,
|
|
974
|
+
* `view('books.show')` — rather than `$this->redirect()` / `$obj->view()` /
|
|
975
|
+
* `Foo::view()`. PHP has no implicit `$this`: a call written without a
|
|
976
|
+
* receiver can only be a function, so a method, field or property that shares
|
|
977
|
+
* the name is never what it calls. Name-matching used to bind BookStack's
|
|
978
|
+
* every `return redirect(…)` to ApiDocsController::redirect and every
|
|
979
|
+
* `return view(…)` to a `$view` field.
|
|
980
|
+
*
|
|
981
|
+
* PHP refs record the column of the call EXPRESSION — `$this->setPageTitle(`
|
|
982
|
+
* sits at `$this` — so a bare call is the one whose text at its column is the
|
|
983
|
+
* name itself (a leading `\` for a fully qualified function is allowed).
|
|
984
|
+
*/
|
|
985
|
+
function isBarePhpCall(ref, context) {
|
|
986
|
+
if (ref.language !== 'php' || ref.referenceKind !== 'calls')
|
|
987
|
+
return false;
|
|
988
|
+
if (!/^\w+$/.test(ref.referenceName))
|
|
989
|
+
return false;
|
|
990
|
+
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
|
|
991
|
+
?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
|
|
992
|
+
if (line === undefined)
|
|
993
|
+
return false;
|
|
994
|
+
const at = line[ref.column] === '\\' ? ref.column + 1 : ref.column;
|
|
995
|
+
if (!line.startsWith(ref.referenceName, at))
|
|
996
|
+
return false;
|
|
997
|
+
CALL_OPENER.lastIndex = at + ref.referenceName.length;
|
|
998
|
+
if (!CALL_OPENER.test(line))
|
|
999
|
+
return false;
|
|
1000
|
+
let end = at;
|
|
1001
|
+
while (end > 0 && WHITESPACE.test(line[end - 1]))
|
|
1002
|
+
end--;
|
|
1003
|
+
// `$obj->name(` / `Foo::name(` / `$obj?->name(`, should a column ever land on the name.
|
|
1004
|
+
return !(end > 0 && (line[end - 1] === '>' || line[end - 1] === ':'));
|
|
1005
|
+
}
|
|
1006
|
+
function isReceiverLessCall(ref, context) {
|
|
1007
|
+
if (ref.referenceKind !== 'calls')
|
|
1008
|
+
return false;
|
|
1009
|
+
if (ref.referenceName.includes('.'))
|
|
1010
|
+
return false;
|
|
1011
|
+
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
|
|
1012
|
+
?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
|
|
1013
|
+
if (line === undefined)
|
|
1014
|
+
return false;
|
|
1015
|
+
// `^<name>\s*[(<]` at the call's column, without compiling a pattern per call.
|
|
1016
|
+
if (!line.startsWith(ref.referenceName, ref.column))
|
|
1017
|
+
return false;
|
|
1018
|
+
CALL_OPENER.lastIndex = ref.column + ref.referenceName.length;
|
|
1019
|
+
if (!CALL_OPENER.test(line))
|
|
1020
|
+
return false;
|
|
1021
|
+
// Nothing but whitespace, an operator or an opener may precede a bare call:
|
|
1022
|
+
// read the text before the column backwards, past trailing whitespace,
|
|
1023
|
+
// instead of end-anchoring a pattern that scans the prefix from its start.
|
|
1024
|
+
let end = Math.min(ref.column, line.length);
|
|
1025
|
+
while (end > 0 && WHITESPACE.test(line[end - 1]))
|
|
1026
|
+
end--;
|
|
1027
|
+
if (end === 0 || !RECEIVER_TAIL_CHAR.test(line[end - 1]))
|
|
1028
|
+
return true;
|
|
1029
|
+
let start = end;
|
|
1030
|
+
while (start > 0 && WORD_CHAR.test(line[start - 1]))
|
|
1031
|
+
start--;
|
|
1032
|
+
return BARE_CALL_KEYWORDS.has(line.slice(start, end));
|
|
1033
|
+
}
|
|
1034
|
+
/** `\s*[(<]` from a given index (sticky). */
|
|
1035
|
+
const CALL_OPENER = /\s*[(<]/y;
|
|
1036
|
+
const WHITESPACE = /\s/;
|
|
1037
|
+
const WORD_CHAR = /\w/;
|
|
1038
|
+
/** A character that ends a receiver: `.`, a word character, `$`, `]` or `)`. */
|
|
1039
|
+
const RECEIVER_TAIL_CHAR = /[.\w$\])]/;
|
|
1040
|
+
/** Keywords after which a name starts an expression, so the call has no receiver. */
|
|
1041
|
+
const BARE_CALL_KEYWORDS = new Set([
|
|
1042
|
+
'return', 'await', 'yield', 'typeof', 'void', 'new', 'else', 'case', 'throw', 'in', 'of', 'instanceof', 'go', 'defer',
|
|
1043
|
+
]);
|
|
1044
|
+
/** Per-context memo: `file\0name` → "the file binds this name locally". */
|
|
1045
|
+
const LOCAL_BINDING_MEMO = new WeakMap();
|
|
1046
|
+
const LOCAL_BINDING_SITES = new WeakMap();
|
|
1047
|
+
const LOCAL_BINDING_SITES_KEEP = 16;
|
|
1048
|
+
const VAR_DECL_SITE = /\b(?:const|let|var)\s/g;
|
|
1049
|
+
const FN_DECL_SITE = /\b(?:function|class)\s/g;
|
|
1050
|
+
function localBindingSites(filePath, source, context) {
|
|
1051
|
+
let cache = LOCAL_BINDING_SITES.get(context);
|
|
1052
|
+
if (!cache) {
|
|
1053
|
+
cache = new Map();
|
|
1054
|
+
LOCAL_BINDING_SITES.set(context, cache);
|
|
1055
|
+
}
|
|
1056
|
+
let sites = cache.get(filePath);
|
|
1057
|
+
if (!sites) {
|
|
1058
|
+
const offsets = (re) => Array.from(source.matchAll(re), (m) => m.index);
|
|
1059
|
+
const arrows = [];
|
|
1060
|
+
for (let a = source.indexOf('=>'); a !== -1; a = source.indexOf('=>', a + 2))
|
|
1061
|
+
arrows.push(a);
|
|
1062
|
+
sites = { varDecls: offsets(VAR_DECL_SITE), fnDecls: offsets(FN_DECL_SITE), arrows };
|
|
1063
|
+
if (cache.size >= LOCAL_BINDING_SITES_KEEP)
|
|
1064
|
+
cache.delete(cache.keys().next().value);
|
|
1065
|
+
cache.set(filePath, sites);
|
|
1066
|
+
}
|
|
1067
|
+
return sites;
|
|
1068
|
+
}
|
|
1069
|
+
/** Sticky binding patterns by name — the same names recur file after file. */
|
|
1070
|
+
const LOCAL_BINDING_PATTERNS = new Map();
|
|
1071
|
+
const LOCAL_BINDING_PATTERNS_CAP = 4096;
|
|
1072
|
+
const JS_BINDING_NAME = /^[\w$]+$/;
|
|
1073
|
+
const ARROW_HEAD_CHAR = /[\w$.]/;
|
|
1074
|
+
const IMPORT_BINDING_VALUE = /^\s*(?:await\s+)?(?:require|import)\s*\(/;
|
|
1075
|
+
function localBindingPatterns(name, flags) {
|
|
1076
|
+
const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1077
|
+
return {
|
|
1078
|
+
// `const { name } = require('./m')` / `= await import('./m')` binds an IMPORT,
|
|
1079
|
+
// not a shadow: the symbol lives in the other file and the call means it.
|
|
1080
|
+
decl: new RegExp('\\b(?:const|let|var)\\s+(?:' + n + '\\b|[{\\[][^;=]*?\\b' + n + '\\b[^;=]*?[}\\]])\\s*(?:=\\s*([^;\\n]*))?', flags),
|
|
1081
|
+
fn: new RegExp('\\b(?:function|class)\\s+' + n + '\\b', flags),
|
|
1082
|
+
// a parameter: every token before the name in the list is itself a
|
|
1083
|
+
// parameter (identifier, optional type, optional default) — so a string
|
|
1084
|
+
// argument containing the word cannot match.
|
|
1085
|
+
// Each earlier parameter has exactly one parse: its first non-space
|
|
1086
|
+
// character after the identifier picks the type (`?`/`:`), default (`=`)
|
|
1087
|
+
// or bare alternative. Written as `(type)?(default)?\s*`, the same
|
|
1088
|
+
// strings split several ways per parameter, and a failing search
|
|
1089
|
+
// backtracked through every combination — 30-40s per name on a vscode
|
|
1090
|
+
// test file whose helper takes nine `name: T = value` parameters.
|
|
1091
|
+
param: new RegExp('\\(\\s*(?:(?:\\.\\.\\.)?[\\w$]+(?:\\s*(?:\\?\\s*)?:[^,()]+|\\s*=[^,()]+|\\s*),\\s*)*' +
|
|
1092
|
+
n + '\\b(?:\\s*\\??\\s*:[^,()]*)?(?:\\s*=[^,()]*)?(?:\\s*,\\s*[^()]*)?\\)\\s*(?::[^=;{]*)?(?:=>|\\{)', flags),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Whether a JS/TS file binds `name` itself — as a `const`/`let`/`var`/
|
|
1097
|
+
* `function`/`class` declaration (destructuring included) or as a parameter
|
|
1098
|
+
* of a function or arrow. Such a binding shadows every same-named symbol in
|
|
1099
|
+
* other files, so a bare call to it has no cross-file candidate: the
|
|
1100
|
+
* `resolve` of `new Promise((resolve, reject) => …)`, a spec's
|
|
1101
|
+
* `const transform = await makeTransform()`, a factory's `const now =
|
|
1102
|
+
* options.now || (() => new Date())`. None of these is a node the graph
|
|
1103
|
+
* holds (a parameter, a const bound to a call result), so without this the
|
|
1104
|
+
* matcher hands the call to whichever other file defines the name — and
|
|
1105
|
+
* once methods stop being candidates for a bare call (#1714), the function
|
|
1106
|
+
* that was out-ranked steps in. Read from source, memoised per file+name.
|
|
1107
|
+
*/
|
|
1108
|
+
function isLocallyBoundJsName(name, filePath, context) {
|
|
1109
|
+
let memo = LOCAL_BINDING_MEMO.get(context);
|
|
1110
|
+
if (!memo) {
|
|
1111
|
+
memo = new Map();
|
|
1112
|
+
LOCAL_BINDING_MEMO.set(context, memo);
|
|
1113
|
+
}
|
|
1114
|
+
const key = filePath + '\0' + name;
|
|
1115
|
+
const hit = memo.get(key);
|
|
1116
|
+
if (hit !== undefined)
|
|
1117
|
+
return hit;
|
|
1118
|
+
const source = context.readFile(filePath) ?? '';
|
|
1119
|
+
const bound = JS_BINDING_NAME.test(name)
|
|
1120
|
+
? bindsAtSites(source, name, localBindingSites(filePath, source, context))
|
|
1121
|
+
: bindsAnywhere(source, name);
|
|
1122
|
+
memo.set(key, bound);
|
|
1123
|
+
return bound;
|
|
1124
|
+
}
|
|
1125
|
+
/** isLocallyBoundJsName's patterns, searched through the whole source. */
|
|
1126
|
+
function bindsAnywhere(source, name) {
|
|
1127
|
+
const { decl, fn, param } = localBindingPatterns(name, 'g');
|
|
1128
|
+
for (const m of source.matchAll(decl)) {
|
|
1129
|
+
if (!IMPORT_BINDING_VALUE.test(m[1] ?? ''))
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1133
|
+
return fn.test(source) || param.test(source) || new RegExp('(?:^|[^\\w$.])' + n + '\\s*=>').test(source);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* bindsAnywhere for a plain identifier, tried only where a match can start:
|
|
1137
|
+
* a declaration at its keyword (in order, resuming past each match exactly as
|
|
1138
|
+
* the global search does), a parameter at the `(` its list opens with — the
|
|
1139
|
+
* last `(` before an occurrence of the name, with no `)` between, since no
|
|
1140
|
+
* earlier parameter can hold a parenthesis — and `name =>` at each arrow.
|
|
1141
|
+
*/
|
|
1142
|
+
function bindsAtSites(source, name, sites) {
|
|
1143
|
+
let patterns = LOCAL_BINDING_PATTERNS.get(name);
|
|
1144
|
+
if (!patterns) {
|
|
1145
|
+
patterns = localBindingPatterns(name, 'y');
|
|
1146
|
+
if (LOCAL_BINDING_PATTERNS.size >= LOCAL_BINDING_PATTERNS_CAP) {
|
|
1147
|
+
LOCAL_BINDING_PATTERNS.delete(LOCAL_BINDING_PATTERNS.keys().next().value);
|
|
1148
|
+
}
|
|
1149
|
+
LOCAL_BINDING_PATTERNS.set(name, patterns);
|
|
1150
|
+
}
|
|
1151
|
+
const { decl, fn, param } = patterns;
|
|
1152
|
+
let from = 0;
|
|
1153
|
+
for (const at of sites.varDecls) {
|
|
1154
|
+
if (at < from)
|
|
1155
|
+
continue;
|
|
1156
|
+
decl.lastIndex = at;
|
|
1157
|
+
const m = decl.exec(source);
|
|
1158
|
+
if (!m)
|
|
1159
|
+
continue;
|
|
1160
|
+
from = at + m[0].length;
|
|
1161
|
+
if (!IMPORT_BINDING_VALUE.test(m[1] ?? ''))
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
for (const at of sites.fnDecls) {
|
|
1165
|
+
fn.lastIndex = at;
|
|
1166
|
+
if (fn.test(source))
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
let tried = -1;
|
|
1170
|
+
for (let at = source.indexOf(name); at !== -1; at = source.indexOf(name, at + 1)) {
|
|
1171
|
+
const open = at > 0 ? source.lastIndexOf('(', at - 1) : -1;
|
|
1172
|
+
if (open < 0 || open === tried || source.lastIndexOf(')', at - 1) > open)
|
|
1173
|
+
continue;
|
|
1174
|
+
tried = open;
|
|
1175
|
+
param.lastIndex = open;
|
|
1176
|
+
if (param.test(source))
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
// `name =>`: the name ends where the whitespace before the arrow starts.
|
|
1180
|
+
for (const arrow of sites.arrows) {
|
|
1181
|
+
let end = arrow;
|
|
1182
|
+
while (end > 0 && WHITESPACE.test(source[end - 1]))
|
|
1183
|
+
end--;
|
|
1184
|
+
const start = end - name.length;
|
|
1185
|
+
if (start >= 0 && source.startsWith(name, start) && (start === 0 || !ARROW_HEAD_CHAR.test(source[start - 1]))) {
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
375
1191
|
/**
|
|
376
1192
|
* Try to resolve a reference by exact name match
|
|
377
1193
|
*/
|
|
@@ -385,15 +1201,65 @@ function matchByExactName(ref, context) {
|
|
|
385
1201
|
// unresolved import refs each scored K same-named import candidates through
|
|
386
1202
|
// findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
|
|
387
1203
|
// large import-heavy (front-end + back-end) repos (#915).
|
|
388
|
-
const
|
|
389
|
-
|
|
1204
|
+
const bareJs = isBareJsCall(ref, context);
|
|
1205
|
+
const bareGo = isBareGoCall(ref, context);
|
|
1206
|
+
const barePhp = isBarePhpCall(ref, context);
|
|
1207
|
+
if (bareJs) {
|
|
1208
|
+
const storeAction = matchJsStoreBindingCall(ref, context);
|
|
1209
|
+
if (storeAction)
|
|
1210
|
+
return storeAction;
|
|
1211
|
+
}
|
|
1212
|
+
// Every rule below judges one candidate on its own, so they run as ONE pass,
|
|
1213
|
+
// the kind/language checks before the ones that read source: a common name
|
|
1214
|
+
// has thousands of same-named nodes, and a chain of filters copied that
|
|
1215
|
+
// list once per rule for every reference.
|
|
1216
|
+
const valueRef = ref.referenceKind === 'references' || ref.referenceKind === 'function_ref';
|
|
1217
|
+
const importRef = ref.referenceKind === 'imports';
|
|
1218
|
+
const inheritanceRef = (0, types_1.isInheritanceRef)(ref);
|
|
1219
|
+
const sameName = context.getNodesByName(ref.referenceName);
|
|
1220
|
+
// `NAME(...)` where NAME is a function-like macro somewhere in the project is
|
|
1221
|
+
// an expansion or a call to a same-named function — never the macro itself
|
|
1222
|
+
// (#1839), and never a type that happens to share the name (#2070: expat's
|
|
1223
|
+
// `PREFIX(scanRef)(…)` bound to an unrelated `struct PREFIX`). Keep
|
|
1224
|
+
// upstream's language gate on the chosen result.
|
|
1225
|
+
const cMacroCall = ref.referenceKind === 'calls' && (ref.language === 'c' || ref.language === 'cpp') &&
|
|
1226
|
+
sameName.some((n) => n.kind === 'constant' && types_1.CPP_DEFINE_SIGNATURE.test(n.signature ?? ''));
|
|
1227
|
+
const candidates = sameName.filter((n) => !(cMacroCall && n.kind !== 'function' && n.kind !== 'method') &&
|
|
1228
|
+
// Type/value references retain same-family eligibility: a native namesake
|
|
1229
|
+
// must not hide the actual web type. Calls still gate only the winner.
|
|
1230
|
+
(!valueRef || sameLanguageFamily(n.language, ref.language)) &&
|
|
1231
|
+
n.kind !== 'import' &&
|
|
1232
|
+
// A receiver-less JS/TS or Go call cannot reach a method (#1714, #1857).
|
|
1233
|
+
!((bareJs || bareGo) && n.kind === 'method') &&
|
|
1234
|
+
// A bare PHP call is a function call: nothing else is callable without a receiver.
|
|
1235
|
+
!(barePhp && n.kind !== 'function') &&
|
|
1236
|
+
// An `extends`/`implements` ref names a supertype, so anything that can't
|
|
1237
|
+
// BE one is not a candidate at all. This is eligibility, not
|
|
1238
|
+
// ranking: kind is only a scoring bonus below (and none is awarded for
|
|
1239
|
+
// inheritance refs), so without this a same-named `enum_member` outranked
|
|
1240
|
+
// the real `trait`, and as the sole candidate was adopted outright by the
|
|
1241
|
+
// single-match shortcut. Restricting the pool BEFORE ranking lets the
|
|
1242
|
+
// legitimate supertype win instead of merely dropping the false edge.
|
|
1243
|
+
(!inheritanceRef || (0, types_1.isSupertypeTarget)(n)) &&
|
|
1244
|
+
// Likewise for `imports`: a member that only exists inside a type is not
|
|
1245
|
+
// importable, so it is not a candidate. Without this a `path`/`id`/`url`
|
|
1246
|
+
// import resolved to some interface's same-named property.
|
|
1247
|
+
(!importRef || (0, types_1.isImportableKind)(n.kind)) &&
|
|
390
1248
|
// Nested locals are only reachable from inside their container (#1230).
|
|
391
|
-
|
|
1249
|
+
isLexicallyReachable(n, ref, context) &&
|
|
1250
|
+
// Preserve import ranking; calls reject the winner without promoting another.
|
|
1251
|
+
(!importRef || n.filePath === ref.filePath ||
|
|
1252
|
+
!ESM_FAMILY.has(n.language) || !isSealedModule(n.filePath, context)) &&
|
|
1253
|
+
// A name the file binds itself (a parameter, a const) shadows every other
|
|
1254
|
+
// file's symbol of that name, so a bare call has no cross-file candidate.
|
|
1255
|
+
!(bareJs && n.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)));
|
|
392
1256
|
if (candidates.length === 0) {
|
|
393
1257
|
return null;
|
|
394
1258
|
}
|
|
395
1259
|
// If only one match, use it — but penalize cross-language matches
|
|
396
1260
|
if (candidates.length === 1) {
|
|
1261
|
+
if (!isCrossFileReachable(candidates[0], ref, context))
|
|
1262
|
+
return null;
|
|
397
1263
|
const isCrossLanguage = candidates[0].language !== ref.language;
|
|
398
1264
|
return {
|
|
399
1265
|
original: ref,
|
|
@@ -412,7 +1278,7 @@ function matchByExactName(ref, context) {
|
|
|
412
1278
|
}
|
|
413
1279
|
// Multiple matches - try to narrow down
|
|
414
1280
|
const bestMatch = findBestMatch(ref, candidates, context);
|
|
415
|
-
if (bestMatch) {
|
|
1281
|
+
if (bestMatch && isCrossFileReachable(bestMatch, ref, context)) {
|
|
416
1282
|
// Lower confidence when the match is from a distant/unrelated module
|
|
417
1283
|
const proximity = computePathProximity(ref.filePath, bestMatch.filePath);
|
|
418
1284
|
const confidence = proximity >= 30 ? 0.7 : 0.4;
|
|
@@ -467,6 +1333,32 @@ function matchByQualifiedName(ref, context) {
|
|
|
467
1333
|
};
|
|
468
1334
|
}
|
|
469
1335
|
}
|
|
1336
|
+
// Erlang qualified refs (#1610): every erlang function's qualifiedName
|
|
1337
|
+
// carries its arity (`mod::f/2`), and refs carry the call-site arity when it
|
|
1338
|
+
// is statically known.
|
|
1339
|
+
if (ref.language === 'erlang' && ref.referenceName.includes('::')) {
|
|
1340
|
+
// A ref WITH arity that missed the exact lookup names an arity that isn't
|
|
1341
|
+
// defined (or a module out of repo). Never fall through to the partial
|
|
1342
|
+
// match — its "last segment" would be the arity digits — and never settle
|
|
1343
|
+
// for a sibling arity: silent beats wrong.
|
|
1344
|
+
if (/\/\d{1,3}$/.test(ref.referenceName))
|
|
1345
|
+
return null;
|
|
1346
|
+
// An arity-LESS qualified ref (dynamic MFA whose args list wasn't a
|
|
1347
|
+
// static literal): resolve only when the module defines exactly ONE arity
|
|
1348
|
+
// of that function; several arities with no signal is a guess.
|
|
1349
|
+
const base = ref.referenceName.slice(ref.referenceName.lastIndexOf('::') + 2);
|
|
1350
|
+
const prefix = `${ref.referenceName}/`;
|
|
1351
|
+
const arityCands = keepForRef(context.getNodesByName(base)).filter((n) => n.qualifiedName.startsWith(prefix) && /^\d{1,3}$/.test(n.qualifiedName.slice(prefix.length)));
|
|
1352
|
+
if (arityCands.length === 1) {
|
|
1353
|
+
return {
|
|
1354
|
+
original: ref,
|
|
1355
|
+
targetNodeId: arityCands[0].id,
|
|
1356
|
+
confidence: 0.85,
|
|
1357
|
+
resolvedBy: 'qualified-name',
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
470
1362
|
// Try partial qualified name match — again preferring the call site's own
|
|
471
1363
|
// file when more than one symbol's qualifiedName ends with the reference.
|
|
472
1364
|
const parts = ref.referenceName.split(/[:.]/);
|
|
@@ -486,6 +1378,11 @@ function matchByQualifiedName(ref, context) {
|
|
|
486
1378
|
}
|
|
487
1379
|
return null;
|
|
488
1380
|
}
|
|
1381
|
+
/** A node a `Receiver.method()` call can name as the method's owning type. */
|
|
1382
|
+
function isMethodOwnerKind(n) {
|
|
1383
|
+
return n.kind === 'class' || n.kind === 'struct' || n.kind === 'union' || n.kind === 'interface' ||
|
|
1384
|
+
(n.language === 'scala' && n.kind === 'module');
|
|
1385
|
+
}
|
|
489
1386
|
/**
|
|
490
1387
|
* When a symbol name is ambiguous across files, prefer the candidate(s) declared
|
|
491
1388
|
* in the call site's own file, keeping the rest in their original order (#1079).
|
|
@@ -507,6 +1404,223 @@ function preferCallSiteFile(nodes, callSiteFile) {
|
|
|
507
1404
|
}
|
|
508
1405
|
return same.length ? [...same, ...other] : nodes;
|
|
509
1406
|
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Languages whose object literals declare callable members — `export const
|
|
1409
|
+
* api = { call() {…}, get: () => {…} }` used as a namespace (#1573).
|
|
1410
|
+
*/
|
|
1411
|
+
const OBJECT_LITERAL_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
|
|
1412
|
+
/** True when `inner`'s source range lies within `outer`'s (lines, then columns on a shared line). */
|
|
1413
|
+
function rangeWithin(inner, outer) {
|
|
1414
|
+
const innerEnd = inner.endLine ?? inner.startLine;
|
|
1415
|
+
const outerEnd = outer.endLine ?? outer.startLine;
|
|
1416
|
+
if (inner.startLine < outer.startLine || innerEnd > outerEnd)
|
|
1417
|
+
return false;
|
|
1418
|
+
if (inner.startLine === outer.startLine && inner.startColumn < outer.startColumn)
|
|
1419
|
+
return false;
|
|
1420
|
+
if (innerEnd === outerEnd && inner.endColumn > outer.endColumn)
|
|
1421
|
+
return false;
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
function sameRange(a, b) {
|
|
1425
|
+
return (a.startLine === b.startLine &&
|
|
1426
|
+
a.startColumn === b.startColumn &&
|
|
1427
|
+
(a.endLine ?? a.startLine) === (b.endLine ?? b.startLine) &&
|
|
1428
|
+
a.endColumn === b.endColumn);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Resolve `container.member` where `container` is a VALUE holding an object
|
|
1432
|
+
* literal — `export const api = { call() {…}, get: () => {…} }` used as the
|
|
1433
|
+
* module's namespace (#1573). The members are extracted as plain functions
|
|
1434
|
+
* with BARE qualified names inside the constant's source extent (there is no
|
|
1435
|
+
* `api::call`), so neither the `Container::member` lookup the class-shaped
|
|
1436
|
+
* kinds use (#825) nor the declared-type inference for singleton instances
|
|
1437
|
+
* (#1292) can reach them, and every such call resolved to nothing — or, via
|
|
1438
|
+
* an import, to the constant itself. This looks the member up by CONTAINMENT:
|
|
1439
|
+
* a node named `member` whose range lies inside the container's, in the
|
|
1440
|
+
* container's own file. A helper declared inside a member's body is not a
|
|
1441
|
+
* member and is skipped; nothing else in the file can donate a match. Calls
|
|
1442
|
+
* take callable kinds only; other references accept value members too.
|
|
1443
|
+
*/
|
|
1444
|
+
function resolveObjectLiteralMember(container, member, ref, context, confidence, resolvedBy) {
|
|
1445
|
+
if (container.kind !== 'constant' && container.kind !== 'variable')
|
|
1446
|
+
return null;
|
|
1447
|
+
if (!OBJECT_LITERAL_LANGUAGES.has(container.language))
|
|
1448
|
+
return null;
|
|
1449
|
+
if (!sameLanguageFamily(container.language, ref.language))
|
|
1450
|
+
return null;
|
|
1451
|
+
const inFile = context.getNodesInFile(container.filePath);
|
|
1452
|
+
const callable = (n) => n.kind === 'function' || n.kind === 'method';
|
|
1453
|
+
const valueMember = (n) => callable(n) || n.kind === 'property' || n.kind === 'variable' || n.kind === 'constant';
|
|
1454
|
+
const accepts = ref.referenceKind === 'calls' ? callable : valueMember;
|
|
1455
|
+
const inside = inFile.filter((n) => n.id !== container.id && rangeWithin(n, container));
|
|
1456
|
+
const property = objectLiteralProperty(container, member, context);
|
|
1457
|
+
if (property === null || property?.binding)
|
|
1458
|
+
return null;
|
|
1459
|
+
let candidates = inside.filter((n) => n.name === member && accepts(n) && (!property || property.contains(n)));
|
|
1460
|
+
if (candidates.length === 0)
|
|
1461
|
+
return null;
|
|
1462
|
+
// Drop a candidate nested inside ANOTHER callable's body within the literal
|
|
1463
|
+
// (`{ run() { const call = () => {}; } }` — `call` is `run`'s local, not a
|
|
1464
|
+
// member). Strict containment: an identically-ranged sibling node for the
|
|
1465
|
+
// same member (a property node over an arrow function) is not a body.
|
|
1466
|
+
const bodies = inside.filter(callable);
|
|
1467
|
+
candidates = candidates.filter((c) => !bodies.some((b) => b.id !== c.id && !sameRange(b, c) && rangeWithin(c, b)));
|
|
1468
|
+
if (candidates.length === 0)
|
|
1469
|
+
return null;
|
|
1470
|
+
// Several survivors (a property AND a function for one arrow member, say):
|
|
1471
|
+
// a callable first, then the earliest in source order.
|
|
1472
|
+
candidates.sort((a, b) => {
|
|
1473
|
+
const ca = callable(a) ? 0 : 1;
|
|
1474
|
+
const cb = callable(b) ? 0 : 1;
|
|
1475
|
+
if (ca !== cb)
|
|
1476
|
+
return ca - cb;
|
|
1477
|
+
return a.startLine - b.startLine || a.startColumn - b.startColumn;
|
|
1478
|
+
});
|
|
1479
|
+
return {
|
|
1480
|
+
original: ref,
|
|
1481
|
+
targetNodeId: candidates[0].id,
|
|
1482
|
+
confidence,
|
|
1483
|
+
resolvedBy,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* The binding an object-literal member names when it is a shorthand property
|
|
1488
|
+
* (`{ getUser }`) or a pair whose value is a bare identifier (`{ getUser:
|
|
1489
|
+
* fetchUser }`) — the usual way an API module assembles its namespace from
|
|
1490
|
+
* standalone functions (#1932). Only the literal's own members count: a member
|
|
1491
|
+
* of a nested object, a word inside a member's body, a comment or a string
|
|
1492
|
+
* never donates one. Null when the member is absent, or when its value is not
|
|
1493
|
+
* a bare identifier (`{ fn: 1 }` names nothing).
|
|
1494
|
+
*/
|
|
1495
|
+
function objectLiteralMemberBinding(container, member, context) {
|
|
1496
|
+
return objectLiteralProperty(container, member, context)?.binding ?? null;
|
|
1497
|
+
}
|
|
1498
|
+
/** The last own property wins; an unknown spread/computed key invalidates earlier evidence. */
|
|
1499
|
+
function objectLiteralProperty(container, member, context) {
|
|
1500
|
+
const lines = context.getFileLines?.(container.filePath) ?? context.readFile(container.filePath)?.split('\n');
|
|
1501
|
+
if (!lines)
|
|
1502
|
+
return undefined;
|
|
1503
|
+
const extentLines = lines.slice(container.startLine - 1, container.endLine);
|
|
1504
|
+
if (!extentLines.length)
|
|
1505
|
+
return undefined;
|
|
1506
|
+
extentLines[extentLines.length - 1] = extentLines[extentLines.length - 1].slice(0, container.endColumn);
|
|
1507
|
+
extentLines[0] = extentLines[0].slice(container.startColumn);
|
|
1508
|
+
const extent = (0, strip_comments_1.stripCommentsForRegex)(extentLines.join('\n'), 'typescript');
|
|
1509
|
+
const code = (0, strip_comments_1.blankStringContents)(extent);
|
|
1510
|
+
// Start at THIS declarator, including its columns, never a sibling on the same line.
|
|
1511
|
+
const open = /^[^=]*=\s*(?:(?:Object\.(?:freeze|seal)\s*)?\(\s*)*\{/.exec(code);
|
|
1512
|
+
if (!open)
|
|
1513
|
+
return undefined;
|
|
1514
|
+
const members = [];
|
|
1515
|
+
let depth = 0;
|
|
1516
|
+
let start = open[0].length;
|
|
1517
|
+
for (let i = start; i < code.length; i++) {
|
|
1518
|
+
const ch = code[i];
|
|
1519
|
+
if (ch === '{' || ch === '(' || ch === '[')
|
|
1520
|
+
depth++;
|
|
1521
|
+
else if (ch === ')' || ch === ']')
|
|
1522
|
+
depth--;
|
|
1523
|
+
else if (ch === '}') {
|
|
1524
|
+
if (depth === 0) {
|
|
1525
|
+
members.push({ start, end: i });
|
|
1526
|
+
break;
|
|
1527
|
+
}
|
|
1528
|
+
depth--;
|
|
1529
|
+
}
|
|
1530
|
+
else if (ch === ',' && depth === 0) {
|
|
1531
|
+
members.push({ start, end: i });
|
|
1532
|
+
start = i + 1;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
let selected = null;
|
|
1536
|
+
for (const part of members) {
|
|
1537
|
+
const text = extent.slice(part.start, part.end).trim();
|
|
1538
|
+
if (/^(?:\.\.\.|\[)/.test(text)) {
|
|
1539
|
+
selected = null;
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const key = /^(?:(?:async|get|set)\s+)?\*?\s*(?:([A-Za-z_$][\w$]*)|['"]([^'"\\]*)['"])(?=\s*(?:[:(<,=]|$))/.exec(text);
|
|
1543
|
+
if ((key?.[1] ?? key?.[2]) !== member)
|
|
1544
|
+
continue;
|
|
1545
|
+
const value = text.slice(key[0].length).trim();
|
|
1546
|
+
const binding = value === '' ? member : /^:\s*([A-Za-z_$][\w$]*)$/.exec(value)?.[1] ?? null;
|
|
1547
|
+
selected = { ...part, binding };
|
|
1548
|
+
}
|
|
1549
|
+
if (!selected)
|
|
1550
|
+
return null;
|
|
1551
|
+
const offset = (node) => {
|
|
1552
|
+
let result = node.startColumn - container.startColumn;
|
|
1553
|
+
for (let line = container.startLine; line < node.startLine; line++)
|
|
1554
|
+
result += lines[line - 1].length + 1;
|
|
1555
|
+
return result;
|
|
1556
|
+
};
|
|
1557
|
+
const property = selected;
|
|
1558
|
+
return { binding: property.binding, contains: (node) => offset(node) >= property.start && offset(node) < property.end };
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Shared lexical lookup for namespace-object aliases (#1932): `api.getUser()` where
|
|
1562
|
+
* `api` is `const api = { getUser }` (or `{ getUser: fetchUser }`) in the
|
|
1563
|
+
* object's own file. The member's function is declared OUTSIDE the literal, so
|
|
1564
|
+
* containment (`resolveObjectLiteralMember`) finds nothing. Follow the binding
|
|
1565
|
+
* the member names — a symbol of this file, else one of its imports — unless a
|
|
1566
|
+
* parameter or nearer declaration shadows that name where the literal is
|
|
1567
|
+
* written (the edge would then name the wrong function).
|
|
1568
|
+
*/
|
|
1569
|
+
function resolveObjectLiteralBinding(container, member, ref, context) {
|
|
1570
|
+
const binding = objectLiteralMemberBinding(container, member, context);
|
|
1571
|
+
if (!binding)
|
|
1572
|
+
return null;
|
|
1573
|
+
const at = { ...ref, filePath: container.filePath, language: container.language,
|
|
1574
|
+
fromNodeId: container.id, line: container.startLine, column: container.startColumn };
|
|
1575
|
+
const inFile = context.getNodesInFile(container.filePath);
|
|
1576
|
+
if (inFile.some((n) => (n.kind === 'function' || n.kind === 'method') &&
|
|
1577
|
+
rangeWithin(container, n) && n.signature &&
|
|
1578
|
+
hasParameterBinding(`${n.signature} {`, binding.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))))
|
|
1579
|
+
return null;
|
|
1580
|
+
const lines = context.getFileLines?.(container.filePath) ?? context.readFile(container.filePath)?.split('\n');
|
|
1581
|
+
if (!lines)
|
|
1582
|
+
return null;
|
|
1583
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(lines.join('\n'), 'typescript'));
|
|
1584
|
+
const offsets = [0];
|
|
1585
|
+
for (let i = 0; i < code.length; i++)
|
|
1586
|
+
if (code[i] === '\n')
|
|
1587
|
+
offsets.push(i + 1);
|
|
1588
|
+
const scopeAt = (node) => {
|
|
1589
|
+
const end = (offsets[node.startLine - 1] ?? code.length) + node.startColumn;
|
|
1590
|
+
const scope = [];
|
|
1591
|
+
for (let i = 0; i < end; i++) {
|
|
1592
|
+
if (code[i] === '{')
|
|
1593
|
+
scope.push(i);
|
|
1594
|
+
else if (code[i] === '}')
|
|
1595
|
+
scope.pop();
|
|
1596
|
+
}
|
|
1597
|
+
return scope;
|
|
1598
|
+
};
|
|
1599
|
+
const scope = scopeAt(container);
|
|
1600
|
+
const callable = (n) => n.kind === 'function' || n.kind === 'method' || n.kind === 'class';
|
|
1601
|
+
const accepts = ref.referenceKind === 'calls'
|
|
1602
|
+
? callable
|
|
1603
|
+
: (n) => callable(n) || n.kind === 'constant' || n.kind === 'variable' || n.kind === 'component';
|
|
1604
|
+
const locals = inFile
|
|
1605
|
+
.filter((n) => n.name === binding && n.id !== container.id &&
|
|
1606
|
+
['function', 'class', 'constant', 'variable', 'component'].includes(n.kind))
|
|
1607
|
+
.map((node) => ({ node, scope: scopeAt(node) }))
|
|
1608
|
+
.filter((entry) => entry.scope.every((position, i) => scope[i] === position))
|
|
1609
|
+
.sort((a, b) => b.scope.length - a.scope.length);
|
|
1610
|
+
// Select the lexical binding BEFORE checking callability: a nearer value
|
|
1611
|
+
// shadows an outer function even if that value cannot be called.
|
|
1612
|
+
const local = locals[0]?.node;
|
|
1613
|
+
if (local)
|
|
1614
|
+
return accepts(local)
|
|
1615
|
+
? { original: ref, targetNodeId: local.id, confidence: 0.85, resolvedBy: 'instance-method' }
|
|
1616
|
+
: null;
|
|
1617
|
+
const imported = context.resolveImport?.({ ...at, referenceName: binding });
|
|
1618
|
+
const target = imported ? context.getNodeById?.(imported.targetNodeId) : null;
|
|
1619
|
+
if (target && accepts(target)) {
|
|
1620
|
+
return { original: ref, targetNodeId: target.id, confidence: 0.85, resolvedBy: 'instance-method' };
|
|
1621
|
+
}
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
510
1624
|
// Exported for the precedence unit tests (#1079): they assert the
|
|
511
1625
|
// preferredFqn → same-file → matches[0] ordering directly.
|
|
512
1626
|
function resolveMethodOnType(typeName, methodName, ref, context, confidence, resolvedBy,
|
|
@@ -542,7 +1656,7 @@ depth = 0) {
|
|
|
542
1656
|
for (const m of methodCandidates) {
|
|
543
1657
|
if (m.kind !== 'method')
|
|
544
1658
|
continue;
|
|
545
|
-
if (m.language
|
|
1659
|
+
if (!sameLanguageFamily(m.language, ref.language))
|
|
546
1660
|
continue;
|
|
547
1661
|
const qn = m.qualifiedName;
|
|
548
1662
|
if (qn === want || qn.endsWith(`::${want}`)) {
|
|
@@ -597,6 +1711,8 @@ depth = 0) {
|
|
|
597
1711
|
// block, so Java/Kotlin import disambiguation — whose target is intentionally
|
|
598
1712
|
// in ANOTHER file (#314) — is unaffected: that block returns early whenever
|
|
599
1713
|
// an import FQN pins the class.
|
|
1714
|
+
if (ref.referenceKind === 'function_ref' && matches.length !== 1)
|
|
1715
|
+
return null;
|
|
600
1716
|
const ordered = preferCallSiteFile(matches, ref.filePath);
|
|
601
1717
|
return {
|
|
602
1718
|
original: ref,
|
|
@@ -736,12 +1852,12 @@ function lookupCalleeReturnType(callee, ref, context) {
|
|
|
736
1852
|
}
|
|
737
1853
|
return candidates.find((n) => n.kind === 'function')?.returnType ?? null;
|
|
738
1854
|
}
|
|
739
|
-
/** Does the graph contain
|
|
1855
|
+
/** Does the graph contain an aggregate type named `name`'s last segment? */
|
|
740
1856
|
function cppClassExists(name, ref, context) {
|
|
741
1857
|
const last = cppLastSegment(name);
|
|
742
1858
|
return context
|
|
743
1859
|
.getNodesByName(last)
|
|
744
|
-
.some((n) => (n.kind === 'class' || n.kind === 'struct') && n.language === ref.language);
|
|
1860
|
+
.some((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'union') && n.language === ref.language);
|
|
745
1861
|
}
|
|
746
1862
|
/**
|
|
747
1863
|
* Infer the class produced by a C++ call/construction expression, using return
|
|
@@ -1065,6 +2181,8 @@ function normalizeInferredTypeName(raw) {
|
|
|
1065
2181
|
const PATTERN_MEMO = new Map();
|
|
1066
2182
|
const PATTERN_MEMO_CAP = 8192;
|
|
1067
2183
|
const INFER_SCAN_STATES = new WeakMap();
|
|
2184
|
+
const AWAITED_TYPE_MEMO = new WeakMap();
|
|
2185
|
+
const AWAITED_FILES = new WeakMap();
|
|
1068
2186
|
function getInferScanStates(context) {
|
|
1069
2187
|
let m = INFER_SCAN_STATES.get(context);
|
|
1070
2188
|
if (!m) {
|
|
@@ -1076,6 +2194,19 @@ function getInferScanStates(context) {
|
|
|
1076
2194
|
/** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
|
|
1077
2195
|
function clearNameMatcherMemos(context) {
|
|
1078
2196
|
INFER_SCAN_STATES.delete(context);
|
|
2197
|
+
PYTHON_MEMBER_LINES.delete(context);
|
|
2198
|
+
AWAITED_TYPE_MEMO.delete(context);
|
|
2199
|
+
AWAITED_FILES.delete(context);
|
|
2200
|
+
C_STATIC_MEMO.delete(context);
|
|
2201
|
+
RUST_TRAIT_IMPL_MEMO.delete(context);
|
|
2202
|
+
SEALED_MODULES.delete(context);
|
|
2203
|
+
LOCAL_BINDING_MEMO.delete(context);
|
|
2204
|
+
LOCAL_BINDING_SITES.delete(context);
|
|
2205
|
+
SELECTOR_NAMES.delete(context);
|
|
2206
|
+
GET_STATE_FILES.delete(context);
|
|
2207
|
+
TS_FIELD_DECL_MEMO.delete(context);
|
|
2208
|
+
TS_CLASS_LINES.delete(context);
|
|
2209
|
+
TARGET_LANGUAGE.delete(context);
|
|
1079
2210
|
}
|
|
1080
2211
|
function memoPatterns(key, build) {
|
|
1081
2212
|
const hit = PATTERN_MEMO.get(key);
|
|
@@ -1114,6 +2245,12 @@ function buildLocalReceiverTypePatterns(language, r) {
|
|
|
1114
2245
|
case 'python':
|
|
1115
2246
|
return [
|
|
1116
2247
|
new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // lg = Logger(...)
|
|
2248
|
+
// A quoted forward reference (`lg: "Logger"`, `lg: 'pkg.Logger'`) is the
|
|
2249
|
+
// same annotation — and what every file under `from __future__ import
|
|
2250
|
+
// annotations` or with a not-yet-defined class writes. The unquoted
|
|
2251
|
+
// pattern below stopped at the quote and read no type at all, so the
|
|
2252
|
+
// call produced no edge (#1684). Tried first: it is the stricter shape.
|
|
2253
|
+
new RegExp(`\\b${r}\\b\\s*:\\s*["']([A-Z][\\w.]*)["']`), // lg: "Logger"
|
|
1117
2254
|
new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // lg: Logger (PEP 526)
|
|
1118
2255
|
];
|
|
1119
2256
|
case 'java':
|
|
@@ -1410,6 +2547,191 @@ function inferLocalReceiverType(receiverName, ref, context) {
|
|
|
1410
2547
|
}
|
|
1411
2548
|
return null;
|
|
1412
2549
|
}
|
|
2550
|
+
/** Infer only a visible awaited binding and its actual local/imported callee.
|
|
2551
|
+
* The signature already carries the return annotation in both extractors, so
|
|
2552
|
+
* multiline declarations and neighboring declarations cannot donate a type.
|
|
2553
|
+
* `null` means no awaited evidence; a null NAME means an awaited receiver whose
|
|
2554
|
+
* type is unknown, which must not fall back to an unrelated method name. */
|
|
2555
|
+
function inferEsmAwaitedCallType(receiverName, ref, context) {
|
|
2556
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(receiverName))
|
|
2557
|
+
return null;
|
|
2558
|
+
let files = AWAITED_FILES.get(context);
|
|
2559
|
+
if (!files) {
|
|
2560
|
+
files = new Map();
|
|
2561
|
+
AWAITED_FILES.set(context, files);
|
|
2562
|
+
}
|
|
2563
|
+
let file = files.get(ref.filePath);
|
|
2564
|
+
if (file === undefined) {
|
|
2565
|
+
const source = context.readFile(ref.filePath) ?? '';
|
|
2566
|
+
file = null;
|
|
2567
|
+
// Raw eligibility is cheap; sanitize and index scopes only when a ref
|
|
2568
|
+
// actually uses one of these names. Comments cannot donate a binding:
|
|
2569
|
+
// the names are checked again after sanitizing on the first real lookup.
|
|
2570
|
+
const names = new Set([...source.matchAll(/\b(?:const|let|var)\s+([\w$]+)\s*=\s*await\s+[\w$]+\s*\(/g)].map(m => m[1]));
|
|
2571
|
+
if (names.size)
|
|
2572
|
+
file = { code: source, ready: false, names, offsets: [], scopes: [], declarations: new Map() };
|
|
2573
|
+
if (files.size >= 256)
|
|
2574
|
+
files.delete(files.keys().next().value);
|
|
2575
|
+
files.set(ref.filePath, file);
|
|
2576
|
+
}
|
|
2577
|
+
if (!file?.names.has(receiverName))
|
|
2578
|
+
return null;
|
|
2579
|
+
if (!file.ready) {
|
|
2580
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(file.code, 'typescript'));
|
|
2581
|
+
const names = new Set([...code.matchAll(/\b(?:const|let|var)\s+([\w$]+)\s*=\s*await\s+[\w$]+\s*\(/g)].map(m => m[1]));
|
|
2582
|
+
const offsets = [0];
|
|
2583
|
+
const scopes = [{ start: -1, end: code.length, parent: -1 }];
|
|
2584
|
+
const stack = [0];
|
|
2585
|
+
for (let i = 0; i < code.length; i++) {
|
|
2586
|
+
if (code[i] === '\n')
|
|
2587
|
+
offsets.push(i + 1);
|
|
2588
|
+
if (code[i] === '{') {
|
|
2589
|
+
scopes.push({ start: i, end: code.length, parent: stack[stack.length - 1] });
|
|
2590
|
+
stack.push(scopes.length - 1);
|
|
2591
|
+
}
|
|
2592
|
+
else if (code[i] === '}' && stack.length > 1)
|
|
2593
|
+
scopes[stack.pop()].end = i;
|
|
2594
|
+
}
|
|
2595
|
+
const declarations = new Map();
|
|
2596
|
+
for (const m of code.matchAll(/\b(?:const|let|var)\s+([\w$]+)\s*=\s*/g)) {
|
|
2597
|
+
if (!names.has(m[1]))
|
|
2598
|
+
continue;
|
|
2599
|
+
const entries = declarations.get(m[1]) ?? [];
|
|
2600
|
+
entries.push({ index: m.index, length: m[0].length });
|
|
2601
|
+
declarations.set(m[1], entries);
|
|
2602
|
+
}
|
|
2603
|
+
Object.assign(file, { code, ready: true, names, offsets, scopes, declarations });
|
|
2604
|
+
if (!names.has(receiverName))
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
let memo = AWAITED_TYPE_MEMO.get(context);
|
|
2608
|
+
if (!memo) {
|
|
2609
|
+
memo = new Map();
|
|
2610
|
+
AWAITED_TYPE_MEMO.set(context, memo);
|
|
2611
|
+
}
|
|
2612
|
+
const key = `${ref.filePath}|${ref.line}|${ref.column}|${receiverName}`;
|
|
2613
|
+
if (memo.has(key))
|
|
2614
|
+
return memo.get(key);
|
|
2615
|
+
const result = resolveAwaitedCallType(receiverName, file, ref, context);
|
|
2616
|
+
if (memo.size >= PATTERN_MEMO_CAP)
|
|
2617
|
+
memo.delete(memo.keys().next().value);
|
|
2618
|
+
memo.set(key, result);
|
|
2619
|
+
return result;
|
|
2620
|
+
}
|
|
2621
|
+
function resolveAwaitedCallType(receiverName, file, ref, context) {
|
|
2622
|
+
const unknown = { name: null, filePath: ref.filePath };
|
|
2623
|
+
const end = (file.offsets[ref.line - 1] ?? file.code.length) + ref.column;
|
|
2624
|
+
const code = file.code.slice(0, end);
|
|
2625
|
+
const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2626
|
+
// Locate scopes in the precomputed brace tree. Rescanning the entire file
|
|
2627
|
+
// for every candidate binding made large test files quadratic in refs.
|
|
2628
|
+
const scopeAt = (offset) => {
|
|
2629
|
+
let lo = 0, hi = file.scopes.length;
|
|
2630
|
+
while (lo + 1 < hi) {
|
|
2631
|
+
const mid = (lo + hi) >>> 1;
|
|
2632
|
+
if (file.scopes[mid].start < offset)
|
|
2633
|
+
lo = mid;
|
|
2634
|
+
else
|
|
2635
|
+
hi = mid;
|
|
2636
|
+
}
|
|
2637
|
+
while (lo > 0 && file.scopes[lo].end < offset)
|
|
2638
|
+
lo = file.scopes[lo].parent;
|
|
2639
|
+
return lo;
|
|
2640
|
+
};
|
|
2641
|
+
const visibleAt = (declaration, use) => {
|
|
2642
|
+
const ancestor = scopeAt(declaration);
|
|
2643
|
+
for (let scope = scopeAt(use); scope >= 0; scope = file.scopes[scope].parent)
|
|
2644
|
+
if (scope === ancestor)
|
|
2645
|
+
return true;
|
|
2646
|
+
return false;
|
|
2647
|
+
};
|
|
2648
|
+
const binding = [...(file.declarations.get(receiverName) ?? [])].reverse()
|
|
2649
|
+
.find(m => m.index < end && visibleAt(m.index, end));
|
|
2650
|
+
if (!binding)
|
|
2651
|
+
return null;
|
|
2652
|
+
const init = code.slice(binding.index + binding.length);
|
|
2653
|
+
if (!/^await\b/.test(init))
|
|
2654
|
+
return null;
|
|
2655
|
+
// Only a bare call result, not a following member/index/conditional expression.
|
|
2656
|
+
const call = /^await\s+([A-Za-z_$][\w$]*)\s*\(/.exec(init);
|
|
2657
|
+
if (!call)
|
|
2658
|
+
return null;
|
|
2659
|
+
let depth = 1, callEnd = call[0].length;
|
|
2660
|
+
for (; callEnd < init.length && depth; callEnd++) {
|
|
2661
|
+
if (init[callEnd] === '(')
|
|
2662
|
+
depth++;
|
|
2663
|
+
else if (init[callEnd] === ')')
|
|
2664
|
+
depth--;
|
|
2665
|
+
}
|
|
2666
|
+
if (depth)
|
|
2667
|
+
return unknown;
|
|
2668
|
+
const tail = init.slice(callEnd);
|
|
2669
|
+
// A following property/index/call is not the callee's annotated value.
|
|
2670
|
+
if (!/^[ \t]*(?:;|\r?\n(?![ \t]*[.(\[?]))/.test(tail))
|
|
2671
|
+
return unknown;
|
|
2672
|
+
const rest = tail;
|
|
2673
|
+
if (new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${escaped}\\b|\\{[^}]*\\b${escaped}\\b)`).test(rest) ||
|
|
2674
|
+
new RegExp(`\\b${escaped}\\s*=(?!=)`).test(rest) || hasParameterBinding(rest, escaped))
|
|
2675
|
+
return unknown;
|
|
2676
|
+
const bindingLine = file.code.slice(0, binding.index).split('\n').length;
|
|
2677
|
+
const bindingRef = { ...ref, line: bindingLine, column: binding.index - file.offsets[bindingLine - 1] };
|
|
2678
|
+
const callee = call[1];
|
|
2679
|
+
const calleeEscaped = callee.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2680
|
+
if (context.getNodesInFile(ref.filePath).some(n => (n.kind === 'function' || n.kind === 'method') && n.startLine <= bindingLine && n.endLine >= bindingLine &&
|
|
2681
|
+
n.signature && hasParameterBinding(`${n.signature} {`, calleeEscaped)))
|
|
2682
|
+
return unknown;
|
|
2683
|
+
const imported = context.getImportMappings(ref.filePath, ref.language).some(m => m.localName === callee);
|
|
2684
|
+
let declaring;
|
|
2685
|
+
if (imported) {
|
|
2686
|
+
if (importShadowedAt(callee, bindingRef, context))
|
|
2687
|
+
return unknown;
|
|
2688
|
+
const resolved = context.resolveImport?.({ ...bindingRef, referenceName: callee, referenceKind: 'calls' });
|
|
2689
|
+
declaring = resolved ? context.getNodeById?.(resolved.targetNodeId) ?? undefined : undefined;
|
|
2690
|
+
}
|
|
2691
|
+
else {
|
|
2692
|
+
const local = context.getNodesByName(callee).filter(n => n.kind === 'function' &&
|
|
2693
|
+
n.filePath === ref.filePath && ESM_FAMILY.has(n.language) && isLexicallyReachable(n, bindingRef, context));
|
|
2694
|
+
if (local.length === 1)
|
|
2695
|
+
declaring = local[0];
|
|
2696
|
+
}
|
|
2697
|
+
if (!declaring || declaring.kind !== 'function' || !declaring.signature)
|
|
2698
|
+
return unknown;
|
|
2699
|
+
if (!imported) {
|
|
2700
|
+
const beforeBinding = code.slice(0, binding.index);
|
|
2701
|
+
const shadows = new RegExp(`\\b(?:const|let|var)\\s+${calleeEscaped}\\b`, 'g');
|
|
2702
|
+
for (const shadow of beforeBinding.matchAll(shadows)) {
|
|
2703
|
+
if (!visibleAt(shadow.index, binding.index))
|
|
2704
|
+
continue;
|
|
2705
|
+
// A typed arrow function may itself be the declared local factory.
|
|
2706
|
+
const line = file.code.slice(0, shadow.index).split('\n').length;
|
|
2707
|
+
if (line !== declaring.startLine || shadow.index - file.offsets[line - 1] > declaring.startColumn)
|
|
2708
|
+
return unknown;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
const signature = declaring.signature;
|
|
2712
|
+
const annotation = signature.slice(signature.lastIndexOf(')') + 1).match(/^\s*:\s*([\s\S]+)$/)?.[1]?.trim();
|
|
2713
|
+
if (!annotation)
|
|
2714
|
+
return unknown;
|
|
2715
|
+
// Do not turn unions, arrays, object/function types, or conditional types into
|
|
2716
|
+
// a project class. Await recursively unwraps promises, but this narrow path
|
|
2717
|
+
// accepts a single named Promise<T> layer only.
|
|
2718
|
+
const returned = annotation.match(/^Promise\s*<\s*([\w$]+)\s*>$/)?.[1] ?? annotation;
|
|
2719
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(returned))
|
|
2720
|
+
return unknown;
|
|
2721
|
+
if (js_builtins_1.TS_PRIMITIVE_TYPES.has(returned))
|
|
2722
|
+
return { name: returned, filePath: declaring.filePath };
|
|
2723
|
+
const typeRef = { ...bindingRef, fromNodeId: declaring.id, filePath: declaring.filePath,
|
|
2724
|
+
language: declaring.language, line: declaring.startLine, column: declaring.startColumn,
|
|
2725
|
+
referenceName: returned, referenceKind: 'references' };
|
|
2726
|
+
const typeImport = context.getImportMappings(declaring.filePath, declaring.language).some(m => m.localName === returned);
|
|
2727
|
+
const resolved = typeImport ? context.resolveImport?.(typeRef) : null;
|
|
2728
|
+
const typeNode = resolved ? context.getNodeById?.(resolved.targetNodeId) :
|
|
2729
|
+
context.getNodesByName(returned).find(n => n.filePath === declaring.filePath &&
|
|
2730
|
+
ESM_FAMILY.has(n.language) && (n.kind === 'class' || n.kind === 'interface'));
|
|
2731
|
+
if (!typeNode || (typeNode.kind !== 'class' && typeNode.kind !== 'interface'))
|
|
2732
|
+
return unknown;
|
|
2733
|
+
return { name: typeNode.name, filePath: typeNode.filePath };
|
|
2734
|
+
}
|
|
1413
2735
|
/**
|
|
1414
2736
|
* Patterns that recover a PHP class property's declared type for a
|
|
1415
2737
|
* `$this->prop` receiver. Deliberately NOT localReceiverTypePatterns: only
|
|
@@ -1547,6 +2869,15 @@ function matchMethodCall(ref, context) {
|
|
|
1547
2869
|
return null;
|
|
1548
2870
|
return resolveMethodOnType(inferredType, phpMethodName, ref, context, 0.9, 'instance-method', importedFqnOf(inferredType, ref, context));
|
|
1549
2871
|
}
|
|
2872
|
+
// A TS/JS call through an ES private field of the enclosing class —
|
|
2873
|
+
// `this.#items.add()`, emitted as `this.#items.add` (#1987) — resolves
|
|
2874
|
+
// exactly like `this.<field>` below (#1496). `#` is outside dotMatch's
|
|
2875
|
+
// receiver class, so the shape is matched here.
|
|
2876
|
+
if (ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx') {
|
|
2877
|
+
const privateField = ref.referenceName.match(/^this\.(#[\w$]+)\.(\w+)$/);
|
|
2878
|
+
if (privateField)
|
|
2879
|
+
return matchTsThisFieldCall(privateField[1], privateField[2], ref, context);
|
|
2880
|
+
}
|
|
1550
2881
|
const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
|
|
1551
2882
|
if (!match) {
|
|
1552
2883
|
return null;
|
|
@@ -1561,9 +2892,16 @@ function matchMethodCall(ref, context) {
|
|
|
1561
2892
|
// shared source-based inferrer. resolveMethodOnType validates the method
|
|
1562
2893
|
// exists on the inferred type, so a mis-inference produces no edge.
|
|
1563
2894
|
if (inferableReceiver) {
|
|
1564
|
-
|
|
2895
|
+
let inferredType = nmTimedT('mc-infer', ref, () => ref.language === 'cpp'
|
|
1565
2896
|
? inferCppReceiverType(objectOrClass, ref, context)
|
|
1566
2897
|
: inferLocalReceiverType(objectOrClass, ref, context));
|
|
2898
|
+
const awaited = !inferredType && ESM_FAMILY.has(ref.language)
|
|
2899
|
+
? inferEsmAwaitedCallType(objectOrClass, ref, context) : null;
|
|
2900
|
+
if (awaited) {
|
|
2901
|
+
if (!awaited.name || js_builtins_1.TS_PRIMITIVE_TYPES.has(awaited.name))
|
|
2902
|
+
return null;
|
|
2903
|
+
inferredType = awaited.name;
|
|
2904
|
+
}
|
|
1567
2905
|
if (inferredType) {
|
|
1568
2906
|
// Java/Kotlin: when two classes share the simple name, the file's import
|
|
1569
2907
|
// pins WHICH one (#314). Other languages disambiguate by call-site file.
|
|
@@ -1572,10 +2910,29 @@ function matchMethodCall(ref, context) {
|
|
|
1572
2910
|
.getImportMappings(ref.filePath, ref.language)
|
|
1573
2911
|
.find((i) => i.localName === inferredType)?.source
|
|
1574
2912
|
: undefined;
|
|
1575
|
-
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, ref, context, 0.9, 'instance-method', importedFqn));
|
|
2913
|
+
const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(inferredType, methodName, awaited ? { ...ref, filePath: awaited.filePath } : ref, context, 0.9, 'instance-method', importedFqn));
|
|
1576
2914
|
if (typedMatch) {
|
|
2915
|
+
if (awaited) {
|
|
2916
|
+
const target = context.getNodeById?.(typedMatch.targetNodeId);
|
|
2917
|
+
if (!target || (target.qualifiedName.startsWith(`${inferredType}::`) && target.filePath !== awaited.filePath))
|
|
2918
|
+
return null;
|
|
2919
|
+
return { ...typedMatch, original: ref };
|
|
2920
|
+
}
|
|
1577
2921
|
return typedMatch;
|
|
1578
2922
|
}
|
|
2923
|
+
if (awaited)
|
|
2924
|
+
return null;
|
|
2925
|
+
// A known JS/TS builtin receiver is external when it has no project
|
|
2926
|
+
// method (#1566). Inference already strips generics (`Map<K, V>` →
|
|
2927
|
+
// `Map`); do not let Strategy 3 guess an unrelated `get`/`set`/`has`.
|
|
2928
|
+
// Keep the validated match above for a project type shadowing a builtin.
|
|
2929
|
+
// A primitive receiver joins the builtins here: `listed.split()` on a
|
|
2930
|
+
// `string` is the built-in method, and Strategy 3 would otherwise hand
|
|
2931
|
+
// it whichever project class happens to declare a lone `split` (#1840).
|
|
2932
|
+
if (ESM_FAMILY.has(ref.language) &&
|
|
2933
|
+
(js_builtins_1.JS_BUILT_INS.has(inferredType) || js_builtins_1.TS_PRIMITIVE_TYPES.has(inferredType))) {
|
|
2934
|
+
return null;
|
|
2935
|
+
}
|
|
1579
2936
|
}
|
|
1580
2937
|
}
|
|
1581
2938
|
// Go 2-hop field chain `base.field.Method` (#1276): the base's type comes
|
|
@@ -1592,6 +2949,39 @@ function matchMethodCall(ref, context) {
|
|
|
1592
2949
|
if (ref.language === 'go' && dotMatch && objectOrClass.includes('.')) {
|
|
1593
2950
|
return matchGoFieldChainCall(objectOrClass, methodName, ref, context);
|
|
1594
2951
|
}
|
|
2952
|
+
// Rust call through a field of the enclosing type — `self.inner.run()`,
|
|
2953
|
+
// emitted as `self.inner.run` (#1585). Same discipline as the Go branch
|
|
2954
|
+
// above, and EXCLUSIVE for the same reason: validated field-type inference
|
|
2955
|
+
// or nothing. Letting this shape reach the bare-name strategies below is
|
|
2956
|
+
// how `self.inner.run()` resolved to a same-named method on an unrelated
|
|
2957
|
+
// type — or to the calling method itself, a self-edge the source doesn't
|
|
2958
|
+
// contain — whenever the field's type was external or merely shared a
|
|
2959
|
+
// method name with something nearby.
|
|
2960
|
+
if (ref.language === 'rust' && dotMatch && objectOrClass.startsWith('self.')) {
|
|
2961
|
+
return matchRustSelfFieldCall(objectOrClass.slice('self.'.length), methodName, ref, context);
|
|
2962
|
+
}
|
|
2963
|
+
// Rust call on the enclosing type itself — `self.reset()`, emitted as
|
|
2964
|
+
// `self.reset` (#1861). Same discipline as the field branch above, and
|
|
2965
|
+
// EXCLUSIVE for the same reason: the owner is written on the `impl` line and
|
|
2966
|
+
// carried in the calling method's qualified name, so it is not a guess.
|
|
2967
|
+
// Letting this shape reach the bare-name strategies below is how
|
|
2968
|
+
// `self.reset()` resolved to a same-named method on an unrelated type
|
|
2969
|
+
// whenever that type's method happened to sit nearer the call site.
|
|
2970
|
+
if (ref.language === 'rust' && dotMatch && objectOrClass === 'self') {
|
|
2971
|
+
return matchRustSelfCall(methodName, ref, context);
|
|
2972
|
+
}
|
|
2973
|
+
// TS/JS call through a field of the enclosing class — `this.mailer.send()`,
|
|
2974
|
+
// emitted as `this.mailer.send` (#1496). Same discipline as the Rust branch
|
|
2975
|
+
// above, and EXCLUSIVE for the same reason: the field's declared type off
|
|
2976
|
+
// the class's own declaration, validated by resolveMethodOnType, or nothing.
|
|
2977
|
+
// Letting the bare name through is how `this.mailer.send()` inside
|
|
2978
|
+
// `Notifier.send()` resolved to the calling method itself — a self-edge the
|
|
2979
|
+
// source does not contain — whenever the two shared a name.
|
|
2980
|
+
if ((ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx') &&
|
|
2981
|
+
dotMatch &&
|
|
2982
|
+
objectOrClass.startsWith('this.')) {
|
|
2983
|
+
return matchTsThisFieldCall(objectOrClass.slice('this.'.length), methodName, ref, context);
|
|
2984
|
+
}
|
|
1595
2985
|
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
|
|
1596
2986
|
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
|
|
1597
2987
|
// the field in the enclosing class to get its declared type, then resolve
|
|
@@ -1611,59 +3001,85 @@ function matchMethodCall(ref, context) {
|
|
|
1611
3001
|
}
|
|
1612
3002
|
}
|
|
1613
3003
|
}
|
|
3004
|
+
// Object-literal namespace receiver (#1573): `api.call()` where `api` is a
|
|
3005
|
+
// same-file `const api = { call() {…}, get: () => {…} }`. Its members are
|
|
3006
|
+
// plain functions with bare names inside the constant's extent — no
|
|
3007
|
+
// `Container::member` qualified name — so none of the class-shaped
|
|
3008
|
+
// strategies below can see them (Strategy 3 only considers `method`
|
|
3009
|
+
// kinds) and the call resolved to nothing at all. Same file only: a
|
|
3010
|
+
// cross-file use reaches the same helper through the import path.
|
|
3011
|
+
if (dotMatch && !objectOrClass.includes('.') && OBJECT_LITERAL_LANGUAGES.has(ref.language)) {
|
|
3012
|
+
const literalMatch = nmTimedT('mc-literal', ref, () => {
|
|
3013
|
+
// Same-file holders only, so the call-site-first ordering is moot.
|
|
3014
|
+
const holders = context.getNodesByName(objectOrClass).filter((n) => (n.kind === 'constant' || n.kind === 'variable') && n.filePath === ref.filePath);
|
|
3015
|
+
for (const holder of holders) {
|
|
3016
|
+
const hit = resolveObjectLiteralMember(holder, methodName, ref, context, 0.85, 'instance-method') ??
|
|
3017
|
+
resolveObjectLiteralBinding(holder, methodName, ref, context);
|
|
3018
|
+
if (hit)
|
|
3019
|
+
return hit;
|
|
3020
|
+
}
|
|
3021
|
+
return null;
|
|
3022
|
+
});
|
|
3023
|
+
if (literalMatch)
|
|
3024
|
+
return literalMatch;
|
|
3025
|
+
}
|
|
1614
3026
|
// Strategy 1: Direct class name match (existing logic). When the receiver
|
|
1615
3027
|
// names a class that exists in several files (`Logger.log()` / `Logger::log()`
|
|
1616
3028
|
// with a `Logger` in both `a/` and `b/`), try the class in the call site's
|
|
1617
3029
|
// own file first — otherwise the first-indexed class wins and a call in `b/`
|
|
1618
3030
|
// resolves to `a/`'s method (#1079).
|
|
1619
3031
|
const strat1 = nmTimedT('mc-class', ref, () => {
|
|
1620
|
-
const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass), ref.filePath);
|
|
3032
|
+
const classCandidates = preferCallSiteFile(context.getNodesByName(objectOrClass).filter(isMethodOwnerKind), ref.filePath);
|
|
1621
3033
|
for (const classNode of classCandidates) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
};
|
|
1637
|
-
}
|
|
3034
|
+
// Skip cross-language class matches
|
|
3035
|
+
if (classNode.language !== ref.language)
|
|
3036
|
+
continue;
|
|
3037
|
+
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
3038
|
+
const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
|
|
3039
|
+
n.name === methodName &&
|
|
3040
|
+
n.qualifiedName.includes(classNode.name));
|
|
3041
|
+
if (methodNode) {
|
|
3042
|
+
return {
|
|
3043
|
+
original: ref,
|
|
3044
|
+
targetNodeId: methodNode.id,
|
|
3045
|
+
confidence: 0.85,
|
|
3046
|
+
resolvedBy: 'qualified-name',
|
|
3047
|
+
};
|
|
1638
3048
|
}
|
|
1639
3049
|
}
|
|
1640
3050
|
return null;
|
|
1641
3051
|
});
|
|
1642
3052
|
if (strat1)
|
|
1643
3053
|
return strat1;
|
|
3054
|
+
// Built-in method names need a validated receiver (#1987). Typed, imported,
|
|
3055
|
+
// object-literal and direct class receivers have had their chance above;
|
|
3056
|
+
// capitalization, word overlap or a unique method name are not evidence
|
|
3057
|
+
// that `list.map()` / `cache.get()` calls a project class.
|
|
3058
|
+
if (ref.referenceKind === 'calls' && JS_FAMILY.has(ref.language) &&
|
|
3059
|
+
objectOrClass !== 'this' && objectOrClass !== 'super' &&
|
|
3060
|
+
js_builtins_1.JS_BUILTIN_METHODS.has(methodName))
|
|
3061
|
+
return null;
|
|
1644
3062
|
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
|
1645
3063
|
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
|
1646
3064
|
const capitalizedReceiver = objectOrClass.charAt(0).toUpperCase() + objectOrClass.slice(1);
|
|
1647
3065
|
if (capitalizedReceiver !== objectOrClass) {
|
|
1648
3066
|
const strat2 = nmTimedT('mc-capital', ref, () => {
|
|
1649
|
-
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver), ref.filePath);
|
|
3067
|
+
const fuzzyClassCandidates = preferCallSiteFile(context.getNodesByName(capitalizedReceiver).filter(isMethodOwnerKind), ref.filePath);
|
|
1650
3068
|
for (const classNode of fuzzyClassCandidates) {
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
3069
|
+
// Skip cross-language class matches
|
|
3070
|
+
if (classNode.language !== ref.language)
|
|
3071
|
+
continue;
|
|
3072
|
+
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
3073
|
+
const methodNode = nodesInFile.find((n) => n.kind === 'method' &&
|
|
3074
|
+
n.name === methodName &&
|
|
3075
|
+
n.qualifiedName.includes(classNode.name));
|
|
3076
|
+
if (methodNode) {
|
|
3077
|
+
return {
|
|
3078
|
+
original: ref,
|
|
3079
|
+
targetNodeId: methodNode.id,
|
|
3080
|
+
confidence: 0.8,
|
|
3081
|
+
resolvedBy: 'instance-method',
|
|
3082
|
+
};
|
|
1667
3083
|
}
|
|
1668
3084
|
}
|
|
1669
3085
|
return null;
|
|
@@ -1814,6 +3230,641 @@ function matchGoFieldChainCall(receiverChain, methodName, ref, context) {
|
|
|
1814
3230
|
}
|
|
1815
3231
|
return null;
|
|
1816
3232
|
}
|
|
3233
|
+
// Rust primitives and the prelude's own types: a field of one of these never
|
|
3234
|
+
// names a project type, so a `self.<field>.<method>()` on it stays unresolved.
|
|
3235
|
+
const RUST_NON_PROJECT_FIELD_TYPES = new Set([
|
|
3236
|
+
'bool', 'char', 'str', 'String',
|
|
3237
|
+
'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
|
|
3238
|
+
'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
|
|
3239
|
+
'f32', 'f64',
|
|
3240
|
+
'Self', 'self',
|
|
3241
|
+
]);
|
|
3242
|
+
/**
|
|
3243
|
+
* Reduce a Rust field's declared type text to the simple name of the type a
|
|
3244
|
+
* method call on that field auto-derefs to, or null when there is none we can
|
|
3245
|
+
* name. Only the layers Rust's method-call auto-deref looks through are
|
|
3246
|
+
* unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
|
|
3247
|
+
* (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
|
|
3248
|
+
* `Inner::run`. Containers that do NOT auto-deref to their parameter
|
|
3249
|
+
* (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
|
|
3250
|
+
* own name and, having no project node, resolve to nothing — `self.items.push()`
|
|
3251
|
+
* must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
|
|
3252
|
+
* the trait, whose method node the interface-impl synthesizer fans out. A
|
|
3253
|
+
* generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
|
|
3254
|
+
* type, or a non-identifier yields null.
|
|
3255
|
+
*/
|
|
3256
|
+
function rustFieldTypeName(raw) {
|
|
3257
|
+
let t = raw.trim();
|
|
3258
|
+
for (;;) {
|
|
3259
|
+
const before = t;
|
|
3260
|
+
t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
|
|
3261
|
+
t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
|
|
3262
|
+
t = t.replace(/^(?:dyn|impl)\s+/, '');
|
|
3263
|
+
if (t === before)
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3266
|
+
// Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
|
|
3267
|
+
// bounds (`dyn Source + Send`); keep the last path segment.
|
|
3268
|
+
t = t.replace(/[<>+].*$/, '').trim();
|
|
3269
|
+
const seg = t.split('::').filter(Boolean).pop();
|
|
3270
|
+
if (!seg || !/^[A-Za-z_]\w*$/.test(seg))
|
|
3271
|
+
return null;
|
|
3272
|
+
if (RUST_NON_PROJECT_FIELD_TYPES.has(seg))
|
|
3273
|
+
return null;
|
|
3274
|
+
if (/^[A-Z]$/.test(seg))
|
|
3275
|
+
return null; // bare single-letter generic parameter
|
|
3276
|
+
return seg;
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* `self.method()` in Rust — the method on the type the call sits inside.
|
|
3280
|
+
*
|
|
3281
|
+
* The owner is the calling method's qualified-name prefix (`Target::run` →
|
|
3282
|
+
* `Target`), which is where the `impl` block's type ends up. A free function
|
|
3283
|
+
* has no `self`, so a caller whose qualified name carries no owner declines.
|
|
3284
|
+
* Exactly one candidate must belong to that owner: a project with two `impl`
|
|
3285
|
+
* blocks for the same type is normal, two same-named methods on it is not, and
|
|
3286
|
+
* guessing between them is the failure this replaces.
|
|
3287
|
+
*/
|
|
3288
|
+
function matchRustSelfCall(methodName, ref, context) {
|
|
3289
|
+
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
3290
|
+
if (!caller?.qualifiedName)
|
|
3291
|
+
return null;
|
|
3292
|
+
const sep = caller.qualifiedName.lastIndexOf('::');
|
|
3293
|
+
if (sep <= 0)
|
|
3294
|
+
return null; // a free fn has no `self`
|
|
3295
|
+
const owner = caller.qualifiedName.slice(0, sep);
|
|
3296
|
+
let owned = context
|
|
3297
|
+
.getNodesByQualifiedName(`${owner}::${methodName}`)
|
|
3298
|
+
.filter((n) => n.kind === 'method' &&
|
|
3299
|
+
n.language === 'rust' &&
|
|
3300
|
+
n.qualifiedName === `${owner}::${methodName}`);
|
|
3301
|
+
// Rust's extracted qualified names omit module paths. Two modules can
|
|
3302
|
+
// each declare `Target`; matching just `Target::reset` does not establish
|
|
3303
|
+
// ownership. In that case require a single owner declaration in the
|
|
3304
|
+
// caller's file and a method in that file. Otherwise leave it unresolved.
|
|
3305
|
+
// A unique owner still permits ordinary impl blocks split across files.
|
|
3306
|
+
const owners = context.getNodesByQualifiedName(owner).filter((n) => n.language === 'rust' && ['struct', 'enum', 'union', 'trait', 'class'].includes(n.kind));
|
|
3307
|
+
if (owners.length > 1) {
|
|
3308
|
+
if (owners.filter((n) => n.filePath === caller.filePath).length !== 1)
|
|
3309
|
+
return null;
|
|
3310
|
+
owned = owned.filter((n) => n.filePath === caller.filePath);
|
|
3311
|
+
}
|
|
3312
|
+
if (owned.length !== 1)
|
|
3313
|
+
return null;
|
|
3314
|
+
return {
|
|
3315
|
+
original: ref,
|
|
3316
|
+
targetNodeId: owned[0].id,
|
|
3317
|
+
confidence: 0.9,
|
|
3318
|
+
resolvedBy: 'qualified-name',
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Resolve a Rust call through a field of the enclosing type —
|
|
3323
|
+
* `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
|
|
3324
|
+
* Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
|
|
3325
|
+
* method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
|
|
3326
|
+
* type comes from the owner struct's OWN declaration lines, and the method is
|
|
3327
|
+
* resolved AND VALIDATED on that type by resolveMethodOnType. The caller
|
|
3328
|
+
* treats this branch as exclusive for `self.<field>` receivers: a field whose
|
|
3329
|
+
* type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
|
|
3330
|
+
* parameter, or not declared where we can see it yields null and the ref stays
|
|
3331
|
+
* unresolved. Rust struct fields are not graph nodes, so the declaration text
|
|
3332
|
+
* is the only place the type lives.
|
|
3333
|
+
*/
|
|
3334
|
+
function matchRustSelfFieldCall(field, methodName, ref, context) {
|
|
3335
|
+
// The extractor only ever emits a single field hop; anything else is not ours.
|
|
3336
|
+
if (!field || field.includes('.'))
|
|
3337
|
+
return null;
|
|
3338
|
+
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
3339
|
+
if (!caller)
|
|
3340
|
+
return null;
|
|
3341
|
+
const sep = caller.qualifiedName.lastIndexOf('::');
|
|
3342
|
+
if (sep <= 0)
|
|
3343
|
+
return null; // a free fn has no `self`
|
|
3344
|
+
const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
|
|
3345
|
+
if (!owner)
|
|
3346
|
+
return null;
|
|
3347
|
+
const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter((n) => (n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
|
|
3348
|
+
n.language === 'rust');
|
|
3349
|
+
const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3350
|
+
// `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
|
|
3351
|
+
// the type text runs to the field separator. A comma inside generic args
|
|
3352
|
+
// (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
|
|
3353
|
+
// reduces to the container's own name — exactly the non-deref case it
|
|
3354
|
+
// refuses anyway.
|
|
3355
|
+
const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
|
|
3356
|
+
for (const s of owners) {
|
|
3357
|
+
const source = context.readFile(s.filePath);
|
|
3358
|
+
if (!source)
|
|
3359
|
+
continue;
|
|
3360
|
+
// Only the struct's own declaration lines, comment-stripped line by line —
|
|
3361
|
+
// same discipline as the Go helper: prose or a same-named identifier
|
|
3362
|
+
// elsewhere in the file can never donate a type.
|
|
3363
|
+
const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
|
|
3364
|
+
for (const rawLine of declLines) {
|
|
3365
|
+
const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
|
|
3366
|
+
const m = line.match(fieldRe);
|
|
3367
|
+
if (!m || !m[1])
|
|
3368
|
+
continue;
|
|
3369
|
+
const fieldType = rustFieldTypeName(m[1]);
|
|
3370
|
+
// The field is declared here; whether or not its type names a project
|
|
3371
|
+
// symbol, this owner is the answer — no other same-named struct applies.
|
|
3372
|
+
if (!fieldType)
|
|
3373
|
+
return null;
|
|
3374
|
+
return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return null;
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Per-context memo for matchTsThisFieldCall's declaration scan: `classId\0field`
|
|
3381
|
+
* → the first line of the class that declares the field (as `: typeof X`,
|
|
3382
|
+
* `: X`, or `= new X`, tried in that order per line) with its captured type,
|
|
3383
|
+
* or null. The scan depends only on the class and the field, but ran for every
|
|
3384
|
+
* `this.<field>.<method>()` call — re-splitting the file and compiling three
|
|
3385
|
+
* patterns each time, a third of all method-call matching on vscode. Derived
|
|
3386
|
+
* from file source: drops with clearNameMatcherMemos.
|
|
3387
|
+
*/
|
|
3388
|
+
const TS_FIELD_DECL_MEMO = new WeakMap();
|
|
3389
|
+
/** The last few classes' declarations — calls arrive file by file. */
|
|
3390
|
+
const TS_CLASS_LINES = new WeakMap();
|
|
3391
|
+
const TS_CLASS_LINES_KEEP = 32;
|
|
3392
|
+
function tsClassDecl(cls, context) {
|
|
3393
|
+
let cache = TS_CLASS_LINES.get(context);
|
|
3394
|
+
if (!cache) {
|
|
3395
|
+
cache = new Map();
|
|
3396
|
+
TS_CLASS_LINES.set(context, cache);
|
|
3397
|
+
}
|
|
3398
|
+
const hit = cache.get(cls.id);
|
|
3399
|
+
if (hit !== undefined)
|
|
3400
|
+
return hit;
|
|
3401
|
+
const source = context.readFile(cls.filePath);
|
|
3402
|
+
const decl = source
|
|
3403
|
+
? {
|
|
3404
|
+
lines: source
|
|
3405
|
+
.split('\n')
|
|
3406
|
+
.slice(Math.max(0, cls.startLine - 1), cls.endLine)
|
|
3407
|
+
.map((rawLine) => rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')),
|
|
3408
|
+
lookups: 0,
|
|
3409
|
+
linesByToken: null,
|
|
3410
|
+
}
|
|
3411
|
+
: null;
|
|
3412
|
+
if (cache.size >= TS_CLASS_LINES_KEEP)
|
|
3413
|
+
cache.delete(cache.keys().next().value);
|
|
3414
|
+
cache.set(cls.id, decl);
|
|
3415
|
+
return decl;
|
|
3416
|
+
}
|
|
3417
|
+
const TS_FIELD_TOKEN = /^[\w$#]+$/;
|
|
3418
|
+
const TS_LINE_TOKEN = /[\w$#]+/g;
|
|
3419
|
+
const TS_TOKEN_CHAR = /[\w$#]/;
|
|
3420
|
+
/**
|
|
3421
|
+
* tsFieldPatterns with the field spelled `[\w$#]+`, tried sticky at a whole-
|
|
3422
|
+
* token occurrence of the field. There the run consumes exactly the field (a
|
|
3423
|
+
* shorter prefix cannot continue: every pattern needs `\s`, `?`, `!`, `:` or
|
|
3424
|
+
* `=` next), so each matches exactly where the field's own pattern would —
|
|
3425
|
+
* without compiling three patterns for every field of every class.
|
|
3426
|
+
*/
|
|
3427
|
+
const TS_FIELD_PATTERNS_AT = [
|
|
3428
|
+
{ re: /(?<![\w$#])[\w$#]+\b\s*[?!]?\s*:\s*(?:readonly\s+)?typeof\s+([A-Za-z_$][\w.$]*)/y, valueType: true },
|
|
3429
|
+
{ re: /(?<![\w$#])[\w$#]+\b\s*[?!]?\s*:\s*(?:readonly\s+)?([A-Za-z_$][\w.$]*)/y, valueType: false },
|
|
3430
|
+
{ re: /(?<![\w$#])[\w$#]+\b\s*=\s*new\s+([A-Za-z_$][\w.$]*)/y, valueType: false },
|
|
3431
|
+
];
|
|
3432
|
+
/** The first declaration of `field` on one line, as `line.match` of its patterns in order would find it. */
|
|
3433
|
+
function tsFieldOnLine(line, field) {
|
|
3434
|
+
let at = null;
|
|
3435
|
+
for (let i = line.indexOf(field); i !== -1; i = line.indexOf(field, i + 1)) {
|
|
3436
|
+
const end = i + field.length;
|
|
3437
|
+
if ((i > 0 && TS_TOKEN_CHAR.test(line[i - 1])) || (end < line.length && TS_TOKEN_CHAR.test(line[end])))
|
|
3438
|
+
continue;
|
|
3439
|
+
(at ??= []).push(i);
|
|
3440
|
+
}
|
|
3441
|
+
if (!at)
|
|
3442
|
+
return null;
|
|
3443
|
+
for (const { re, valueType } of TS_FIELD_PATTERNS_AT) {
|
|
3444
|
+
for (const i of at) {
|
|
3445
|
+
re.lastIndex = i;
|
|
3446
|
+
const m = re.exec(line);
|
|
3447
|
+
if (m && m[1])
|
|
3448
|
+
return { valueType, type: m[1] };
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
return null;
|
|
3452
|
+
}
|
|
3453
|
+
/** Indices of the class lines holding `token` as a whole `[\w$#]` run, ascending. */
|
|
3454
|
+
function tsClassLinesWithToken(decl, token) {
|
|
3455
|
+
if (!decl.linesByToken) {
|
|
3456
|
+
const index = new Map();
|
|
3457
|
+
for (let i = 0; i < decl.lines.length; i++) {
|
|
3458
|
+
for (const m of decl.lines[i].matchAll(TS_LINE_TOKEN)) {
|
|
3459
|
+
const rows = index.get(m[0]);
|
|
3460
|
+
if (!rows)
|
|
3461
|
+
index.set(m[0], [i]);
|
|
3462
|
+
else if (rows[rows.length - 1] !== i)
|
|
3463
|
+
rows.push(i);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
decl.linesByToken = index;
|
|
3467
|
+
}
|
|
3468
|
+
return decl.linesByToken.get(token) ?? [];
|
|
3469
|
+
}
|
|
3470
|
+
/** Compiled declaration patterns by field name — fields recur across classes. */
|
|
3471
|
+
const TS_FIELD_PATTERNS = new Map();
|
|
3472
|
+
const TS_FIELD_PATTERNS_CAP = 4096;
|
|
3473
|
+
function tsFieldPatterns(field) {
|
|
3474
|
+
const hit = TS_FIELD_PATTERNS.get(field);
|
|
3475
|
+
if (hit)
|
|
3476
|
+
return hit;
|
|
3477
|
+
const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3478
|
+
// A word boundary cannot open a private name; it also lets a public
|
|
3479
|
+
// `items` match `#items`. Keep the two field namespaces distinct (#1987).
|
|
3480
|
+
const fieldStart = '(?<![\\w$#])';
|
|
3481
|
+
const patterns = [
|
|
3482
|
+
// `storage: typeof DraftHubStorage` — the type OF a value: an object
|
|
3483
|
+
// literal used as a namespace. Its members are bare-named functions inside
|
|
3484
|
+
// the constant's extent (#1573), so they are found by containment, not by
|
|
3485
|
+
// `Type::method`. Tried first: the declared-type pattern below would
|
|
3486
|
+
// otherwise capture the word `typeof`.
|
|
3487
|
+
{
|
|
3488
|
+
re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?typeof\\s+([A-Za-z_$][\\w.$]*)`),
|
|
3489
|
+
valueType: true,
|
|
3490
|
+
},
|
|
3491
|
+
// `private readonly mailer?: Mailer` — a class field or a constructor
|
|
3492
|
+
// parameter property; the capture stops at `<`, `[` or `|`, so a generic
|
|
3493
|
+
// or union type yields its head and resolveMethodOnType decides.
|
|
3494
|
+
{
|
|
3495
|
+
re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?([A-Za-z_$][\\w.$]*)`),
|
|
3496
|
+
valueType: false,
|
|
3497
|
+
},
|
|
3498
|
+
// `mailer = new Mailer()` / `this.mailer = new Mailer()`
|
|
3499
|
+
{ re: new RegExp(`${fieldStart}${fieldEsc}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), valueType: false },
|
|
3500
|
+
];
|
|
3501
|
+
if (TS_FIELD_PATTERNS.size >= TS_FIELD_PATTERNS_CAP)
|
|
3502
|
+
TS_FIELD_PATTERNS.delete(TS_FIELD_PATTERNS.keys().next().value);
|
|
3503
|
+
TS_FIELD_PATTERNS.set(field, patterns);
|
|
3504
|
+
return patterns;
|
|
3505
|
+
}
|
|
3506
|
+
function tsFieldDeclaration(cls, field, context) {
|
|
3507
|
+
let memo = TS_FIELD_DECL_MEMO.get(context);
|
|
3508
|
+
if (!memo) {
|
|
3509
|
+
memo = new Map();
|
|
3510
|
+
TS_FIELD_DECL_MEMO.set(context, memo);
|
|
3511
|
+
}
|
|
3512
|
+
const key = cls.id + '\0' + field;
|
|
3513
|
+
const hit = memo.get(key);
|
|
3514
|
+
if (hit !== undefined)
|
|
3515
|
+
return hit;
|
|
3516
|
+
let found = null;
|
|
3517
|
+
const decl = tsClassDecl(cls, context);
|
|
3518
|
+
if (decl && TS_FIELD_TOKEN.test(field)) {
|
|
3519
|
+
// Every pattern needs the field as a whole `[\w$#]` run — the lookbehind
|
|
3520
|
+
// bars one before it, and nothing after it but `\s`, `?`, `!`, `:` or `=`
|
|
3521
|
+
// can continue a match — so only lines holding that token can match. A
|
|
3522
|
+
// class asked about several fields is indexed by token once.
|
|
3523
|
+
const rows = ++decl.lookups > 1 ? tsClassLinesWithToken(decl, field) : null;
|
|
3524
|
+
const count = rows ? rows.length : decl.lines.length;
|
|
3525
|
+
for (let k = 0; k < count && !found; k++) {
|
|
3526
|
+
const line = decl.lines[rows ? rows[k] : k];
|
|
3527
|
+
// Every pattern spells the field literally, so only a line that contains
|
|
3528
|
+
// it can match — and most of a class's lines never mention a given field.
|
|
3529
|
+
if (line.includes(field))
|
|
3530
|
+
found = tsFieldOnLine(line, field);
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
else if (decl) {
|
|
3534
|
+
const patterns = tsFieldPatterns(field);
|
|
3535
|
+
scan: for (const line of decl.lines) {
|
|
3536
|
+
if (!line.includes(field))
|
|
3537
|
+
continue;
|
|
3538
|
+
for (const { re, valueType } of patterns) {
|
|
3539
|
+
const m = line.match(re);
|
|
3540
|
+
if (!m || !m[1])
|
|
3541
|
+
continue;
|
|
3542
|
+
found = { valueType, type: m[1] };
|
|
3543
|
+
break scan;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
memo.set(key, found);
|
|
3548
|
+
return found;
|
|
3549
|
+
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Resolve a TS/JS `this.<field>.<method>()` call (#1496) through the field's
|
|
3552
|
+
* declared type, read off the ENCLOSING class's own declaration lines:
|
|
3553
|
+
* a field or constructor-parameter property (`private mailer: Mailer`,
|
|
3554
|
+
* `mailer?: Mailer`, `readonly mailer: Mailer`) or an initializer
|
|
3555
|
+
* (`mailer = new Mailer()`, `this.mailer = new Mailer()`). The method is then
|
|
3556
|
+
* VALIDATED on that type by resolveMethodOnType. Null — never a bare-name
|
|
3557
|
+
* fallback — when the field is not declared there or its type is external,
|
|
3558
|
+
* a builtin (`this.items.push()`) or not spelled out.
|
|
3559
|
+
*/
|
|
3560
|
+
function matchTsThisFieldCall(field, methodName, ref, context) {
|
|
3561
|
+
if (!field || field.includes('.'))
|
|
3562
|
+
return null;
|
|
3563
|
+
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
3564
|
+
if (!caller)
|
|
3565
|
+
return null;
|
|
3566
|
+
const sep = caller.qualifiedName.lastIndexOf('::');
|
|
3567
|
+
if (sep <= 0)
|
|
3568
|
+
return null; // not inside a class
|
|
3569
|
+
const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
|
|
3570
|
+
if (!owner)
|
|
3571
|
+
return null;
|
|
3572
|
+
const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter((n) => (n.kind === 'class' || n.kind === 'component') && sameLanguageFamily(n.language, ref.language));
|
|
3573
|
+
for (const cls of owners) {
|
|
3574
|
+
const decl = tsFieldDeclaration(cls, field, context);
|
|
3575
|
+
if (!decl)
|
|
3576
|
+
continue;
|
|
3577
|
+
if (decl.valueType) {
|
|
3578
|
+
// The value's declaration may live in another file (it is imported);
|
|
3579
|
+
// the call site's file is preferred when several share the name.
|
|
3580
|
+
const holderName = decl.type.split('.').pop();
|
|
3581
|
+
const holders = preferCallSiteFile(context.getNodesByName(holderName), ref.filePath).filter((n) => (n.kind === 'constant' || n.kind === 'variable') && sameLanguageFamily(n.language, ref.language));
|
|
3582
|
+
for (const holder of holders) {
|
|
3583
|
+
const hit = resolveObjectLiteralMember(holder, methodName, ref, context, 0.85, 'instance-method');
|
|
3584
|
+
if (hit)
|
|
3585
|
+
return hit;
|
|
3586
|
+
}
|
|
3587
|
+
return null;
|
|
3588
|
+
}
|
|
3589
|
+
// `ns.Mailer` → `Mailer`; a primitive or builtin names no project type.
|
|
3590
|
+
const typeName = decl.type.split('.').pop();
|
|
3591
|
+
if (!/^[A-Z]/.test(typeName))
|
|
3592
|
+
return null;
|
|
3593
|
+
// Two apps in one repo may each declare a `UserService`. The bare-name
|
|
3594
|
+
// path this replaces broke that tie by directory proximity, so keep the
|
|
3595
|
+
// same signal: among the type's declarations of the method, prefer the
|
|
3596
|
+
// one closest to the call site's directory (its own app), never index
|
|
3597
|
+
// order. resolveMethodOnType still answers the single-declaration and
|
|
3598
|
+
// supertype cases.
|
|
3599
|
+
const declared = context
|
|
3600
|
+
.getNodesByName(methodName)
|
|
3601
|
+
.filter((n) => n.kind === 'method' &&
|
|
3602
|
+
sameLanguageFamily(n.language, ref.language) &&
|
|
3603
|
+
(n.qualifiedName === `${typeName}::${methodName}` || n.qualifiedName.endsWith(`::${typeName}::${methodName}`)));
|
|
3604
|
+
if (declared.length > 1) {
|
|
3605
|
+
const callDirs = ref.filePath.split('/').slice(0, -1);
|
|
3606
|
+
const shared = (fp) => {
|
|
3607
|
+
const dirs = fp.split('/').slice(0, -1);
|
|
3608
|
+
let i = 0;
|
|
3609
|
+
while (i < dirs.length && i < callDirs.length && dirs[i] === callDirs[i])
|
|
3610
|
+
i++;
|
|
3611
|
+
return i;
|
|
3612
|
+
};
|
|
3613
|
+
const nearest = [...declared].sort((a, b) => shared(b.filePath) - shared(a.filePath) || a.filePath.localeCompare(b.filePath))[0];
|
|
3614
|
+
return { original: ref, targetNodeId: nearest.id, confidence: 0.85, resolvedBy: 'instance-method' };
|
|
3615
|
+
}
|
|
3616
|
+
return resolveMethodOnType(typeName, methodName, ref, context, 0.85, 'instance-method');
|
|
3617
|
+
}
|
|
3618
|
+
return null;
|
|
3619
|
+
}
|
|
3620
|
+
/**
|
|
3621
|
+
* The one fallback a TS/JS/Python call-receiver chain keeps (#1683): a STORE
|
|
3622
|
+
* ACCESSOR. Zustand's `get()` inside the store factory and
|
|
3623
|
+
* `useStore.getState()` outside it hand back the store whose actions are
|
|
3624
|
+
* indexed as functions (#1573). JS/TS resolves the member within that store;
|
|
3625
|
+
* the existing Python fallback still requires a unique callable. Nothing else
|
|
3626
|
+
* qualifies: a chain rooted in a project value still says nothing about what
|
|
3627
|
+
* the inner call RETURNS — `db.prepare(sql).all()` would bind to any project
|
|
3628
|
+
* function named `all` — so it resolves to nothing, exactly like a chain
|
|
3629
|
+
* rooted in a parameter (`d.setdefault(k, []).append(v)`).
|
|
3630
|
+
*/
|
|
3631
|
+
function matchStoreAccessorChain(ref, context) {
|
|
3632
|
+
const m = ref.referenceName.match(/^([\w$.]+)\(\)\.(\w+)$/);
|
|
3633
|
+
if (!m || !m[1] || !m[2])
|
|
3634
|
+
return null;
|
|
3635
|
+
const inner = m[1];
|
|
3636
|
+
const method = m[2];
|
|
3637
|
+
if (!(inner === 'get' || inner === 'getState' || inner.endsWith('.getState')))
|
|
3638
|
+
return null;
|
|
3639
|
+
if (JS_FAMILY.has(ref.language)) {
|
|
3640
|
+
return resolveStoreAction(inner, method, ref, context);
|
|
3641
|
+
}
|
|
3642
|
+
const callables = context
|
|
3643
|
+
.getNodesByName(method)
|
|
3644
|
+
.filter((n) => (n.kind === 'function' || n.kind === 'method') && sameLanguageFamily(n.language, ref.language) && n.id !== ref.fromNodeId);
|
|
3645
|
+
if (callables.length !== 1)
|
|
3646
|
+
return null;
|
|
3647
|
+
return { original: ref, targetNodeId: callables[0].id, confidence: 0.6, resolvedBy: 'exact-match' };
|
|
3648
|
+
}
|
|
3649
|
+
/** Resolve the implementation inside the identified store, not a namesake or
|
|
3650
|
+
* an interface signature elsewhere in the project. Import resolution already
|
|
3651
|
+
* follows aliases/barrels; containment already excludes nested action locals. */
|
|
3652
|
+
function resolveStoreAction(inner, member, ref, context, selector = false) {
|
|
3653
|
+
let holders;
|
|
3654
|
+
if (inner === 'get' || inner === 'getState') {
|
|
3655
|
+
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
3656
|
+
if (!caller)
|
|
3657
|
+
return null;
|
|
3658
|
+
holders = context.getNodesInFile(ref.filePath).filter((n) => {
|
|
3659
|
+
if ((n.kind !== 'constant' && n.kind !== 'variable') || !rangeWithin(caller, n))
|
|
3660
|
+
return false;
|
|
3661
|
+
const source = context.readFile(n.filePath)?.split('\n').slice(n.startLine - 1, caller.startLine).join('\n') ?? '';
|
|
3662
|
+
// The accessor must actually be a parameter of the enclosing factory.
|
|
3663
|
+
return new RegExp(`\\(\\s*[\\w$]+\\s*,\\s*${inner}\\s*(?:,\\s*[\\w$]+\\s*)?\\)\\s*=>`).test(source);
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
else {
|
|
3667
|
+
const name = inner.slice(0, -'.getState'.length);
|
|
3668
|
+
if (!/^[\w$]+$/.test(name))
|
|
3669
|
+
return null;
|
|
3670
|
+
const imported = context.resolveImport?.({ ...ref, referenceName: name, referenceKind: 'references' });
|
|
3671
|
+
const node = imported && context.getNodeById?.(imported.targetNodeId);
|
|
3672
|
+
if (node && importShadowedAt(name, ref, context))
|
|
3673
|
+
return null;
|
|
3674
|
+
holders = node ? [node] : context.getNodesByName(name).filter((n) => n.filePath === ref.filePath && isLexicallyReachable(n, ref, context));
|
|
3675
|
+
}
|
|
3676
|
+
if (holders.length !== 1)
|
|
3677
|
+
return null;
|
|
3678
|
+
const holder = holders[0];
|
|
3679
|
+
if (selector) {
|
|
3680
|
+
// Only a Zustand hook promises to return the selector's result. An
|
|
3681
|
+
// arbitrary function accepting that callback is not a store binding.
|
|
3682
|
+
const text = context.readFile(holder.filePath)?.split('\n').slice(holder.startLine - 1, holder.endLine).join('\n') ?? '';
|
|
3683
|
+
const escaped = holder.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3684
|
+
const factory = new RegExp(`\\b(?:const|let)\\s+${escaped}\\s*=\\s*([\\w$]+)\\s*[<(]`).exec(text)?.[1];
|
|
3685
|
+
if (!factory || !context.getImportMappings(holder.filePath, holder.language).some(m => m.localName === factory && m.source === 'zustand' && (m.exportedName === 'create' || m.isDefault)))
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
return resolveObjectLiteralMember(holder, member, ref, context, 0.9, 'instance-method');
|
|
3689
|
+
}
|
|
3690
|
+
// Eligibility is a file property, not a call-site property. Cache both answers
|
|
3691
|
+
// within the same stable-source window as the resolver's file cache; sync drops
|
|
3692
|
+
// it via clearNameMatcherMemos. Keep only booleans, FIFO-capped like PATTERN_MEMO
|
|
3693
|
+
// to avoid per-hit LRU churn. Eviction merely repeats the source scan.
|
|
3694
|
+
const GET_STATE_FILES = new WeakMap();
|
|
3695
|
+
const GET_STATE_FILES_CAP = 8192;
|
|
3696
|
+
/** A const destructuring is a bound reference, so it is eligible even though
|
|
3697
|
+
* arbitrary locally-bound bare calls must never guess a cross-file target. */
|
|
3698
|
+
function matchDestructuredStoreCall(ref, context) {
|
|
3699
|
+
let files = GET_STATE_FILES.get(context);
|
|
3700
|
+
if (!files) {
|
|
3701
|
+
files = new Map();
|
|
3702
|
+
GET_STATE_FILES.set(context, files);
|
|
3703
|
+
}
|
|
3704
|
+
let eligible = files.get(ref.filePath);
|
|
3705
|
+
let source;
|
|
3706
|
+
if (eligible === undefined) {
|
|
3707
|
+
source = context.readFile(ref.filePath);
|
|
3708
|
+
eligible = source?.includes('.getState') ?? false;
|
|
3709
|
+
if (files.size >= GET_STATE_FILES_CAP) {
|
|
3710
|
+
const oldest = files.keys().next().value;
|
|
3711
|
+
if (oldest !== undefined)
|
|
3712
|
+
files.delete(oldest);
|
|
3713
|
+
}
|
|
3714
|
+
files.set(ref.filePath, eligible);
|
|
3715
|
+
}
|
|
3716
|
+
if (!eligible)
|
|
3717
|
+
return null;
|
|
3718
|
+
source ??= context.readFile(ref.filePath);
|
|
3719
|
+
if (!source)
|
|
3720
|
+
return null;
|
|
3721
|
+
const lines = source.split('\n');
|
|
3722
|
+
const start = enclosingScopeStartLine(ref, context) - 1;
|
|
3723
|
+
const before = lines.slice(start, ref.line - 1).concat(lines[ref.line - 1].slice(0, ref.column)).join('\n');
|
|
3724
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(before, 'typescript'));
|
|
3725
|
+
const name = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3726
|
+
const binding = /\bconst\s*\{([^{}]*)\}\s*=\s*([\w$]+)\.getState\s*\(\s*\)/g;
|
|
3727
|
+
// Compare block identities, not just nesting depth: a binding in a sibling
|
|
3728
|
+
// or already-closed block is not in scope at this call.
|
|
3729
|
+
const stackAt = (end) => {
|
|
3730
|
+
const stack = [];
|
|
3731
|
+
for (let i = 0; i < end; i++) {
|
|
3732
|
+
if (code[i] === '{')
|
|
3733
|
+
stack.push(i);
|
|
3734
|
+
else if (code[i] === '}')
|
|
3735
|
+
stack.pop();
|
|
3736
|
+
}
|
|
3737
|
+
return stack;
|
|
3738
|
+
};
|
|
3739
|
+
const callScope = stackAt(code.length);
|
|
3740
|
+
for (const m of [...code.matchAll(binding)].reverse()) {
|
|
3741
|
+
// Plain named bindings only; defaults, rest and computed keys need their
|
|
3742
|
+
// own value tracing rather than a same-name guess.
|
|
3743
|
+
if (!m[1].split(',').some(part => part.trim() === ref.referenceName))
|
|
3744
|
+
continue;
|
|
3745
|
+
const scope = stackAt(m.index);
|
|
3746
|
+
if (!scope.every((pos, i) => callScope[i] === pos))
|
|
3747
|
+
continue;
|
|
3748
|
+
const rest = code.slice(m.index + m[0].length);
|
|
3749
|
+
// Keep the guard when another declaration shadows the captured const.
|
|
3750
|
+
if (new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${name}\\b|\\{[^}]*\\b${name}\\b)`).test(rest))
|
|
3751
|
+
return null;
|
|
3752
|
+
return resolveStoreAction(`${m[2]}.getState`, ref.referenceName, ref, context);
|
|
3753
|
+
}
|
|
3754
|
+
return null;
|
|
3755
|
+
}
|
|
3756
|
+
/** Bound action names need not have a same-named definition (selectors may
|
|
3757
|
+
* rename them). The resolver's symbol-existence prefilter must allow them. */
|
|
3758
|
+
function matchJsStoreBindingCall(ref, context) {
|
|
3759
|
+
if (!isBareJsCall(ref, context))
|
|
3760
|
+
return null;
|
|
3761
|
+
return matchDestructuredStoreCall(ref, context) ?? matchSelectedStoreCall(ref, context);
|
|
3762
|
+
}
|
|
3763
|
+
/** A qualified untyped chain is useful source evidence, not permission to
|
|
3764
|
+
* infer a property type. Framework resolution runs before this guard. */
|
|
3765
|
+
function isUnresolvedJsMemberCall(ref) {
|
|
3766
|
+
return ref.referenceKind === 'calls' && JS_FAMILY.has(ref.language) &&
|
|
3767
|
+
!/^(?:this|window)\./.test(ref.referenceName) &&
|
|
3768
|
+
/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){2,}$/.test(ref.referenceName);
|
|
3769
|
+
}
|
|
3770
|
+
const SELECTOR_NAMES = new WeakMap();
|
|
3771
|
+
/** A selector returns the named action from one identified store. Keep the
|
|
3772
|
+
* lexical block identity so closures may capture it but sibling scopes and
|
|
3773
|
+
* shadowing parameters/declarations cannot donate a binding. */
|
|
3774
|
+
function matchSelectedStoreCall(ref, context) {
|
|
3775
|
+
const source = context.readFile(ref.filePath);
|
|
3776
|
+
if (!source?.includes('=>'))
|
|
3777
|
+
return null;
|
|
3778
|
+
let files = SELECTOR_NAMES.get(context);
|
|
3779
|
+
if (!files) {
|
|
3780
|
+
files = new Map();
|
|
3781
|
+
SELECTOR_NAMES.set(context, files);
|
|
3782
|
+
}
|
|
3783
|
+
let names = files.get(ref.filePath);
|
|
3784
|
+
if (!names) {
|
|
3785
|
+
names = new Set([...source.matchAll(/\bconst\s+([\w$]+)\s*=\s*[\w$]+\s*\(\s*(?:\(\s*[\w$]+\s*\)|[\w$]+)\s*=>/g)].map(m => m[1]));
|
|
3786
|
+
files.set(ref.filePath, names);
|
|
3787
|
+
}
|
|
3788
|
+
if (!names.has(ref.referenceName))
|
|
3789
|
+
return null;
|
|
3790
|
+
const name = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3791
|
+
const lines = source.split('\n');
|
|
3792
|
+
const before = lines.slice(0, ref.line - 1).concat(lines[ref.line - 1].slice(0, ref.column)).join('\n');
|
|
3793
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(before, 'typescript'));
|
|
3794
|
+
const binding = new RegExp(`\\bconst\\s+${name}\\s*=\\s*([\\w$]+)\\s*\\(\\s*(?:\\(\\s*([\\w$]+)\\s*\\)|([\\w$]+))\\s*=>\\s*([\\w$]+)\\.([\\w$]+)\\s*\\)`, 'g');
|
|
3795
|
+
const stackAt = (end) => {
|
|
3796
|
+
const stack = [];
|
|
3797
|
+
for (let i = 0; i < end; i++) {
|
|
3798
|
+
if (code[i] === '{')
|
|
3799
|
+
stack.push(i);
|
|
3800
|
+
else if (code[i] === '}')
|
|
3801
|
+
stack.pop();
|
|
3802
|
+
}
|
|
3803
|
+
return stack;
|
|
3804
|
+
};
|
|
3805
|
+
const callScope = stackAt(code.length);
|
|
3806
|
+
for (const m of [...code.matchAll(binding)].reverse()) {
|
|
3807
|
+
if ((m[2] ?? m[3]) !== m[4])
|
|
3808
|
+
continue;
|
|
3809
|
+
if (!stackAt(m.index).every((pos, i) => callScope[i] === pos))
|
|
3810
|
+
continue;
|
|
3811
|
+
const rest = code.slice(m.index + m[0].length);
|
|
3812
|
+
if (new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${name}\\b|\\{[^}]*\\b${name}\\b)`).test(rest) ||
|
|
3813
|
+
hasParameterBinding(rest, name))
|
|
3814
|
+
return null;
|
|
3815
|
+
return resolveStoreAction(`${m[1]}.getState`, m[5], ref, context, true);
|
|
3816
|
+
}
|
|
3817
|
+
return null;
|
|
3818
|
+
}
|
|
3819
|
+
/** Import resolution names the module binding; a nearer parameter or block
|
|
3820
|
+
* declaration can shadow that binding at this particular call site. */
|
|
3821
|
+
function importShadowedAt(name, ref, context) {
|
|
3822
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3823
|
+
for (const fn of context.getNodesInFile(ref.filePath)) {
|
|
3824
|
+
if ((fn.kind === 'function' || fn.kind === 'method') && fn.startLine <= ref.line && fn.endLine >= ref.line &&
|
|
3825
|
+
fn.signature && hasParameterBinding(`${fn.signature} {`, escaped))
|
|
3826
|
+
return true;
|
|
3827
|
+
}
|
|
3828
|
+
const lines = (context.readFile(ref.filePath) ?? '').split('\n');
|
|
3829
|
+
const before = lines.slice(0, ref.line - 1).concat(lines[ref.line - 1]?.slice(0, ref.column) ?? '').join('\n');
|
|
3830
|
+
const code = (0, strip_comments_1.blankStringContents)((0, strip_comments_1.stripCommentsForRegex)(before, 'typescript'));
|
|
3831
|
+
const stackAt = (end) => {
|
|
3832
|
+
const stack = [];
|
|
3833
|
+
for (let i = 0; i < end; i++) {
|
|
3834
|
+
if (code[i] === '{')
|
|
3835
|
+
stack.push(i);
|
|
3836
|
+
else if (code[i] === '}')
|
|
3837
|
+
stack.pop();
|
|
3838
|
+
}
|
|
3839
|
+
return stack;
|
|
3840
|
+
};
|
|
3841
|
+
const scope = stackAt(code.length);
|
|
3842
|
+
const declarations = new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${escaped}\\b|\\{[^}]*\\b${escaped}\\b)`, 'g');
|
|
3843
|
+
return [...code.matchAll(declarations)].some(m => stackAt(m.index).every((p, i) => scope[i] === p));
|
|
3844
|
+
}
|
|
3845
|
+
/** Balanced parameter lists also cover function-typed parameters, whose own
|
|
3846
|
+
* parentheses must not make the outer shadow invisible. Conservative when a
|
|
3847
|
+
* parameter's type mentions the same name: leave that call unresolved. */
|
|
3848
|
+
function hasParameterBinding(code, escapedName) {
|
|
3849
|
+
const name = new RegExp(`\\b${escapedName}\\b`);
|
|
3850
|
+
if (new RegExp(`\\b${escapedName}\\s*=>`).test(code))
|
|
3851
|
+
return true;
|
|
3852
|
+
for (let i = 0; i < code.length; i++) {
|
|
3853
|
+
if (code[i] !== '(' || /\b(?:if|while|for|switch|with)\s*$/.test(code.slice(0, i)))
|
|
3854
|
+
continue;
|
|
3855
|
+
let depth = 1, j = i + 1;
|
|
3856
|
+
for (; j < code.length && depth; j++) {
|
|
3857
|
+
if (code[j] === '(')
|
|
3858
|
+
depth++;
|
|
3859
|
+
else if (code[j] === ')')
|
|
3860
|
+
depth--;
|
|
3861
|
+
}
|
|
3862
|
+
if (depth === 0 && name.test(code.slice(i + 1, j - 1)) &&
|
|
3863
|
+
/^\s*(?::[^=;{]*)?(?:=>|\{)/.test(code.slice(j)))
|
|
3864
|
+
return true;
|
|
3865
|
+
}
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
1817
3868
|
/**
|
|
1818
3869
|
* Split a camelCase or PascalCase string into words.
|
|
1819
3870
|
*/
|
|
@@ -1912,6 +3963,7 @@ function findBestMatch(ref, candidates, _context) {
|
|
|
1912
3963
|
if (ref.referenceKind === 'instantiates') {
|
|
1913
3964
|
if (candidate.kind === 'class' ||
|
|
1914
3965
|
candidate.kind === 'struct' ||
|
|
3966
|
+
candidate.kind === 'union' ||
|
|
1915
3967
|
candidate.kind === 'interface') {
|
|
1916
3968
|
score += 25;
|
|
1917
3969
|
}
|
|
@@ -1952,11 +4004,38 @@ function matchFuzzy(ref, context) {
|
|
|
1952
4004
|
const candidates = context.getNodesByLowerName(lowerName);
|
|
1953
4005
|
// Filter to callable kinds only (function, method, class)
|
|
1954
4006
|
const callableKinds = new Set(['function', 'method', 'class']);
|
|
1955
|
-
const callableCandidates =
|
|
4007
|
+
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind))
|
|
4008
|
+
.filter((n) => (ref.referenceKind !== 'references' && ref.referenceKind !== 'function_ref') ||
|
|
4009
|
+
sameLanguageFamily(n.language, ref.language));
|
|
1956
4010
|
// Prefer same-language matches
|
|
1957
4011
|
const sameLanguageCandidates = callableCandidates.filter(n => n.language === ref.language);
|
|
1958
4012
|
const finalCandidates = sameLanguageCandidates.length > 0 ? sameLanguageCandidates : callableCandidates;
|
|
1959
|
-
|
|
4013
|
+
// Both post-pipeline visibility guards (#1745 language-local + #1719 sealed
|
|
4014
|
+
// module). The sealed-module test rejects the survivor and never filters the
|
|
4015
|
+
// set that produced it: removing a sealed candidate from a crowd would leave
|
|
4016
|
+
// a lone one and manufacture a 0.5 guess out of an ambiguity fuzzy declines.
|
|
4017
|
+
// Also decline a bare JS/TS call whose only survivor is a method or a
|
|
4018
|
+
// cross-file name the file already binds locally (#1714).
|
|
4019
|
+
// A function nested inside another function is only callable from inside
|
|
4020
|
+
// its container (#1230), so a builtin method call (`res.text()`) whose only
|
|
4021
|
+
// same-named project symbol is some file's closure must decline (#1708).
|
|
4022
|
+
// The check sits on the ONE candidate this strategy would commit to, not on
|
|
4023
|
+
// the candidate set: filtering the unreachable ones out of a crowd would
|
|
4024
|
+
// leave a single survivor and hand it every call of that name — on vite,
|
|
4025
|
+
// `import { resolve } from 'node:path'` in a dozen playground configs onto
|
|
4026
|
+
// the one reachable `resolve` method (#1709). Reachability may reject a
|
|
4027
|
+
// unique guess; it must never manufacture one.
|
|
4028
|
+
if (finalCandidates.length === 1 &&
|
|
4029
|
+
isVisibleAcrossFiles(finalCandidates[0], ref, context) &&
|
|
4030
|
+
isCrossFileReachable(finalCandidates[0], ref, context) &&
|
|
4031
|
+
!(isBareJsCall(ref, context) &&
|
|
4032
|
+
(finalCandidates[0].kind === 'method' ||
|
|
4033
|
+
(finalCandidates[0].filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)))) &&
|
|
4034
|
+
!(finalCandidates[0].kind === 'method' && isBareGoCall(ref, context)) &&
|
|
4035
|
+
// A bare PHP call is a function call (case-insensitive, so fuzzy may find
|
|
4036
|
+
// one) — never the class `View` for `view(…)`, never a method.
|
|
4037
|
+
!(finalCandidates[0].kind !== 'function' && isBarePhpCall(ref, context)) &&
|
|
4038
|
+
isLexicallyReachable(finalCandidates[0], ref, context)) {
|
|
1960
4039
|
const isCrossLanguage = finalCandidates[0].language !== ref.language;
|
|
1961
4040
|
return {
|
|
1962
4041
|
original: ref,
|
|
@@ -2012,6 +4091,9 @@ function dumpNameMatcherProfile(label) {
|
|
|
2012
4091
|
}
|
|
2013
4092
|
}
|
|
2014
4093
|
function matchReference(ref, context) {
|
|
4094
|
+
return gateLanguageMatch(matchReferenceInner(ref, context), ref, context);
|
|
4095
|
+
}
|
|
4096
|
+
function matchReferenceInner(ref, context) {
|
|
2015
4097
|
// Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
|
|
2016
4098
|
// never the fuzzy/qualified fallthrough below (a wrong callback edge is
|
|
2017
4099
|
// worse than none).
|
|
@@ -2069,6 +4151,48 @@ function matchReference(ref, context) {
|
|
|
2069
4151
|
resolvedBy: 'exact-match',
|
|
2070
4152
|
};
|
|
2071
4153
|
}
|
|
4154
|
+
// Erlang call/fun refs carry the call-site arity (`f/1` — #1610) because
|
|
4155
|
+
// arity is part of the function's identity and every erlang function's
|
|
4156
|
+
// qualifiedName carries it (`mod::f/1`). Resolve ONLY to a definition of
|
|
4157
|
+
// that exact arity: the call site's own file first (a local call targets its
|
|
4158
|
+
// own module by language semantics; `-import`ed functions ride the
|
|
4159
|
+
// cross-file branch), and when no definition of that arity exists anywhere,
|
|
4160
|
+
// resolve to NOTHING rather than a sibling arity — the real target may be
|
|
4161
|
+
// macro-generated or out of repo, and a wrong-arity edge is worse than none.
|
|
4162
|
+
if (ref.language === 'erlang' &&
|
|
4163
|
+
!ref.referenceName.includes('::') &&
|
|
4164
|
+
(ref.referenceKind === 'calls' || ref.referenceKind === 'references')) {
|
|
4165
|
+
const am = /^(.+)\/(\d{1,3})$/.exec(ref.referenceName);
|
|
4166
|
+
if (am) {
|
|
4167
|
+
// endsWith is length-anchored, so `/1` cannot match `…/11`.
|
|
4168
|
+
const arityTail = `/${am[2]}`;
|
|
4169
|
+
const candidates = context
|
|
4170
|
+
.getNodesByName(am[1])
|
|
4171
|
+
.filter((n) => n.language === 'erlang' && n.kind === 'function' && n.qualifiedName.endsWith(arityTail));
|
|
4172
|
+
if (candidates.length > 0) {
|
|
4173
|
+
const sameFile = candidates.find((n) => n.filePath === ref.filePath);
|
|
4174
|
+
if (sameFile) {
|
|
4175
|
+
return { original: ref, targetNodeId: sameFile.id, confidence: 0.95, resolvedBy: 'exact-match' };
|
|
4176
|
+
}
|
|
4177
|
+
if (candidates.length === 1) {
|
|
4178
|
+
return { original: ref, targetNodeId: candidates[0].id, confidence: 0.8, resolvedBy: 'exact-match' };
|
|
4179
|
+
}
|
|
4180
|
+
const best = findBestMatch(ref, candidates, context);
|
|
4181
|
+
if (best) {
|
|
4182
|
+
const proximity = computePathProximity(ref.filePath, best.filePath);
|
|
4183
|
+
return {
|
|
4184
|
+
original: ref,
|
|
4185
|
+
targetNodeId: best.id,
|
|
4186
|
+
confidence: proximity >= 30 ? 0.7 : 0.4,
|
|
4187
|
+
resolvedBy: 'exact-match',
|
|
4188
|
+
};
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
if (isUnresolvedJsMemberCall(ref))
|
|
4195
|
+
return null;
|
|
2072
4196
|
// Try strategies in order of confidence
|
|
2073
4197
|
let result;
|
|
2074
4198
|
// 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
|
|
@@ -2116,6 +4240,16 @@ function matchReference(ref, context) {
|
|
|
2116
4240
|
if (result)
|
|
2117
4241
|
return result;
|
|
2118
4242
|
}
|
|
4243
|
+
// A call-receiver chain the extractor encoded as `<inner>().<method>` for a
|
|
4244
|
+
// language with no chain resolver above (TS/JS, Python — #1683) is a
|
|
4245
|
+
// receiver whose type is unknown. Nothing below may guess for it: the
|
|
4246
|
+
// method-call pattern rejects the parens, exact name never matches, but the
|
|
4247
|
+
// fuzzy strategy splits on `.` and would hand `make().run` to any `run` —
|
|
4248
|
+
// the fabricated edge the encoding exists to prevent.
|
|
4249
|
+
if (ref.referenceName.includes('().') &&
|
|
4250
|
+
(ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'python')) {
|
|
4251
|
+
return nmTimed('storeAccessorChain', ref, () => matchStoreAccessorChain(ref, context));
|
|
4252
|
+
}
|
|
2119
4253
|
// 2. Method call pattern
|
|
2120
4254
|
result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
|
|
2121
4255
|
if (result)
|