Repository navigation
Python: _svc.delete() on a module-level instance gets no edge unless the method name is unique #2582
Description
Summary
In Python, a method call through a module-level instance, such as _svc = SceneService() followed by _svc.delete(obj) inside a function, gets no calls edge when another class in the project also has a method with that name. The same call through a function-local svc = SceneService() resolves, and so does the method value _svc.delete passed as a callback. A module-level service instance is a common way to structure a service layer, so most calls into such a layer have no callers.
Reproduction (verified)
On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, WASM extractor (CODEGRAPH_KERNEL=0). Three files:
# base.py
class GenericCRUDService:
def delete(self, obj):
return obj# other.py
class Unrelated:
def delete(self, obj):
return None
def purge(self):
return None# svc.py
from base import GenericCRUDService
class SceneService(GenericCRUDService):
def purge(self):
return 1
_svc = SceneService()
def delete_scene(obj):
_svc.delete(obj) # line 13
def delete_local(obj):
svc = SceneService()
svc.delete(obj) # line 18
def purge_scene():
_svc.purge() # line 22codegraph init -i, then the calls / imports / instantiates edges (source, kind, target, line, resolvedBy/confidence):
svc.py -imports-> GenericCRUDService [base.py:1] @1 import/0.9
_svc -instantiates-> SceneService [svc.py:4] @9 exact-match/0.9
delete_local -instantiates-> SceneService [svc.py:4] @17 exact-match/0.9
delete_local -calls-> GenericCRUDService::delete [base.py:2] @18 instance-method/0.9
There is no edge from delete_scene (line 13) or purge_scene (line 22).
Expected: delete_scene -calls-> GenericCRUDService::delete (inherited) and purge_scene -calls-> SceneService::purge (the class's own method), as for the local receiver on line 18.
Variants, same build:
| Change | delete_scene (line 13) |
purge_scene (line 22) |
|---|---|---|
| as above | no edge | no edge |
other.py removed, so both names are unique |
GenericCRUDService::delete, instance-method/0.7 |
SceneService::purge, instance-method/0.7 |
added def submit_delete(pool, obj): pool.submit(_svc.delete, obj) |
submit_delete -references-> GenericCRUDService::delete (function-ref) |
— |
So the receiver's type is not used for the call. The edge appears only when the method name is unique in the project. The method-value form, handled since #2074, does resolve through the same module global.
Likely cause (inferred from reading the code)
- The call path in
matchMethodCallinfers a receiver's type withinferLocalReceiverType. Its doc comment says the scan is bounded to the enclosing function, so a module-scope_svc = SceneService()is never read. - With no type, the call reaches Strategy 3, which matches by method name. That only succeeds when the name is unique, which explains the 0.7 edge in the second variant.
- Module-global typing already exists:
pythonGlobalMembershandles one or several candidate classes and inherited methods. But it is only reached frommatchMemberFunctionRef, viamatchFunctionRef, which handles method values, not calls. Using it for a Pythoncallsref whose receiver is a module global of the same file would probably close this.
Where it was found
A real 257-file Python service whose modules each hold a module-level service instance and call _svc.create(...), _svc.list(...), _svc.delete(...) from their functions. Those methods are inherited from a generic CRUD base and the names repeat across classes, so the calls had no edges.
Related
- Method values on untyped Python module globals still produce no callers edge (follow-up to #1820) #2074 (closed): the same receiver shape for method values (
pool.submit(settings.docStoreConn.delete_payload_fields)). Its fix covers method values only. This issue is the call form. - TypeScript: calls through an imported singleton instance resolve to the exported constant instead of the class method #1292 / fix(resolution): calls through an imported singleton resolve to the method #1315: calls through an imported singleton (TypeScript), resolved from the exporting file's declaration. This is the Python same-file case.
- Local-variable receiver-type inference is C++-only — instance method calls through a local don't resolve in other languages #1108: local-variable receiver-type inference across languages. The local form above works; the module-scope form does not.
- Tracking: chained factory/singleton call resolution across statically-typed languages #750: tracking issue for singleton and factory call resolution in statically typed languages.
A build of the inth3shadows fork that carries open PRs #1704 and #1926 (Python attribute-receiver typing) gives the same output for all three variants.
Not checked
- An imported module global (
from svc import _svcthen_svc.delete()in another file). - A global annotated with a type (
_svc: SceneService = ...), or rebound later in the module or throughglobal. - The native kernel extractor (only
CODEGRAPH_KERNEL=0was run). - Windows and macOS.
Activity