Summary
When a function calls a static method via ClassName.staticMethod() across files, CodeGraph does not create a calls edge to the method. Instead, the edge is mis-resolved into a ClassName instantiates-style edge pointing at the class, and the target method is dropped entirely.
As a result, callers/impact for the static method return nothing (false negatives), and the CLI's fallback bare-name search introduces unrelated same-name hits (false positives). Both failure modes happen at once.
Confirmed still present on main (commit 7db4c1d2), not just on the released 0.9.9.
Minimal reproduction
// helpers.ts
export class Foo {
static bar(x: number) { return x + 1; }
}
// caller.ts
import { Foo } from './helpers';
export function run() { return Foo.bar(41); }
Index this and run:
codegraph callers "Foo.bar" --json # => empty
codegraph callers "bar" --json # => empty (or unrelated same-name hits)
Expected: run is reported as a caller of Foo.bar.
Actual: no caller is found for the method.
Root cause (confirmed by querying the SQLite edges directly)
When resolving the call Foo.bar():
- The call's receiver is
Foo, which is a class name.
- In
resolveOne, there is logic that promotes a calls edge to instantiates when the calls target resolves to a class.
- Because the static-call receiver
Foo matches the class first, the whole edge gets re-classified as run instantiates Foo, pointing at the class Foo instead of the method Foo::bar.
- The intended
run --calls--> Foo::bar edge is never created; method bar is discarded.
A direct SQLite check confirms there are zero calls edges pointing at the static method. So callers/impact cannot find them, and the CLI's bare-name fallback then surfaces unrelated symbols with the same name.
This does not appear to be by design: matchMethodCall looks intended to support static calls, but the "calls → instantiates promotion" logic fires first and intercepts it. It reads as a logic-ordering conflict / bug rather than a deliberate limitation.
Impact
callers <staticMethod> → false negatives (real callers missing).
impact <staticMethod> → blast radius severely under-reported.
- CLI bare-name fallback → false positives (unrelated same-name symbols).
This is significant because ClassName.staticMethod() is a very common pattern in TypeScript (static utility/helper classes).
Environment
- Released version:
0.9.9 (npm)
- Also reproduced from source on
main @ commit 7db4c1d2
- Languages: TypeScript
Suggested direction
Only promote a calls edge to instantiates for actual construction (e.g. new ClassName(...)), not when the class name is merely the receiver of a static method call. When the receiver resolves to a class and a matching static method exists on that class, resolve the edge to the method (ClassName::method) instead of to the class.
Notes
A separate, lower-priority issue was also observed: affected only accepts bare relative paths — a ./ prefix or absolute path silently returns 0 (path not normalized). Happy to file that separately if useful.
Summary
When a function calls a static method via
ClassName.staticMethod()across files, CodeGraph does not create acallsedge to the method. Instead, the edge is mis-resolved into aClassName instantiates-style edge pointing at the class, and the target method is dropped entirely.As a result,
callers/impactfor the static method return nothing (false negatives), and the CLI's fallback bare-name search introduces unrelated same-name hits (false positives). Both failure modes happen at once.Confirmed still present on
main(commit7db4c1d2), not just on the released0.9.9.Minimal reproduction
Index this and run:
Expected:
runis reported as a caller ofFoo.bar.Actual: no caller is found for the method.
Root cause (confirmed by querying the SQLite edges directly)
When resolving the call
Foo.bar():Foo, which is a class name.resolveOne, there is logic that promotes acallsedge toinstantiateswhen thecallstarget resolves to a class.Foomatches the class first, the whole edge gets re-classified asrun instantiates Foo, pointing at the classFooinstead of the methodFoo::bar.run --calls--> Foo::baredge is never created; methodbaris discarded.A direct SQLite check confirms there are zero
callsedges pointing at the static method. Socallers/impactcannot find them, and the CLI's bare-name fallback then surfaces unrelated symbols with the same name.This does not appear to be by design:
matchMethodCalllooks intended to support static calls, but the "calls → instantiates promotion" logic fires first and intercepts it. It reads as a logic-ordering conflict / bug rather than a deliberate limitation.Impact
callers <staticMethod>→ false negatives (real callers missing).impact <staticMethod>→ blast radius severely under-reported.This is significant because
ClassName.staticMethod()is a very common pattern in TypeScript (static utility/helper classes).Environment
0.9.9(npm)main@ commit7db4c1d2Suggested direction
Only promote a
callsedge toinstantiatesfor actual construction (e.g.new ClassName(...)), not when the class name is merely the receiver of a static method call. When the receiver resolves to a class and a matching static method exists on that class, resolve the edge to the method (ClassName::method) instead of to the class.Notes
A separate, lower-priority issue was also observed:
affectedonly accepts bare relative paths — a./prefix or absolute path silently returns 0 (path not normalized). Happy to file that separately if useful.