Skip to content

TypeScript extraction emits internal edges whose endpoint nodes are absent #2143

Open

Description

@kitsupanic

Environment

  • graphifyy 0.9.25
  • macOS
  • Expo / React Native TypeScript corpus
  • code-only, undirected, no clustering

Reproduction

graphify extract . --out /tmp/graphify-diagnostic --no-cluster --code-only --force
graphify diagnose multigraph \
  --graph /tmp/graphify-diagnostic/graphify-out/graph.json \
  --undirected --max-examples 20

After excluding 93 intentional external ref_* imports covered by #2130 and #2132, 21 missing endpoints remain. Every missing ID is internal to the scanned corpus.

Observed internal failures

Nested function callers are referenced but never emitted (8 edges)

One TSX component emits six calls edges whose source is a nested capture callback ID. No node with that ID exists, while the targets are valid emitted nodes. The callback calls functions for region/date ordering, preview cropping, crop variants, candidate extraction, vertical-region filtering, and translation.

The same pattern occurs for nested submit and changeDate functions in another TSX component.

Representative fixture shape:

import { target } from "./target";

export function Component() {
  const capture = async () => target();
  return null;
}

Test-file caller IDs use an absent absolute-path form (6 edges)

Six indirect_call edges use source IDs shaped like:

users_<redacted>_src_components_additemscreen_test_tsx

The actual emitted file node uses a root-relative ID shaped like:

src_components_additemscreen_test

Equivalent mismatches occur across four TSX component test files.

Imported internal constants/assets have no target node (7 edges)

Missing targets represent:

  • four exported scalar constants imported from TypeScript service modules
  • one imported JSON asset
  • two generated migration imports (a SQL module and migration journal JSON)

Representative fixture shapes:

// constants.ts
export const LIMIT = 14;

// consumer.ts
import { LIMIT } from "./constants";
import data from "./data.json";

Expected behavior

Every non-external edge endpoint should resolve to an emitted node. Depending on the construct, Graphify could:

  1. emit a node for the nested function, constant, asset, or generated module;
  2. anchor the edge to the nearest emitted containing function or file node; or
  3. omit the edge rather than emit structurally invalid extraction data.

Absolute-path caller IDs should be normalized consistently against the extraction root.

Impact

build_from_json() drops all 21 edges, removing genuine internal call and import relationships. After #2132 classifies external imports separately, these remain as true dangling_endpoint_edges.

Suggested regression assertions

  • A nested TS/TSX callback calling an imported function has an emitted source endpoint.
  • Test-file indirect_call edges use the same root-relative file ID as the emitted file node.
  • Imported scalar constants and JSON/generated modules resolve to nodes or their containing file.
  • After excluding classified external imports, dangling_endpoint_edges == 0.

I can provide reduced real cache entries for these three patterns if useful.

Activity

  1. itsLucas02 commented on Oct 14, 2026

    @itsLucas02

    Adding a small Windows reproduction on graphifyy 0.9.89 for the nested-function omission described here. This fixture emits neither nested function nor their call edge; it does not produce a dangling endpoint in the raw output, so it isolates the missing-node part of the report.

    Environment

    • Windows 11, version 10.0.26340, x64
    • Python 3.13.11
    • graphifyy==0.9.89
    • tree-sitter==0.25.2
    • tree-sitter-typescript==0.23.2
    • Local AST-only extraction; no semantic backend

    Minimal reproduction

    Create repro/index.ts:

    export function createQueueTools() {
      const updateVisitAfterCall = (visitId: string) => visitId;
      const callVisit = async (visitId: string): Promise<string> => {
        return updateVisitAfterCall(visitId);
      };
      return { callVisit };
    }

    Run:

    graphify extract ./repro --code-only --no-cluster

    Inspect repro/graphify-out/graph.json:

    import json
    
    with open("repro/graphify-out/graph.json", encoding="utf-8") as f:
        graph = json.load(f)
    
    print([(node["id"], node["label"]) for node in graph["nodes"]])
    print(graph.get("edges", graph.get("links", [])))

    Observed

    Extraction succeeds and reports 2 nodes, 1 edge. The raw output contains:

    {
      "nodes": [
        {"id": "index", "label": "index.ts"},
        {"id": "index_createqueuetools", "label": "createQueueTools()"}
      ],
      "edges": [
        {
          "source": "index",
          "target": "index_createqueuetools",
          "relation": "contains"
        }
      ]
    }

    The JSON above is a projection of the actual output, omitting unrelated attributes. There is no node for callVisit or updateVisitAfterCall, and no call edge between them.

    Expected and impact

    For symbol-level caller analysis, I expected nodes for both named nested arrow functions and a calls edge from callVisit to updateVisitAfterCall (the source call is on line 4).

    The same shape occurs in a real TypeScript hook: the outer hook and top-level helpers are represented, but its named nested handlers are absent, preventing tracing that handler-to-helper relationship. The reproduction above is self-contained and includes no proprietary application source.

    Is omission of named nested typed-arrow functions an intended scope limitation, or should this be covered by the extraction fix for this issue? A regression asserting both nested nodes and their source-located call edge would cover this case.

    Prepared with AI assistance; the version information and emitted nodes/edge were checked against a local run.

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