fix(extract): exclude class refs from indirect_call edges (#2137)#2142
Open
Rishet11 wants to merge 2 commits into
Open
fix(extract): exclude class refs from indirect_call edges (#2137)#2142Rishet11 wants to merge 2 commits into
Rishet11 wants to merge 2 commits into
Conversation
…abs#2137) Classes are callable via their constructor but are frequently referenced as descriptive values, not invoked: ORM args (select(Model), db.get(Model, id)), exception tuples (except (ErrorA, ErrorB)), and string-literal getattr resolving to a same-named class. The indirect_call guard treated any callable-def target identically, so these produced false edges (~41% of indirect_call edges in the reported sample targeted classes), inflating centrality and traversals. Track class defs in a callable_class_nids set parallel to callable_def_nids, mark class nodes with a _callable_class attribute, and exclude class targets from indirect_call emission in both the intra-file (_emit_indirect_by_name) and cross-file resolver paths. Marker is stripped before output like _callable. Covers all languages: both class-node creation sites (the generic config.class_types branch and the Ruby Struct.new/Class.new/Data.define synthesis) register into the new set. Tradeoff: suppression is context-blind, so a genuine higher-order class callback (e.g. map(Point, coords)) also loses its indirect_call edge. This is far rarer than the false-positive noise removed and matches the issue framing. Verified before/after on the same input: 4 class-targeted indirect_call edges -> 0, function callbacks preserved.
There was a problem hiding this comment.
Pull request overview
This PR reduces false-positive indirect_call edges by distinguishing class definitions from function/method definitions during extraction, and suppressing indirect_call edges that would otherwise target classes.
Changes:
- Track class-definition node IDs separately (
callable_class_nids) and tag class nodes with_callable_classin the extractor engine. - Suppress
indirect_callemission when the resolved target is a class in both the intra-file resolver (_emit_indirect_by_name) and the cross-file resolver guard inextract.py. - Add a regression test asserting class references in ORM args / exception tuples / getattr-literals do not become
indirect_calltargets, while a real function callback still does.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/test_indirect_dispatch.py | Adds a regression test for suppressing class-target indirect_call edges while preserving real function-callback edges. |
| graphify/extractors/engine.py | Introduces callable_class_nids and marks class defs with _callable_class; suppresses intra-file indirect edges to classes. |
| graphify/extract.py | Excludes _callable_class targets from cross-file indirect_call emission and strips _callable_class before output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…raphify-Labs#2137) Copilot flagged that the Graphify-Labs#2137 regression test only exercised the intra-file suppression path; a regression in the cross-file resolver guard in extract.py would still pass. Add a cross-file test: class and function imported from another module, asserting the imported class is never an indirect_call target while a genuine imported callback still emits its edge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2137.
Problem
indirect_calldetection treats any callable-def target identically, but classes are callable only via their constructor and are frequently referenced as descriptive values, not invoked. The guard therefore emitted false edges for:select(KbArticle),db.get(KbArticle, id)except (ErrorA, ErrorB) as exc:getattr(run, "KbArticle", 0)In the reported sample, ~287/689 (41%) of
indirect_calledges targeted classes, inflating node centrality and creating misleading traversals.Fix
One root cause covers all three symptoms: distinguish class defs from function defs.
callable_class_nidsset parallel to the existingcallable_def_nids._callable_classattribute (stripped before output, like_callable).indirect_callemission in both paths: intra-file_emit_indirect_by_nameand the cross-file resolver guard.Both class-node creation sites register into the new set, so coverage is language-agnostic:
config.class_typesbranch (Python/JS/TS/Java/C#/Swift/…)Struct.new/Class.new/Data.definesynthesisTradeoff (intentional)
Suppression is context-blind: a genuine higher-order class callback that is invoked per element (e.g.
map(Point, coords),sorted(items, key=SomeClass)) also loses itsindirect_calledge. This false-negative is far rarer than the false-positive noise removed and matches the issue's framing ("classes are passed as descriptors, not invoked"). Flagging it so this isn't mistaken for a zero-tradeoff fix.Verification
test_class_ref_is_not_indirect_callcovers all three contexts (except-tuple, getattr, ORM arg) plus a real function callback that must still emit.git stash): 4 class-targetedindirect_calledges → 0, function callbacks preserved.tests/test_indirect_dispatch*+tests/test_extract.py: all pass; no regressions in the surrounding suites.