Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …)
Summary
codegraph callers <method> (and codegraph impact) miss a caller when the method is passed as a first-class reference (a callback) rather than invoked at a call site. Direct calls of the same method resolve correctly, so the gap is specifically function/method-reference arguments like executor.submit(self.store.method, ...), map(obj.method, xs), functools.partial(obj.method, ...), decorators, or storing obj.method in a variable/list.
Version: codegraph 1.6.0 (Python codebase, index freshly rebuilt).
Why it matters
A method that is only ever dispatched via a callback looks like it has no production callers — in our case a live retrieval method showed only its unit test as a caller, so callers/impact reported it as effectively dead/test-only code. For anyone using codegraph to judge blast radius before a change ("what breaks if I touch this?"), an undercounted call graph is a silent correctness gap in exactly the decision the tool is meant to support.
Minimal repro
# store.py
class Base:
pass
class Store(Base):
def fetch(self, ids): # subclass-only method
return ids
# consumer.py
from concurrent.futures import ThreadPoolExecutor
class Consumer:
def __init__(self, store: Base): # attribute typed as the base
self.store = store
def direct(self, ids):
return self.store.fetch(ids) # (A) direct call
def via_callback(self, ids, pool: ThreadPoolExecutor):
return pool.submit(self.store.fetch, ids) # (B) passed as a referencecodegraph callers fetchlistsConsumer.direct(A) ✅- It does not list
Consumer.via_callback(B) ❌ — even thoughvia_callbackwill callfetchat runtime.
What we actually observed (same pattern in a real repo)
Two methods, both defined only on a concrete subclass, both accessed through a base-typed attribute (self.dataStore: DocStoreConnection, concrete QdrantConnection):
| Method | How it's used | codegraph callers result |
|---|---|---|
get_points_payloads |
self.dataStore.get_points_payloads(ids, idx) — direct call |
production caller found ✅ |
retrieve_vision_vectors |
await thread_pool_exec(self.dataStore.retrieve_vision_vectors, ids, name) — passed as a callback |
only the unit test (which calls it directly) — production caller missed ❌ |
The contrast rules out the base-typed attribute as the cause (the directly-called subclass method resolves fine); the differentiator is the callback/reference passing.
Expected vs actual
- Expected: a
caller(andimpact) edge is recorded wherever a method is referenced as a callable —f(obj.method),partial(obj.method, …),[obj.method],x = obj.method, decorators — not only where it is called asobj.method(...). - Actual: only call-expression sites (
obj.method(...)) create edges; bare attribute references passed as arguments/values are dropped.
Notes
- Language: Python. Async wrapper (
thread_pool_exec→ thread pool) is incidental — a plainThreadPoolExecutor.submit(obj.method, …)reproduces it. - Happy to test a fix build against the real repo if useful.
Thanks for codegraph — it's become part of our pre-change review loop; this is the one gap we hit while auditing call fan-out.
Source: colbymchenry/codegraph