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:
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.
- 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
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:module.append(x)call through a plain (non-aliased) module import produces nocallsedge. Its neighbourmodule.path()on the very next line resolves fine, so the loss is name-dependent, not import-dependent..append(...)on a local collection, reached through a call-expression receiver (d.setdefault(k, []).append(v)), degrades to a bareappendref and exact-matches the project's top-levelappendas the only same-named symbol.Net effect:
codegraph callers appendreturns 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:
from pkg import module) — zero recall on a common test/namespacing pattern #578 / Python: module-attribute callers still missed in large package tree after #715 / 1.0.1 #899 → fix(python): resolve call edges through imported modules (#578) #715 fixed plain module-attribute calls, but only for names that do not collide with a builtin collection method.mod.path()works;mod.append()does not.", ".join(...)) and nested-local scope. A call-expression receiver still degrades to a bare ref and still fabricates.import x as y,from p import x as y) still produce zero call edges — remaining gap after #578 / #715 #1626 (aliased module imports). Nothing here is aliased —from . import ledgerthenledger.append(...).Environment
@colbymchenry/codegraph1.5.0 and 1.6.0 (current npm latest) — reproduced identically on bothnpm-shim.jscodegraph initper run; no stale indexMinimal repro (12 lines)
Observed
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:ledger.append(row)ledger.path()ledger.pathimport", ".join(vals)out.append(k)d.setdefault(k,[]).append(v)ledger.appendexact-matchB 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.tsandsrc/extraction/tree-sitter.tsonmain:isBuiltInOrExternal()classifiesx.method()as a builtin collection call whenevermethodis a common collection-method name, unless the capitalized receiver matches a known class. It never checks whether the receiver is a known module.ledgeris a module, soledger.append(row)is judgedlist.append,resolveOne()returnsnullearly, and the import resolver that correctly handlesledger.path()is never reached.receiver.methodshape is preserved only when the receiver is a plain identifier. A call-chain receiver degrades to a bareappendref, which then passes the codebase-wide name check (a real top-levelappendexists) 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/closeis 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.appendhas >= 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
codegraphCLI and direct SQL against.codegraph/codegraph.db— which agree.