From f11fccd2c0e3013bacc91bbe8f87f90db47690bf Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sun, 1 Nov 2026 03:03:21 -0600 Subject: [PATCH] fix(rust): a use through a module with a file per cfg reaches the item one file holds tokio's `use crate::process::unix::GlobalOrphanQueue;` linked nothing on main, where 1.6.2 linked it. Since PR #3014 a `use` path walks the crate's modules, and tokio's `mod unix { pub(crate) use super::imp::*; }` globs a module with a `#[path]` for each cfg: `unix/mod.rs` or `windows.rs`. rustSubmodule answered null for such a module, so the glob couldn't be followed and the import had no target. The fix: rustSubmodule keeps every file the alternatives name (RustModule.choices), and rustCfgChoiceItems looks a name up in all of them. What one alternative holds is that one's, whichever the build takes; a name two of them hold is the build's choice and stays unread, as PR #3064 pinned. A step further down the path walks each alternative the same way. Validation: - rust-path-attribute-modules.test.ts: 2 of its new cases fail on main and all 9 pass. - A/B against main 4499d007: tokio +7 imports, all true (the reported edge, `Wait` and `OrphanQueue` through `imp::orphan`, `AtomicU64` through tokio's cfg'd `imp`), and 2 calls keep their target by the import instead of by name. ripgrep, clap, serde, dioxus, gleam and axum byte-identical (axum's 24 "moved" are the diff tool's duplicate route nodes, identical by row); controls flask and express identical. - CPU, dioxus, median of 5 interleaved: 33.97 to 33.28 s. Resolution only, so no kernel change. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- __tests__/rust-path-attribute-modules.test.ts | 30 +++++++++++-- src/resolution/name-matcher.ts | 45 ++++++++++++++++--- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 106f2518a..39105179c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,7 +190,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308) - In Rust, a name a `use` renames, like `consume()` after `use crate::util::take as consume;`, now links to the item its path names, and a `use` written inside a function or a block counts only there, ahead of the file's own `use`s and items, so `fn first() { use crate::util::take; take() }` reaches util's `take` and the next function's own `use` its own. A call to a parameter, a closure or a local that shares a function's name, like `take()` inside `fn run(take: fn())` or after `let take = || {};`, no longer links to that function, while `let test = test::;` still calls `test`, and a `use` of an enum's variant, like `use crate::models::Mode::Fast;`, links `Fast(…)` to that enum's variant. A path through a module, like `super::helper()`, `crate::config()`, `super::shared::helper()` inside a `mod tests { … }` or `tokio::task::yield_now()`, now reaches the item that module declares or passes on with a `pub use`, in your own crate or another crate of the workspace, and its `use` links to it; that includes binaries whose manifest gives them a path of their own, like ripgrep's, and packages whose manifest lists `name` after other fields, like axum-core's. A `use` written inside a string no longer counts. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308) - In Rust, a call through a type that a path or a `use` names, like `sim::items::ItemPool::for_def(…)` into another crate of your workspace, `ItemPool::for_def(…)` under `use sim::items::ItemPool;` or `crate::models::Mode::Fast(1)`, now links to that type's own function or variant, even when a type of the same name in another crate or module has one too, and a `use` of a `pub const` or `static`, like `use sim::RANK_COUNT;`, links to it. A type written on the line below its module path, as rustfmt wraps a long signature (`scheduler::` then `Handle`), now links by that path rather than by a `use` of its name. A workspace member listed outside the project, like `../shared`, is no longer read. Re-index Rust projects after upgrading. Thanks @BGamboa13. (#2631) -- In Rust, a module loaded with `#[path = "…"]`, like tests kept in another folder as `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`, is now part of the module that declares it: its `use super::*` brings in that module's items, private ones included, `super::` and `crate::` start from there, and the path is read as Rust reads it, from the declaring file's folder or an inline module's own. A call through such a module, like `support::make()` in each test that loads a shared helper, reaches the file it names. A module whose `#[cfg]` alternatives each name a file of their own, or whose path a `cfg_attr` sets, depends on the build and is left alone. An example, test or bench that `Cargo.toml` places at a path of its own, like `examples/07-fullstack/handling_errors.rs`, is now its own crate, so its `crate::` starts from it. Re-index Rust projects after upgrading. Thanks @seonghobae. (#1892) +- In Rust, a module loaded with `#[path = "…"]`, like tests kept in another folder as `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`, is now part of the module that declares it: its `use super::*` brings in that module's items, private ones included, `super::` and `crate::` start from there, and the path is read as Rust reads it, from the declaring file's folder or an inline module's own. A call through such a module, like `support::make()` in each test that loads a shared helper, reaches the file it names. A module whose `#[cfg]` alternatives each name a file of their own, or whose path a `cfg_attr` sets, depends on the build and is left alone. An example, test or bench that `Cargo.toml` places at a path of its own, like `examples/07-fullstack/handling_errors.rs`, is now its own crate, so its `crate::` starts from it. A name only one of a module's `cfg` alternatives holds now links through it, like `crate::process::unix::GlobalOrphanQueue` through tokio's `#[path]` modules for Unix and Windows. Re-index Rust projects after upgrading. Thanks @seonghobae. (#1892) - Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326) - In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345. - In Spring projects, a route now links to the method its mapping annotation is written on when that method has no access modifier, like `@GetMapping List
list()` in a package-private controller, and a method-level `@RequestMapping(value = "/feed", method = GET)` on such a method now makes a route. Before, such a handler was skipped, so its route linked the next method declared `public`, `private` or `protected`, or nothing, and the handler looked unused. An overloaded handler under a tall stack of annotations, like `@Operation` and `@ApiResponses` above `@GetMapping`, now links the overload it annotates rather than its neighbour. Re-index Spring projects after upgrading. Thanks @Liurchen. diff --git a/__tests__/rust-path-attribute-modules.test.ts b/__tests__/rust-path-attribute-modules.test.ts index 617d7c515..6ea76ca4d 100644 --- a/__tests__/rust-path-attribute-modules.test.ts +++ b/__tests__/rust-path-attribute-modules.test.ts @@ -4,8 +4,9 @@ * module, wherever the file lies, so `use super::*` brings in that module's * items, private ones included, and `crate::` is the declaring file's crate. * The path is read from the declaring file's directory, or from an inline - * module's own directory. A module whose cfg alternatives each name a file, - * or whose path a `cfg_attr` sets, depends on the build and stays unread. + * module's own directory. A name only one of a module's cfg alternatives holds + * is that one's; a name two of them hold, or a module whose path a `cfg_attr` + * sets, depends on the build and stays unread. * A file several declarations load is each one's from its own side, and an * example the manifest places at a path of its own is its own crate's root. */ @@ -23,7 +24,13 @@ beforeAll(async () => { const files: Record = { 'Cargo.toml': '[workspace]\nmembers = ["crates/core", "crates/py"]\nresolver = "2"\n', 'crates/core/Cargo.toml': '[package]\nname = "core-crate"\nversion = "0.1.0"\nedition = "2021"\n\n[[example]]\nname = "demo"\npath = "examples/nested/demo.rs"\n', - 'crates/core/src/lib.rs': 'pub mod scaling;\npub mod platform;\npub mod util;\n\npub fn root_helper() -> i32 {\n 0\n}\n\npub mod gen_user;\n', + 'crates/core/src/lib.rs': 'pub mod scaling;\npub mod platform;\npub mod util;\n\npub fn root_helper() -> i32 {\n 0\n}\n\npub mod gen_user;\npub mod process;\npub mod park;\n', + // tokio's process module: an inline module globs what a `#[path]` loads, one file per cfg. + 'crates/core/src/process/mod.rs': '#[path = "unix/mod.rs"]\n#[cfg(unix)]\nmod imp;\n\n#[cfg(unix)]\npub(crate) mod unix {\n pub(crate) use super::imp::*;\n}\n\n#[path = "windows.rs"]\n#[cfg(windows)]\nmod imp;\n\npub(crate) fn via_windows() {\n imp::only_on_windows();\n}\n\npub(crate) fn via_either() {\n imp::on_each();\n}\n', + 'crates/core/src/process/unix/mod.rs': 'pub(crate) mod orphan;\n\npub(crate) struct GlobalOrphanQueue;\n\npub(crate) fn on_each() {}\n', + 'crates/core/src/process/unix/orphan.rs': 'pub(crate) trait OrphanQueue {}\n', + 'crates/core/src/process/windows.rs': 'pub(crate) fn only_on_windows() {}\n\npub(crate) fn on_each() {}\n', + 'crates/core/src/park.rs': 'use crate::process::imp::orphan::OrphanQueue;\nuse crate::process::unix::GlobalOrphanQueue;\n\npub fn park(queue: GlobalOrphanQueue, orphans: &dyn OrphanQueue) {}\n', 'crates/core/src/util.rs': 'pub fn helper() -> i32 {\n 10\n}\n', // A folder whose name only starts with two dots is inside the project. 'crates/core/src/gen_user.rs': '#[path = "..generated/gen.rs"]\nmod gen;\n\npub fn via_gen() -> i32 {\n gen::make_gen()\n}\n', @@ -141,8 +148,23 @@ describe('Rust modules a #[path] attribute loads', () => { expect(calls('crates/core/examples/nested/demo.rs', 'main')).toEqual(['helper crates/core/examples/nested/util.rs:1']); }); - it('leave a module whose cfg alternatives name a file each, or whose path a cfg_attr sets, unread', () => { + it('leave a name two cfg alternatives hold, or a module whose path a cfg_attr sets, unread', () => { expect(calls('crates/core/src/platform.rs', 'via_imp')).toEqual([]); + expect(calls('crates/core/src/process/mod.rs', 'via_either')).toEqual([]); expect(calls('crates/core/src/platform.rs', 'via_documented')).toEqual([]); }); + + it('reach what only one of its cfg alternatives holds', () => { + expect(calls('crates/core/src/process/mod.rs', 'via_windows')).toEqual(['only_on_windows crates/core/src/process/windows.rs:1']); + }); + + it('let a `use` through a glob of cfg alternatives, or on through a module of one, reach the item', () => { + const park = cg.getNodesInFile('crates/core/src/park.rs').find((n) => n.kind === 'file'); + const imported = cg.getOutgoingEdgesFrom([park!.id]).filter((e) => e.kind === 'imports') + .map((e) => cg.getNode(e.target)!).map((t) => `${t.name} ${t.filePath}:${t.startLine}`).sort(); + expect(imported).toEqual([ + 'GlobalOrphanQueue crates/core/src/process/unix/mod.rs:3', + 'OrphanQueue crates/core/src/process/unix/orphan.rs:1', + ]); + }); }); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 6dffd00b9..8a55a7f23 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9918,6 +9918,8 @@ interface RustModule { root: boolean; /** The package's other root, which holds crate-root items too: the `main.rs` beside its `lib.rs`. */ alt?: string; + /** Each file a `#[path]` names for it, one per cfg: `file` is only the first (rustCfgChoiceItems reads them all). */ + choices?: readonly RustModule[]; } const rustJoin = (dir: string, name: string): string => (dir === '' ? name : `${dir}/${name}`); @@ -10250,17 +10252,22 @@ function rustLibModule(crateDir: string, context: ResolutionContext): RustModule * that name — dioxus's `pub use dioxus_hooks as hooks;`. One a `#[path]` * puts elsewhere is the file it names when `m` declares it once, whatever * else loads that file too (a helper several tests load is each test's); - * null when cfg alternatives name a file each (tokio's `process::imp` is - * `unix/mod.rs` or `windows.rs`) or a `cfg_attr` sets the path, as for one - * with no file. + * cfg alternatives that name a file each (tokio's `process::imp` is + * `unix/mod.rs` or `windows.rs`) are all of them (rustCfgChoiceItems); null + * when a `cfg_attr` sets the path, as for one with no file. */ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext, depth = 0): RustModule | null { + if (m.choices) return rustCfgChoice(m.choices.map((c) => rustSubmodule(c, name, context, depth)).filter((c) => c !== null)); const dir = rustJoin(m.dir, name); const layout = rustUsesOf(m.file, context); if (layout.pathMods.has(name)) { const decls = rustPathDeclsIn(m.file, context).filter((d) => d.name === name && rustBlockAt(layout.blocks, d.at) === m.block); - const target = decls.length === 1 ? decls[0]!.target : null; - return target !== null && context.fileExists(target) ? { file: target, dir: rustParentDir(target), block: null, root: false } : null; + const files = new Set(); + for (const { target } of decls) { + if (target === null || !context.fileExists(target)) return null; + files.add(target); + } + return rustCfgChoice([...files].map((file) => ({ file, dir: rustParentDir(file), block: null, root: false }))); } const block = layout.blocks.find((b) => b.name === name && b.parent === m.block); if (block) return { file: m.file, dir, block, root: false }; @@ -10276,6 +10283,11 @@ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext, return null; } +/** One module for the cfg alternatives `mods`; null for none. */ +function rustCfgChoice(mods: RustModule[]): RustModule | null { + return mods.length > 1 ? { ...mods[0]!, choices: mods } : mods[0] ?? null; +} + /** * The module `m` is declared in — what `super::` names — or null above a * crate's root. A file a `#[path]` loads is declared where that attribute is, @@ -10363,6 +10375,7 @@ function rustDeclaredItems(m: RustModule, name: string, context: ResolutionConte * use_effect::*` doesn't pass on that module's own `use dioxus_core::*`. */ function rustModuleItems(m: RustModule, name: string, context: ResolutionContext, seen: Set, publicOnly = false): Node[] | null { + if (m.choices) return rustCfgChoiceItems(m.choices, name, context, seen, publicOnly); const key = `${m.file}\0${m.block?.open ?? -1}\0${name}\0${publicOnly ? 1 : 0}`; if (seen.has(key)) return []; if (seen.size >= 32) return null; @@ -10392,6 +10405,28 @@ function rustModuleItems(m: RustModule, name: string, context: ResolutionContext } } +/** + * The items named `name` in a module whose cfg alternatives name a file each. + * What only one of them holds is that one's, whichever the build takes: + * tokio's `crate::process::unix::GlobalOrphanQueue` reaches `unix/mod.rs` + * through `mod unix { pub(crate) use super::imp::*; }`, as `windows.rs` has + * none. A name two of them hold is the build's choice: null, as for any module + * that can't be followed. + */ +function rustCfgChoiceItems(choices: readonly RustModule[], name: string, context: ResolutionContext, seen: Set, publicOnly: boolean): Node[] | null { + let found: Node[] = []; + let unknown = false; + for (const choice of choices) { + const items = rustModuleItems(choice, name, context, seen, publicOnly); + if (items === null) unknown = true; + else if (items.length > 0) { + if (found.length > 0) return null; + found = items; + } + } + return found.length > 0 || !unknown ? found : null; +} + /** * The items named `name` that globs bring in, each walked from the module * `fromOf` gives for it — what the module a glob names lets the importer see: