feat(csharp-scope): parity Unit 6d — using-static member injection

Closes 2 parity failures (6 → 4). `using static X.Y.Z;` now injects
every public static method of class Z into the importer's module
scope, so `Record("hi")` (without `Logger.` qualifier) resolves to
`Logger.Record` as a free call.

`languages/csharp/namespace-siblings.ts`: regex-scan each file's
source for `using static X.Y.Z;` directives. For each, look up the
class Z in the `X.Y` namespace bucket, walk its owning file's
localDefs for method/function members with `ownerId === Z.nodeId`,
and inject them as `origin: 'import'` bindings in the importer's
module-scope finalized bindings map. `findCallableBindingInScope`
then picks them up via its imported-bindings check.

Closes: variadic `Record(params string[])` + heritage arity
narrowing `WriteAudit`.

Python parity 204/204 on both flag paths; legacy C# 175/175 green;
4 C# parity failures remain (interface-dispatch pass + type-based
overload disambiguation).
This commit is contained in:
Gergo Magyar 2026-04-21 21:30:23 +01:00
parent 5ee345271f
commit 8828fcacf6

View file

@ -27,6 +27,9 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
/** `namespace Foo.Bar { ... }` or `namespace Foo.Bar;` — capture the dotted name. */
const NAMESPACE_RE = /\bnamespace\s+([A-Za-z_][A-Za-z0-9_.]*)\s*[;{]/g;
/** `using static Foo.Bar.Baz;` — capture the dotted static-class path. */
const USING_STATIC_RE = /\busing\s+static\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/g;
/** Content of a file, keyed by filePath. Caller sources this from the
* pipeline's file list before the pass runs. */
export interface CsharpSiblingInputs {
@ -186,6 +189,62 @@ export function populateCsharpNamespaceSiblings(
}
}
// `using static X.Y.Z;` — expose every public static method of
// class Z as a free-callable binding in the importer's module
// scope, so `Record(...)` (without `Logger.` qualifier) resolves
// to `Logger.Record`.
for (const parsed of parsedFiles) {
const content = inputs.fileContents.get(parsed.filePath);
if (content === undefined) continue;
const moduleScope = parsed.scopes.find((s) => s.kind === 'Module');
if (moduleScope === undefined) continue;
USING_STATIC_RE.lastIndex = 0;
let sm: RegExpExecArray | null;
while ((sm = USING_STATIC_RE.exec(content)) !== null) {
const fullPath = sm[1]!;
const lastDot = fullPath.lastIndexOf('.');
if (lastDot === -1) continue;
const className = fullPath.slice(lastDot + 1);
const enclosingNs = fullPath.slice(0, lastDot);
// Find the target class in the named namespace bucket.
const bucket = buckets.get(enclosingNs);
if (bucket === undefined) continue;
const targetDef = bucket.classDefs.find((d) => {
const q = d.qualifiedName ?? '';
const simple = q.includes('.') ? q.slice(q.lastIndexOf('.') + 1) : q;
return simple === className;
});
if (targetDef === undefined) continue;
// Inject the class's member methods into the importer's module
// scope. `memberByOwner` wasn't built yet here, so we walk the
// file's localDefs to find members with `ownerId === targetDef.nodeId`.
const targetFile = parsedFiles.find((p) => p.filePath === targetDef.filePath);
if (targetFile === undefined) continue;
for (const memberDef of targetFile.localDefs) {
if ((memberDef as { ownerId?: string }).ownerId !== targetDef.nodeId) continue;
if (memberDef.type !== 'Method' && memberDef.type !== 'Function') continue;
const mq = memberDef.qualifiedName ?? '';
const simpleName = mq.includes('.') ? mq.slice(mq.lastIndexOf('.') + 1) : mq;
if (simpleName === '') continue;
// Add to `indexes.bindings[moduleScope]` so
// `findCallableBindingInScope` picks it up.
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 === memberDef.nodeId)) continue;
existing.push({ def: memberDef, origin: 'import' });
scopeBindings.set(simpleName, existing);
}
}
}
// 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