feat(csharp-scope): parity Unit 5d — cross-file typeBinding mirror

Closes 3 parity failures (15 → 12).

`languages/csharp/namespace-siblings.ts`: extend the pass to mirror
method return-type bindings from accessible sibling files' Module
scopes into the importer's Module scope. "Accessible" =
same-namespace siblings + `using namespace X;` targets.

Without this mirror, `var u = svc.GetUser()` in App.cs couldn't
chain-follow to User even after Unit 5b's module-scope hoist:
`GetUser → User` lived on User.cs's Module scope, which isn't on
the ancestor chain of App.cs's function scope, and
`propagateImportedReturnTypes` only mirrors across explicit
ImportEdge targets (not same-namespace implicit visibility).

Closes: var-invocation return type, async/await u.Save (ambient
namespace), cross-file return-type propagation (via u.Save /
u.GetName in Program.cs).

Python parity 204/204 on both flag paths; legacy C# 175/175;
12 C# parity failures remain.
This commit is contained in:
Gergo Magyar 2026-04-21 20:39:05 +01:00
parent ada782d350
commit 13086508e5

View file

@ -130,6 +130,50 @@ export function populateCsharpNamespaceSiblings(
// for module-scope typeBindings).
const finalized = indexes.bindings as Map<ScopeId, Map<string, BindingRef[]>>;
// Cross-namespace type-binding propagation: for each file, mirror
// method return-type bindings from same-namespace sibling files and
// from files in namespaces the importer `using`s, into the
// importer's Module scope typeBindings. This enables
// chain-follow from `var u = svc.GetUser()` → `GetUser → User`
// even across files — without it the chain stalls at `GetUser`
// because the return binding lives in the defining file's Module
// scope, which isn't an ancestor of the importer's scope chain.
for (const parsed of parsedFiles) {
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
if (moduleScope === undefined) continue;
const moduleTypeBindings = moduleScope.typeBindings as Map<
string,
import('gitnexus-shared').TypeRef
>;
// Accessible namespaces = this file's own namespaces + every
// `using namespace X;` target.
const accessibleNamespaces = new Set<string>();
const fileContent = inputs.fileContents.get(parsed.filePath) ?? '';
NAMESPACE_RE.lastIndex = 0;
let nm: RegExpExecArray | null;
while ((nm = NAMESPACE_RE.exec(fileContent)) !== null) accessibleNamespaces.add(nm[1]!);
if (accessibleNamespaces.size === 0) accessibleNamespaces.add('');
for (const imp of parsed.parsedImports) {
if (imp.kind === 'namespace' && imp.targetRaw !== null) {
accessibleNamespaces.add(imp.targetRaw);
}
}
for (const nsName of accessibleNamespaces) {
const bucket = buckets.get(nsName);
if (bucket === undefined) continue;
for (const scopeInfo of bucket.scopes) {
if (scopeInfo.filePath === parsed.filePath) continue;
if (scopeInfo.scope.kind !== 'Module') continue;
for (const [boundName, typeRef] of scopeInfo.scope.typeBindings) {
if (moduleTypeBindings.has(boundName)) continue;
moduleTypeBindings.set(boundName, typeRef);
}
}
}
}
// Cross-namespace imports: for each file's `using X;` directive,
// if `X` matches a known namespace bucket, inject that bucket's
// classes into the importer's module scope. This is what makes