Skip to content

TypeScript: unresolved calls bound to same-named exports in unimported packages → phantom cross-package calls edges (conf 0.8) #1659

Closed

Description

@leonaburime-ucla

Summary

graphify's call resolver attributes an unresolved call to a same-named export in an unrelated package that was never imported, producing a phantom cross-package calls edge at confidence 0.8. On a monorepo this fabricates dependency relationships that don't exist.

Minimal repro

Two packages, no import between them:

pkg-a/src/index.ts (imports nothing, defines validate nowhere):

declare function validate(x: number): boolean; // ambient/unresolved locally
export function run(x: number): boolean {
  return validate(x); // pkg-b is never imported
}

pkg-b/src/index.ts:

export function validate(name: string): boolean { return name.length > 0; }

Run:

graphify update <dir> --no-cluster

Observed: a cross-package edge pkg-a.run() --calls--> pkg-b.validate() (confidence 0.8).
Expected: no cross-package edge — pkg-a has no import path to pkg-b, so the call cannot resolve there. It should stay unresolved (or ambient) rather than being bound to an unrelated package's export by name alone.

Real-world impact

On a ~14-package TypeScript monorepo (Apache-2.0, open-design), this produced edges like platform → registry-protocol and sidecar → registry-protocol even though platform and sidecar import nothing (verified against package.json and every import statement). registry-protocol exports many generically-named symbols (*Schema, etc.), so ambiguous/unresolved calls across the repo collapsed onto it — it appeared "depended on" by 14 packages spuriously. A reader trusting the graph would conclude a dependency direction that does not exist; only cross-checking against source caught it.

Likely root cause

When a call has no local definition and no matching import, the resolver appears to fall back to any same-named exported symbol repo-wide, and emits the edge at 0.8 rather than leaving it unresolved. The fallback isn't gated on an actual import/module-resolution path existing between the two files.

Suggestion

Gate cross-file/cross-package calls attribution on there being a real import (or module-resolution) path from caller to callee; when the only candidate lives in a package with no such path, leave the call unresolved (or emit it as ambiguous/inferred at markedly lower confidence) instead of a 0.8 calls edge. A diagnose-style warning for "cross-package edge with no supporting import" would also help users spot these.

Related

Environment

  • graphify 0.8.50
  • TypeScript sources

Activity

  1. safishamsi commented on Jul 4, 2026

    @safishamsi
    Member

    Fixed in 62b8eb1 (on v8, ships in 0.9.7). Thanks for the precise root-cause and the real-world open-design example — that made the fix straightforward.

    Your diagnosis was exactly right: when a callee had a single same-named definition repo-wide, the cross-file resolver bound to it at INFERRED/0.8 with no import path required. The fix takes your first suggestion, scoped by language: JS/TS modules have no implicit cross-module scope, so a direct cross-file calls edge is now gated on import evidence (a real import/module-resolution path from caller to callee) and left unresolved otherwise. Your pkg-a.run() -> pkg-b.validate() repro now produces no edge; a genuinely-imported call still resolves (and is promoted to EXTRACTED).

    Scope notes:

    • Other languages keep the existing single-candidate resolution — C/C++ headers, Ruby autoload, and same-package implicit scope legitimately call across files without an explicit import, so gating them would drop real edges.
    • The indirect_call path (callbacks passed by name) is unchanged; it was already INFERRED and gated on the target being a real callable.
    • While in there I also hardened the caller/candidate → file mapping so a path-resolution/symlink mismatch can't spuriously fail import evidence — a side benefit is that more legitimately-imported cross-file calls now land as EXTRACTED rather than INFERRED.

    A diagnose-style 'cross-package edge with no supporting import' warning is a nice idea for a follow-up; leaving it out of this fix since the edges are no longer emitted in the first place.

  2. added a commit that references this issue on Jul 6, 2026
  3. added a commit that references this issue on Jul 18, 2026
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