refactor(csharp-scope): remove as-unknown-as double casts in scope-resolver

Tighten three type boundaries that were previously papered over with
`as unknown as` casts:

  * `CsharpResolveContext.allFilePaths`: `Set<string>` → `ReadonlySet<string>`.
    The orchestrator only hands out a read-only view; drop the widening
    cast at the resolver-adapter site.
  * `resolveCsharpImportTarget`: call passes the narrow context directly.
    `WorkspaceIndex` is `unknown` in the shared contract, so the
    `as unknown as WorkspaceIndex` cast was gratuitous — structural
    assignability covers it.
  * `csharpMergeBindings`: drop unused `_scope: Scope` parameter. The
    implementation never read it; the cast chain in `scope-resolver.ts`
    existed only to satisfy an unused slot. LanguageProvider.mergeBindings
    now wraps with a tiny arrow adapter; ScopeResolver.mergeBindings
    passes through directly.

No runtime behavior change. `grep 'as unknown as' csharp/scope-resolver.ts`
returns zero matches.

Verified:
  - npx tsc --noEmit           clean
  - C# unit + integration      462/462 passing (incl. Python integration)
This commit is contained in:
Gergo Magyar 2026-04-22 08:45:38 +01:00
parent 0f18456539
commit 952350801c
5 changed files with 24 additions and 29 deletions

View file

@ -159,7 +159,7 @@ export const csharpProvider = defineLanguage({
interpretTypeBinding: interpretCsharpTypeBinding,
bindingScopeFor: csharpBindingScopeFor,
importOwningScope: csharpImportOwningScope,
mergeBindings: csharpMergeBindings,
mergeBindings: (_scope, bindings) => csharpMergeBindings(bindings),
receiverBinding: csharpReceiverBinding,
arityCompatibility: csharpArityCompatibility,
resolveImportTarget: resolveCsharpImportTarget,

View file

@ -22,13 +22,17 @@ import type { ParsedImport, WorkspaceIndex } from 'gitnexus-shared';
export interface CsharpResolveContext {
readonly fromFile: string;
readonly allFilePaths: Set<string>;
readonly allFilePaths: ReadonlySet<string>;
}
export function resolveCsharpImportTarget(
parsedImport: ParsedImport,
workspaceIndex: WorkspaceIndex,
): string | null {
// WorkspaceIndex is `unknown` in the shared contract (Ring 1
// placeholder). The scope-resolution orchestrator hands us a
// CsharpResolveContext-shaped object; narrow structurally rather
// than via a cast chain so unexpected shapes return null cleanly.
const ctx = workspaceIndex as CsharpResolveContext | undefined;
if (
ctx === undefined ||

View file

@ -24,7 +24,7 @@
* earlier binding.
*/
import type { BindingRef, Scope } from 'gitnexus-shared';
import type { BindingRef } from 'gitnexus-shared';
const TIER_LOCAL = 0;
const TIER_IMPORT = 1;
@ -46,10 +46,7 @@ function tierOf(b: BindingRef): number {
}
}
export function csharpMergeBindings(
_scope: Scope,
bindings: readonly BindingRef[],
): readonly BindingRef[] {
export function csharpMergeBindings(bindings: readonly BindingRef[]): readonly BindingRef[] {
if (bindings.length === 0) return bindings;
let bestTier = Number.POSITIVE_INFINITY;

View file

@ -6,7 +6,7 @@
* canonical shape.
*/
import type { ParsedFile, Scope, WorkspaceIndex } from 'gitnexus-shared';
import type { ParsedFile } 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';
@ -27,24 +27,19 @@ const csharpScopeResolver: ScopeResolver = {
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>,
};
const ws: CsharpResolveContext = { fromFile, allFilePaths };
// `WorkspaceIndex` is an opaque `unknown` placeholder in the
// shared contract, so `ws` passes structurally without a cast.
return resolveCsharpImportTarget(
{ kind: 'namespace', localName: '_', importedName: '_', targetRaw },
ws as unknown as WorkspaceIndex,
ws,
);
},
// C# shadowing: local > using > using static.
mergeBindings: (existing, incoming, scopeId) => {
const fakeScope = { id: scopeId } as unknown as Scope;
return [...csharpMergeBindings(fakeScope, [...existing, ...incoming])];
},
// C# shadowing: local > using > using static. The per-scope id is
// unused by the C# implementation (shadowing is computed purely
// from the binding tier), so we don't need to synthesize a Scope.
mergeBindings: (existing, incoming) => [...csharpMergeBindings([...existing, ...incoming])],
// Adapter: csharpArityCompatibility uses (def, callsite); the
// contract is (callsite, def).

View file

@ -85,7 +85,6 @@ 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 =>
@ -94,43 +93,43 @@ describe('csharpMergeBindings — shadowing precedence', () => {
it('local declaration shadows `using` import', () => {
const local = binding('local', 'L');
const imp = binding('import', 'I');
expect(csharpMergeBindings(scope, [imp, local])).toEqual([local]);
expect(csharpMergeBindings([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]);
expect(csharpMergeBindings([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]);
expect(csharpMergeBindings([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]);
expect(csharpMergeBindings([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);
expect(csharpMergeBindings([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);
expect(csharpMergeBindings([ns, re, imp])).toHaveLength(3);
});
it('empty in → empty out', () => {
expect(csharpMergeBindings(scope, [])).toEqual([]);
expect(csharpMergeBindings([])).toEqual([]);
});
});