From 3c7905808817f9aaab703b1dd7aeb1586dafebe9 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 17:22:40 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20Unit=204=20=E2=80=94=20me?= =?UTF-8?q?rgeBindings=20(using=20precedence)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-tier shadowing, same shape as Python's LEGB merge: 0: local — class members, locals, parameters 1: using — namespace / named / reexport (equal tier; compiler requires explicit qualifier if two using collide) 2: wildcard — `using static X.Y;` static-member imports Within the surviving tier, de-dup by DefId (last-write-wins) so a re-declared `using` cleanly replaces its earlier binding. Explicit interface implementations bind under their qualified name in the extractor layer, so they don't collide with plain simple names here. 7 new tests pin precedence + dedup semantics. 54/54 C# scope-resolution unit tests pass. --- .../languages/csharp/merge-bindings.ts | 62 +++++++++++++++++++ .../csharp/csharp-hooks.test.ts | 61 +++++++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 gitnexus/src/core/ingestion/languages/csharp/merge-bindings.ts diff --git a/gitnexus/src/core/ingestion/languages/csharp/merge-bindings.ts b/gitnexus/src/core/ingestion/languages/csharp/merge-bindings.ts new file mode 100644 index 000000000..671970424 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/csharp/merge-bindings.ts @@ -0,0 +1,62 @@ +/** + * C# shadowing precedence for the `mergeBindings` hook. + * + * Tier ranking (lower wins in shadowing): + * + * - 0: `local` — a class member, method, local variable, or parameter + * declared in this scope. + * - 1: `import` / `namespace` / `reexport` — `using System;`, + * `using System.Collections.Generic;`, `using Alias = Foo;`. + * All three using flavors that introduce a name at this scope + * tier together; the compiler resolves ambiguity by requiring + * an explicit qualifier when two `using`s collide, but for + * receiver-typed dispatch we treat them as equivalent tiers. + * - 2: `wildcard` — `using static System.Math;` brings static + * members in; any local or `using` with the same simple name + * shadows. + * + * Explicit interface implementations (`void IFoo.Bar() { }`) bind under + * the qualified name in the extractor layer, so they never collide with + * a plain `Bar` at this layer. + * + * Within a surviving tier we de-dup by `DefId`, last-write-wins so a + * `using` re-declared further down the file cleanly replaces the + * earlier binding. + */ + +import type { BindingRef, Scope } from 'gitnexus-shared'; + +const TIER_LOCAL = 0; +const TIER_IMPORT = 1; +const TIER_WILDCARD = 2; +const TIER_UNKNOWN = 3; + +function tierOf(b: BindingRef): number { + switch (b.origin) { + case 'local': + return TIER_LOCAL; + case 'reexport': + case 'import': + case 'namespace': + return TIER_IMPORT; + case 'wildcard': + return TIER_WILDCARD; + default: + return TIER_UNKNOWN; + } +} + +export function csharpMergeBindings( + _scope: Scope, + bindings: readonly BindingRef[], +): readonly BindingRef[] { + if (bindings.length === 0) return bindings; + + let bestTier = Number.POSITIVE_INFINITY; + for (const b of bindings) bestTier = Math.min(bestTier, tierOf(b)); + const survivors = bindings.filter((b) => tierOf(b) === bestTier); + + const seen = new Map(); + for (const b of survivors) seen.set(b.def.nodeId, b); + return [...seen.values()]; +} diff --git a/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts b/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts index 302bcb2ed..eeb81ba0a 100644 --- a/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts +++ b/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts @@ -16,7 +16,16 @@ import { csharpImportOwningScope, csharpReceiverBinding, } from '../../../../src/core/ingestion/languages/csharp/simple-hooks.js'; -import type { CaptureMatch, ParsedImport, Scope, ScopeTree, TypeRef } from 'gitnexus-shared'; +import { csharpMergeBindings } from '../../../../src/core/ingestion/languages/csharp/merge-bindings.js'; +import type { + BindingRef, + CaptureMatch, + ParsedImport, + Scope, + ScopeTree, + SymbolDefinition, + TypeRef, +} from 'gitnexus-shared'; function fakeScope( kind: Scope['kind'], @@ -73,6 +82,56 @@ describe('csharpImportOwningScope', () => { }); }); +describe('csharpMergeBindings — shadowing precedence', () => { + const scope = fakeScope('Function'); + const def = (nodeId: string): SymbolDefinition => + ({ nodeId, filePath: 't.cs', type: 'Function' }) as SymbolDefinition; + const binding = (origin: BindingRef['origin'], nodeId: string): BindingRef => + ({ def: def(nodeId), origin }) as BindingRef; + + it('local declaration shadows `using` import', () => { + const local = binding('local', 'L'); + const imp = binding('import', 'I'); + expect(csharpMergeBindings(scope, [imp, local])).toEqual([local]); + }); + + it('explicit `using` shadows `using static` (wildcard)', () => { + const imp = binding('import', 'I'); + const wc = binding('wildcard', 'W'); + expect(csharpMergeBindings(scope, [wc, imp])).toEqual([imp]); + }); + + it('local shadows both `using` and `using static`', () => { + const local = binding('local', 'L'); + const imp = binding('import', 'I'); + const wc = binding('wildcard', 'W'); + expect(csharpMergeBindings(scope, [wc, imp, local])).toEqual([local]); + }); + + it('keeps overload siblings at the same tier', () => { + const a = binding('local', 'A'); + const b = binding('local', 'B'); + expect(csharpMergeBindings(scope, [a, b])).toEqual([a, b]); + }); + + it('dedupes same-nodeId bindings', () => { + const a = binding('local', 'A'); + const a2 = binding('local', 'A'); + expect(csharpMergeBindings(scope, [a, a2])).toHaveLength(1); + }); + + it('namespace and reexport tie with explicit import (same tier)', () => { + const ns = binding('namespace', 'N'); + const re = binding('reexport', 'R'); + const imp = binding('import', 'I'); + expect(csharpMergeBindings(scope, [ns, re, imp])).toHaveLength(3); + }); + + it('empty in → empty out', () => { + expect(csharpMergeBindings(scope, [])).toEqual([]); + }); +}); + describe('csharpReceiverBinding', () => { it('returns the `this` type binding for an instance method scope', () => { const binding: TypeRef = { rawName: 'User', source: 'self' } as unknown as TypeRef;