You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
A member call whose receiver is a call expression loses the receiver at extraction time, degrades to a bare method-name ref, and then exact-matches any top-level project symbol with that name — fabricating a call edge from an unrelated function.
Reproduced in Python and JavaScript, so this is the shared extractor, not a per-language heuristic.
Not the same as #1230/#1317 (literal receivers, fixed — control C below passes) or #645 (C++ chained getters binding to the wrong class; here the receiver is dropped entirely and the edge lands on a top-level function).
Environment
@colbymchenry/codegraph 1.5.0 and 1.6.0 (npm latest), Linux, fresh codegraph init per run.
// src/use.jsimport*aslibfrom'./lib.js';exportfunctionrealNamespaceCall(row){lib.push(row);// A -> calls lib.push "import" (correct)}exportfunctionplainIdent(out,k){out.push(k);// C -> no edge (correct)}exportfunctionchained(m,k,v){m.getOrInit(k,[]).push(v);// D -> calls lib.push "exact-match" WRONG}
fabricated exact-match edge to the top-level append/push
Only the receiver shape differs between the correct and incorrect rows.
Impact
Any project with a top-level export named like a common collection method (append, push, get, update, run, close) accumulates fabricated inbound edges proportional to how often that method is used on chained locals elsewhere. callers/impact then return a populated, plausible list that is wrong — worse than an empty one, which is visibly wrong.
Suspected mechanism
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 method ref. That bare ref passes the codebase-wide name check (a real top-level symbol of that name exists) and is exact-matched as the sole candidate.
Verification
Both versions, both languages, fresh index each time; cross-checked via the codegraph CLI and direct SQL against .codegraph/codegraph.db, which agree.
Adding the fix. I have this working locally against a patched build; the full upstream suite (2902 tests) passed with it applied.
The receiver is dropped in the extractor's method-call branch: only a plain-identifier receiver keeps the qualified receiver.method shape, everything else falls through to a bare methodName. Keeping the receiver's source text as a qualifier is enough — the ref can then only resolve through import/module-member resolution, and an unresolvable qualifier becomes a silent miss instead of a wrong edge.
// src/extraction/tree-sitter.ts — method-call receiver handling}elseif(receiver){// Any other receiver shape — attribute chain (`self.data`), subscript// (`d[k]`), call chain (`rows.setdefault(k, []).append(x)`) — previously// fell through to a bare methodName. A bare name matching a common// list/dict/str method (`append`, `get`, `update`, ...) can exact-match// an unrelated top-level project function sharing that name, fabricating// a call edge (same class as #1230/#1276). Keep the receiver's source// text as a qualifier so the ref can only resolve through import/// module-member resolution, never the bare-name fallback; an// unresolvable qualifier is a silent miss, never a wrong edge.constreceiverText=getNodeText(receiver,this.source).replace(/\s+/g,' ').trim();calleeName=receiverText ? `${receiverText}.${methodName}` : methodName;}
One important caveat about my own version of this patch: I originally wrote it gated as this.language === 'python' && receiver, because I had only found the Python case. The JavaScript reproduction in this issue shows that gate is wrong — the same fabrication happens in JS, so the branch should be language-agnostic as written above. If you take the Python-gated form, this issue stays open for JS/TS.
Worth checking before merging: the qualifier is raw source text, so rows.setdefault(k, []).append becomes a long referenceName. That is harmless for correctness (it simply will not match anything, which is the intent) but it does land in unresolved_refs, so it is worth confirming it does not bloat that table on a large repo. Normalising whitespace, as above, keeps it stable across formatting.
I am not opening a PR — my checkout of this repo is kept read-only for bug-hunting, and the language-gating decision above is yours to make. Happy for anyone to take the patch as-is.
Third language: Go. This is no longer a Python/JS pair — and the Go case is the sharpest demonstration, because the correct target exists in the same file and a control line resolves to it.
// store/store.gopackage store
funcGet(kstring) string { returnk } // package-level func, line 3typeBagstruct{ mmap[string]string }
func (b*Bag) Get(kstring) string { returnb.m[k] } // method, line 7
lookup returns *store.Bag, so line 10 should reach Bag.Get at :7 — exactly what line 14 reaches from the same type. Instead the receiver is dropped and the bare Get exact-matches the package-level function at :3.
Line 14 proves the resolver can type this receiver and find the right method; line 10 proves it never gets the chance. Only the receiver shape differs.
Consequence for the fix
The patch I posted above is in the right place, but this settles the language-gating question: it must not be gated to Python. Confirmed fabricating in Python, JavaScript and Go, with the same exact-match signature in all three — it is the shared extractor path, so the branch should be unconditional on receiver.
Go also shows the failure is not limited to "a name that looks like a collection method" — Get here collides with an ordinary package-level function. Any project where a method name is also a package-level function name is exposed.
Verified on 1.6.0, fresh codegraph init, read back via direct SQL.
Confirming this on Rust as well, on v1.6.0 (darwin-arm64), which supports the read that it's the shared extractor rather than a per-language heuristic.
Minimal repro — Cargo.toml plus one src/lib.rs:
use std::path::{Path,PathBuf};/// A project method named `join`. Only `real_caller` below calls it.pubstructEvidenceTier(pubu8);implEvidenceTier{pubfnjoin(&self,other:&EvidenceTier) -> EvidenceTier{EvidenceTier(self.0.max(other.0))}}/// The ONLY legitimate caller of EvidenceTier::join.pubfnreal_caller() -> EvidenceTier{EvidenceTier(1).join(&EvidenceTier(2))}/// Calls std PathBuf::join — plain receiver.pubfnstdlib_receiver(root:&Path) -> PathBuf{
root.join("subdir")}/// Call-expression receiver — the shape this issue describes.pubfncall_expr_receiver(root:&Path) -> PathBuf{
root.to_path_buf().join("nested")}
$ codegraph init && codegraph sync
● 11 nodes, 21 edges
$ codegraph callers join
Callers of "join" (3):
function real_caller
src/lib.rs:12
function stdlib_receiver <- fabricated
src/lib.rs:17
function call_expr_receiver <- fabricated
src/lib.rs:22
$ codegraph callees stdlib_receiver
Callees of "stdlib_receiver" (1):
method join
src/lib.rs:6 <- resolves to EvidenceTier::join
Ground truth is one caller. Two of three are fabricated.
Worth noting for scoping: stdlib_receiver is a plain receiver (root.join(...) where root: &Path), not a call-expression receiver. So on Rust the trigger is broader than the title suggests — an unresolved receiver of any kind falls through to bare-name matching, and a std method on a std type is enough. call_expr_receiver reproduces the exact shape in the report on top of that.
The downstream cost is the part I'd flag: the same mechanism ranks codegraph's importance signals. On a 376k-line Rust workspace the top fan-in symbols came out as join, matches, iter, len — all std methods collapsed onto same-named project symbols — so it isn't only edge noise, it reorders which functions the tool says matter.
Verified on Linux against current main, then fixed in #1748.
Repro (pre-fix): call-expression receivers fabricated exact-match edges — Python chained_receiver → ledger.append, JS chained → lib.push. Plain-ident and literal receivers stayed correct; real namespace lib.push still resolved via import.
Fix: rebased #1692 onto main (post-#1746): keep <inner>().<method> at extraction (TS + kernel) and refuse name-match guessing for that shape. Issue fixtures and __tests__/call-receiver-no-fabrication.test.ts now pass (kernel path and CODEGRAPH_KERNEL=0).
Activity
inth3shadows commentedon Sep 4, 2026
Adding the fix. I have this working locally against a patched build; the full upstream suite (2902 tests) passed with it applied.
The receiver is dropped in the extractor's method-call branch: only a plain-identifier receiver keeps the qualified
receiver.methodshape, everything else falls through to a baremethodName. Keeping the receiver's source text as a qualifier is enough — the ref can then only resolve through import/module-member resolution, and an unresolvable qualifier becomes a silent miss instead of a wrong edge.One important caveat about my own version of this patch: I originally wrote it gated as
this.language === 'python' && receiver, because I had only found the Python case. The JavaScript reproduction in this issue shows that gate is wrong — the same fabrication happens in JS, so the branch should be language-agnostic as written above. If you take the Python-gated form, this issue stays open for JS/TS.Worth checking before merging: the qualifier is raw source text, so
rows.setdefault(k, []).appendbecomes a longreferenceName. That is harmless for correctness (it simply will not match anything, which is the intent) but it does land inunresolved_refs, so it is worth confirming it does not bloat that table on a large repo. Normalising whitespace, as above, keeps it stable across formatting.I am not opening a PR — my checkout of this repo is kept read-only for bug-hunting, and the language-gating decision above is yours to make. Happy for anyone to take the patch as-is.
inth3shadows commentedon Sep 4, 2026
Third language: Go. This is no longer a Python/JS pair — and the Go case is the sharpest demonstration, because the correct target exists in the same file and a control line resolves to it.
Observed (1.6.0, fresh index)
r.lookup(k).Get(v)store.Get@store.go:3exact-matchr.lookup(k)lookup@main.go:7instance-methodb.Get(k)Bag.Get@store.go:7instance-methodstore.Get(k)store.Get@store.go:3importlookupreturns*store.Bag, so line 10 should reachBag.Getat :7 — exactly what line 14 reaches from the same type. Instead the receiver is dropped and the bareGetexact-matches the package-level function at :3.Line 14 proves the resolver can type this receiver and find the right method; line 10 proves it never gets the chance. Only the receiver shape differs.
Consequence for the fix
The patch I posted above is in the right place, but this settles the language-gating question: it must not be gated to Python. Confirmed fabricating in Python, JavaScript and Go, with the same
exact-matchsignature in all three — it is the shared extractor path, so the branch should be unconditional onreceiver.Go also shows the failure is not limited to "a name that looks like a collection method" —
Gethere collides with an ordinary package-level function. Any project where a method name is also a package-level function name is exposed.Verified on 1.6.0, fresh
codegraph init, read back via direct SQL.Nicolas0315 commentedon Sep 4, 2026
Confirming this on Rust as well, on v1.6.0 (darwin-arm64), which supports the read that it's the shared extractor rather than a per-language heuristic.
Minimal repro —
Cargo.tomlplus onesrc/lib.rs:Ground truth is one caller. Two of three are fabricated.
Worth noting for scoping:
stdlib_receiveris a plain receiver (root.join(...)whereroot: &Path), not a call-expression receiver. So on Rust the trigger is broader than the title suggests — an unresolved receiver of any kind falls through to bare-name matching, and a std method on a std type is enough.call_expr_receiverreproduces the exact shape in the report on top of that.The downstream cost is the part I'd flag: the same mechanism ranks
codegraph's importance signals. On a 376k-line Rust workspace the top fan-in symbols came out asjoin,matches,iter,len— all std methods collapsed onto same-named project symbols — so it isn't only edge noise, it reorders which functions the tool says matter.docs(registry): the fabricated-edge blind spot is cross-language, not…
colbymchenry commentedon Sep 8, 2026
Verified on Linux against current
main, then fixed in #1748.Repro (pre-fix): call-expression receivers fabricated
exact-matchedges — Pythonchained_receiver→ledger.append, JSchained→lib.push. Plain-ident and literal receivers stayed correct; real namespacelib.pushstill resolved viaimport.Fix: rebased #1692 onto main (post-#1746): keep
<inner>().<method>at extraction (TS + kernel) and refuse name-match guessing for that shape. Issue fixtures and__tests__/call-receiver-no-fabrication.test.tsnow pass (kernel path andCODEGRAPH_KERNEL=0).Closing as fixed by #1748.
fix(extraction): never fabricate an edge from a call-result receiver (#…