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.
In TypeScript and JavaScript a bare call —
serialize(...)with no receiver — can never bind to a class method. Methods requirethis., 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:
Index and resolve:
Expected:
Record::serialize --calls--> serialize(the module-scope function).Why it lands there
matchByExactNamegets both same-named nodes as candidates. Both are in the ref's file and both are lexically reachable, sofindBestMatchscores 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.4confidence iscomputePathProximityreturning< 30for 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
methodcandidates 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-
selfaside, 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:exact-match@0.4import { serialize } from './format'import@0.9export * from)import@0.9fmt.serialize)import@0.9So 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.