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

Closed

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

  1. inth3shadows commented on Sep 4, 2026

    @inth3shadows
    ContributorAuthor

    Checked against unreleased main as well, so this is not something a pending release already closes.

    So shape A (false negative) is unfixed on main, not just in the published 1.6.0.

  2. inth3shadows commented on Sep 4, 2026

    @inth3shadows
    ContributorAuthor

    Adding the fix for the false-negative half (shape A, ledger.append(row) producing no edge). Written and tested locally; the full upstream suite (2902 tests) passed with it applied, and after re-indexing, ledger.append's callers came back as exactly the two real ones with none of the fabricated entries.

    isBuiltInOrExternal only lets a common collection-method name through when the capitalized receiver matches a known class. It never asks whether the receiver is an imported module, so ledger.append(row) is classified as list.append and resolveOne() returns null before resolveViaImport / resolvePythonModuleMember — which already handle it correctly, as ledger.path() on the next line proves.

    // src/resolution/index.ts — inside isBuiltInOrExternal
    // But allow if the capitalized receiver matches a known codebase class,
    // OR the receiver is itself an imported module in this file — a module
    // can export a top-level function sharing a common collection-method
    // name (`ledger.append`, `import ledger` / `from . import ledger`), and
    // that call is a real project dependency, not `list.append`.
    // Without this, the qualified ref never reaches resolveViaImport /
    // resolvePythonModuleMember, which already resolve it correctly.
    const isKnownClass = this.knownNames?.has(capitalized) ?? false;
    const isImportedModule =
      !isKnownClass &&
      this.context
        .getImportMappings(ref.filePath, ref.language)
        .some((imp) => imp.localName === receiver);
    if (!isKnownClass && !isImportedModule) {
      // ... existing builtin classification
    }

    The false-positive half of this issue (shape E, the chained receiver) has its own fix, which I have posted on #1683 — it turned out to be language-agnostic rather than Python-specific, so it is cleaner to track there. This issue is closed by the two together.

    Not opening a PR: my checkout of this repo is kept read-only for bug-hunting. The patch is yours to take as-is.

  3. colbymchenry commented on Sep 8, 2026

    @colbymchenry
    Owner

    Both halves are addressed:

    Verified on Linux against current main + #1749: before the FN change, real_module_call only had a path edge and append callers were empty while chained_receiver already produced none; after, the module-qualified append edge is present and the chained receiver still does not fabricate.

    Closing — thank you @inth3shadows for the diagnosis and the #1704 patch this lands a slice of.

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