Repository navigation
Description
In TypeScript and JavaScript a bare call — serialize(...) with no receiver — can never bind to a class method. Methods require this., a receiver, or a destructured/bound reference. But when a method and a module-scope function in the same file share a name, the bare call inside the method resolves onto the method itself, as a self-edge.
Reproduction
One file:
function serialize(value: string): string {
return value.trim();
}
export class Record {
constructor(private readonly raw: string) {}
serialize(): string {
return serialize(this.raw); // the free function above
}
}Index and resolve:
Record::serialize --calls--> Record::serialize
{ "confidence": 0.4, "resolvedBy": "exact-match", "refName": "serialize" }
Expected: Record::serialize --calls--> serialize (the module-scope function).
Why it lands there
matchByExactName gets both same-named nodes as candidates. Both are in the ref's file and both are lexically reachable, so findBestMatch scores them, and its same-file line-proximity term (score += max(0, 20 - distance / 10)) favours whichever definition is nearer the call site. The enclosing method always is — the call is inside it. So the method wins its own call.
The 0.4 confidence is computePathProximity returning < 30 for a same-file match, which reads oddly on its own and may be a second, smaller thing worth a look.
Suggested rule
For a receiver-less ref in the JS/TS family, drop method candidates before scoring. The language guarantees the ref is not a method call: it can only be a local, a module-scope binding, or an import.
This has to be language-gated. Java, C# and Python-with-self aside, several languages do let an instance method be called bare from inside the class, so the same filter would be wrong there. In JS/TS it is unambiguous.
A narrower variant, if the broad rule is too blunt: only refuse the candidate when it is the ref's own enclosing method — that kills the self-edge without changing anything else.
Scope
Distinct from #1691, which resolves this.<field>.<method>() on the field's declared type — that path has a receiver and this one has none, which I think is what @danusha2345 meant by "a different receiver-less path".
Correcting my own earlier description of this: I reported it as a method calling a same-named imported free function. That was wrong, and it matters for anyone reproducing it. I re-tested three variants against #1691's head (15c7ea6); only the same-file one misresolves:
| shape | result |
|---|---|
| same-file module-scope function | self-edge, exact-match @0.4 |
import { serialize } from './format' |
correct, import @0.9 |
barrel re-export (export * from) |
correct, import @0.9 |
namespace import (fmt.serialize) |
correct, import @0.9 |
So the import resolver already handles the imported cases; the gap is only where no import statement exists to consult.
I counted ten of these on a private ~600-file repo indexed on #1691's head. That count is from that earlier run and I have not re-measured it; the reproduction above is the part I have verified directly.
Happy to open a PR for whichever rule you prefer.
Activity
colbymchenry commentedon Sep 8, 2026
Fixed in #1759 (rebased landing of #1735). Verified on Linux: bare
serialize(...)insideRecord.serializenow binds the module-scope function instead of a self-edge.fix(resolution): a receiver-less JS/TS call never binds to a method
fix(resolution): a receiver-less JS/TS call never binds to a method (c…
danusha2345 commentedon Sep 8, 2026
Follow-up to the landed fix: #1760 — the local-binding rule must not treat
const { action } = useStore.getState()/const action = useStore((s) => s.action)as definitions, or store-action edges disappear (3 tests onmainatcd4e65b).Merge origin/main (8733c28) into fork/consolidated
fix(resolution): a receiver-less Go call never binds to a method (#1857…
fix(resolution): JS/TS calls never pick a class member by name alone …