fix(resolver): kind-filter same-file bare-name lookup for receiver-bearing calls#2026
Conversation
…aring calls resolveCallTargets's same-file bare-name lookup (call-resolver.ts) and resolveByGlobal's exact-name fallback (resolver/strategy.ts) ran unconditionally for every call, unfiltered by symbol kind. A call with a receiver (this.x(), obj.x()) is logically "invoke a member of some instance" — a same-file class/interface/struct/variable that merely shared the call's bare name could win outright, before any more specific resolution tier (receiver typing, the Object.defineProperty accessor fallback, etc.) ever got a chance to run. Both lookups now restrict receiver-bearing calls to CALLABLE_SYMBOL_KINDS (function/method), a new shared constant in shared/kinds.ts. A genuinely bare call (no receiver at all) stays unfiltered, since at this layer it is indistinguishable from a `new ClassName()` constructor invocation, which legitimately targets a class-kind definition. Mirrored the identical fix in the native Rust engine (resolve_call_targets / resolve_exact_global_match in build_edges.rs), which reproduced the same bug. A broader fix (trying receiver-typed resolution before the bare lookup for concrete object receivers) was attempted but reverted: it regressed issue-1517's computed-property object-literal test, since that pattern emits two node representations for the same method and the bare lookup finds the more precise one. Filed #2025 to track that narrower residual. Refs #1888
Greptile SummaryThis PR fixes a bug where receiver-bearing calls (
Confidence Score: 4/5Safe to merge; the change is narrowly scoped to two fallback tiers of the resolution cascade, both engines are patched consistently, and the regression guard for constructor-call resolution is explicitly tested. The logic is correct and the guard condition (truthy receiver → filter; no receiver → unfiltered) is consistently applied across both the TypeScript same-file lookup and the Rust mirror. All three key scenarios — false class match, correct method match, and bare constructor call — are covered by new unit and integration tests on both engines. No edge cases were found that would cause incorrect resolution under normal graph inputs. No files require special attention. The Rust and TypeScript changes are symmetrical and independently verified by the integration test running on both engines. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[resolveCallTargets] --> B{call.receiver\npresent?}
B -->|Yes| C[Same-file lookup\nfiltered by CALLABLE_SYMBOL_KINDS\nfunction / method only]
B -->|No| D[Same-file lookup\nunfiltered\nallows class for constructor calls]
C --> E{matches\nfound?}
D --> E
E -->|Yes| F[Return targets]
E -->|No| G[resolveByMethodOrGlobal]
G --> H{receiver is\nconcrete object?}
H -->|Yes| I[resolveByReceiver\ntype-aware cascade]
H -->|No - bare / this / self / super| J[resolveByGlobal]
J --> K[resolveViaAccessorThisDispatch]
K --> L{found?}
L -->|Yes| F
L -->|No| M[resolveExactGlobalMatch\nfiltered by CALLABLE_SYMBOL_KINDS\nwhen receiver present]
M --> N{unique best\ncandidate?}
N -->|Yes| F
N -->|No / tie| O[resolveViaSameClassSibling]
O --> F
style C fill:#d4edda,stroke:#28a745
style M fill:#d4edda,stroke:#28a745
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[resolveCallTargets] --> B{call.receiver\npresent?}
B -->|Yes| C[Same-file lookup\nfiltered by CALLABLE_SYMBOL_KINDS\nfunction / method only]
B -->|No| D[Same-file lookup\nunfiltered\nallows class for constructor calls]
C --> E{matches\nfound?}
D --> E
E -->|Yes| F[Return targets]
E -->|No| G[resolveByMethodOrGlobal]
G --> H{receiver is\nconcrete object?}
H -->|Yes| I[resolveByReceiver\ntype-aware cascade]
H -->|No - bare / this / self / super| J[resolveByGlobal]
J --> K[resolveViaAccessorThisDispatch]
K --> L{found?}
L -->|Yes| F
L -->|No| M[resolveExactGlobalMatch\nfiltered by CALLABLE_SYMBOL_KINDS\nwhen receiver present]
M --> N{unique best\ncandidate?}
N -->|Yes| F
N -->|No / tie| O[resolveViaSameClassSibling]
O --> F
style C fill:#d4edda,stroke:#28a745
style M fill:#d4edda,stroke:#28a745
Reviews (1): Last reviewed commit: "fix(resolver): kind-filter same-file bar..." | Re-trigger Greptile |
Codegraph Impact Analysis3 functions changed → 21 callers affected across 4 files
|
Summary
resolveCallTargets's same-file bare-name lookup (lookup.byNameAndFile(call.name, relPath)incall-resolver.ts) andresolveByGlobal's exact-name fallback (resolveExactGlobalMatchinresolver/strategy.ts) both ran unconditionally for every call, unfiltered by symbol kind. A call with a receiver —this.x(),self.x(),super.x(), orobj.x()— is logically "invoke a member of some instance", so a same-file class/interface/struct/variable that merely shared the call's bare name could win outright, before any more specific resolution tier (receiver typing, theObject.definePropertyaccessor fallback, etc.) ever got a chance to run.Confirmed identical on both engines before the fix (WASM and native Rust) using the issue's own repro:
Before:
getter -> bar(kind:class) — the coincidentally-named class pre-empted the correctly-typedRegistry.barmethod. After:getter -> Registry.bar(kind:method), andbarno longer receives a spuriouscallsedge.Fix
Both lookups now restrict receiver-bearing calls to a new shared
CALLABLE_SYMBOL_KINDSconstant (function/method) insrc/shared/kinds.ts— consolidating what was already duplicated inline at several other call sites in this exact resolver for the identical purpose (resolveDefinePropertyAccessorTarget's tier 2,resolveKotlinReflectionPreQualified,resolveReflectionKeyExprFallback). A genuinely bare call (no receiver at all) is left unfiltered: at this resolution layer it's indistinguishable from anew ClassName()constructor invocation, which legitimately targets a class-kind definition — kind-filtering it would break constructor-call resolution across every language (verified empirically; an earlier draft that filtered unconditionally broke theobj -> Registryconstructor-call edge in the repro above).Native engine: the identical bug existed in the mirrored Rust
resolve_call_targets/resolve_exact_global_match(crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs) — fixed the same way, reusing the existingis_callable_kindhelper already in that file. Verified both engines now produce byte-identicalcallsedges for the repro.What's out of scope (filed separately)
A broader fix — trying
resolveByReceiver's type-aware cascade before the bare lookup whenever a concrete object receiver is present (obj.method(), notthis/self/super) — was implemented and then reverted: it regressedtests/integration/issue-1517-computed-prop-resolution.test.ts. That fixture's computed-key object-literal methods ({ ['formatDate'](date) {} }) get two node representations for the same physical method (a bare-name node from #1517's own fix, and a qualified-name node from the general object-literal-function extraction pass); reordering caused the composite-pts-key fallback to find the less-precise qualified node instead of the correct bare one.This means a concrete-receiver call can still, in principle, match an unrelated same-file function/method (not class/interface/etc. — those are now excluded) before
resolveByReceivergets a chance to type-check the receiver. Filed #2025 to track this narrower residual, with the #1517 regression details and a suggested fix direction (likely deduplicating the two node representations rather than reordering resolution).Using
Refs #1888rather thanClosessince the issue named both "unfiltered by kind" (fixed here, for all receiver shapes) and "runs before type-aware dispatch" (fixed forthis/self/super; the concrete-receiver sub-case is#2025) — mirrors the precedent in PR #1943.Test plan
tests/integration/issue-1888-same-file-bare-name-kind-filter.test.ts: the issue's exact repro, run on both engines, asserting the correctRegistry.baredge, the absence of the falsebar(class) edge, and that thenew Registry()constructor-call edge still resolves.tests/unit/call-resolver.test.tscoveringresolveCallTargetsandresolveByMethodOrGlobal/resolveByGlobal's kind filter directly, plus the bare-call (no receiver) constructor-compatibility case.npm test— full suite green (245 files, 3927 passed, 0 failed) with both WASM and a locally-built+codesigned native addon exercised; one unrelated, already-documented flaky test (Flaky cycle-node ordering in checkNoNewCycles full-suite run (tests/integration/check.test.ts) #1990, cycle-node ordering) confirmed flaky in isolation, not a regression.tests/benchmarks/resolution/resolution-benchmark.test.ts— all 206 tests pass across all 34 languages, no precision/recall regression.npm run lint— clean (one pre-existing, unrelated warning in an untouched fixture file).codegraph diff-impact --staged -T— impact contained to the three changed functions (resolveCallTargets,resolveExactGlobalMatch, and the incidentalCALLABLE_KINDS→CALLABLE_SYMBOL_KINDSrename infindEnclosingCallable), 21 transitive callers across 4 files, all expected.Stacked on #1887 (base branch
fix/issue-1887-native-engine-skips-object-defineproperty) — only the resolver/kinds/test diff is this issue's change.