mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
refactor(scope-resolution): extract language-specific accessor unwrap to provider hook
Optimizer pass: move C# Dictionary-family `.Values`/`.Keys` handling out of the shared `compound-receiver.ts` (where it had hardcoded regex + accessor names) into a provider-level `unwrapCollectionAccessor` hook. The shared pass now takes an arbitrary language-specific unwrap function; C# supplies its Dictionary implementation in `languages/csharp/accessor-unwrap.ts`. Related cleanup in `receiver-bound-calls.ts` Case 3b: replace the hardcoded `tail === 'Values' || tail === 'Keys'` accessor check with a try-dotted-walk-first / fall-back-to-call-form strategy. This removes the last C#-specific branch in the shared pass and makes the logic generalize cleanly to other languages that use property-style accessors for collection views (Kotlin `.size`, future languages). Changes: - `scope-resolution/contract/scope-resolver.ts`: new optional `unwrapCollectionAccessor(receiverType, accessor) => string | undefined` hook. Documented as language-specific with examples. - `scope-resolution/passes/compound-receiver.ts`: delete `extractDictionaryArgs`, accept `unwrapCollectionAccessor` via options, call it for trailing accessor segments. - `scope-resolution/passes/receiver-bound-calls.ts`: plumb the hook through to `resolveCompoundReceiverClass`, remove the C#-hardcoded Case 3b accessor check. - `languages/csharp/accessor-unwrap.ts` (new): C# Dictionary-family regex + element-type extraction. - `languages/csharp/scope-resolver.ts`: opt in. Audit outcome: everything else added across the 19 C# migration commits is either correctly scoped to `languages/csharp/` (query, captures, namespace-siblings, receiver-binding, interpret, imports) or correctly generic in shared paths (argumentTypes field, collapseMemberCallsByCallerTarget flag, overload narrowing via parameterTypes, interface-dispatch via IMPLEMENTS edges, class-like owner extension for Interface/Struct/Record/Enum, type-tagged node IDs, module-scope return-type lookup fallback). 175/175 C# green on both flag paths; 204/204 Python green on both flag paths; TypeScript clean.
This commit is contained in:
parent
788c10aacd
commit
591452a911
5 changed files with 123 additions and 54 deletions
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* C# collection-accessor unwrapping.
|
||||
*
|
||||
* When the compound-receiver resolver encounters a trailing
|
||||
* `.Values` / `.Keys` on a dotted member-access chain, it calls the
|
||||
* provider's `unwrapCollectionAccessor` hook to find the element
|
||||
* type. This module supplies the C# implementation — recognizing
|
||||
* Dictionary-family generics and returning the value or key type.
|
||||
*
|
||||
* Other languages (Python, Java, TypeScript) use method-call syntax
|
||||
* for the same access (`.values()` / `.keys()`), which the compound-
|
||||
* receiver's call-expression branch already handles; they leave this
|
||||
* hook undefined.
|
||||
*/
|
||||
|
||||
/** Extract (K, V) from `Dictionary<K, V>` / `IDictionary<K, V>` /
|
||||
* `IReadOnlyDictionary<K, V>` / `SortedDictionary<K, V>` /
|
||||
* `ConcurrentDictionary<K, V>` / `ImmutableDictionary<K, V>`.
|
||||
* Returns undefined if the type name doesn't match 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() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `data.Values` / `data.Keys` on a Dictionary-like receiver
|
||||
* to its element-type simple name. Returns `undefined` for any
|
||||
* receiver / accessor combination we don't recognize, letting the
|
||||
* compound-receiver pass fall through to the regular field walk.
|
||||
*/
|
||||
export function unwrapCsharpCollectionAccessor(
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
): string | undefined {
|
||||
if (accessor !== 'Values' && accessor !== 'Keys') return undefined;
|
||||
const args = extractDictionaryArgs(receiverType);
|
||||
if (args === undefined) return undefined;
|
||||
return accessor === 'Values' ? args.value : args.key;
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
type CsharpResolveContext,
|
||||
} from './index.js';
|
||||
import { populateCsharpNamespaceSiblings } from './namespace-siblings.js';
|
||||
import { unwrapCsharpCollectionAccessor } from './accessor-unwrap.js';
|
||||
|
||||
const csharpScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.CSharp,
|
||||
|
|
@ -72,6 +73,11 @@ const csharpScopeResolver: ScopeResolver = {
|
|||
fieldFallbackOnMethodLookup: false,
|
||||
propagatesReturnTypesAcrossImports: true,
|
||||
|
||||
// `data.Values` / `data.Keys` on Dictionary-like receivers unwrap
|
||||
// to the value / key element type. Other languages use method-call
|
||||
// syntax for the same access and leave this hook undefined.
|
||||
unwrapCollectionAccessor: unwrapCsharpCollectionAccessor,
|
||||
|
||||
// C# matches legacy DAG by collapsing member-call CALLS edges to
|
||||
// `(caller, target)` — multiple `g.Greet(...)` sites from Main
|
||||
// yield ONE edge, not one per site.
|
||||
|
|
|
|||
|
|
@ -210,6 +210,26 @@ export interface ScopeResolver {
|
|||
*/
|
||||
readonly fieldFallbackOnMethodLookup?: boolean;
|
||||
|
||||
/**
|
||||
* Unwrap a collection-accessor expression on a typed receiver to
|
||||
* its element type. Called by `resolveCompoundReceiverClass` when
|
||||
* walking dotted member-access chains like `data.Values` where
|
||||
* `data` is Dictionary-like. The provider returns the element
|
||||
* type's simple name, or `undefined` when the accessor doesn't
|
||||
* unwrap (letting the regular field-walk resume).
|
||||
*
|
||||
* C#: `{ receiverType: 'Dictionary<string, User>', accessor: 'Values' }`
|
||||
* → `'User'`.
|
||||
* Other languages (Python, Java, TypeScript) don't share C#'s
|
||||
* property-access convention for Dictionary views, so leave this
|
||||
* undefined and use method-call shapes (`.values()`) via the
|
||||
* regular call-expression branch.
|
||||
*/
|
||||
readonly unwrapCollectionAccessor?: (
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
) => string | undefined;
|
||||
|
||||
/**
|
||||
* Collapse member-call CALLS edges by `(caller, target)` rather
|
||||
* than per-site. Default `false` (scope-resolution's contract
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ interface ResolveCompoundReceiverOptions {
|
|||
* class, walk its fields and try the lookup on each field's class.
|
||||
* Phase-9C "unified fixpoint" — Python-shaped heuristic. */
|
||||
readonly fieldFallback?: boolean;
|
||||
/** Language-specific accessor unwrap — `data.Values` on a
|
||||
* Dictionary<K,V>-typed receiver yields V (C#), etc. Returns the
|
||||
* element type's simple name, or `undefined` to let the regular
|
||||
* field-walk handle the access. */
|
||||
readonly unwrapCollectionAccessor?: (
|
||||
receiverType: string,
|
||||
accessor: string,
|
||||
) => string | undefined;
|
||||
}
|
||||
|
||||
export function resolveCompoundReceiverClass(
|
||||
|
|
@ -156,23 +164,22 @@ 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) {
|
||||
// Language-specific collection-accessor suffix (C#'s `data.Values`
|
||||
// on Dictionary<K,V>, etc.). When the provider hook recognizes
|
||||
// the final segment and unwraps the receiver's generic, return
|
||||
// the element class directly. Resolved before the field-walk
|
||||
// because Dictionary-family types aren't local class defs.
|
||||
if (options.unwrapCollectionAccessor !== undefined && parts.length >= 2) {
|
||||
const last = parts[parts.length - 1]!;
|
||||
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.
|
||||
// because the hook inspects the raw generic args (e.g.
|
||||
// `Dictionary<string, User>`).
|
||||
const headInner = parts[0]!;
|
||||
let cur = findReceiverTypeBinding(inScope, headInner, scopes);
|
||||
for (let i = 1; i < parts.length - 1 && cur !== undefined; i++) {
|
||||
|
|
@ -187,9 +194,8 @@ export function resolveCompoundReceiverClass(
|
|||
prefixType = cur;
|
||||
}
|
||||
if (prefixType !== undefined) {
|
||||
const args = extractDictionaryArgs(prefixType.rawName);
|
||||
if (args !== undefined) {
|
||||
const elemName = last === 'Values' ? args.value : args.key;
|
||||
const elemName = options.unwrapCollectionAccessor(prefixType.rawName, last);
|
||||
if (elemName !== undefined) {
|
||||
return findClassBindingInScope(prefixType.declaredAtScope, elemName, scopes);
|
||||
}
|
||||
}
|
||||
|
|
@ -210,35 +216,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ import { resolveDefGraphId } from '../graph-bridge/ids.js';
|
|||
* refactors lighter — callers only need to populate what we read. */
|
||||
type ReceiverBoundProviderSubset = Pick<
|
||||
ScopeResolver,
|
||||
'isSuperReceiver' | 'fieldFallbackOnMethodLookup' | 'collapseMemberCallsByCallerTarget'
|
||||
| 'isSuperReceiver'
|
||||
| 'fieldFallbackOnMethodLookup'
|
||||
| 'collapseMemberCallsByCallerTarget'
|
||||
| 'unwrapCollectionAccessor'
|
||||
>;
|
||||
|
||||
export function emitReceiverBoundCalls(
|
||||
|
|
@ -184,7 +187,7 @@ export function emitReceiverBoundCalls(
|
|||
site.inScope,
|
||||
scopes,
|
||||
index,
|
||||
{ fieldFallback },
|
||||
{ fieldFallback, unwrapCollectionAccessor: provider.unwrapCollectionAccessor },
|
||||
);
|
||||
if (currentClass !== undefined) {
|
||||
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
|
||||
|
|
@ -302,20 +305,26 @@ 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(
|
||||
isAccessor ? typeRef.rawName : typeRef.rawName + '()',
|
||||
// Try the plain dotted-field walk first — covers property /
|
||||
// collection-accessor shapes (`.Values`, Kotlin `.size`) and
|
||||
// field chains. Fall back to call-form (`x()`) which treats
|
||||
// the last segment as a method invocation.
|
||||
let ownerDef = resolveCompoundReceiverClass(
|
||||
typeRef.rawName,
|
||||
typeRef.declaredAtScope,
|
||||
scopes,
|
||||
index,
|
||||
{ fieldFallback },
|
||||
{ fieldFallback, unwrapCollectionAccessor: provider.unwrapCollectionAccessor },
|
||||
);
|
||||
if (ownerDef === undefined) {
|
||||
ownerDef = resolveCompoundReceiverClass(
|
||||
typeRef.rawName + '()',
|
||||
typeRef.declaredAtScope,
|
||||
scopes,
|
||||
index,
|
||||
{ fieldFallback, unwrapCollectionAccessor: provider.unwrapCollectionAccessor },
|
||||
);
|
||||
}
|
||||
if (ownerDef !== undefined) {
|
||||
const chain = [ownerDef.nodeId, ...scopes.methodDispatch.mroFor(ownerDef.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue