Repository navigation
Incremental update drops indirect_call edges to unchanged callable targets #2438
Description
Summary
A full build resolves an imported callback passed to a recognized dispatcher into an indirect_call edge. Rebuilding only the caller drops that edge when the callable target's file is unchanged.
Reproduction
b.py:
def handler():
return 1a.py:
from b import handler
def run(pool):
return pool.submit(handler)- Full build: the
run -> handlerindirect_calledge is present. - Edit only
a.py(keeping the callback reference) and rebuild incrementally withchanged_paths=[a.py]. - The
indirect_calledge is gone. It only comes back after a full rebuild.
Why the #2406 fix does not cover this
The shared direct-call resolution context added in #2406 supplies persisted node identity (id, label, source_file, type) to the shared direct call pass. indirect_call needs something that context cannot carry:
- Indirect-call resolution gates its target on the
_callableand_callable_classmarkers, which say "this symbol is actually invocable" rather than "a symbol with this name exists". - Those markers are intentionally stripped before
graph.jsonis written, so they are absent from any persisted node an incremental rebuild could load. - Inferring callability from the persisted label instead would reintroduce the same-named data-symbol false positives that Extend indirect_call to remaining dispatch patterns (dict tables, assignments, other languages) #1566 and indirect_call callable guard admits classes → false edges for select(Model), except tuples, and getattr literals #2137 removed — a module-level constant named
handlerwould once again look like a callable target.
So the guard correctly refuses to bind, and the edge is dropped rather than mis-resolved.
Possible directions
- Persist the callable markers (or a narrower, explicitly-versioned "is invocable" flag) in a way that is safe to trust across rebuilds.
- Or give the indirect-call resolver a separate conservative context that carries provenance strong enough to distinguish a callable definition from a same-named data symbol.
Either way the callability signal must be derived at extraction time and carried forward, never re-inferred from labels.
Constraints any fix must keep
- Removing the callback reference must still remove the old edge. Preserving prior
indirect_calledges out of changed files is not an acceptable workaround. - Same-named data symbols must never resolve as callable targets (the Extend indirect_call to remaining dispatch patterns (dict tables, assignments, other languages) #1566 / indirect_call callable guard admits classes → false edges for select(Model), except tuples, and getattr literals #2137 regression guard).
Tracking
tests/test_watch.py::test_incremental_rebuild_drops_indirect_call_to_unchanged_target is an xfail(strict=True) covering exactly this reproduction. Closing this issue should make it pass and the marker should then be deleted.
Related to #2406 (which fixes the shared direct-call case only, and does not address this one).
Activity
aryanbonigala commentedon Aug 4, 2026
I’m investigating this on top of the incremental resolution context from #2439, focusing on preserving callable provenance without exposing private extractor markers or weakening the existing false-positive guards.
fix(extract,watch): C# assembly-aware partial merge, incremental edge…
safishamsi commentedon Aug 5, 2026
Fixed in v0.9.33. The
_callable/_callable_classmarkers now persist tograph.json(like_origin), soindirect_calledges from a changed file into an unchanged callable target survive incremental rebuilds. Thanks.fix(extract,watch): C# assembly-aware partial merge, incremental edge…
feat(go): resolve member calls through the receiver's declared type
Mark ObjC declarations with _callable / _callable_class (Graphify-Lab…
Mark ObjC declarations with _callable / _callable_class (Graphify-Lab…
Mark ObjC declarations with _callable / _callable_class (Graphify-Lab…
Mark ObjC declarations with _callable / _callable_class (Graphify-Lab…
feat(go): resolve member calls through the receiver's declared type
feat(go): resolve member calls through the receiver's declared type
feat(go): resolve member calls through the receiver's declared type
Mark ObjC declarations with _callable / _callable_class (Graphify-Lab…
feat(go): resolve member calls through the receiver's declared type
feat(go): resolve member calls through the receiver's declared type
Mark ObjC declarations with _callable / _callable_class (#3228)