Skip to content

TypeScript: calls via namespace import or aliased named import produce no calls edge #3346

Closed

Description

@munishjindal931

TypeScript/JS: calls through a namespace import or an aliased named import produce no calls edge

Version: graphifyy 0.9.54 (uv tool install), macOS, Python 3.13

Summary

The TypeScript extractor resolves a call to a function imported by name (import { foo } → foo()), but not the same call reached through either

  • a namespace import — import * as api → api.foo(), or
  • an aliased named import — import { foo as bar } → bar().

No calls edge is emitted in either case. Both look like the same root cause: the call is resolved on the callee identifier as written, rather than on the import binding it refers to.

This fails quietly. Extraction succeeds, nodes for both the caller and the callee exist, and only the edge between them is missing — so nothing in GRAPH_REPORT.md or the Step 4.5 health check flags it, and the graph reads as complete while an entire layer is disconnected. The visible symptom is the opposite of an error: functions that are heavily used appear to have no callers, which reads as dead code.

Minimal reproduction

Four files:

// api.ts
export function fetchThing(id: string): string { return id; }
export function otherThing(id: string): string { return id; }
// namespace.ts
import * as api from './api';
export function useNamespaceCall(id: string) { return api.fetchThing(id); }   // no edge
// named.ts
import { otherThing } from './api';
export function useNamedCall(id: string) { return otherThing(id); }           // edge
// alias.ts
import { otherThing as renamed } from './api';
export function useAliasCall(id: string) { return renamed(id); }              // no edge

Run:

from graphify.extract import collect_files, extract
from pathlib import Path

res = extract(collect_files(Path('repro')), cache_root=Path('repro'))
print('nodes:', sorted(n['label'] for n in res['nodes']))
for e in res['edges']:
    if e.get('relation') == 'calls':
        print('CALLS:', e['source'], '->', e['target'])

Actual:

nodes: ['alias.ts', 'api.ts', 'fetchThing()', 'named.ts', 'namespace.ts', 'otherThing()',
        'useAliasCall()', 'useNamedCall()', 'useNamespaceCall()']
  CALLS: named_usenamedcall -> api_otherthing
calls edges: 1

Expected: three calls edges. All six function nodes extract correctly; only the two binding-mediated edges are absent.

Impact on a real corpus

A TypeScript monorepo (~330 files, 1967 nodes, 4459 edges) whose shared package routes its data layer through one barrel module:

  • 18 of 126 functions in packages/shared/src/api/ had any inbound calls edge — 14%.
  • The 18 that resolved were exactly those called from feature code by plain named import. The other 108 are reached only from the package's own hook layer, which uses import * as api — 114 call sites across two files.
  • Net effect: the hooks → api layer was invisible. graphify query traversals through it under-reported silently, and "what calls this function?" returned nothing for 86% of the module.

The triggering pattern — a barrel module plus a wrapper layer calling api.someFn(client, …) — seems common enough that I doubt this corpus is unusual.

Workaround, and what it suggests about a fix

A post-processing pass that rebuilds the import bindings textually and re-attaches the edges takes this corpus from 18/126 to 123/126. The three still showing no callers turned out to be genuine dead code — which is the answer the graph should have been giving all along, and was the point of asking.

Three details from writing it that may be worth folding into a real fix:

  • Calls split across lines. api\n .saveLoanPayment(sb, …) appears in formatted/chained code (4 of the 114 sites above). A line-oriented matcher misses these.
  • Aliased named imports are the same bug, per the repro. A fix that carries a binding table should close both at once.
  • Re-export barrels. import * as api from '../api' resolves through api/index.ts to sibling modules, so the target search has to span the barrel's directory rather than just the file the specifier names.

Notes

  • Only the calls relation is affected. imports / imports_from edges to the module are emitted normally, so the file-level graph looks correct.
  • Untested whether this extends to other member-expression calls (obj.method()) or to other languages — the repro covers only the two TS import forms above.

Possibly a second issue — happy to split out

create function bodies are not descended into for table references, so a plpgsql function emits contains from its migration file but no reads_from to anything it queries. In this corpus public.estimate_capital_gain() — a 216-line returns table (…) body reading eight tables and views — had zero reads_from edges. 23 of 51 SQL functions in the corpus do emit them, so it looks shape-dependent rather than missing outright. The same post-processing pass recovers 156 such edges corpus-wide by scanning from/join inside each function body and keeping only targets matching an already-extracted relation node, which incidentally filters out plpgsql locals and CTE aliases.

Say the word and I'll open that separately with its own repro.

Activity

  1. safishamsi commented on Sep 12, 2026

    @safishamsi
    Member

    Fixed in v0.9.60 by #3517 (thanks @abhay-codes07) — calls in exported functions and aliased imports resolve. Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.60

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