Skip to content

Python: _svc.delete() on a module-level instance gets no edge unless the method name is unique #2582

Closed

Description

@inth3shadows

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 22

codegraph 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 matchMethodCall infers a receiver's type with inferLocalReceiverType. 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: pythonGlobalMembers handles one or several candidate classes and inherited methods. But it is only reached from matchMemberFunctionRef, via matchFunctionRef, which handles method values, not calls. Using it for a Python calls ref 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

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 _svc then _svc.delete() in another file).
  • A global annotated with a type (_svc: SceneService = ...), or rebound later in the module or through global.
  • The native kernel extractor (only CODEGRAPH_KERNEL=0 was run).
  • Windows and macOS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions