From a1382d3cf73a9eb39d83047668392b64628aaa10 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 20 Apr 2026 09:31:11 +0100 Subject: [PATCH] feat(python-scope): super() receiver dispatches up the MRO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/core/ingestion/python-scope-emit.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/gitnexus/src/core/ingestion/python-scope-emit.ts b/gitnexus/src/core/ingestion/python-scope-emit.ts index 23fd4e879..21caa798c 100644 --- a/gitnexus/src/core/ingestion/python-scope-emit.ts +++ b/gitnexus/src/core/ingestion/python-scope-emit.ts @@ -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(); + 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. */