feat(csharp-scope): parity Unit 3c — static call + default-namespace

Closes 2 parity failures (22 → 21).

- `receiver-bound-calls.ts`: add Case 5 for class-as-receiver. When
  `Animal.Classify()` has an identifier receiver that resolves to a
  Class binding (rather than a variable with a typeBinding), look up
  the member on the class's MRO chain. Covers C#-style static calls
  and any type-qualified member access. Python doesn't hit this
  because `ClassName.method()` is syntactically identical to a free
  call there.
- `namespace-siblings.ts`: treat files with no `namespace X;`
  declaration as living in the default (empty-name) bucket, so
  types declared in no-namespace files share cross-file visibility.
  Required for fixtures without explicit namespaces (e.g. the
  method-enrichment fixture's Animal/App/Dog classes).

Legacy 175/175 green; 21 parity failures remain.
This commit is contained in:
Gergo Magyar 2026-04-21 19:20:43 +01:00
parent f0805640e9
commit a77153d860
2 changed files with 34 additions and 1 deletions

View file

@ -73,7 +73,10 @@ export function populateCsharpNamespaceSiblings(
NAMESPACE_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = NAMESPACE_RE.exec(content)) !== null) names.push(m[1]!);
if (names.length === 0) continue;
// Files with no `namespace X;` declaration still share visibility
// — they all live in the default (global) namespace. Use the
// empty-string bucket key for this case.
if (names.length === 0) names.push('');
const namespaceScopes = parsed.scopes.filter((s) => s.kind === 'Namespace');
// With file-scoped namespaces (`namespace X;`), the Namespace

View file

@ -287,6 +287,36 @@ export function emitReceiverBoundCalls(
emitted++;
handledSites.add(siteKey);
}
continue;
}
}
}
// ── Case 5: class-as-receiver (static call / type member) ────
// `Animal.Classify()` — receiver name resolves to a Class
// binding, not a typeBinding. Look up the member on the class's
// MRO chain. Python syntactically collapses this with free
// calls; C# (and other statically-typed languages) distinguish
// via the member_access_expression shape.
if (typeRef === undefined) {
const classDef = findClassBindingInScope(site.inScope, receiverName, scopes);
if (classDef !== undefined) {
const chain = [classDef.nodeId, ...scopes.methodDispatch.mroFor(classDef.nodeId)];
let memberDef: SymbolDefinition | undefined;
for (const ownerId of chain) {
memberDef = findOwnedMember(ownerId, memberName, index);
if (memberDef !== undefined) break;
}
if (memberDef !== undefined) {
const reason =
site.kind === 'write' || site.kind === 'read'
? site.kind
: 'scope-resolution: class-receiver';
const ok = tryEmitEdge(graph, scopes, nodeLookup, site, memberDef, reason, seen);
if (ok) {
emitted++;
handledSites.add(siteKey);
}
}
}
}