feat(python-scope): super() receiver dispatches up the MRO

Unit 8 — `super().method()` inside a class method walks the enclosing
class's MRO chain (skipping self) and resolves to the first ancestor
that owns the method.

New receiver branch in `emitReceiverBoundCalls` recognizes
`super(...)` syntactically (regex-cheap), finds the enclosing class
via a new `findEnclosingClassDef` scope-walk helper, then re-uses
`scopes.methodDispatch.mroFor` + `findOwnedMember` from the existing
class-receiver path. Handled before the compound-receiver case so
`super()` doesn't fall into the bare-identifier branch where `super`
isn't a binding.

Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 21 fail / 170 pass (was 22/169; `super().save() inside
  User to BaseModel.save` now passes).
- tsc --noEmit clean.
This commit is contained in:
Gergo Magyar 2026-04-20 09:31:11 +01:00
parent eb897540f3
commit a1382d3cf7

View file

@ -375,6 +375,33 @@ function emitReceiverBoundCalls(
const receiverName = site.explicitReceiver.name;
const memberName = site.name;
// ── super() — resolve to the enclosing class's PARENT (first MRO entry).
// Python's `super()` inside a method dispatches up the MRO chain.
if (/^super\s*\(/.test(receiverName)) {
const enclosingClass = findEnclosingClassDef(site.inScope, scopes);
if (enclosingClass !== undefined) {
const ancestors = scopes.methodDispatch.mroFor(enclosingClass.nodeId);
let memberDef: SymbolDefinition | undefined;
for (const ownerId of ancestors) {
memberDef = findOwnedMember(ownerId, memberName, parsedFiles);
if (memberDef !== undefined) break;
}
if (memberDef !== undefined) {
const ok = tryEmitEdge(
graph,
scopes,
nodeLookup,
site,
memberDef,
'python-scope: super-receiver',
seen,
);
if (ok) emitted++;
continue;
}
}
}
// ── Case 0: compound receiver (`user.address.save()` or
// `svc.get_user().save()`) — walk the dotted/call chain,
// resolving each segment to a class via field types or
@ -575,6 +602,29 @@ function emitFreeCallFallback(
return emitted;
}
/** Walk a scope chain upward looking for the innermost enclosing
* Class scope and return that class's def. Used by the `super()`
* receiver case to discover the dispatch base. */
function findEnclosingClassDef(
startScope: ScopeId,
scopes: ScopeResolutionIndexes,
): SymbolDefinition | undefined {
let currentId: ScopeId | null = startScope;
const visited = new Set<ScopeId>();
while (currentId !== null) {
if (visited.has(currentId)) return undefined;
visited.add(currentId);
const scope = scopes.scopeTree.getScope(currentId);
if (scope === undefined) return undefined;
if (scope.kind === 'Class') {
const cd = scope.ownedDefs.find((d) => d.type === 'Class');
if (cd !== undefined) return cd;
}
currentId = scope.parent;
}
return undefined;
}
/** Max depth for compound-receiver chain resolution (`a().b().c().d()`).
* Practical Python rarely exceeds 3-4 hops; the cap just prevents
* pathological recursion if the receiver text turns out to be malformed. */