What happened?
In Rust, a type reference that appears above the type's declaration in the same file becomes a sourceless stub node. That is expected at first: ensure_named_node in extractors/rust.py only resolves names already in seen_ids, so a forward reference looks the same as a cross-file one.
The stub is never folded back if another file defines a type with the same name. _rewire_unique_stub_nodes in extract.py rewires a stub only when exactly one definition with that label exists in the whole corpus. As a result the edge points at a stub instead of the struct defined a few lines further down in the same file.
The practical effect is that graphify affected and graphify explain on the real struct silently miss those users. It is common in any codebase with parallel modules: per-platform implementations, v1/v2 modules, or a type mirrored between a Rust backend and a TS frontend. In our ~600-file Tauri repo (a macOS and a Windows engine that share type names), it leaves 140 stubs holding 300 edges. Every one of them is for a label defined in two or more files, and none is for a unique label, whose stubs do get rewired. One struct's biggest consumer (a function taking it as a parameter, declared above the struct) is missing from affected entirely.
Steps to reproduce
mkdir repro && cd repro && git init -q && mkdir src
cat > src/engine_a.rs <<'EOF'
fn before(s: &Sink) {}
struct Sink {
n: u32,
}
fn after(s: &Sink) {}
EOF
cat > src/engine_b.rs <<'EOF'
struct Sink {
n: u32,
}
EOF
graphify update .
Then list the Sink nodes and the references edges into them from graphify-out/graph.json.
Error output or graph output
With both files:
node src_engine_a_sink | source_file='src/engine_a.rs' L3
node src_engine_b_sink | source_file='src/engine_b.rs' L1
node sink | source_file=''
edge src_engine_a_after -> src_engine_a_sink L7
edge src_engine_a_before -> sink L1 <- should be src_engine_a_sink
Control, with src/engine_b.rs deleted and the graph rebuilt from scratch: the stub is rewired as expected.
[('src_engine_a_sink', 'src/engine_a.rs')]
[('src_engine_a_after', 'src_engine_a_sink'), ('src_engine_a_before', 'src_engine_a_sink')]
In a larger corpus the stubs also collide across files, and _disambiguate_colliding_node_ids bakes the file stem into their IDs (..._engine_a_rs_sink). They then look like a third real Sink in explain.
Graphify version
0.9.66 (graphifyy from PyPI)
Environment
macOS 27.0 arm64, Python 3.11.13 (uv tool install). The code path is platform-independent.
Additional context
Two possible fixes. Either one alone would resolve the repro:
- Same-file forward references: pre-scan a file's top-level type declarations (
struct_item, enum_item, trait_item, type_item, union_item) before the main walk, so ensure_named_node resolves them regardless of order.
- Ambiguous labels: when a stub's label has several definitions, prefer the one whose
source_file equals the stub's origin_file (already recorded on the stub) before giving up.
The first fixes it at the source. The second is a smaller change in the rewire pass. Both cover only the same-file case, which seems right: a genuine cross-file reference to an ambiguous name has no single correct target, so it should stay a stub.
Related: #1402 (closed) fixed a different cause of the same phantom-stub symptom (sourced stubs blocking the rewire). This one is sourceless stubs that the uniqueness rule declines.
What happened?
In Rust, a type reference that appears above the type's declaration in the same file becomes a sourceless stub node. That is expected at first:
ensure_named_nodeinextractors/rust.pyonly resolves names already inseen_ids, so a forward reference looks the same as a cross-file one.The stub is never folded back if another file defines a type with the same name.
_rewire_unique_stub_nodesinextract.pyrewires a stub only when exactly one definition with that label exists in the whole corpus. As a result the edge points at a stub instead of the struct defined a few lines further down in the same file.The practical effect is that
graphify affectedandgraphify explainon the real struct silently miss those users. It is common in any codebase with parallel modules: per-platform implementations,v1/v2modules, or a type mirrored between a Rust backend and a TS frontend. In our ~600-file Tauri repo (a macOS and a Windows engine that share type names), it leaves 140 stubs holding 300 edges. Every one of them is for a label defined in two or more files, and none is for a unique label, whose stubs do get rewired. One struct's biggest consumer (a function taking it as a parameter, declared above the struct) is missing fromaffectedentirely.Steps to reproduce
Then list the
Sinknodes and thereferencesedges into them fromgraphify-out/graph.json.Error output or graph output
With both files:
Control, with
src/engine_b.rsdeleted and the graph rebuilt from scratch: the stub is rewired as expected.In a larger corpus the stubs also collide across files, and
_disambiguate_colliding_node_idsbakes the file stem into their IDs (..._engine_a_rs_sink). They then look like a third realSinkinexplain.Graphify version
0.9.66 (
graphifyyfrom PyPI)Environment
macOS 27.0 arm64, Python 3.11.13 (uv tool install). The code path is platform-independent.
Additional context
Two possible fixes. Either one alone would resolve the repro:
struct_item,enum_item,trait_item,type_item,union_item) before the main walk, soensure_named_noderesolves them regardless of order.source_fileequals the stub'sorigin_file(already recorded on the stub) before giving up.The first fixes it at the source. The second is a smaller change in the rewire pass. Both cover only the same-file case, which seems right: a genuine cross-file reference to an ambiguous name has no single correct target, so it should stay a stub.
Related: #1402 (closed) fixed a different cause of the same phantom-stub symptom (sourced stubs blocking the rewire). This one is sourceless stubs that the uniqueness rule declines.