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:
- 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
- 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.
Bug
resolveThisDispatch(src/domain/graph/builder/cha.ts) resolvesthis.method()/super.method()/baresuper(...)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: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:super-dispatch.ts(a different, unrelated hierarchy):Building this project produces:
hierarchy.ts'sShapehas no explicit constructor (implicit default), soCircle.constructor's baresuper()call should resolve to no target (there is no declaredShape.constructorsymbol inhierarchy.tsat all) — instead it cross-contaminates with the unrelatedShapeclass insuper-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/Handlerclass) is at risk of spuriousthis/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
foundhas no same-file candidate, don't immediately return the global match — either:extendschain (cross-file heritage — legitimate, e.g. importing a base class from another file) before accepting it, orchaCtx.parentspastcurrentlooking 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'srunPostNativeThisDispatchpost-pass both call intoresolveThisDispatch), so a single fix incha.tscovers both.