From 8828fcacf609402b3ec9e87437358de368d476f0 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 21:30:23 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20parity=20Unit=206d=20?= =?UTF-8?q?=E2=80=94=20using-static=20member=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../languages/csharp/namespace-siblings.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts b/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts index 34cf4f7c3..4a15eecfa 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/namespace-siblings.ts @@ -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(); + 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