Skip to content

A bare call resolves onto the enclosing method when a same-file function shares its name #1714

Description

@bompus

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions