feat(csharp-scope): Unit 6 — wire csharpScopeResolver + register

Creates the public barrel (index.ts) and ScopeResolver (scope-resolver.ts)
and plumbs them into the provider + registry:

- `languages/csharp/index.ts` — re-exports the hook entry points and
  documents the 8 known limitations of the registry-primary path
  (csproj-driven namespace resolution, multi-file namespace expansion,
  type-based overload resolution, nested generics, dynamic, preprocessor
  branches, cross-file global using, expression-bodied members).
- `languages/csharp/scope-resolver.ts` — ScopeResolver shape mirroring
  Python's. `isSuperReceiver` matches the literal `base` keyword.
  `fieldFallbackOnMethodLookup: false` since C# is statically typed
  — the type-binding layer already produces precise owner types;
  `propagatesReturnTypesAcrossImports: true` since signatures are
  authoritative.
- `languages/csharp.ts` — adds the 9 hook entry points to the provider
  (emitScopeCaptures, interpretImport, interpretTypeBinding, four
  simple hooks, mergeBindings, arityCompatibility, resolveImportTarget).
- `scope-resolution/pipeline/registry.ts` — registers csharpScopeResolver
  alongside the Python entry.

MIGRATED_LANGUAGES stays at {Python} — the resolver sits idle until
Unit 7's parity gate confirms ≥99% fixture parity. 368/368
scope-resolution unit tests pass; tsc clean.
This commit is contained in:
Gergo Magyar 2026-04-21 17:34:49 +01:00
parent ac8b55ba2c
commit 0e352ac669
4 changed files with 172 additions and 1 deletions

View file

