#1820·codegraph

Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …)

Author: JosefAschauerCreated Sep 9, 2026Updated Sep 17, 2026

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

python
# 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 reference
  • codegraph callers fetch lists Consumer.direct (A) ✅
  • It does not list Consumer.via_callback (B) ❌ — even though via_callback will call fetch at 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 (and impact) 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 as obj.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 plain ThreadPoolExecutor.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.