Skip to content

Cross-module references to functions land on a name-only stub node — _rewire_unique_stub_nodes excludes function labels #1781

Description

@EmilNyg

Summary

For a function defined in one module and referenced from other modules, the extractor produces two nodes: the resolved definition (with source_file/source_location) and an unresolved bare-name stub (source_file: ""). All cross-module references edges attach to the stub, never to the definition — so "who references this function" queries against the definition node return nothing.

Observed on graphifyy 0.8.46 (PyPI), Python extraction, on a large FastAPI codebase: a dependency-injection function referenced from 600+ files had zero incoming edges on its definition node, while its bare-name stub held all ~670 reference edges. Class/type symbols do not suffer from this — only functions/methods.

Minimal repro sketch

pkg/dependencies.py:   def get_db(): ...
pkg/endpoint.py:       from pkg.dependencies import get_db
                       def route(db: Annotated[Session, Depends(get_db)]): ...

Extract → graph.json contains dependencies_get_db (definition, 0 in-refs) and a sourceless stub get_db holding the references edge from route().

Root cause (traced in 0.8.46 source)

  1. extract.pyensure_named_node (~L2464–2485): a referenced name not defined in the current file falls back to a bare-name node id + a sourceless stub node — deliberately deferring to a corpus-level rewire pass (per the code comment).
  2. That pass, _rewire_unique_stub_nodes (~L7706–7744), gates merge candidates through _is_type_like_definition (~L7695–7703), which returns False for any label ending in ). But the extractor labels every function/method name() (~L3138) — so function definitions can never be rewire targets; only class/type-like stubs are repaired.
  3. Nothing downstream compensates: the Python symbol-resolution facts only cover call expressions inside function bodies (not annotation/argument references, ~L8743–8757); _resolve_cross_file_imports also excludes ()-labels (~L8832); dedup.py intentionally never label-merges file_type == "code" nodes (--update / build_merge dedup merges distinct same-named code symbols #1205) nor sourceless groups (Bug Report — graphify --update performs a destructive, net-negative fuzzy node merge on an already-current code graph #1178).

_node_label_key already strips non-alphanumerics, so the stub get_db and the definition label get_db() produce the same key — the only thing blocking the merge is the _is_type_like_definition filter.

Suggested fix directions

  1. Resolve imports before minting reference targets: give ensure_named_node access to the file's from X import name [as alias] table (tree-sitter already parses these) and mint the file-qualified id when the referenced name is a known import — mirroring what the import-guided call resolution already does for calls edges.
  2. Or widen the post-pass: in _rewire_unique_stub_nodes, allow function definitions (labels name() / .name()) as merge candidates when the label-key match is globally unique. Optionally restrict to stubs whose referencing files import that exact name, to keep same-name-different-module cases safe (the same evidence the Java shadow-stub repair uses).

Happy to provide more detail or test a patched build against our corpus. Thanks for the tool — the graph is genuinely useful for agent-driven code orientation; this is the one gap we had to work around client-side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions