mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-27 04:49:43 +08:00
fix(python): a call through a module-level instance links its class's method (#2608)
A Python call through a module-level instance of the same file, like _svc.delete(obj) after _svc = SceneService(), now links to the method its class has or inherits, as a method value through that global already did. On django and fastapi: 50 correct links added, 42 moved to the right class (DEBUG_ENGINE's Engine, the dispatch tests' Signal), confidence raised on about 400 more. CPU time on django is unchanged. Fixes issue 2582. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cc1eb1f942
commit
e48565067e
@@ -335,10 +335,9 @@ function matchMemberFunctionRef(ref: UnresolvedRef, context: ResolutionContext):
|
||||
type = pythonFieldType(receiver, owner, ref, context);
|
||||
} else {
|
||||
type = pythonLocalType(receiver, ref, context);
|
||||
const global = type === null
|
||||
? context.getNodesInFile(ref.filePath).find(n => n.name === receiver && isPythonModuleGlobal(n, context)) : undefined;
|
||||
if (global && !pythonBindsLocally(receiver, ref, context, true)) {
|
||||
return result(pythonGlobalMembers(global, member, ref, context));
|
||||
const viaGlobal = type === null ? pythonGlobalReceiverTarget(receiver, member, ref, context) : undefined;
|
||||
if (viaGlobal !== undefined) {
|
||||
return viaGlobal ? { original: ref, targetNodeId: viaGlobal.id, confidence: 0.9, resolvedBy: 'function-ref' } : null;
|
||||
}
|
||||
}
|
||||
// A type name used directly (`Store.fetch`) is scoped just like an annotation.
|
||||
@@ -931,6 +930,44 @@ function pythonDynamicGlobalWrite(name: string, filePath: string, context: Resol
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `receiver.member` reaches through a module-level instance of this
|
||||
* file — `_svc = SceneService()` at module scope, then `_svc.delete` (#2074)
|
||||
* or `_svc.delete(obj)` (#2582). Undefined when `receiver` is not that global
|
||||
* where the ref is: a parameter, a local, or a `def`/`class` written directly
|
||||
* in an enclosing function shadows it. Otherwise the one method or function every class the global
|
||||
* may hold has or inherits — never a property, an attribute or the referring
|
||||
* node itself (allowed for a `call`: recursion) — or null when there is none.
|
||||
*/
|
||||
function pythonGlobalReceiverTarget(
|
||||
receiver: string, member: string, ref: UnresolvedRef, context: ResolutionContext, call = false,
|
||||
): Node | null | undefined {
|
||||
const global = pythonNameScan(context, `global\0${ref.filePath}\0${receiver}`, () =>
|
||||
context.getNodesInFile(ref.filePath).find(n => n.name === receiver && isPythonModuleGlobal(n, context)) ?? null);
|
||||
if (!global || pythonBindsLocally(receiver, ref, context, true)) return undefined;
|
||||
// A `def _svc` / `class _svc` nested in an enclosing function binds the name there.
|
||||
const nodes = context.getNodesInFile(ref.filePath);
|
||||
const scopes = nodes.filter(n => (n.kind === 'function' || n.kind === 'method') && n.startLine <= ref.line && n.endLine >= ref.line);
|
||||
const innermost = (n: Node) => nodes
|
||||
.filter(c => c.id !== n.id && (c.kind === 'function' || c.kind === 'method' || c.kind === 'class') &&
|
||||
c.startLine < n.startLine && c.endLine >= n.startLine)
|
||||
.sort((a, b) => b.startLine - a.startLine)[0];
|
||||
if (nodes.some(n => n.name === receiver && (n.kind === 'class' || n.kind === 'function') &&
|
||||
scopes.some(f => f.id === innermost(n)?.id))) return undefined;
|
||||
if (!pythonOwnGlobalWrites(global, context) || pythonExternalWrites(global, context).writers.has(ref.filePath)) {
|
||||
return null;
|
||||
}
|
||||
const classes = pythonGlobalClasses(global, ref, context);
|
||||
if (!classes) return null;
|
||||
// A class without the member (a base from outside the project may supply it) leaves it unknown.
|
||||
if (classes.some(cls => pythonMembers(cls, member, ref, context).length === 0)) return null;
|
||||
const pool = pythonGlobalMembers(global, member, ref, context).filter(n => sameLanguageFamily(n.language, ref.language));
|
||||
const target = pool.length === 1 ? pool[0]! : undefined;
|
||||
// A method value naming its own method is no reference; a call through the global is recursion.
|
||||
return target && (target.kind === 'function' || target.kind === 'method') && (call || target.id !== ref.fromNodeId) &&
|
||||
!isPythonProperty(target, context) ? target : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method a module global's value can dispatch to: one class resolves to
|
||||
* its own method; several bind to the nearest declaration they all inherit,
|
||||
@@ -15738,6 +15775,14 @@ export function matchMethodCall(
|
||||
// `subprocess.run(…)` after `import subprocess`: a function of a module
|
||||
// from outside the project, not a same-named project method (#2599).
|
||||
if (!inferredType && ref.language === 'python' && dotMatch && isPythonOutsideModuleReceiver(objectOrClass!, ref, context)) return null;
|
||||
// A module-level instance of this file — `_svc = SceneService()` — called
|
||||
// from a function: the method its class has or inherits (#2582), as for a
|
||||
// method value through the same global (#2074). Otherwise the call goes on
|
||||
// to the strategies below, as before.
|
||||
if (!inferredType && ref.language === 'python' && dotMatch && /^\w+$/.test(objectOrClass!)) {
|
||||
const viaGlobal = pythonGlobalReceiverTarget(objectOrClass!, methodName!, ref, context, true);
|
||||
if (viaGlobal) return { original: ref, targetNodeId: viaGlobal.id, confidence: 0.9, resolvedBy: 'instance-method' };
|
||||
}
|
||||
// A Go receiver's declaration in scope, as the scope reader finds it
|
||||
// (`declared`), decides over the patterns, which scan back for the name
|
||||
// without block scope and can read another declaration of it:
|
||||
|
||||
Reference in New Issue
Block a user