From ea6ee46ce2e6dfccf8837ce5bee7f5d397b54d15 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 19:49:56 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20parity=20Unit=205a=20?= =?UTF-8?q?=E2=80=94=20IMPORTS=20edge=20+=20static-using=20mapping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 1 parity failure (21 → 20). Fixes cross-file IMPORTS edge emission for C#: - `languages/csharp/interpret.ts`: map `using static X.Y;` to `kind: 'namespace'` rather than `'wildcard'`. The File→File IMPORTS edge needs a non-wildcard kind to survive finalize's Phase 4 (wildcard-expanded edges drop to empty when the provider doesn't implement `expandsWildcardTo`). Unqualified static-member access is a deferred limitation — covered by the namespace-siblings cross-namespace pass for type lookups, and documented under the module's Known Limitations. - `languages/csharp/import-target.ts`: progressive prefix stripping. `using CrossFile.Models;` in a repo laid out `Models/User.cs` (no `CrossFile/` directory) works because the legacy resolver consults csproj; the scope-resolver tries each suffix of the dotted path against `.cs` files. Also handles `using static NS.Type;` by stripping leading segments until a direct match lands. - `test/unit/scope-resolution/csharp/csharp-imports.test.ts`: update the `using static` test to the new namespace-kind shape. 376/376 scope-resolution unit tests pass; legacy 175/175 green; 20 parity failures remain. --- .../languages/csharp/import-target.ts | 41 ++++++++++++++++++- .../ingestion/languages/csharp/interpret.ts | 17 ++++++-- .../csharp/csharp-imports.test.ts | 21 +++++++--- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/csharp/import-target.ts b/gitnexus/src/core/ingestion/languages/csharp/import-target.ts index aab98759b..5079b23e2 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/import-target.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/import-target.ts @@ -83,5 +83,44 @@ export function resolveCsharpImportTarget( if (exactFile !== null) return exactFile; if (suffixFile !== null) return suffixFile; - return directoryChild; + if (directoryChild !== null) return directoryChild; + + // Progressive prefix stripping — mirrors csproj's root-namespace + // mapping without the csproj. `using CrossFile.Models;` in a repo + // laid out `Models/User.cs` (no `CrossFile/` prefix) works because + // the legacy resolver consults csproj; the scope-resolver layer + // doesn't have csproj, so we try each suffix of the namespace path + // against `.cs` files and directories. + // + // Also handles `using static CrossFile.Models.UserFactory;` — + // strip the leading segment, try `Models/UserFactory.cs`; strip + // two, try `UserFactory.cs`. + const segments = pathLike.split('/').filter(Boolean); + for (let skip = 1; skip < segments.length; skip++) { + const tail = segments.slice(skip).join('/'); + if (tail === '') continue; + const tailFile = `${tail}.cs`; + const tailSuffix = `/${tailFile}`; + const tailDir = `${tail}/`; + const tailSuffixDir = `/${tailDir}`; + let tailDirectChild: string | null = null; + for (const raw of ctx.allFilePaths) { + const f = raw.replace(/\\/g, '/'); + if (!f.endsWith('.cs')) continue; + if (f === tailFile) return raw; + if (f.endsWith(tailSuffix)) return raw; + if (tailDirectChild === null) { + const atRoot = f.startsWith(tailDir); + const atNested = f.includes(tailSuffixDir); + if (atRoot || atNested) { + const idx = atRoot ? 0 : f.indexOf(tailSuffixDir) + 1; + const after = f.slice(idx + tailDir.length); + if (after.length > 0 && !after.includes('/')) tailDirectChild = raw; + } + } + } + if (tailDirectChild !== null) return tailDirectChild; + } + + return null; } diff --git a/gitnexus/src/core/ingestion/languages/csharp/interpret.ts b/gitnexus/src/core/ingestion/languages/csharp/interpret.ts index 34f4702c5..7024ed22b 100644 --- a/gitnexus/src/core/ingestion/languages/csharp/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/csharp/interpret.ts @@ -51,10 +51,19 @@ export function interpretCsharpImport(captures: CaptureMatch): ParsedImport | nu } case 'static': { // `using static System.Math;` — brings static members of Math into - // unqualified scope. Semantically closest to a wildcard: any name - // can resolve to a static member of the target type. Merge-bindings - // (Unit 4) ranks wildcards lowest so locals still shadow. - return { kind: 'wildcard', targetRaw: sourceCap.text }; + // unqualified scope. Semantically closest to a wildcard, but we + // map to `namespace` here so finalize emits the File→File IMPORTS + // edge without requiring `expandsWildcardTo` (which would list + // every exported member). Static-member unqualified-access is a + // deferred limitation; the usual cross-file lookup via + // namespace-siblings covers `Target.Member` calls. + const lastSegment = sourceCap.text.split('.').pop() ?? sourceCap.text; + return { + kind: 'namespace', + localName: lastSegment, + importedName: sourceCap.text, + targetRaw: sourceCap.text, + }; } default: return null; diff --git a/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts b/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts index 2eeccc384..8c69a0b9d 100644 --- a/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts @@ -55,12 +55,21 @@ describe('interpretCsharpImport — using flavors', () => { }); }); - it('interprets `using static X.Y;` as a wildcard import', () => { - // `using static` brings static members of the target type into - // unqualified scope. Merge-bindings (Unit 4) ranks wildcards - // lowest so locals still shadow them. + it('interprets `using static X.Y;` as a namespace import targeting the type', () => { + // `using static` brings static members into unqualified scope. + // Initially this was mapped to `kind: 'wildcard'` but that + // requires `expandsWildcardTo` to materialize any IMPORTS edge; + // we map to `namespace` so the File→File edge still emits and + // the namespace-siblings pass (which walks known namespaces) + // picks up the target file's classes. Unqualified static-member + // access is a deferred limitation — see csharp/index.ts. const [imp] = importsFor('using static System.Math;\nclass A {}'); - expect(imp).toEqual({ kind: 'wildcard', targetRaw: 'System.Math' }); + expect(imp).toEqual({ + kind: 'namespace', + localName: 'Math', + importedName: 'System.Math', + targetRaw: 'System.Math', + }); }); it('strips `global::` qualifier — `using global::X.Y;` → namespace X.Y', () => { @@ -91,7 +100,7 @@ describe('interpretCsharpImport — using flavors', () => { `; const imps = importsFor(src); expect(imps).toHaveLength(4); - expect(imps.map((p) => p.kind)).toEqual(['namespace', 'namespace', 'alias', 'wildcard']); + expect(imps.map((p) => p.kind)).toEqual(['namespace', 'namespace', 'alias', 'namespace']); }); });