diff --git a/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts new file mode 100644 index 000000000..c45e5c0d4 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/csharp/simple-hooks.ts @@ -0,0 +1,74 @@ +/** + * Trivial / no-op-ish hooks for the C# provider. Kept together because + * each is a few lines and they share a common theme: they make the + * provider's choice explicit rather than relying on "absence == default" + * so reviewers don't have to re-derive the analysis. + */ + +import type { + CaptureMatch, + ParsedImport, + Scope, + ScopeId, + ScopeTree, + TypeRef, +} from 'gitnexus-shared'; + +// ─── bindingScopeFor ────────────────────────────────────────────────────── + +/** C# has block scope, but the central extractor's "innermost enclosing + * scope" default already handles it correctly: class-body declarations + * attach to the innermost Class scope, method-body declarations attach + * to the innermost Function scope, and namespace-body declarations + * attach to the innermost Namespace scope (which the scope query emits + * for both `namespace X { }` and `namespace X;` forms). + * + * Returns `null` to delegate. */ +export function csharpBindingScopeFor( + _decl: CaptureMatch, + _innermost: Scope, + _tree: ScopeTree, +): ScopeId | null { + return null; +} + +// ─── importOwningScope ──────────────────────────────────────────────────── + +/** `using` inside `namespace X { }` binds to that namespace's scope (its + * types are visible only within that namespace's members). File-level + * `using` delegates to the module default. Class-body `using` is not + * legal C# — defensively handle it by attaching to the class if it + * ever slips through. + * + * `global using X;` (C# 10+) at the compilation_unit level is treated + * as a file-scoped using for Unit 2's purposes; cross-file propagation + * will be addressed if Unit 7's parity gate flags it. */ +export function csharpImportOwningScope( + _imp: ParsedImport, + innermost: Scope, + _tree: ScopeTree, +): ScopeId | null { + if (innermost.kind === 'Namespace' || innermost.kind === 'Class' || innermost.kind === 'Function') + return innermost.id; + return null; +} + +// ─── receiverBinding ────────────────────────────────────────────────────── + +/** Look up `this` or `base` in the function scope's type bindings. + * Returns `null` for free functions (no `this`), static methods (no + * `this` binding synthesized), and non-Function scopes. + * + * `this` and `base` are synthesized as type bindings on instance + * methods during capture emission (receiver-binding.ts, planned for a + * follow-up unit). Until that synthesis lands this hook returns `null` + * for every instance method, which matches the legacy fallback + * behavior — the central extractor then walks the enclosing class + * scope to recover the receiver type. + * + * Matches `pythonReceiverBinding`'s shape so the two provider wirings + * stay symmetric. */ +export function csharpReceiverBinding(functionScope: Scope): TypeRef | null { + if (functionScope.kind !== 'Function') return null; + return functionScope.typeBindings.get('this') ?? functionScope.typeBindings.get('base') ?? null; +} diff --git a/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts b/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts new file mode 100644 index 000000000..302bcb2ed --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/csharp/csharp-hooks.test.ts @@ -0,0 +1,98 @@ +/** + * Unit 3 coverage for C# simple hooks. + * + * Exercises the small-surface hooks that mirror Python's simple-hooks: + * `bindingScopeFor`, `importOwningScope`, `receiverBinding`. Each hook + * is tiny, but the tests pin the delegation semantics so refactors + * don't silently re-route bindings. + * + * `isSuperReceiver` lives on the ScopeResolver contract (Unit 6) rather + * than the LanguageProvider, so it isn't exercised here. + */ + +import { describe, it, expect } from 'vitest'; +import { + csharpBindingScopeFor, + csharpImportOwningScope, + csharpReceiverBinding, +} from '../../../../src/core/ingestion/languages/csharp/simple-hooks.js'; +import type { CaptureMatch, ParsedImport, Scope, ScopeTree, TypeRef } from 'gitnexus-shared'; + +function fakeScope( + kind: Scope['kind'], + id = 's1', + typeBindings = new Map(), +): Scope { + return { + id, + kind, + parentId: null, + childrenIds: [], + bindings: new Map(), + typeBindings, + } as unknown as Scope; +} + +const fakeTree = {} as ScopeTree; +const fakeCapture = {} as CaptureMatch; +const fakeImport: ParsedImport = { + kind: 'namespace', + localName: 'System', + importedName: 'System', + targetRaw: 'System', +}; + +describe('csharpBindingScopeFor', () => { + it('delegates to innermost for method-body declarations', () => { + const fn = fakeScope('Function'); + expect(csharpBindingScopeFor(fakeCapture, fn, fakeTree)).toBe(null); + }); + + it('delegates to innermost for namespace-body class declarations', () => { + const ns = fakeScope('Namespace'); + expect(csharpBindingScopeFor(fakeCapture, ns, fakeTree)).toBe(null); + }); +}); + +describe('csharpImportOwningScope', () => { + it('binds `using` inside a namespace to the namespace scope', () => { + const ns = fakeScope('Namespace', 'ns-1'); + expect(csharpImportOwningScope(fakeImport, ns, fakeTree)).toBe('ns-1'); + }); + + it('delegates file-level `using` to the module default', () => { + const mod = fakeScope('Module'); + expect(csharpImportOwningScope(fakeImport, mod, fakeTree)).toBe(null); + }); + + it('attaches `using` inside a function scope to that function', () => { + // Not legal C# at the source level, but defensive — Unit 7 parity + // gate flags any regression. + const fn = fakeScope('Function', 'fn-1'); + expect(csharpImportOwningScope(fakeImport, fn, fakeTree)).toBe('fn-1'); + }); +}); + +describe('csharpReceiverBinding', () => { + it('returns the `this` type binding for an instance method scope', () => { + const binding: TypeRef = { rawName: 'User', source: 'self' } as unknown as TypeRef; + const fn = fakeScope('Function', 'm-1', new Map([['this', binding]])); + expect(csharpReceiverBinding(fn)).toBe(binding); + }); + + it('falls back to `base` when `this` is absent', () => { + const binding: TypeRef = { rawName: 'Parent', source: 'self' } as unknown as TypeRef; + const fn = fakeScope('Function', 'm-1', new Map([['base', binding]])); + expect(csharpReceiverBinding(fn)).toBe(binding); + }); + + it('returns null for a static method (no synthesized `this`/`base`)', () => { + const fn = fakeScope('Function', 'm-1'); + expect(csharpReceiverBinding(fn)).toBe(null); + }); + + it('returns null for non-Function scopes', () => { + expect(csharpReceiverBinding(fakeScope('Class'))).toBe(null); + expect(csharpReceiverBinding(fakeScope('Module'))).toBe(null); + }); +});