feat(csharp-scope): parity Unit 6a — class-like owner extension

Closes 1 parity failure (10 → 9). Extends `populateClassOwnedMembers`
to recognize Interface / Struct / Record / Enum / Trait as class-like
owners, not just Class.

The C# scope query collapses interface_declaration / struct_declaration
/ record_declaration / enum_declaration to @scope.class (they share
body-scope semantics), but the declaration-side tags produce defs of
type Interface / Struct / Record / Enum. `populateClassOwnedMembers`
previously only looked for Class-typed defs in class scopes, so
interface members (including C# 8+ default methods) never got
ownerIds — making them invisible to `findOwnedMember` via
`memberByOwner`.

With this fix, `user.Validate()` on a variable typed as `IValidator`
resolves correctly: receiver-bound-calls Case 4 finds IValidator via
findClassBindingInScope (which already accepted Interface), walks the
chain, and findOwnedMember locates Validate now that the interface
default has a proper ownerId.

Legacy C# 175/175 green; Python parity 204/204 on both flag paths;
9 C# parity failures remain.
This commit is contained in:
Gergo Magyar 2026-04-21 20:47:08 +01:00
parent ac2b667922
commit 2de104150e

View file

@ -183,13 +183,25 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
// on `class U: def save(self): def helper(): ...` — helper.ownerId will
// remain undefined. The theoretical concern is real only if the
// extractor ever stops creating scopes for inner defs.
// Class-like def types: Class scope covers C#'s interface/struct/
// record/enum too (they all collapse to @scope.class per the query
// contract). Interface default methods land as children of the
// Interface def here.
const isClassLike = (t: string): boolean =>
t === 'Class' ||
t === 'Interface' ||
t === 'Struct' ||
t === 'Record' ||
t === 'Enum' ||
t === 'Trait';
for (const scope of parsed.scopes) {
// Methods: function scope whose parent is a Class scope. Owner is
// the parent's Class def.
// the parent's class-like def.
if (scope.parent !== null) {
const parentScope = scopesById.get(scope.parent);
if (parentScope !== undefined && parentScope.kind === 'Class') {
const classDef = parentScope.ownedDefs.find((d) => d.type === 'Class');
const classDef = parentScope.ownedDefs.find((d) => isClassLike(d.type));
if (classDef !== undefined) {
for (const def of scope.ownedDefs) {
(def as { ownerId?: string }).ownerId = classDef.nodeId;
@ -199,9 +211,9 @@ export function populateClassOwnedMembers(parsed: ParsedFile): void {
}
}
// Class-body fields: defs directly owned by a Class scope (the
// class def itself excluded).
// class-like def itself excluded).
if (scope.kind === 'Class') {
const classDef = scope.ownedDefs.find((d) => d.type === 'Class');
const classDef = scope.ownedDefs.find((d) => isClassLike(d.type));
if (classDef !== undefined) {
for (const def of scope.ownedDefs) {
if (def === classDef) continue;