Skip to content

TypeScript external named-import calls resolve to unrelated local same-name functions #330

Open

Description

@klaatu-code

Summary

For TypeScript/TSX, a call through a named import from an external package can be resolved to an unrelated same-named function inside the repository.

This produces a false calls edge rather than an undercount. graft callers, repo-map hotspots, and transitive blast-radius walks can therefore report that production code depends on a test helper or mock that it never imports.

Because traversal does not expose inferred confidence, these false edges are indistinguishable from extracted ones. The graph therefore cannot be trusted for repository orientation or change-impact decisions without manually verifying the underlying imports and source.

Environment

  • @nanonets/graft 0.16.0
  • macOS arm64
  • Node.js 24.17.0
  • Also confirmed in current main at 3a85a2f40e283ac0c9b617d9a4bead18d73c3c70

Minimal reproduction

Create two TypeScript files:

// src/component.ts
import { useRouter } from "next/navigation";

export function Component() {
  return useRouter();
}
// test/next-navigation-mock.ts
export function useRouter() {
  return { push() {} };
}

Then run:

graft build .
graft callers useRouter --json --no-refresh

Actual behavior

The local test/next-navigation-mock.ts#useRouter symbol receives an incoming calls edge from src/component.ts#Component, even though that file imports useRouter from the external next/navigation package.

The same pattern occurs with test-framework imports: calls to expect() imported from vitest can be attached to an unrelated unique local helper named expect.

Expected behavior

The external call must not resolve to the local mock/helper. If the imported module cannot be resolved to a file in the repository, the call should remain external/unresolved or be omitted.

Root cause

The extractor already collects TypeScript named-import bindings, including the local name, exported name, and module specifier:

That provenance is used for non-call references, but it is not attached when a call edge is captured. The raw call keeps only the bare callee name, member flag, and source file:

The resolver then sends the bare name through resolveName(). If there is exactly one same-family function with that name anywhere in the repository, it becomes the target with confidence: "inferred":

This is especially misleading because traversal drops edge confidence. EdgeHit and the JSON/human callers output do not expose that the edge was inferred:

Why this matters

  • A unique collision is treated as a positive match; adding a second unrelated local definition would instead make the edge disappear as ambiguous.
  • Test helpers and mocks can become false production hubs.
  • --depth all can propagate the false edge into a much larger reported blast radius.
  • The normal and JSON outputs look as authoritative as extracted edges, so callers cannot filter or audit inferred results.

Suggested fix

  1. Preserve import provenance on bare call edges. If the callee identifier is a named import, carry its exported name and module specifier into RawEdge.
  2. Resolve that call only within the resolved in-repo target module. If the specifier is external or unresolved, do not fall back to the repo-wide name index.
  3. Respect lexical shadowing: a same-file local binding should win when it actually shadows the import.
  4. Preserve confidence through EdgeHit, CLI JSON/human output, MCP results, and transitive walks so any remaining inferred edges are visible and filterable.

Suggested regression cases:

  • external named import colliding with one local function
  • in-repo named import with another same-named function elsewhere
  • aliased named import
  • local lexical binding shadowing an import
  • callers output preserving extracted versus inferred confidence

Related issue

#68 discusses opt-in inferred calls for C/C++ and explicitly requires confidence labeling. It does not cover JS/TS named-import provenance or this external-import false-positive behavior.

Activity

  1. added a commit that references this issue on Sep 12, 2026
    1e62f87
  2. added 2 commits that reference this issue on Sep 16, 2026
    f6435a5
    ae683a1
  3. klaatu-code commented on Oct 5, 2026

    @klaatu-code
    Author

    I'm honestly getting pretty worried about this. I reported #330 and #329 on September 9, and nearly a month later both are still open, with the relevant behaviour still present on main.

    These bugs hit the two things I need to trust before using Graft seriously:

    What is happening with these fixes? #335 and #336 are still open, currently mergeable, and have no formal reviews. #335 also has independent validation and added integration tests in the discussion. #333 is still open and now has conflicts, and explicitly covers only the suppression-flag part of #329.

    I understand that maintaining an open-source project takes time. But these are core trust issues, and the lack of a maintainer response on the two reports makes it hard to tell whether they're being prioritised at all. A context graph that can confidently report dependencies that aren't there seriously calls its usefulness for agent navigation and change-impact decisions into question. An init preview that leaves out machine-wide removals makes it hard to trust the setup process too.

    Graft has a lot of potential. That's exactly why this is so frustrating — I want to be able to rely on it, but right now I still have to manually verify the graph's claims and the changes init might make.

    Could a maintainer please clarify whether you accept the proposed fixes, what is blocking their review/merge, and whether there is a plan to address the remaining dry-run omissions? If the current approach isn't acceptable, please say what needs to change. At this point a clear answer would help a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions