Skip to content

Python: module-scope dict member calls can resolve to unrelated project methods #1652

Closed

Description

@zyyyyynnn

Defect

When a Python module declares a module-scope dictionary (or other built-in collection) and calls methods on it (such as dict.get), unresolved method calls can fall through to bare-name method matching and fabricate calls edges to unrelated project classes that happen to declare a same-named method (e.g. LRUCache.get).

Repro

# settings.py
DEFAULTS = {"answer": "42"}

def read_setting(name):
    return DEFAULTS.get(name, None)

# cache.py
class LRUCache:
    def __init__(self):
        self._store = {}

    def get(self, key):
        return self._store.get(key)
$ codegraph callers get --limit 500 --json
{
  "symbol": "get",
  "callers": [
    { "name": "read_setting", "kind": "function", "filePath": "settings.py", "startLine": 4 }
  ]
}

read_setting in settings.py never imports or references LRUCache, but DEFAULTS.get() is incorrectly attributed to LRUCache.get.

Impact on Circular Dependency Analysis

GraphQueryManager.getFileDependencies walks resolved call/reference edges. When a false call edge is created across files, it introduces a false file dependency. On projects with a one-way import structure, this can report hundreds of phantom cycles (e.g. 136 reported cycles vs 1 genuine import cycle).

Scope & Relation

Discussed in #1566 (which addressed the TypeScript/JavaScript/TSX/ArkTS receiver resolution and conformance boundaries). This issue tracks extending similar receiver type inference and fallback boundaries to Python module-scope and instance member calls.

Activity

  1. inth3shadows commented on Sep 4, 2026

    @inth3shadows
    Contributor

    Tried to build a reproduction for this and could not on 1.6.0 (npm latest) — posting the attempt so it is not re-attempted blind, and so you can tell me what my fixture is missing.

    # pkg/api.py
    def get(k):    return k
    def update(d): return d
    # pkg/use.py
    CONFIG = {"a": 1}
    REG = dict()
    
    def reads(k):
        return CONFIG.get(k)              # module-scope dict member  -> no edge
    
    def writes(d):
        REG.update(d)                     # module-scope dict member  -> no edge
    
    def chained(m, k, v):
        return m.setdefault(k, {}).get(v) # -> calls pkg/api.py:get   FABRICATED

    Result on a fresh index: the two module-scope dict calls produce no edges at all (correct), while the third line fabricates exact-match -> api.get.

    So with a plain {...} / dict() module-scope binding and a same-named top-level function present, I do not see the misresolution this issue describes. What I do see is that the receiver shape decides it: a plain identifier receiver is filtered correctly, a call-expression receiver is not.

    Two possibilities:

    1. Your case involves a receiver that is not a plain identifier (a subscript, an attribute chain, a call result), in which case it is the same root cause as Call-expression receivers drop the receiver and fabricate a calls edge to any same-named top-level symbol — Python AND JavaScript, live in 1.6.0 #1683 and would be closed by the same fix.
    2. It needs something my fixture lacks — the dict built by a comprehension or a function call, a TypedDict/annotation, or a call through an import rather than same-file.

    If you can share the receiver expression and how the dict is bound, I am happy to re-run it. Details that would pin it: the exact call line, how the dict is created, and whether the wrongly-resolved target is a module-level function or a class method.

    Environment: 1.6.0 (also checked 1.5.0), Linux, fresh codegraph init, verified via both the CLI and direct SQL against .codegraph/codegraph.db.

  2. colbymchenry commented on Sep 8, 2026

    @colbymchenry
    Owner

    Fixed in #1776.

    DEFAULTS.get(name) (and other module-scope dict/list/set/tuple/frozenset member calls) no longer fabricate calls edges onto unrelated project methods such as LRUCache.get.

    Root cause: the Python builtin-method filter treated any symbol in knownNames as a “known class,” so the variable DEFAULTS itself escaped the filter; matchMethodCall Strategy 3 then single-survivor-bound the only get method in the project.

    Fix: decline when the receiver is a same-file module-scope collection binding (signature gate), and require an actual Python class/struct/interface for the class escape. Imported project-module calls (ledger.append, #1681) stay resolvable.

    Verified on Linux against the issue fixture (callers get → empty) and a 15-case regression suite (fail→pass vs main).

  3. added a commit that references this issue on Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions