From c27cc0454d8e9b6ab31ef8d23f4bcf2cf1960d6e Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 20:25:14 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20parity=20Unit=205b=20?= =?UTF-8?q?=E2=80=94=20return-type=20module=20hoist=20+=20chain=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 1 parity failure (20 → 19) and lays groundwork for Unit 6. Based on investigation-agent findings, addresses cluster of 7 cross-file + chain tests whose return-type bindings were stuck at Class scope and invisible to the chain-follow and propagation passes. Changes: - `languages/csharp/simple-hooks.ts::csharpBindingScopeFor`: when the declaration is a `@type-binding.return`, hoist the binding all the way to the Module scope. The central extractor's auto-hoist only promotes one level (Function → Class); for C# methods the parent is always a Class, so without this override the return binding never reaches Module where chain-follow and cross-file `propagateImportedReturnTypes` read from. - `scope-resolution/passes/compound-receiver.ts`: when the class-scope typeBindings lookup at `objClass.typeBindings.get( methodName)` misses, walk up from the class scope through the parent chain (→ Module) for a return-type binding. Preserves the existing class-scope fast-path while restoring owner-chain lookup for languages that hoist to Module. Python parity suite stays 204/204 green on both flag paths; legacy C# 175/175 green; 19 C# parity failures remain. --- .../languages/csharp/simple-hooks.ts | 27 ++++++++++++++++--- .../passes/compound-receiver.ts | 20 ++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts index c45e5c0d4..e8e7e6987 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts @@ -23,12 +23,31 @@ import type { * attach to the innermost Namespace scope (which the scope query emits * for both `namespace X { }` and `namespace X;` forms). * - * Returns `null` to delegate. */ + * Exception: **method return-type bindings** (`@type-binding.return`) + * must hoist all the way to the Module scope. The default auto-hoist + * in the central extractor only promotes one level (Function → its + * parent). For C# methods the parent is always a Class, so without + * this override the return binding gets stuck at the Class scope, + * where it's invisible to: + * - chain-follow's parent-chain walk in `followChainPostFinalize` + * (tests: `var u = GetUser(); u.Save()` single-file); + * - cross-file `propagateImportedReturnTypes`, which reads only + * `sourceModule.typeBindings`. + * Walking to Module restores both paths. */ export function csharpBindingScopeFor( - _decl: CaptureMatch, - _innermost: Scope, - _tree: ScopeTree, + decl: CaptureMatch, + innermost: Scope, + tree: ScopeTree, ): ScopeId | null { + if (decl['@type-binding.return'] !== undefined) { + let cur: Scope | undefined = innermost; + while (cur !== undefined && cur.kind !== 'Module') { + const parentId: ScopeId | null = cur.parent ?? null; + if (parentId === null) break; + cur = tree.getScope(parentId); + } + if (cur !== undefined && cur.kind === 'Module') return cur.id; + } return null; } diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts index 7dc0fe65a..1798a937b 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/compound-receiver.ts @@ -107,6 +107,26 @@ export function resolveCompoundReceiverClass( retType = candidate; break; } + // Fallback: walk up from the class scope looking for a return- + // type binding on an ancestor (Module) scope. Some languages + // (C#) hoist method return-type bindings to Module scope so + // `propagateImportedReturnTypes` can mirror them cross-file; + // this loop restores the owner-chain lookup path for those + // languages without forcing a class-scope copy. + if (cs !== undefined) { + let curId: ScopeId | null = cs.parent; + while (curId !== null) { + const curScope = scopes.scopeTree.getScope(curId); + if (curScope === undefined) break; + const cand = curScope.typeBindings.get(methodName); + if (cand !== undefined) { + retType = cand; + break; + } + curId = curScope.parent; + } + if (retType !== undefined) break; + } } if (retType === undefined && fieldFallback) {