From a77153d86091735064a2ce6fa1715e851418a695 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 19:20:43 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20parity=20Unit=203c=20?= =?UTF-8?q?=E2=80=94=20static=20call=20+=20default-namespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 2 parity failures (22 → 21). - `receiver-bound-calls.ts`: add Case 5 for class-as-receiver. When `Animal.Classify()` has an identifier receiver that resolves to a Class binding (rather than a variable with a typeBinding), look up the member on the class's MRO chain. Covers C#-style static calls and any type-qualified member access. Python doesn't hit this because `ClassName.method()` is syntactically identical to a free call there. - `namespace-siblings.ts`: treat files with no `namespace X;` declaration as living in the default (empty-name) bucket, so types declared in no-namespace files share cross-file visibility. Required for fixtures without explicit namespaces (e.g. the method-enrichment fixture's Animal/App/Dog classes). Legacy 175/175 green; 21 parity failures remain. --- .../languages/csharp/namespace-siblings.ts | 5 +++- .../passes/receiver-bound-calls.ts | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts b/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts index 376c2fcf2..c99b68ae9 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts @@ -73,7 +73,10 @@ export function populateCsharpNamespaceSiblings( NAMESPACE_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = NAMESPACE_RE.exec(content)) !== null) names.push(m[1]!); - if (names.length === 0) continue; + // Files with no `namespace X;` declaration still share visibility + // — they all live in the default (global) namespace. Use the + // empty-string bucket key for this case. + if (names.length === 0) names.push(''); const namespaceScopes = parsed.scopes.filter((s) => s.kind === 'Namespace'); // With file-scoped namespaces (`namespace X;`), the Namespace diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts index fceaae8d2..688d770d3 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/receiver-bound-calls.ts @@ -287,6 +287,36 @@ export function emitReceiverBoundCalls( emitted++; handledSites.add(siteKey); } + continue; + } + } + } + + // ── Case 5: class-as-receiver (static call / type member) ──── + // `Animal.Classify()` — receiver name resolves to a Class + // binding, not a typeBinding. Look up the member on the class's + // MRO chain. Python syntactically collapses this with free + // calls; C# (and other statically-typed languages) distinguish + // via the member_access_expression shape. + if (typeRef === undefined) { + const classDef = findClassBindingInScope(site.inScope, receiverName, scopes); + if (classDef !== undefined) { + const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)]; + let memberDef: SymbolDefinition | undefined; + for (const ownerId of chain) { + memberDef = findOwnedMember(ownerId, memberName, index); + if (memberDef !== undefined) break; + } + if (memberDef !== undefined) { + const reason = + site.kind === 'write' || site.kind === 'read' + ? site.kind + : 'scope-resolution: class-receiver'; + const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, reason, seen); + if (ok) { + emitted++; + handledSites.add(siteKey); + } } } }