Skip to content

fix(resolution): resolve cross-file static method calls to the method, not the class (#825)#833

Open
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/825-static-method-calls
Open

fix(resolution): resolve cross-file static method calls to the method, not the class (#825)#833
maxmilian wants to merge 1 commit into
colbymchenry:mainfrom
maxmilian:fix/825-static-method-calls

Conversation

@maxmilian

Copy link
Copy Markdown
Contributor

Fixes #825.

Problem

A cross-file static method call resolves to the class, not the method, and is then recorded as a construction:

// 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); }

codegraph callers "Foo.bar" → empty. run ends up with a single instantiates → Foo edge and no calls → Foo::bar, so callers/impact for the static method are empty (false negatives) and the CLI's bare-name fallback surfaces unrelated same-named symbols (false positives).

Root cause

In resolveViaImport (src/resolution/import-resolver.ts), the generic named-import loop matches ref.referenceName.startsWith(imp.localName + '.') for the ref Foo.bar. For a non-namespace import memberName is left null, so findExportedSymbol returns the class Foo. resolveViaImport returns that at confidence 0.9, and resolveOne returns immediately — so the name-matcher's matchMethodCall (which already resolves Foo.barFoo::bar via its class-name strategy) never runs. createEdges then promotes the calls edge to instantiates because the target is a class, dropping the method.

So the calls → instantiates promotion is the relabel; the actual interception is the import resolver picking the class as the target. Guarding the promotion alone would leave the edge pointing at the class with the method still dropped — hence the fix is in the import resolver.

Fix

When the imported receiver is a class/struct and the calls ref is shaped localName.member, resolve the trailing member to that class's method (keyed on the exact owner segment of the qualifiedName, so a substring-named sibling like FooBar::bar can't soak the call) and return it — preserving the exact-file precision the import gives over a name-match fallback. If no matching method exists, behavior is unchanged. Genuine new Foo() construction is unaffected (still instantiates).

Tests

Added a regression test in __tests__/resolution.test.ts that locks:

  • run --calls--> Foo::bar (exactly one edge), not mis-promoted to instantiates
  • the call is not soaked into the substring-named sibling FooBar::bar
  • getCallers(Foo::bar) surfaces run
  • the new Foo() boundary still instantiates the class

Full suite green locally (npm run build && npm test): 1429 passed, 2 skipped.

🤖 Generated with Claude Code

…, not the class (colbymchenry#825)

`Foo.bar()` after `import { Foo } from './helpers'` was a `calls` ref whose
named-import resolution matched the `Foo.` prefix and resolved the receiver to
the class `Foo`. createEdges then promoted the `calls` edge to `instantiates`
on the class and dropped the method, so callers/impact for the static method
came back empty and a bare-name fallback surfaced unrelated same-named symbols.

When the imported receiver is a class/struct and the trailing member names one
of its methods, resolve to that method — keyed on the exact owner segment of
the qualifiedName so a substring-named sibling (`FooBar::bar`) can't soak the
call. Genuine `new Foo()` construction still instantiates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maxmilian maxmilian marked this pull request as ready for review June 12, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant