From 0e352ac669cfaf5f79cda12c6b1700838402ab91 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 21 Apr 2026 17:34:49 +0100 Subject: [PATCH] =?UTF-8?q?feat(csharp-scope):=20Unit=206=20=E2=80=94=20wi?= =?UTF-8?q?re=20csharpScopeResolver=20+=20register?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/core/ingestion/languages/csharp.ts | 25 +++++++ .../core/ingestion/languages/csharp/index.ts | 73 +++++++++++++++++++ .../languages/csharp/scope-resolver.ts | 69 ++++++++++++++++++ .../scope-resolution/pipeline/registry.ts | 6 +- 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 gitnexus/src/core/ingestion/languages/csharp/index.ts create mode 100644 gitnexus/src/core/ingestion/languages/csharp/scope-resolver.ts diff --git a/gitnexus/src/core/ingestion/languages/csharp.ts b/gitnexus/src/core/ingestion/languages/csharp.ts index a491f339a..2e46a1944 100644 --- a/gitnexus/src/core/ingestion/languages/csharp.ts +++ b/gitnexus/src/core/ingestion/languages/csharp.ts @@ -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 = 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, }); diff --git a/gitnexus/src/core/ingestion/languages/csharp/index.ts b/gitnexus/src/core/ingestion/languages/csharp/index.ts new file mode 100644 index 000000000..5c396002d --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/csharp/index.ts @@ -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` binds the + * bound name to `User` via the single-arg-generic stripper; + * nested generics (`Dictionary>`) 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'; diff --git a/gitnexus/src/core/ingestion/languages/csharp/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/csharp/scope-resolver.ts new file mode 100644 index 000000000..750c7cfd5 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/csharp/scope-resolver.ts @@ -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, + }; + 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 }; diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts index baf8e75b7..cad45fa22 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts @@ -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 = new Map< SupportedLanguages, ScopeResolver ->([[SupportedLanguages.Python, pythonScopeResolver]]); +>([ + [SupportedLanguages.Python, pythonScopeResolver], + [SupportedLanguages.CSharp, csharpScopeResolver], +]);