Skip to content

Cross-file static method calls (ClassName.staticMethod()) are dropped: calls edge mis-promoted to instantiates #825

@contextFlow-lab

Description

@contextFlow-lab

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.

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