mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-28 17:26:48 +00:00
fix(extract): exclude class refs from indirect_call edges (#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.
This commit is contained in:
@@ -504,3 +504,42 @@ def test_typescript_typed_params_and_arrow_consts(tmp_path):
|
||||
assert (nid["via"], nid["handler"]) in indirect
|
||||
assert (file_nid, nid["handler"]) in indirect
|
||||
assert (file_nid, nid["cb"]) in indirect
|
||||
|
||||
|
||||
def test_class_ref_is_not_indirect_call(tmp_path):
|
||||
"""A class referenced BY NAME as a value is a descriptor, not an invocation, so it
|
||||
must NOT emit an indirect_call edge — across all three symptom families of #2137:
|
||||
ORM args (`select(Model)`, `db.get(Model, id)`), exception tuples
|
||||
(`except (ErrorA, ErrorB)`), and string-literal getattr resolving to a same-named
|
||||
class. A real function callback in the same file still emits its edge."""
|
||||
src = (
|
||||
"class ErrorA(Exception):\n pass\n"
|
||||
"class ErrorB(Exception):\n pass\n"
|
||||
"class KbArticle:\n pass\n"
|
||||
"\n"
|
||||
"def handler(x):\n return x\n"
|
||||
"\n"
|
||||
"def use_except():\n"
|
||||
" try:\n pass\n"
|
||||
" except (ErrorA, ErrorB) as e: # exception tuple -> classes, not calls\n"
|
||||
" return e\n"
|
||||
"\n"
|
||||
"def use_getattr(run):\n"
|
||||
' return getattr(run, "KbArticle", 0) # literal resolving to a class\n'
|
||||
"\n"
|
||||
"def use_orm(db, i):\n"
|
||||
" db.get(KbArticle, i) # class as descriptor\n"
|
||||
" return select(KbArticle) # class as descriptor\n"
|
||||
"\n"
|
||||
"def register(pool):\n"
|
||||
" pool.submit(handler) # real fn callback -> indirect_call\n"
|
||||
)
|
||||
(tmp_path / "orm.py").write_text(src)
|
||||
r = extract_python(tmp_path / "orm.py")
|
||||
nid = {n["label"].rstrip("()"): n["id"] for n in r["nodes"]}
|
||||
indirect = _rels(r, "indirect_call")
|
||||
class_ids = {nid["ErrorA"], nid["ErrorB"], nid["KbArticle"]}
|
||||
# no class is ever an indirect_call target (except-tuple, getattr, or ORM arg)
|
||||
assert not any(t in class_ids for _, t in indirect)
|
||||
# genuine function callback preserved
|
||||
assert (nid["register"], nid["handler"]) in indirect
|
||||
|
||||
Reference in New Issue
Block a user