feat(csharp-scope): parity Unit 3a — cross-namespace using binding

Closes 2 parity failures (27 → 25). Extends the namespace-siblings
pass to resolve `using X;` directives against known namespace
buckets: for each `using` that targets a namespace declared
somewhere in the workspace, inject that namespace's classes into
the importer's module scope with origin='namespace'.

This is the scope-resolution analog of legacy's csproj-driven
directory↔namespace mapping. Without it, `new User()` in
`Services/UserService.cs` (namespace MyApp.Services) can't see the
User class in `Models/User.cs` (namespace MyApp.Models) even with
`using MyApp.Models;` — the scope-resolver layer doesn't have
csproj metadata to translate the dotted namespace path into a
directory lookup.

Legacy 175/175 green; 25 parity failures remain.
This commit is contained in:
Gergo Magyar 2026-04-21 19:06:03 +01:00
parent 0b0fefcbff
commit cb8a268031

View file

@ -126,6 +126,41 @@ export function populateCsharpNamespaceSiblings(
// pattern (see `propagateImportedReturnTypes` which does the same
// for module-scope typeBindings).
const finalized = indexes.bindings as Map<ScopeId, Map<string, BindingRef[]>>;
// 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
// `new User()` in `namespace App;` resolve to `User` declared in
// a sibling file with `namespace Models;` when the importer says
// `using Models;`. Legacy uses csproj directory↔namespace mapping;
// the scope-resolver layer uses the declared namespace directly.
for (const parsed of parsedFiles) {
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
if (moduleScope === undefined) continue;
for (const imp of parsed.parsedImports) {
if (imp.kind !== 'namespace') continue;
const targetNs = imp.targetRaw;
if (targetNs === null || targetNs === '') continue;
const bucket = buckets.get(targetNs);
if (bucket === undefined) continue;
for (const def of bucket.classDefs) {
if (def.filePath === parsed.filePath) continue;
const q = def.qualifiedName ?? '';
const simpleName = q.includes('.') ? q.slice(q.lastIndexOf('.') + 1) : q;
if (simpleName === '') continue;
let scopeBindings = finalized.get(moduleScope.id);
if (scopeBindings === undefined) {
scopeBindings = new Map<string, BindingRef[]>();
finalized.set(moduleScope.id, scopeBindings);
}
const existing = scopeBindings.get(simpleName) ?? [];
if (existing.some((b) => b.def.nodeId === def.nodeId)) continue;
existing.push({ def, origin: 'namespace' });
scopeBindings.set(simpleName, existing);
}
}
}
for (const [, bucket] of buckets) {
// De-dup by (nodeId, filePath) across multiple declarations (e.g.
// partial classes declaring the same name in two files — we take