@ -25,6 +25,17 @@ import { csharpMethodConfig } from '../method-extractors/configs/csharp.js';
import { createVariableExtractor } from '../variable-extractors/generic.js';
import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
import {
emitCsharpScopeCaptures,
interpretCsharpImport,
interpretCsharpTypeBinding,
csharpBindingScopeFor,
csharpImportOwningScope,
csharpMergeBindings,
csharpReceiverBinding,
csharpArityCompatibility,
resolveCsharpImportTarget,
} from './csharp/index.js';
const BUILT_INS: ReadonlySet<string> = new Set([
'Console',
@ -138,4 +149,18 @@ export const csharpProvider = defineLanguage({
classExtractor: createClassExtractor(csharpClassConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.CSharp),
builtInNames: BUILT_INS,
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
// C# is the second migration after Python. See ./csharp/index.ts for
// the full per-hook rationale and the canonical capture vocabulary
// in ./csharp/query.ts (CSHARP_SCOPE_QUERY constant).
emitScopeCaptures: emitCsharpScopeCaptures,
interpretImport: interpretCsharpImport,
interpretTypeBinding: interpretCsharpTypeBinding,
bindingScopeFor: csharpBindingScopeFor,
importOwningScope: csharpImportOwningScope,
mergeBindings: csharpMergeBindings,
receiverBinding: csharpReceiverBinding,
arityCompatibility: csharpArityCompatibility,
resolveImportTarget: resolveCsharpImportTarget,
});

View file

@ -0,0 +1,73 @@
/**
* C# scope-resolution hooks (RFC #909 Ring 3, RFC §5).
*
* Public API barrel. Consumers should import from this file rather than
* the individual modules.
*
* Module layout (each file is a single concern):
*
* - `query.ts` tree-sitter query + lazy parser/query singletons
* - `captures.ts` `emitCsharpScopeCaptures` orchestrator
* - `import-decomposer.ts` each `using` ParsedImport-shaped captures
* - `interpret.ts` capture-match `ParsedImport` / `ParsedTypeBinding`
* - `simple-hooks.ts` small/no-op hooks made explicit
* - `merge-bindings.ts` C# `using` precedence
* - `arity.ts` C# arity compatibility (`params`, default values)
* - `arity-metadata.ts` synthesize arity metadata from declarations
* - `import-target.ts` `(ParsedImport, WorkspaceIndex) → file path` adapter
* - `scope-resolver.ts` `ScopeResolver` registered in `SCOPE_RESOLVERS`
* - `cache-stats.ts` PROF_SCOPE_RESOLUTION cache hit/miss counters
*
* ## Known limitations
*
* The C# registry-primary path intentionally does NOT resolve the
* following. Each is a conscious trade-off at migration time.
*
* 1. **csproj-driven namespace resolution** the legacy path
* consults `csharpConfigs` (the parsed .csproj workspace) to map
* `using X.Y;` back to the exact files declaring `namespace X.Y`.
* The scope-resolver contract passes only `allFilePaths`, so we
* fall back to suffix matching on `.cs` files. Unit 7's parity
* gate flags any divergence.
* 2. **Multi-file namespace expansion** a single `using X.Y;` in
* the legacy path can emit multiple IMPORTS edges (every file
* declaring that namespace). The scope-resolver contract returns
* a single target, so we pick the first match; partial-class
* aggregation runs at graph-bridge time.
* 3. **Overload resolution by parameter type** arity narrowing is
* wired (`arity.ts` + `arity-metadata.ts`), but type-based
* disambiguation (`F(int)` vs `F(string)` at a call with a typed
* argument) is left to the registry's type-binding layer.
* 4. **Generic type parameter resolution** `List<User>` binds the
* bound name to `User` via the single-arg-generic stripper;
* nested generics (`Dictionary<K, List<V>>`) fall through the
* receiver-type heuristic.
* 5. **`dynamic` typed expressions** runtime dispatch through
* `dynamic` is not followed.
* 6. **Preprocessor-conditional code** `#if DEBUG` blocks parse
* as usual; branch selection is ignored, so both arms contribute
* bindings.
* 7. **Global using propagation across files** treated as a
* file-scoped using for the declaring file. Unit 7 parity gate
* will flag cases where this matters.
* 8. **Expression-bodied `=>` members** handled by the method
* extractor, but receiver synthesis for `=> this.Field` shortcuts
* follows the same path as block-bodied methods.
*
* Shadow-harness corpus parity is the authoritative signal for which
* of these matter in practice. The CI parity gate blocks any PR that
* regresses either the legacy or registry-primary run of
* `test/integration/resolvers/csharp.test.ts`.
*/
export { emitCsharpScopeCaptures } from './captures.js';
export { getCsharpCaptureCacheStats, resetCsharpCaptureCacheStats } from './cache-stats.js';
export { interpretCsharpImport, interpretCsharpTypeBinding } from './interpret.js';
export { csharpMergeBindings } from './merge-bindings.js';
export { csharpArityCompatibility } from './arity.js';
export { resolveCsharpImportTarget, type CsharpResolveContext } from './import-target.js';
export {
csharpBindingScopeFor,
csharpImportOwningScope,
csharpReceiverBinding,
} from './simple-hooks.js';

View file

@ -0,0 +1,69 @@
/**
* C# `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
*
* Second migration after Python see `pythonScopeResolver` for the
* canonical shape.
*/
import type { ParsedFile, Scope, WorkspaceIndex } from 'gitnexus-shared';
import { SupportedLanguages } from 'gitnexus-shared';
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
import { csharpProvider } from '../csharp.js';
import {
csharpArityCompatibility,
csharpMergeBindings,
resolveCsharpImportTarget,
type CsharpResolveContext,
} from './index.js';
const csharpScopeResolver: ScopeResolver = {
language: SupportedLanguages.CSharp,
languageProvider: csharpProvider,
importEdgeReason: 'csharp-scope: using',
resolveImportTarget: (targetRaw, fromFile, allFilePaths) => {
// CsharpResolveContext expects a mutable Set; the orchestrator
// hands us a ReadonlySet — safe to widen since the resolver only
// reads.
const ws: CsharpResolveContext = {
fromFile,
allFilePaths: allFilePaths as Set<string>,
};
return resolveCsharpImportTarget(
{ kind: 'namespace', localName: '_', importedName: '_', targetRaw },
ws as unknown as WorkspaceIndex,
);
},
// C# shadowing: local > using > using static.
mergeBindings: (existing, incoming, scopeId) => {
const fakeScope = { id: scopeId } as unknown as Scope;
return [...csharpMergeBindings(fakeScope, [...existing, ...incoming])];
},
// Adapter: csharpArityCompatibility uses (def, callsite); the
// contract is (callsite, def).
arityCompatibility: (callsite, def) => csharpArityCompatibility(def, callsite),
buildMro: (graph, parsedFiles, nodeLookup) =>
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
// C# uses `base` for super-class dispatch, not `super`. Match as a
// plain identifier (no `()` call like Python's `super(...)`) — `base`
// is a keyword-like receiver, not a callable.
isSuperReceiver: (text) => text.trim() === 'base',
// C# is statically typed — type information is reliable. Field-
// fallback heuristic stays off (the type-binding layer already
// produces precise owner types); return-type propagation on is fine
// since signatures are authoritative.
fieldFallbackOnMethodLookup: false,
propagatesReturnTypesAcrossImports: true,
};
export { csharpScopeResolver };

View file

@ -12,6 +12,7 @@
import { SupportedLanguages } from 'gitnexus-shared';
import type { ScopeResolver } from '../contract/scope-resolver.js';
import { pythonScopeResolver } from '../../languages/python/scope-resolver.js';
import { csharpScopeResolver } from '../../languages/csharp/scope-resolver.js';
/** Map of `SupportedLanguages` `ScopeResolver`. The phase iterates
* this map intersected with `MIGRATED_LANGUAGES` (the per-language
@ -20,4 +21,7 @@ import { pythonScopeResolver } from '../../languages/python/scope-resolver.js';
export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = new Map<
SupportedLanguages,
ScopeResolver
>([[SupportedLanguages.Python, pythonScopeResolver]]);
>([
[SupportedLanguages.Python, pythonScopeResolver],
[SupportedLanguages.CSharp, csharpScopeResolver],
]);