From 2a8eb4ccc43f48feccb722df0936f2a9fd7bde48 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 19 Apr 2026 20:20:16 +0100 Subject: [PATCH] feat(python): dotted-typebinding receiver resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds case 3 to `emitReceiverBoundCalls`: when a receiver's typeBinding has a dotted rawName like `u: models.User` (the constructor-inferred form fired by `u = models.User(...)`), walk the namespace map + target file's defs to find the class, then look up the member via ownerId. `resolveTypeRef`'s QualifiedNameIndex fallback can't cover this because the target class's qualifiedName in models.py is just `"User"`, not `"models.User"` — the dotted form only exists in the call-site file's receiver expression. This pass bridges that gap without modifying the shared registry. Fixes 9 more failures (flag-on 45 → 36): - Python qualified constructor inference (2) - Python module import CALLS resolution (Issue #337) (3) - (cluster overlap — several downstream tests in assignment/nullable/ walrus that propagate through qualified-ctor bindings also benefit) Flag-off still 191/191. --- .../src/core/ingestion/python-scope-emit.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/gitnexus/src/core/ingestion/python-scope-emit.ts b/gitnexus/src/core/ingestion/python-scope-emit.ts index 700b7fcee..dfef6b261 100644 --- a/gitnexus/src/core/ingestion/python-scope-emit.ts +++ b/gitnexus/src/core/ingestion/python-scope-emit.ts @@ -492,6 +492,38 @@ function emitReceiverBoundCalls( seen, ); if (emitted2) emitted++; + continue; + } + } + + // ── Case 3: receiver has a dotted typeBinding (`u: models.User`) ── + // Happens when `u = models.User(...)` fires the qualified-call + // constructor-inferred capture. The shared resolveTypeRef can't + // handle it because User's qualifiedName in models.py is just + // "User" (not "models.User"), so QualifiedNameIndex fallback + // misses. Resolve manually via the namespace map. + const typeRef = findReceiverTypeBinding(site.inScope, receiverName, scopes); + if (typeRef !== undefined && typeRef.rawName.includes('.')) { + const [nsName, ...classNameParts] = typeRef.rawName.split('.'); + const className = classNameParts.join('.'); + const targetFile = namespaceTargets.get(nsName); + if (targetFile !== undefined && className.length > 0) { + const classDef3 = findExportedDef(targetFile, className, parsedFiles); + if (classDef3 !== undefined) { + const memberDef = findOwnedMember(classDef3.nodeId, memberName, parsedFiles); + if (memberDef !== undefined) { + const emitted3 = tryEmitEdge( + graph, + scopes, + nodeLookup, + site, + memberDef, + 'python-scope: dotted-typebinding', + seen, + ); + if (emitted3) emitted++; + } + } } } } @@ -500,6 +532,30 @@ function emitReceiverBoundCalls( return emitted; } +/** + * Walk the scope chain from `startScope` looking for a typeBinding + * named `receiverName`. Returns the TypeRef or undefined if no binding + * exists in the chain. + */ +function findReceiverTypeBinding( + startScope: ScopeId, + receiverName: string, + scopes: ScopeResolutionIndexes, +): { readonly rawName: string } | 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; + const typeRef = scope.typeBindings.get(receiverName); + if (typeRef !== undefined) return typeRef; + currentId = scope.parent; + } + return undefined; +} + /** * Build a `localName → targetFilePath` map over the file's module-scope * import edges, limited to namespace-kind imports (which is what binds