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
this.someField.doThing() resolves to the calling method instead of the
field's method, whenever the calling method happens to be named doThing too.
The result is a stored calls self-edge — A::doThing → A::doThing — so callers, callees, impact, and trace are wrong, silently and plausibly.
The identical call resolves correctly if the calling method has any other
name. Name collision is the only variable; no framework, no decorators, no DI
involved.
Version 1.5.0 (npm, darwin-arm64 bundle). Language: TypeScript.
// src/relay.ts — identical call, calling method has a DIFFERENT nameimport{Mailer}from'./mailer';exportclassRelay{privatemailer=newMailer();forward(msg: string): string{returnthis.mailer.send(msg);}}
Outbox::send is missing. The stored edges show where it went:
$ sqlite3 .codegraph/codegraph.db "SELECT n1.qualified_name, n2.qualified_name FROM edges e JOIN nodes n1 ON n1.id=e.source JOIN nodes n2 ON n2.id=e.target WHERE e.kind='calls';"Outbox::send -> Outbox::send ❌ self-edgeRelay::forward -> Mailer::send ✅
Expected
Both should resolve to Mailer::send. Outbox::send does not call itself.
The field style does not matter
Same fixture, varying only how mailer is declared. Only the name collision
changes the outcome:
field declaration
calling method
result
private mailer = new Mailer()
forward
✅ Mailer::send
private mailer: Mailer (assigned in ctor)
forward
✅ Mailer::send
constructor(private readonly mailer: Mailer)
forward
✅ Mailer::send
constructor(private readonly mailer: Mailer)
send
❌ self-edge
private mailer = new Mailer()
send
❌ self-edge
Scale on a real codebase
A TypeScript monorepo (NestJS server + Electron tooling), 2,106 files,
26,583 nodes, 77,741 edges. All this.<field>.<method>() call sites where the
field has a declared type, in apps/server and tools/strauss:
enclosing method name
sites sampled
resolved correctly
same as callee
60
0 (0%)
different
60
28 (47%)
732 such sites total, 181 of them colliding. Repo-wide there are 335 calls self-edges; six sampled at random were all this bug, none real
recursion:
The last two show it is not limited to this. receivers: ThrottlerModule.forRoot(
is a static call on an imported class, and it also collapsed onto the enclosing AppModule::forRoot.
The concentration is not accidental. Delegation — a method forwarding to a
same-named method on a collaborator — is a normal pattern, and it is the exact
shape that fails. In frameworks that encourage it (NestJS controllers calling
same-named service methods) essentially every delegation edge is wrong.
Hypothesis (not verified — reading the shipped bundle only)
preferCallSiteFile in src/resolution/name-matcher.ts ranks same-file
candidates ahead of all others (#1079), and the precedence comment nearby
describes the order as preferredFqn → same-file → matches[0]. For this.<field>.<method>() the receiver does not appear in the reference
(referenceName is the bare method name), so receiver-type inference has
nothing to key on, and resolution falls through to name matching — where the
call site's own file always wins.
That would explain both columns above: with a collision the same-file candidate
shadows the correct cross-file one; without a collision there is no same-file
candidate, so the single remaining match happens to be right.
If that is the mechanism, the fix is presumably to attempt receiver-type
inference for member-expression receivers before the same-file preference
applies, rather than to weaken preferCallSiteFile — the #1079 behaviour is
clearly right for genuinely ambiguous bare names.
A missing edge is a visible gap. This produces a confident wrong edge that
reads as a legitimate self-call, so anything built on callers / impact —
blast-radius counts, review tooling, agent context — inherits the error with no
signal that it happened.
Summary
this.someField.doThing()resolves to the calling method instead of thefield's method, whenever the calling method happens to be named
doThingtoo.The result is a stored
callsself-edge —A::doThing → A::doThing— socallers,callees,impact, andtraceare wrong, silently and plausibly.The identical call resolves correctly if the calling method has any other
name. Name collision is the only variable; no framework, no decorators, no DI
involved.
Version
1.5.0(npm,darwin-arm64bundle). Language: TypeScript.Reproduction
Three files, no dependencies.
Outbox::sendis missing. The stored edges show where it went:Expected
Both should resolve to
Mailer::send.Outbox::senddoes not call itself.The field style does not matter
Same fixture, varying only how
maileris declared. Only the name collisionchanges the outcome:
private mailer = new Mailer()forwardMailer::sendprivate mailer: Mailer(assigned in ctor)forwardMailer::sendconstructor(private readonly mailer: Mailer)forwardMailer::sendconstructor(private readonly mailer: Mailer)sendprivate mailer = new Mailer()sendScale on a real codebase
A TypeScript monorepo (NestJS server + Electron tooling), 2,106 files,
26,583 nodes, 77,741 edges. All
this.<field>.<method>()call sites where thefield has a declared type, in
apps/serverandtools/strauss:732 such sites total, 181 of them colliding. Repo-wide there are 335
callsself-edges; six sampled at random were all this bug, none realrecursion:
The last two show it is not limited to
this.receivers:ThrottlerModule.forRoot(is a static call on an imported class, and it also collapsed onto the enclosing
AppModule::forRoot.The concentration is not accidental. Delegation — a method forwarding to a
same-named method on a collaborator — is a normal pattern, and it is the exact
shape that fails. In frameworks that encourage it (NestJS controllers calling
same-named service methods) essentially every delegation edge is wrong.
Hypothesis (not verified — reading the shipped bundle only)
preferCallSiteFileinsrc/resolution/name-matcher.tsranks same-filecandidates ahead of all others (#1079), and the precedence comment nearby
describes the order as
preferredFqn → same-file → matches[0]. Forthis.<field>.<method>()the receiver does not appear in the reference(
referenceNameis the bare method name), so receiver-type inference hasnothing to key on, and resolution falls through to name matching — where the
call site's own file always wins.
That would explain both columns above: with a collision the same-file candidate
shadows the correct cross-file one; without a collision there is no same-file
candidate, so the single remaining match happens to be right.
If that is the mechanism, the fix is presumably to attempt receiver-type
inference for member-expression receivers before the same-file preference
applies, rather than to weaken
preferCallSiteFile— the #1079 behaviour isclearly right for genuinely ambiguous bare names.
Related
The pattern it added is present in 1.5.0 and matches these field declarations;
it does not help here, which is what suggests the problem is precedence rather
than a missing pattern.
resolve to the wrong class. Same failure character, different language and a
different tie-break (index order there, call-site file here).
callersoutput, but at query time via fuzzysubstitution. Distinct: the edges here are already wrong in the database, as
the raw
edgesquery above shows.Why this one is costly
A missing edge is a visible gap. This produces a confident wrong edge that
reads as a legitimate self-call, so anything built on
callers/impact—blast-radius counts, review tooling, agent context — inherits the error with no
signal that it happened.