Summary
In Python extraction, an unresolved attribute call x.get(k) is resolved by bare name to a module-level function get defined in the project, producing a calls edge that does not exist. Any codebase that both defines a short, common function name (get, set, run, …) and uses the same method name on dicts/clients gets that function inflated into the #1 god node.
This is the Python call-site counterpart of #2241 (JS/TS closure arguments, fixed in 0.9.29) and looks closely related to #2315 (Python @receiver.name(...) decorators binding to an unrelated module-level function of the same bare name) — possibly the same bare-name fallback in the Python resolver.
Minimal reproduction (~30s)
fixture.py, alone in an empty directory:
def get(k):
"""Module-level function that shares a name with dict/mapping .get."""
return k
class Store:
def __init__(self):
self.store = {}
def read(self, k):
return self.store.get(k)
def other(d):
return d.get('x')
def real():
return get('real')
Then count calls edges whose target is the node labelled get() (the module-level function; methods are labelled .name()):
import json
g = json.load(open("graphify-out/graph.json"))
t = {n["id"] for n in g["nodes"] if n.get("label") == "get()"}
print([l for l in g["links"] if l.get("relation") == "calls" and l["target"] in t])
Expected: 1 edge — real() -> get().
Actual: 3 edges — real() -> get(), other() -> get(), Store.read() -> get().
self.store.get(k) is a dict lookup and d.get('x') is a call on an unannotated parameter; neither can be resolved to the module-level get, but both are bound to it.
Version sweep
Same fixture, run via uv tool run --from graphifyy==<version> graphify .:
| version |
inbound calls into get() |
verdict |
| 0.9.32, 0.9.31, 0.9.30, 0.9.29, 0.9.28, 0.9.27, 0.9.26, 0.9.24, 0.9.22, 0.9.20, 0.9.17, 0.9.14, 0.9.13, 0.9.5, 0.9.0 |
3 |
❌ buggy |
15/15 buggy — so this is not a regression introduced by a recent release. (0.8.0 could not be installed here: dependency resolution fails.)
Impact on a real repo
On a ~3.3k-node Python project, one rebuild made the project's get() in settings_store.py jump from degree 27 to 149, taking the #1 god-node slot. A single module contributed 42 of those edges while never importing settings_store — it simply contains 127 .get( call sites. Across that rebuild, 122 of the 200 newly-added edges (61%) were false edges of this shape (net edge count 6,927 → 7,110).
Downstream, this corrupts god-node ranking, explain <node>, and community assignment, and there is no version to pin to as a workaround.
Suggested direction
Don't fall back to bare-name matching for attribute calls when the receiver is unresolved. If a fallback is needed for recall, emit it at a lower confidence and/or as a distinct relation so consumers can filter it, rather than as a plain calls edge.
Environment
- graphify 0.9.32 (and the 14 older versions above), installed via
uv tool / uv tool run
- macOS 26.5.2 (arm64), uv-managed CPython 3.13.12
Summary
In Python extraction, an unresolved attribute call
x.get(k)is resolved by bare name to a module-level functiongetdefined in the project, producing acallsedge that does not exist. Any codebase that both defines a short, common function name (get,set,run, …) and uses the same method name on dicts/clients gets that function inflated into the #1 god node.This is the Python call-site counterpart of #2241 (JS/TS closure arguments, fixed in 0.9.29) and looks closely related to #2315 (Python
@receiver.name(...)decorators binding to an unrelated module-level function of the same bare name) — possibly the same bare-name fallback in the Python resolver.Minimal reproduction (~30s)
fixture.py, alone in an empty directory:graphify .Then count
callsedges whose target is the node labelledget()(the module-level function; methods are labelled.name()):Expected: 1 edge —
real() -> get().Actual: 3 edges —
real() -> get(),other() -> get(),Store.read() -> get().self.store.get(k)is a dict lookup andd.get('x')is a call on an unannotated parameter; neither can be resolved to the module-levelget, but both are bound to it.Version sweep
Same fixture, run via
uv tool run --from graphifyy==<version> graphify .:callsintoget()15/15 buggy — so this is not a regression introduced by a recent release. (0.8.0 could not be installed here: dependency resolution fails.)
Impact on a real repo
On a ~3.3k-node Python project, one rebuild made the project's
get()insettings_store.pyjump from degree 27 to 149, taking the #1 god-node slot. A single module contributed 42 of those edges while never importingsettings_store— it simply contains 127.get(call sites. Across that rebuild, 122 of the 200 newly-added edges (61%) were false edges of this shape (net edge count 6,927 → 7,110).Downstream, this corrupts god-node ranking,
explain <node>, and community assignment, and there is no version to pin to as a workaround.Suggested direction
Don't fall back to bare-name matching for attribute calls when the receiver is unresolved. If a fallback is needed for recall, emit it at a lower confidence and/or as a distinct relation so consumers can filter it, rather than as a plain
callsedge.Environment
uv tool/uv tool run