Skip to content

Python: a top-level function named like a collection method (append/update/get) gets ZERO real callers and fabricated ones — uncovered shapes left by #715 and #1317, live in 1.6.0 #1681

Description

@inth3shadows

Summary

On Python, a top-level project function whose name collides with a common collection-method name (append, update, get, close, run, …) is wrong in both directions at once, and the two errors compound into a silently plausible answer:

  • False negative — a real module.append(x) call through a plain (non-aliased) module import produces no calls edge. Its neighbour module.path() on the very next line resolves fine, so the loss is name-dependent, not import-dependent.
  • False positive — an unrelated .append(...) on a local collection, reached through a call-expression receiver (d.setdefault(k, []).append(v)), degrades to a bare append ref and exact-matches the project's top-level append as the only same-named symbol.

Net effect: codegraph callers append returns a non-empty, confident-looking list in which every entry is fabricated and every real caller is missing. An empty result would be safer — it is visibly wrong. This one is not.

This is not a regression. It is the shape left uncovered by two shipped fixes:

Environment

  • @colbymchenry/codegraph 1.5.0 and 1.6.0 (current npm latest) — reproduced identically on both
  • Linux (WSL2), Node via the package's own npm-shim.js
  • Fresh codegraph init per run; no stale index

Minimal repro (12 lines)

# pkg/ledger.py
def path():   return "/tmp/x"
def append(row):  return row     # top-level function named like a collection method
def join(vals):   return vals
# pkg/uses.py
from . import ledger

def real_module_call(row):
    ledger.append(row)              # A  -> NO EDGE          (expected: calls ledger.append)
    return ledger.path()            # B  -> calls, "import"  (correct)

def literal_receiver(vals):
    return ", ".join(vals)          # C  -> no edge          (correct, #1317)

def plain_identifier(out, k):
    out.append(k)                   # D  -> no edge          (correct)

def chained_receiver(d, k, v):
    d.setdefault(k, []).append(v)   # E  -> calls ledger.append, "exact-match"  (FABRICATED)

Observed

$ codegraph callers append

Callers of "append" (1):

function    chained_receiver
  pkg/uses.py:17

One caller, and it is the wrong one. real_module_call — the only genuine caller — is absent.

Edge table for uses.py, both versions, identical:

line call site edge resolvedBy
A ledger.append(row) none
B ledger.path() calls -> ledger.path import
C ", ".join(vals) none
D out.append(k) none
E d.setdefault(k,[]).append(v) calls -> ledger.append exact-match

B vs A is the whole false-negative story: same file, same receiver, same import, adjacent lines — only the method name differs.
D vs E is the whole false-positive story: same method, same target; only the receiver shape differs.

Suspected mechanism

Reading src/resolution/index.ts and src/extraction/tree-sitter.ts on main:

  1. isBuiltInOrExternal() classifies x.method() as a builtin collection call whenever method is a common collection-method name, unless the capitalized receiver matches a known class. It never checks whether the receiver is a known module. ledger is a module, so ledger.append(row) is judged list.append, resolveOne() returns null early, and the import resolver that correctly handles ledger.path() is never reached.
  2. In the extractor, the qualified receiver.method shape is preserved only when the receiver is a plain identifier. A call-chain receiver degrades to a bare append ref, which then passes the codebase-wide name check (a real top-level append exists) and exact-matches it.

Both stem from one assumption: "common method name ⇒ builtin, unless the receiver is a known class" — with no case for a project module that exports a function of that name.

Impact

Blast radius and "who calls this" are the queries this tool exists to answer, and for a name in this class it answers with fabricated confidence rather than an absence. Any Python project with a top-level append/update/get/run/close is affected; the false-positive count scales with how often that name appears as an ordinary collection method elsewhere in the repo.

Found while measuring a test-selection tool against its own index: a spot-check asserting "ledger.append has >= 5 callers" passed on 7 edges, all fabricated, while the two real callers were missing — so a commit breaking the function's only write path was selected by nothing.

Verification performed

Activity

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