Skip to content

fix(rust): a module a #[path] attribute loads is the module that declares it - #3064

Merged
colbymchenry merged 1 commit into
mainfrom
claude/rust-path-attribute-modules
Oct 30, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/rust-path-attribute-modules

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Builds on #1892 by @seonghobae, on the module walk of #3014 and the cross-crate paths of #3025: a Rust module loaded with #[path = "…"] is now the module that declares it.

  • fast-mlsirm's #[path = "../../../tests/unit/scaling_tests.rs"] mod tests; in crates/core/src/scaling.rs: the test file's use super::* brings in scaling.rs's items, private ones included, so predict_rating_multi(1) links to scaling.rs's and not to another crate's namesake; its crate:: is core's.
  • From the declaring side, a path through such a module reaches the file it names, even when several files load that file: each of dioxus's headless tests declares #[path = "./utils.rs"] mod utils; and calls utils::mock_event(…).
  • tokio's atomic_u64_as_mutex.rs loads atomic_u64_static_const_new.rs (or …_once_cell.rs) with a #[path]: their use super::AtomicU64 is its AtomicU64.
  • The path is read as the Rust Reference reads it: from the declaring file's directory, or inside an inline mod from that module's own. A module whose cfg alternatives each name a file (tokio's process::imp: unix/mod.rs or windows.rs), or whose path a cfg_attr sets, depends on the build and stays unknown; a path that leaves the project (escapesRoot) names no file.
  • An example, test or bench that Cargo.toml places at a path of its own ([[example]] path = "examples/07-fullstack/handling_errors.rs") is its own crate's root, like [lib]/[[bin]] paths since fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules #3014.

Root cause

rustSuperModule, rustFileModule, rustCrateRootModule and the visibility checks placed every file by its directory, and rustSubmodule returned nothing for a module with a #[path]. A #[path] file's directory says nothing about where it sits in the module tree: tests/unit/scaling_tests.rs is scaling.rs's mod tests.

Fix

  • rustPathDeclsIn: a file's own #[path] declarations, read from its text (strings and comments blanked) after a byte check for path, with each target resolved per the Reference.
  • Declaring side, rustSubmodule: a module declared once with a #[path] is the file it names.
  • File side, rustPathClaim: a file the regular rules don't place is the module of the one #[path] declaration that loads it, if exactly one does. rustPlacedByName places crate roots, files with a fn main / #![no_main] of their own (trybuild cases, fuzz targets), and files a plain mod name; declares in the module owning their directory or in an inline module of one above it (dioxus's mod components { pub mod home; }). Only for a file left over does the resolver read the project's Rust files that write path = for their declarations, once per resolver. With a claim, rustSuperModule is the declaring module (the inline one around the attribute, if any), rustFileModule owns the file's directory (rustc reads every #[path] file as a mod.rs), rustCrateRootModule is the declaring file's crate, and rustModuleWithin, isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the declaring module.
  • A path's 2015 crate-root fallback no longer reaches past a module the file declares with a #[path] to a namesake at the crate root.
  • rustCrateGraph: [[example]], [[test]] and [[bench]] paths are target roots, kept inside the project through the same join.
  • The import resolver's super is the same walk since fix(rust): use bindings follow Rust's scopes, and module paths walk the crate's modules #3014.

Validation

  • Tests: fix(rust): resolve use super::* callees in #[path] modules #1892's rust-path-super-glob.test.ts (credited) and rust-path-attribute-modules.test.ts (private items through use super::*, crate::, an inline module's own directory, a helper two tests load, a folder named ..generated, an example at its own path, cfg alternatives and cfg_attr left unknown): on main all 8 fail; all pass. Every rust-* suite passes.

  • A/B against main, native kernel, the 7 Rust repos, fast-mlsirm's Rust files and 2 controls:

    repo added retargeted same item, now in scope removed
    fast-mlsirm (Rust files, 115 #[path] modules) 2,796 71 87 0
    dioxus 27 0 0 0
    tokio 2 0 0 0
    ripgrep, axum, clap, serde, gleam 0 0 0 0
    express, flask 0 0 0 0

    fast-mlsirm: 1,839 calls, 427 references, 334 constructions and 196 imports added, mostly names use super::* brings in from the declaring module; retargets are the declaring module's own items (exposure_tests.rs's Lcg::next_f64 from cdm_tests.rs's private Lcg to exposure.rs's, the declaring module's gh_rule and helpers instead of bifactor_grm.rs's); the 87 reach the same item as before at confidence 0.9 instead of a 0.4 name guess. dioxus: the five headless tests' utils::… calls into their shared helper. tokio: the two use super::AtomicU64. Sampled and checked; every other repo is site-for-site identical.

  • Cost (native kernel, medians of 3 interleaved runs):

    repo CPU wall peak RSS
    fast-mlsirm (Rust files) 15.3 → 15.8 s 4.2 → 5.2 s 825 → 882 MB
    tokio 26.1 → 26.7 s 10.4 → 11.2 s 1,023 → 1,028 MB
    dioxus 34.8 → 35.6 s 10.3 → 11.2 s 1,125 → 1,120 MB
    gleam 24.1 → 23.8 s 6.6 → 6.7 s 1,027 → 1,023 MB

    The extra is reading the project’s #[path] declarations once per resolver: the main thread and each pool worker that reaches a file the regular rules don’t place (tokio’s atomic_u64_*.rs and process/unix/mod.rs, dioxus’s headless-test helper, fast-mlsirm’s test files). On this Windows machine that read is about 0.4 ms a file (file opens), 0.1–0.6 s per resolver; axum and ripgrep never do it, serde and gleam once on the main thread. With the resolver pool off (measured one revision earlier), fast-mlsirm is 9.8 → 10.4 s CPU, 4.7 → 5.0 s wall and 513 → 512 MB.

  • Full suite (native kernel rebuilt from main, CODEGRAPH_KERNEL_EXPECT=1, daemon allowed, 4 workers) on this commit, on 2d98c1b: 815 files (809 passed, 6 skipped); 9,690 tests: 9,604 passed, 0 failed, 86 skipped.

Left out

  • A file several #[path] declarations load (a shared test helper) has no single module, so its own super:: and crate:: stay unknown; calls into it from each declaring file resolve.
  • Modules declared by a macro (serde_core's crate_root!) or by automod::dir! (clap's test suites) are not read as declarations, so those files still send the resolver to read the project's #[path] declarations once; they keep their directory's module.
  • Each resolver pool worker reads the declarations for itself; sharing the main thread's would take the per-worker read off the repos that have #[path] modules.

🤖 Generated with Claude Code

…ares it

A Rust module loaded with `#[path = "…"]` was found by its file's directory:
fast-mlsirm's `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`
in `crates/core/src/scaling.rs` made `tests/unit/scaling_tests.rs`'s
`use super::*` walk to a `tests/unit` module that doesn't exist, so its
`predict_rating_multi(1)` linked to nothing, or to a same-named function of
another crate; its `crate::` had no crate, and the parent's private items
looked out of its reach. From the declaring side, a path through such a
module (`utils::mock_event(…)` under `#[path = "./utils.rs"] mod utils;`)
reached nothing.

The fix reads each file's `#[path]` declarations as the Rust Reference
reads the attribute (from the declaring file's directory, or inside an
inline `mod` from that module's own; a path that leaves the project, by
escapesRoot, or that a `cfg_attr` sets, names no file):
- rustSubmodule: a module the module declares once with a `#[path]` is
  the file it names, whatever else loads that file too (a helper several
  tests load is each test's); cfg alternatives that name a file each stay
  unknown, and a path's 2015 crate-root fallback no longer reaches past
  such a module to a namesake at the crate root.
- rustPathClaim: a file the regular rules don't place is the module of the
  one `#[path]` declaration that loads it, if exactly one does. Placed
  (rustPlacedByName) are crate roots, files with a `fn main` or
  `#![no_main]` of their own (trybuild cases, fuzz targets), and files a
  plain `mod name;` declares in the module owning their directory or in an
  inline module of one above it (dioxus's `mod components { pub mod home; }`
  in main.rs). Only for a file left over are the project's Rust files that
  write `path =` read (a byte check first), once per resolver. With a
  claim, rustSuperModule is the declaring module, rustFileModule owns the
  file's directory (rustc reads every `#[path]` file as a mod.rs),
  rustCrateRootModule is the declaring file's crate, and rustModuleWithin,
  isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the
  declaring module, so `use super::*` brings in its private items.
- rustCrateGraph: `[[example]]`, `[[test]]` and `[[bench]]` paths are
  target roots, as `[lib]`/`[[bin]]` ones are since PR #3014, and such an
  example is its own crate's root (dioxus's
  `examples/07-fullstack/handling_errors.rs`).

Validation:
- rust-path-super-glob.test.ts (PR 1892's case, credited) and
  rust-path-attribute-modules.test.ts (7 cases): on main all 8 fail; all
  pass. Every rust-* suite passes.
- A/B against main, native kernel: fast-mlsirm (its Rust files) +2,796
  links (1,839 calls, 427 references, 334 constructions, 196 imports), 71
  retargeted (exposure_tests.rs's `Lcg::next_f64` from cdm_tests.rs's
  private `Lcg` to exposure.rs's, the declaring module's helpers instead
  of bifactor_grm.rs's), 87 reaching the same item through the declaring
  module (confidence 0.4 to 0.9), 0 removed; dioxus +27 (each headless
  test's `utils::mock_event(…)` into the helper five tests load); tokio +2
  (`use super::AtomicU64` in the files atomic_u64_as_mutex.rs loads);
  ripgrep, axum, clap, serde, gleam, express and flask unchanged.
- Cost, native kernel, medians of 3 interleaved runs: fast-mlsirm CPU 15.3
  -> 15.8 s, wall 4.2 -> 5.2 s, peak RSS 825 -> 882 MB; tokio CPU 26.1 ->
  26.7 s, wall 10.4 -> 11.2 s, peak RSS 1,023 -> 1,028 MB; dioxus CPU 34.8
  -> 35.6 s, wall 10.3 -> 11.2 s, peak RSS 1,125 -> 1,120 MB; gleam CPU
  24.1 -> 23.8 s, wall 6.6 -> 6.7 s, peak RSS 1,027 -> 1,023 MB.

Builds on PR #1892 by @seonghobae.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit cc17978 into main Oct 30, 2026
@colbymchenry
colbymchenry deleted the claude/rust-path-attribute-modules branch October 30, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant