From c5221b98f50b26a6bdf536bf56932eebba2f54c8 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 20 Apr 2026 08:51:40 +0100 Subject: [PATCH] feat(python-scope): resolve dotted receivers via class-scope field types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit 4 partial — the dotted-receiver case (`user.address.save()`). Class-body annotations like `class User: address: Address` already land in the class scope's typeBindings via the existing `@type-binding.annotation` capture. This commit consumes that signal: - Build a `Map` from every parsed file's class scopes once per resolution pass. - New Case 0 in `emitReceiverBoundCalls`: when the receiver's name contains a dot, walk the chain — resolve the head's type, then for each remaining segment look up that field's type in the owner class's scope.typeBindings, then emit the call against the final class with MRO walk. - Cross-scope lookups use each TypeRef's `declaredAtScope` so an imported `Address` resolves in the file that owns the field declaration, not the file holding the call site. Verification: - Flag-off: 191/191 (identical baseline). - Flag-on: 29 fail / 162 pass (was 31/160; both `Field type resolution` fixtures now pass — same-file and cross-file disambig). - tsc --noEmit clean. Remaining Unit 4 work (write ACCESSES, `self.X` for-loop iteration) needs Unit 6's tuple/iterable destructuring before it can land — `for u in self.users` requires the iterable typing path. --- .../src/core/ingestion/python-scope-emit.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/gitnexus/src/core/ingestion/python-scope-emit.ts b/gitnexus/src/core/ingestion/python-scope-emit.ts index cb81b16f9..72e8e02b5 100644 --- a/gitnexus/src/core/ingestion/python-scope-emit.ts +++ b/gitnexus/src/core/ingestion/python-scope-emit.ts @@ -338,6 +338,18 @@ function emitReceiverBoundCalls( } } + // Class def → Class scope map (for field-chain field-type lookup). + // The class scope's `ownedDefs` contains the Class def per pass2's + // structural-ownership rule. + const classScopeByDefId = new Map(); + for (const p of parsedFiles) { + for (const scope of p.scopes) { + if (scope.kind !== 'Class') continue; + const cd = scope.ownedDefs.find((d) => d.type === 'Class'); + if (cd !== undefined) classScopeByDefId.set(cd.nodeId, scope); + } + } + for (const parsed of parsedFiles) { const namespaceTargets = collectNamespaceTargets(parsed, scopes); @@ -348,6 +360,52 @@ function emitReceiverBoundCalls( const receiverName = site.explicitReceiver.name; const memberName = site.name; + // ── Case 0: dotted receiver (`user.address.save()`) ────────── + // Walk the dotted chain via class-scope typeBindings (fields). + if (receiverName.includes('.')) { + const parts = receiverName.split('.'); + const head = parts[0]!; + const headType = findReceiverTypeBinding(site.inScope, head, scopes); + let currentClass: SymbolDefinition | undefined = headType + ? findClassBindingInScope(headType.declaredAtScope, headType.rawName, scopes) + : undefined; + for (let i = 1; i < parts.length && currentClass !== undefined; i++) { + const fieldName = parts[i]!; + const cs = classScopeByDefId.get(currentClass.nodeId); + const fieldType = cs?.typeBindings.get(fieldName); + if (fieldType === undefined) { + currentClass = undefined; + break; + } + currentClass = findClassBindingInScope( + fieldType.declaredAtScope, + fieldType.rawName, + scopes, + ); + } + if (currentClass !== undefined) { + const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)]; + let memberDef: SymbolDefinition | undefined; + for (const ownerId of chain) { + memberDef = findOwnedMember(ownerId, memberName, parsedFiles); + if (memberDef !== undefined) break; + } + if (memberDef !== undefined) { + const ok = tryEmitEdge( + graph, + scopes, + nodeLookup, + site, + memberDef, + 'python-scope: field-chain', + seen, + ); + if (ok) emitted++; + continue; + } + } + } + // ── Case 1: namespace receiver (`import models; models.X()`) ─ const targetFile = namespaceTargets.get(receiverName); if (targetFile !== undefined) {