diff --git a/gitnexus/src/core/ingestion/languages/python/receiver-binding.ts b/gitnexus/src/core/ingestion/languages/python/receiver-binding.ts index 486cf97d9..1d49ae412 100644 --- a/gitnexus/src/core/ingestion/languages/python/receiver-binding.ts +++ b/gitnexus/src/core/ingestion/languages/python/receiver-binding.ts @@ -126,13 +126,24 @@ export function synthesizeReceiverTypeBinding(fnNode: SyntaxNode): CaptureMatch * (subscript, await, comprehension, bare name) is left alone — that is the * "name-only guess" this module deliberately refuses. */ -function constructorCallTypeName(right: SyntaxNode | null): string | undefined { +function constructorCallTypeName( + right: SyntaxNode | null, + receiverName: string, +): string | undefined { if (right === null || right.type !== 'call') return undefined; const callee = right.childForFieldName('function'); if (callee === null) return undefined; if (callee.type !== 'identifier' && callee.type !== 'attribute') return undefined; const text = callee.text.trim(); - return text.length > 0 ? text : undefined; + if (text.length === 0) return undefined; + // `self.p = self.build()` is a METHOD call, not a construction. Accepting it + // bound `p` to the non-type `"self.build"`, which resolves to nothing — and + // because it shares this weakest tier, a later such assignment DISPLACED an + // earlier real `self.p = Outer()`, leaving the field untyped again. Measured: + // both `self.q = self.make()` and the displacement pair emitted no CALLS edge + // at all. Rejecting a callee rooted at the receiver keeps the construction. + if (text === receiverName || text.startsWith(`${receiverName}.`)) return undefined; + return text; } /** @@ -217,7 +228,7 @@ export function synthesizeConstructorFieldTypeBindings(fnNode: SyntaxNode): Capt // an explicit annotation or a parameter annotation still wins. const constructedType = explicitType === null && parameterType === undefined - ? constructorCallTypeName(right) + ? constructorCallTypeName(right, receiverName) : undefined; const typeName = explicitType?.text ?? parameterType ?? constructedType; if (typeName !== undefined) { diff --git a/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts b/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts index a497636e0..60f4523e0 100644 --- a/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts +++ b/gitnexus/test/integration/resolvers/inferred-field-receiver-matrix.test.ts @@ -142,6 +142,18 @@ class AssignedField: def run(self, x): return self.q.inner().compute(x) + + +class ReassignedField: + def __init__(self): + self.r = Outer() + self.r = self.rebuild() + + def rebuild(self): + return Outer() + + def run(self, x): + return self.r.inner().compute(x) `; // ── Ruby ───────────────────────────────────────────────────────────────────── @@ -376,6 +388,19 @@ const CASES: readonly LanguageCase[] = [ targets: [`Method:${PY_FILE}:Outer.inner#0`], status: 'resolves', }, + // A method call is not a construction. `self.r = Outer()` followed by + // `self.r = self.rebuild()` must keep the FIRST binding: both would sit + // in the weakest tier, so accepting `self.rebuild()` as a constructor let + // the later one displace the real type and the field went untyped again — + // measured as zero CALLS edges before `constructorCallTypeName` learned to + // reject a callee rooted at the receiver. This row fails without that + // rejection, which is the only reason it exists. + { + name: 'reassigned-from-method-call', + callerId: `Method:${PY_FILE}:ReassignedField.run#1`, + targets: [`Method:${PY_FILE}:Outer.inner#0`], + status: 'resolves', + }, ], }, {