Skip to content

[Bug]: Rust: a use rooted at a child module (pub use item::X;) still mints a stub that makes affected/explain ambiguous #4564

Closed

Description

@almothafar-maf

Pre-flight checks

  • I have checked the Troubleshooting section in the README

What happened?

Follow-up to #4439, which ef1d256 and 5faa05c fixed in 0.9.91 for use paths rooted at crate, self or super. A use whose path starts at a child module declared beside it, such as pub use item::Invoice; next to mod item;, is still not named for the stub rewire. When nothing else lends the rewire a node under the bare id, the imports_from edge dangles, build mints the external stub invoice, and affected and explain tie on it exactly as #4439 described.

Expected: the edge lands on Invoice in src/model/item.rs, no stub is minted, and both commands resolve the name.

Steps to reproduce

# Cargo.toml
[package]
name = "repro"
version = "0.1.0"
edition = "2021"
// src/lib.rs
pub mod model;

// src/model.rs
mod item;
pub use item::Invoice;

// src/model/item.rs
pub struct Invoice(pub u64);
graphify extract . --code-only --cargo --force
graphify cluster-only . --no-viz --no-label
graphify affected Invoice
graphify explain Invoice

Spelling the re-export pub use self::item::Invoice; makes both commands resolve.

Error output or graph output

graphify external stub imports_from target affected Invoice explain Invoice
0.9.62 none edge dropped resolves resolves
0.9.91, 0.9.92 id: invoice, label: invoice, type: external, source_file: "" invoice, the stub No unique node match for Invoice, exit 0 Ambiguous: 'Invoice' matches 2 nodes in different files., exit 3

Graphify version

0.9.91 and 0.9.92, the latest. 0.9.63 to 0.9.90 mint the same stub through #4439 itself, and 0.9.62 is not affected.

Operating System

macOS 26.5.2 (arm64)

Python Version

3.15.0

Installation Method

uv: uv tool run --from 'graphifyy==<version>' graphify …

Additional Environment Details

No provider environment variables are set, every run uses --code-only, and the repro is a clean directory holding only the four files above.

Additional context

From the 0.9.92 source:

  • graphify/extractors/rust.py, use_declaration branch: name_use sets _rust_use_name only when the path's first segment is crate, self or super, so item::Invoice gets none and extract.py lends the rewire no node for it.
  • A larger repro can hide this. When another file imports the same item from crate:: and that edge is still on the bare id at the rewire, its lent node rescues the re-export's edge too. On the private ~440-file Cargo workspace from [Bug]: Rust: use edges target a bare name, and since 0.9.63 the stub they leave makes affected/explain ambiguous #4439, those imports reach _rewire_unique_stub_nodes on per-file placeholder ids (<file>_rs_<name>) instead, so the re-export's edge stays alone on the bare id.
  • On that workspace, 90 names that 0.9.62 resolves still tie with an external stub on 0.9.92, down from 444 on 0.9.89. 83 of them are left on the stub only by child-module paths: 103 edges, all in a mod.rs, lib.rs or main.rs, 93 of them pub use re-exports.

A rule that should not reopen the serde::Serialize concern behind 5faa05c: also name a use whose first segment is a mod declared in the same scope, unless the path starts with ::. Rust resolves such a segment to the local module before a crate of that name. With serde as a dependency and mod serde { pub struct Local; } declared, use serde::Serialize; fails with E0432 on rustc 1.97.1, while use ::serde::Serialize; reaches the crate. Two details for the check: name_use strips a leading :: before it reads the first segment, and an inline mod block is a scope of its own, so a use serde::… inside mod tests { … } still names the crate.

Related: @liam-mcelhaney122's comment on #4439 covers the same re-export from the importing side (pub use item::Stack in core/src/lib.rs). #4134 would mask this for affected but not for explain.

Activity

  1. github-actions commented on Oct 19, 2026

    @github-actions

    Thanks for opening this issue, @almothafar-maf. A maintainer will take a look soon.

    If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.

    To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.

  2. Deepjyoti-Sarmah commented on Oct 19, 2026

    @Deepjyoti-Sarmah
    Contributor

    I reproduced this on v8 (86c57be) with your four files, and got the same result as your table. The edge is src_model -> invoice, the stub is invoice (external, empty source_file), affected Invoice exits 0 with "No unique node match", and explain Invoice exits 3. Spelling it self::item::Invoice resolves both.

    I have a fix on a branch, following the rule you proposed. In extractors/rust.py, name_use also sets _rust_use_name when the path's first segment is a mod declared beside the use in the same scope. A path that starts with :: is never named. The scope is the use node's siblings, so pub use item::Invoice; before mod item; works, and a use serde::… inside mod tests { … } doesn't see a top-level mod serde. _AST_CACHE_SCHEMA goes from 17 to 18.

    I checked what Rust itself does with cargo check on rustc 1.95.0, using a stand-in serde crate:

    • mod serde plus use serde::Serialize; in the same scope fails with E0432, as a file module and as an inline module.
    • use ::serde::Serialize; beside mod serde reaches the crate.
    • use serde::Serialize; inside mod tests { … } with a top-level mod serde reaches the crate.

    A separate session that didn't write the fix ran 20 cases against v8 and the branch:

    • These now land on the real declaration with no stub: your repro, the use written before the mod, a group item::{Invoice, Receipt}, a nested item::sub::Credit, an inline mod outer { mod item { … } pub use item::Invoice; }, pub(crate) use, and as Bill.
    • These are unchanged and never land on a corpus Serialize: use ::serde::Serialize;, use ::serde::{Serialize};, the mod tests inner scope, and a plain use serde::Serialize;.
    • Two different Invoice types, each re-exported from its own parent, stay two nodes. Neither import lands on the other's type.

    Limits:

    1. An incremental graphify extract (without --force) skips files whose content hash matches manifest.json before it reads the AST cache. The schema bump never reaches them, so an existing graph keeps the stub until --force or an edit to the file. This affects every schema bump, [Bug]: Rust: use edges target a bare name, and since 0.9.63 the stub they leave makes affected/explain ambiguous #4439's included, because the manifest doesn't record the extractor version or schema.
    2. _make_id("Invoice as Bill") puts the alias in the raw target id (invoice_as_bill), for crate::, self:: and bare paths alike. It's invisible whenever the rewire resolves the edge.
    3. A use inside a fn body gets no edge, because the extractor doesn't visit fn bodies. r#item paths are not matched. Both behave the same as on v8.

    New tests are in tests/test_rust_use_trees.py: 10 fail and 5 pass on v8, and all 15 pass on the branch. The 5 that pass on v8 guard the serde cases, the second-import case and the two-types case. The full suite passes: 8407 passed, 14 skipped. I'll open the PR unless you'd rather go a different way.

  3. safishamsi commented on Oct 20, 2026

    @safishamsi
    Member

    Fixed in 0.9.93 by #4567. A Rust use rooted at a sibling mod now reaches its declaration. Thanks @almothafar-maf for the report. Live on PyPI: https://pypi.org/project/graphifyy/0.9.93/

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