From f4f51910ddfccc3d2582eb65ac7cf7b42e8a624d Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 20 Apr 2026 11:51:17 +0100 Subject: [PATCH] fix(python-scope): drop dead pre-seeding from receiver-bound pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-seeding loop at the top of \`emitReceiverBoundCalls\` populated \`seen\` with every reference the shared resolver had already resolved. That was useful when emit-references ran FIRST. After Unit 9 reversed the order (emit-references runs after the Python passes and uses \`handledSites\` to skip what we processed), the pre-seed only causes harm: when an MRO walk in Case 0 (compound receiver) and Case 4 (simple typeBinding) both touch the same site at the same position but resolve to different targets, the pre-seed suppresses the second emission because the shared resolver had already entered the wrong target into \`seen\`. Concrete case: \`c.greet().save()\` — Case 0 emits the outer save edge to Greeting.save; Case 4 then resolves the inner \`c.greet()\` to A.greet via MRO walk. With pre-seed both edges should emit (different targets, different rel.ids); without removing the pre-seed the inner emission was being deduped against an already-seeded entry and the A.greet edge was lost. Verification: - Flag-off: 191/191 (identical baseline). - Flag-on: 8 fail / 183 pass (was 9/182; +1 — \`c.greet() to A#greet via MRO walk\` now passes). - tsc --noEmit clean. --- .../src/core/ingestion/python-scope-emit.ts | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/gitnexus/src/core/ingestion/python-scope-emit.ts b/gitnexus/src/core/ingestion/python-scope-emit.ts index 06e45a5fa..bb502b42c 100644 --- a/gitnexus/src/core/ingestion/python-scope-emit.ts +++ b/gitnexus/src/core/ingestion/python-scope-emit.ts @@ -340,28 +340,16 @@ function emitReceiverBoundCalls( handledSites: Set, ): number { let emitted = 0; - // Share the same dedup shape as `emitReferencesViaLookup` so we never - // double-count a resolution that the shared path already produced. + // `seen` is the per-pass dedup so the multiple Cases below don't + // double-emit if two of them resolve the same site to the same + // target. Pre-seeding from the shared resolver was historically + // useful when emit-references ran FIRST, but now the order is + // reversed (emit-references comes after this pass and uses + // `handledSites` to skip what we processed). Keeping the pre-seed + // would suppress legitimate emissions for sites the shared resolver + // happened to also resolve. const seen = new Set(); - for (const refs of referenceIndex.bySourceScope.values()) { - for (const r of refs) { - const targetDef = scopes.defs.get(r.toDef); - if (targetDef === undefined) continue; - // Seed using the same dedup key as emit-references/emit-edge use. - // We recompute by calling the shared helpers indirectly via - // tryEmitEdge shape; cheaper to dupe the key construction here - // since we need the graph ids anyway. - const callerGraphId = resolveCallerGraphId(r.fromScope, scopes, nodeLookup); - if (callerGraphId === undefined) continue; - const tgtGraphId = resolveDefGraphId(targetDef.filePath, targetDef, nodeLookup); - if (tgtGraphId === undefined) continue; - const kind = mapReferenceKindToEdgeType(r.kind); - if (kind === undefined) continue; - seen.add( - `${kind}:${callerGraphId}->${tgtGraphId}:${r.atRange.startLine}:${r.atRange.startCol}`, - ); - } - } + void referenceIndex; // kept in signature for parity with future passes // Class def → Class scope map (for field-chain field-type lookup). // The class scope's `ownedDefs` contains the Class def per pass2's @@ -857,11 +845,20 @@ function resolveCompoundReceiverClass( depth + 1, ); if (objClass === undefined) return undefined; - const methodClassScope = classScopeByDefId.get(objClass.nodeId); - // Method's return-type binding lives on the class scope (because - // the method's function_definition auto-hoists its return-type - // binding to the parent scope == class scope). - const retType = methodClassScope?.typeBindings.get(methodName); + // Walk the MRO so methods inherited from ancestors resolve. A class + // owning the method itself doesn't always exist — `class C(B)` may + // inherit `greet` from B (or B's parent A), and the return-type + // typeBinding lives on the SCOPE OF THE CLASS THAT OWNS THE METHOD. + let retType: TypeRef | undefined; + const ownerChain = [objClass.nodeId, ...scopes.methodDispatch.mroFor(objClass.nodeId)]; + for (const ownerId of ownerChain) { + const cs = classScopeByDefId.get(ownerId); + const candidate = cs?.typeBindings.get(methodName); + if (candidate !== undefined) { + retType = candidate; + break; + } + } if (retType === undefined) return undefined; return findClassBindingInScope(retType.declaredAtScope, retType.rawName, scopes); }