Skip to content

resolveThisDispatch resolves this/super dispatch to the wrong file when a same-named base class exists elsewhere with no same-file match #2062

Description

@carlos-alm

Bug

resolveThisDispatch (src/domain/graph/builder/cha.ts) resolves this.method()/super.method()/bare super(...) calls by walking the CHA parents map and looking up ${current}.${methodName} by name only (lookup.byName(qualified)), across the whole project. It has same-file preference logic, but that logic only helps when a same-file candidate exists alongside cross-file candidates:

const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
if (found.length > 0) {
  if (callerFile && found.some((n) => n.file === callerFile)) {
    return found.filter((n) => n.file === callerFile);
  }
  return found; // <-- BUG: no same-file candidate exists, but a same-named class
                //     in a completely unrelated file is returned anyway, instead
                //     of continuing the walk or returning no match.
}

When the qualified name (e.g. Shape.constructor) exists in the project only in a different file than the caller — because the caller's own file's same-named base class has no explicit definition of that method (e.g. an implicit default constructor) — the walk returns the wrong file's match instead of either continuing up the hierarchy or giving up.

Repro (observed empirically while validating PR for #1929)

Two independent fixture files, each defining an unrelated class named Shape:

hierarchy.ts:

export class Shape {
  area(): number { return 0; }
}
export class Circle extends Shape {
  constructor(private radius: number) { super(); }
  ...
}

super-dispatch.ts (a different, unrelated hierarchy):

export class Shape {
  constructor(public name: string) {}
}
export class Polygon extends Shape {
  constructor(name: string, public sides: number) { super(name); }
}

Building this project produces:

Circle.constructor@hierarchy.ts -> Shape.constructor@super-dispatch.ts       (WRONG — cross-file, unrelated class)
Rectangle.constructor@hierarchy.ts -> Shape.constructor@super-dispatch.ts    (WRONG)
Polygon.constructor@super-dispatch.ts -> Circle.constructor@hierarchy.ts    (WRONG)
Polygon.constructor@super-dispatch.ts -> Rectangle.constructor@hierarchy.ts (WRONG)
Polygon.constructor@super-dispatch.ts -> Ellipse.constructor@hierarchy.ts   (WRONG)

hierarchy.ts's Shape has no explicit constructor (implicit default), so Circle.constructor's bare super() call should resolve to no target (there is no declared Shape.constructor symbol in hierarchy.ts at all) — instead it cross-contaminates with the unrelated Shape class in super-dispatch.ts.

This dropped TypeScript resolution-benchmark precision from ~100% to 73.8% in local testing (caught before merging, so the actual fixtures were renamed to avoid the collision rather than shipping the false positives).

Impact

Any project with multiple small classes sharing a common base-class name across different files (e.g. many files defining their own local Base/Shape/Handler class) is at risk of spurious this/super/bare-super(...) call edges pointing to a completely unrelated file, whenever the caller's own same-named ancestor happens not to define the specific method being dispatched.

Suggested fix

When found has no same-file candidate, don't immediately return the global match — either:

  1. Verify the candidate is reachable via the caller's own extends chain (cross-file heritage — legitimate, e.g. importing a base class from another file) before accepting it, or
  2. Continue walking chaCtx.parents past current looking for a resolvable same-file (or heritage-verified) match, only falling back to a global match when the class hierarchy has been fully verified to cross file boundaries via actual import/extends relationships (not just name equality).

This affects the shared TS resolver used by both engines (WASM's own buildCallEdgesJS/buildChaPostPass, and the native engine's runPostNativeThisDispatch post-pass both call into resolveThisDispatch), so a single fix in cha.ts covers both.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions