Skip to content

Python: bare method calls (x.get(...)) bind to a same-named module-level function — false calls edges create a god node (reproduces on every 0.9.x I tested) #2417

Description

@dispather

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')
graphify .

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions