Skip to content

Rust: a type used above its declaration stays a sourceless stub when the name is defined in 2+ files #3782

Description

@sayajin101

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:

  1. 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.
  2. 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.

Activity

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