mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-21 00:21:30 +00:00
feat(csharp-scope): parity Unit 6c — Dictionary.Values / .Keys unwrap
Closes 2 parity failures (8 → 6).
Dictionary<K,V>.Values in a foreach binds the element to V; .Keys
binds to K. Without this, `foreach (var user in data.Values)` where
`data: Dictionary<string, User>` couldn't propagate user's type to
User, and `user.Save()` stayed unresolved.
Changes:
- `languages/csharp/interpret.ts`: don't strip the qualifier when
the final dotted segment is a known collection accessor
(`Values` / `Keys`). Preserves the dotted form so downstream
resolvers can unwrap the receiver's generic type based on the
suffix.
- `scope-resolution/passes/compound-receiver.ts`: new
`extractDictionaryArgs` helper splits `Dictionary<K, V>` at the
top-level comma. In the dotted-access walk, detect trailing
`.Values` / `.Keys` and return V/K via findClassBindingInScope
instead of the normal class-walk (Dictionary itself isn't a
local class def).
- Handles nested cases: `this.data.Values` walks `this.data`
recursively (resolving `data` as a field on `this`'s class)
before applying the unwrap.
- `scope-resolution/passes/receiver-bound-calls.ts` Case 3b: when
the typeRef's trailing segment is an accessor, pass the raw
dotted path to `resolveCompoundReceiverClass` without appending
`()` — the extra parens would misroute to the call-expression
branch.
Python parity 204/204 on both flag paths; legacy C# 175/175 green;
6 C# parity failures remain.
This commit is contained in:
parent
5e54fee523
commit
5ee345271f
3 changed files with 91 additions and 3 deletions
|
|
@ -94,6 +94,12 @@ export function interpretCsharpTypeBinding(captures: CaptureMatch): ParsedTypeBi
|
|||
return { boundName: nameCap.text, rawTypeName: rawType, source };
|
||||
}
|
||||
|
||||
/** Member accesses we want to preserve through qualifier stripping.
|
||||
* Dictionary/collection views (`data.Values`, `data.Keys`) survive
|
||||
* so the compound-receiver pass can unwrap the receiver's generic
|
||||
* type (Dictionary<K,V>) based on the suffix. */
|
||||
const COLLECTION_ACCESSOR_SUFFIXES = new Set(['Values', 'Keys']);
|
||||
|
||||
/** `User?` → `User`. */
|
||||
function stripNullable(text: string): string {
|
||||
if (text.endsWith('?')) return text.slice(0, -1).trim();
|
||||
|
|
@ -117,9 +123,15 @@ function stripGeneric(text: string): string {
|
|||
return text;
|
||||
}
|
||||
|
||||
/** `System.Collections.User` → `User`. */
|
||||
/** `System.Collections.User` → `User`. Preserves dotted paths whose
|
||||
* final segment is a known Dictionary/collection accessor (`.Values`,
|
||||
* `.Keys`, `.Count`, etc.) so downstream resolvers can unwrap the
|
||||
* receiver's generic type based on the suffix — `data.Values` →
|
||||
* element type of `data`'s Dictionary<K,V>. */
|
||||
function stripQualifier(text: string): string {
|
||||
const lastDot = text.lastIndexOf('.');
|
||||
if (lastDot === -1) return text;
|
||||
return text.slice(lastDot + 1);
|
||||
const tail = text.slice(lastDot + 1);
|
||||
if (COLLECTION_ACCESSOR_SUFFIXES.has(tail)) return text;
|
||||
return tail;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,46 @@ export function resolveCompoundReceiverClass(
|
|||
|
||||
// Pure dotted access `obj.field[.field]…` — walk fields.
|
||||
const parts = text.split('.');
|
||||
|
||||
// Collection accessor suffix — if the final segment is
|
||||
// `.Values` / `.Keys`, resolve the prefix first (recursively for
|
||||
// nested cases like `this.data.Values`) and unwrap its
|
||||
// Dictionary<K,V> generic. Handled before the class-resolution
|
||||
// step because Dictionary isn't a local class def.
|
||||
const last = parts[parts.length - 1]!;
|
||||
if ((last === 'Values' || last === 'Keys') && parts.length >= 2) {
|
||||
const prefix = parts.slice(0, -1).join('.');
|
||||
// Find the receiver type for the prefix. Single-segment: direct
|
||||
// typeBinding; multi-segment: recurse to resolve class + walk fields.
|
||||
let prefixType: TypeRef | undefined;
|
||||
if (parts.length === 2) {
|
||||
prefixType = findReceiverTypeBinding(inScope, prefix, scopes);
|
||||
} else {
|
||||
// Recursive resolution: walk the prefix as a dotted class chain
|
||||
// to find its typeRef. We need the TypeRef (not the class def)
|
||||
// because we want to inspect its Dictionary<K,V> generic args.
|
||||
const headInner = parts[0]!;
|
||||
let cur = findReceiverTypeBinding(inScope, headInner, scopes);
|
||||
for (let i = 1; i < parts.length - 1 && cur !== undefined; i++) {
|
||||
const cls = findClassBindingInScope(cur.declaredAtScope, cur.rawName, scopes);
|
||||
if (cls === undefined) {
|
||||
cur = undefined;
|
||||
break;
|
||||
}
|
||||
const cs = classScopeByDefId.get(cls.nodeId);
|
||||
cur = cs?.typeBindings.get(parts[i]!);
|
||||
}
|
||||
prefixType = cur;
|
||||
}
|
||||
if (prefixType !== undefined) {
|
||||
const args = extractDictionaryArgs(prefixType.rawName);
|
||||
if (args !== undefined) {
|
||||
const elemName = last === 'Values' ? args.value : args.key;
|
||||
return findClassBindingInScope(prefixType.declaredAtScope, elemName, scopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const head = parts[0]!;
|
||||
const headType = findReceiverTypeBinding(inScope, head, scopes);
|
||||
let currentClass: SymbolDefinition | undefined = headType
|
||||
|
|
@ -170,6 +210,35 @@ export function resolveCompoundReceiverClass(
|
|||
return currentClass;
|
||||
}
|
||||
|
||||
/** Extract (K, V) from `Dictionary<K, V>` / `IDictionary<K, V>` /
|
||||
* `IReadOnlyDictionary<K, V>` / `SortedDictionary<K, V>`. Returns
|
||||
* undefined if the type name doesn't match a Dictionary-family
|
||||
* generic or the argument list isn't exactly two top-level args. */
|
||||
function extractDictionaryArgs(rawName: string): { key: string; value: string } | undefined {
|
||||
const match = rawName.match(
|
||||
/^(?:[A-Za-z_][A-Za-z0-9_.]*\.)?(?:Dictionary|IDictionary|IReadOnlyDictionary|SortedDictionary|ConcurrentDictionary|ImmutableDictionary)<(.+)>$/,
|
||||
);
|
||||
if (match === null) return undefined;
|
||||
const inner = match[1]!;
|
||||
// Split on the top-level comma (tolerate nested `<...>`).
|
||||
let depth = 0;
|
||||
let commaIdx = -1;
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
const ch = inner[i];
|
||||
if (ch === '<') depth++;
|
||||
else if (ch === '>') depth--;
|
||||
else if (ch === ',' && depth === 0) {
|
||||
commaIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (commaIdx === -1) return undefined;
|
||||
return {
|
||||
key: inner.slice(0, commaIdx).trim(),
|
||||
value: inner.slice(commaIdx + 1).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Find the index of the `(` that matches the trailing `)` of a
|
||||
* call-expression text. Returns -1 if unbalanced. */
|
||||
function matchingOpenParen(text: string): number {
|
||||
|
|
|
|||
|
|
@ -240,8 +240,15 @@ export function emitReceiverBoundCalls(
|
|||
!typeRef.rawName.includes('(') &&
|
||||
!namespaceTargets.has(typeRef.rawName.split('.')[0]!)
|
||||
) {
|
||||
// For collection-accessor suffixes (`.Values`, `.Keys`) the
|
||||
// raw typeRef already describes a plain dotted access that
|
||||
// `resolveCompoundReceiverClass` can walk directly — don't
|
||||
// append `()` which would misroute to its call-expression
|
||||
// branch.
|
||||
const tail = typeRef.rawName.split('.').pop() ?? '';
|
||||
const isAccessor = tail === 'Values' || tail === 'Keys';
|
||||
const ownerDef = resolveCompoundReceiverClass(
|
||||
typeRef.rawName + '()',
|
||||
isAccessor ? typeRef.rawName : typeRef.rawName + '()',
|
||||
typeRef.declaredAtScope,
|
||||
scopes,
|
||||
index,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue