mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
feat(python-scope): chain receiver via call-expression return types
Unit 5 — extends the compound-receiver case to handle call-expression
receivers (`svc.get_user().save()`).
`resolveCompoundReceiverClass` is the single recursive entry point for
all compound receivers. Three shapes:
- bare identifier — typeBinding chain
- dotted `obj.field[.field]…` — class-scope field types
- call `expr.method()` — recurse into expr, look up method's
return-type typeBinding on its class scope
Method return-type bindings auto-hoist to the parent (class) scope per
Unit 3, so `methodClassScope.typeBindings.get(methodName)` is the
canonical lookup. Free-call return types (`get_user()`) walk the
caller's scope chain.
Depth-capped at 4 hops to bound recursion.
Verification:
- Flag-off: 191/191 (identical baseline).
- Flag-on: 28 fail / 163 pass (was 29/162; `Python chained method
call resolution` now passes).
- tsc --noEmit clean.
Two related tests (`city.save() via method chain`, `c.greet().save()
depth-2 MRO`) still fail because the captures yield typeBindings
shaped like `city → user.get_city` (no trailing parens — the capture
grabs the attribute text). Resolving those needs a follow step that
detects the call-shape rawName and feeds it through the compound
recurser. Lands with the chain-typeBinding work in a follow-up.
This commit is contained in:
parent
c5221b98f5
commit
42e1146b76
1 changed files with 173 additions and 24 deletions
|
|
@ -360,29 +360,18 @@ function emitReceiverBoundCalls(
|
|||
const receiverName = site.explicitReceiver.name;
|
||||
const memberName = site.name;
|
||||
|
||||
// ── Case 0: dotted receiver (`user.address.save()`) ──────────
|
||||
// Walk the dotted chain via class-scope typeBindings (fields).
|
||||
if (receiverName.includes('.')) {
|
||||
const parts = receiverName.split('.');
|
||||
const head = parts[0]!;
|
||||
const headType = findReceiverTypeBinding(site.inScope, head, scopes);
|
||||
let currentClass: SymbolDefinition | undefined = headType
|
||||
? findClassBindingInScope(headType.declaredAtScope, headType.rawName, scopes)
|
||||
: undefined;
|
||||
for (let i = 1; i < parts.length && currentClass !== undefined; i++) {
|
||||
const fieldName = parts[i]!;
|
||||
const cs = classScopeByDefId.get(currentClass.nodeId);
|
||||
const fieldType = cs?.typeBindings.get(fieldName);
|
||||
if (fieldType === undefined) {
|
||||
currentClass = undefined;
|
||||
break;
|
||||
}
|
||||
currentClass = findClassBindingInScope(
|
||||
fieldType.declaredAtScope,
|
||||
fieldType.rawName,
|
||||
scopes,
|
||||
);
|
||||
}
|
||||
// ── Case 0: compound receiver (`user.address.save()` or
|
||||
// `svc.get_user().save()`) — walk the dotted/call chain,
|
||||
// resolving each segment to a class via field types or
|
||||
// method return types.
|
||||
if (receiverName.includes('.') || receiverName.includes('(')) {
|
||||
const currentClass = resolveCompoundReceiverClass(
|
||||
receiverName,
|
||||
site.inScope,
|
||||
scopes,
|
||||
parsedFiles,
|
||||
classScopeByDefId,
|
||||
);
|
||||
if (currentClass !== undefined) {
|
||||
const chain = [currentClass.nodeId, ...scopes.methodDispatch.mroFor(currentClass.nodeId)];
|
||||
let memberDef: SymbolDefinition | undefined;
|
||||
|
|
@ -397,7 +386,7 @@ function emitReceiverBoundCalls(
|
|||
nodeLookup,
|
||||
site,
|
||||
memberDef,
|
||||
'python-scope: field-chain',
|
||||
'python-scope: chain-receiver',
|
||||
seen,
|
||||
);
|
||||
if (ok) emitted++;
|
||||
|
|
@ -507,6 +496,166 @@ function emitReceiverBoundCalls(
|
|||
return emitted;
|
||||
}
|
||||
|
||||
/** Max depth for compound-receiver chain resolution (`a().b().c().d()`).
|
||||
* Practical Python rarely exceeds 3-4 hops; the cap just prevents
|
||||
* pathological recursion if the receiver text turns out to be malformed. */
|
||||
const COMPOUND_RECEIVER_MAX_DEPTH = 4;
|
||||
|
||||
/**
|
||||
* Resolve a compound-receiver expression's TYPE (the class def of the
|
||||
* value it produces). Handles three shapes:
|
||||
* - bare identifier `name` — look up via typeBinding chain
|
||||
* - dotted `obj.field[.field]…` — walk fields via class-scope typeBindings
|
||||
* - call `expr.method()` — recurse into expr, find method's return-type
|
||||
* typeBinding on its class, resolve to a class
|
||||
*
|
||||
* Returns the class `SymbolDefinition` or undefined if the chain dead-ends.
|
||||
* Depth-capped at COMPOUND_RECEIVER_MAX_DEPTH.
|
||||
*/
|
||||
function resolveCompoundReceiverClass(
|
||||
receiverText: string,
|
||||
inScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
classScopeByDefId: ReadonlyMap<string, Scope>,
|
||||
depth = 0,
|
||||
): SymbolDefinition | undefined {
|
||||
if (depth > COMPOUND_RECEIVER_MAX_DEPTH) return undefined;
|
||||
const text = receiverText.trim();
|
||||
if (text.length === 0) return undefined;
|
||||
|
||||
// Bare identifier — resolve via typeBinding then class lookup.
|
||||
if (!text.includes('.') && !text.includes('(')) {
|
||||
const tb = findReceiverTypeBinding(inScope, text, scopes);
|
||||
if (tb === undefined) return undefined;
|
||||
return findClassBindingInScope(tb.declaredAtScope, tb.rawName, scopes);
|
||||
}
|
||||
|
||||
// Trailing `()` — call expression. Strip it and resolve the function
|
||||
// expression's return type. We only handle the canonical `f()` /
|
||||
// `obj.method()` shape; nested-arg expressions like `f(g())` are
|
||||
// out of scope for V1 (depth-capped recursion catches infinite loops).
|
||||
if (text.endsWith(')')) {
|
||||
// Find the matching `(` by walking from end with a depth counter
|
||||
// so nested parens in args don't fool us.
|
||||
const openIdx = matchingOpenParen(text);
|
||||
if (openIdx === -1) return undefined;
|
||||
const fnExpr = text.slice(0, openIdx).trim();
|
||||
if (fnExpr.length === 0) return undefined;
|
||||
|
||||
// Split into receiver and method name on the LAST dot.
|
||||
const lastDot = fnExpr.lastIndexOf('.');
|
||||
if (lastDot === -1) {
|
||||
// Free call `name()`. Look up function in scope, then its
|
||||
// return-type typeBinding (which lives in the function's
|
||||
// enclosing scope per Pass 4 hoist).
|
||||
const fnDef = findExportedDefByName(fnExpr, inScope, scopes, parsedFiles);
|
||||
if (fnDef === undefined) return undefined;
|
||||
// The return-type binding key == the function's simple name in
|
||||
// the scope where the function is bound. Walk for it.
|
||||
const retType = findReceiverTypeBinding(inScope, fnExpr, scopes);
|
||||
if (retType === undefined) return undefined;
|
||||
return findClassBindingInScope(retType.declaredAtScope, retType.rawName, scopes);
|
||||
}
|
||||
// `obj.method()` — resolve obj's class, look up method, then its
|
||||
// return-type typeBinding on that class scope.
|
||||
const objExpr = fnExpr.slice(0, lastDot);
|
||||
const methodName = fnExpr.slice(lastDot + 1);
|
||||
const objClass = resolveCompoundReceiverClass(
|
||||
objExpr,
|
||||
inScope,
|
||||
scopes,
|
||||
parsedFiles,
|
||||
classScopeByDefId,
|
||||
depth + 1,
|
||||
);
|
||||
if (objClass === undefined) return undefined;
|
||||
const methodClassScope = classScopeByDefId.get(objClass.nodeId);
|
||||
// Method's return-type binding lives on the class scope (because
|
||||
// the method's function_definition auto-hoists its return-type
|
||||
// binding to the parent scope == class scope).
|
||||
const retType = methodClassScope?.typeBindings.get(methodName);
|
||||
if (retType === undefined) return undefined;
|
||||
return findClassBindingInScope(retType.declaredAtScope, retType.rawName, scopes);
|
||||
}
|
||||
|
||||
// Pure dotted access `obj.field[.field]…` — walk fields.
|
||||
const parts = text.split('.');
|
||||
const head = parts[0]!;
|
||||
const headType = findReceiverTypeBinding(inScope, head, scopes);
|
||||
let currentClass: SymbolDefinition | undefined = headType
|
||||
? findClassBindingInScope(headType.declaredAtScope, headType.rawName, scopes)
|
||||
: undefined;
|
||||
for (let i = 1; i < parts.length && currentClass !== undefined; i++) {
|
||||
const fieldName = parts[i]!;
|
||||
const cs = classScopeByDefId.get(currentClass.nodeId);
|
||||
const fieldType = cs?.typeBindings.get(fieldName);
|
||||
if (fieldType === undefined) return undefined;
|
||||
currentClass = findClassBindingInScope(fieldType.declaredAtScope, fieldType.rawName, scopes);
|
||||
}
|
||||
return currentClass;
|
||||
}
|
||||
|
||||
/** Find the index of the `(` that matches the trailing `)` of a
|
||||
* call-expression text. Returns -1 if unbalanced. */
|
||||
function matchingOpenParen(text: string): number {
|
||||
if (!text.endsWith(')')) return -1;
|
||||
let depth = 0;
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
const ch = text[i];
|
||||
if (ch === ')') depth++;
|
||||
else if (ch === '(') {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Look up a free-function def by simple name across all parsed files
|
||||
* whose scope chain from `inScope` includes the binding. Used by the
|
||||
* free-call branch of `resolveCompoundReceiverClass`. */
|
||||
function findExportedDefByName(
|
||||
name: string,
|
||||
inScope: ScopeId,
|
||||
scopes: ScopeResolutionIndexes,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): SymbolDefinition | undefined {
|
||||
// Walk the call site's scope chain looking for a binding.
|
||||
let currentId: ScopeId | null = inScope;
|
||||
const visited = new Set<ScopeId>();
|
||||
while (currentId !== null) {
|
||||
if (visited.has(currentId)) break;
|
||||
visited.add(currentId);
|
||||
const scope = scopes.scopeTree.getScope(currentId);
|
||||
if (scope === undefined) break;
|
||||
const local = scope.bindings.get(name);
|
||||
if (local !== undefined) {
|
||||
for (const b of local) {
|
||||
if (b.def.type === 'Function' || b.def.type === 'Method') return b.def;
|
||||
}
|
||||
}
|
||||
const finalized = scopes.bindings.get(currentId)?.get(name);
|
||||
if (finalized !== undefined) {
|
||||
for (const b of finalized) {
|
||||
if (b.def.type === 'Function' || b.def.type === 'Method') return b.def;
|
||||
}
|
||||
}
|
||||
currentId = scope.parent;
|
||||
}
|
||||
// Fallback: scan parsed files for any matching simple-name def.
|
||||
for (const f of parsedFiles) {
|
||||
for (const def of f.localDefs) {
|
||||
if (def.type !== 'Function' && def.type !== 'Method') continue;
|
||||
const qn = def.qualifiedName;
|
||||
if (qn === undefined) continue;
|
||||
const simple = qn.lastIndexOf('.') === -1 ? qn : qn.slice(qn.lastIndexOf('.') + 1);
|
||||
if (simple === name) return def;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate `ownerId` on Method/Function/Field defs that live structurally
|
||||
* inside a `Class` scope.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue