feat(csharp-scope): Unit 3 — simple hooks (binding/import/receiver)

Adds simple-hooks.ts mirroring Python's pattern:

- `csharpBindingScopeFor` — delegates to innermost (block scope is
  already captured by @scope.block in the query).
- `csharpImportOwningScope` — binds `using` inside a namespace to that
  namespace's scope so imports don't leak into sibling namespaces.
  File-level using delegates to module. Function-body using (not legal
  C# but possible from malformed input) attaches to the function.
- `csharpReceiverBinding` — looks up `this` / `base` in the function
  scope's type bindings; returns null for statics, free functions, and
  non-Function scopes. `this` / `base` synthesis itself is deferred to
  a follow-up (matches Python's receiver-binding.ts pattern).

9 new tests pin delegation semantics. 47/47 C# scope-resolution unit
tests pass; tsc clean.
This commit is contained in:
Gergo Magyar 2026-04-21 17:10:41 +01:00
parent a3a2c65cc7
commit dd7e553d07
2 changed files with 172 additions and 0 deletions

View file

@ -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;
}

View file

@ -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<string, TypeRef>(),
): 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);
});
});