Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i8>;` 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<Article> 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.
Expand Down
30 changes: 26 additions & 4 deletions __tests__/rust-path-attribute-modules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -23,7 +24,13 @@ beforeAll(async () => {
const files: Record<string, string> = {
'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',
Expand Down Expand Up @@ -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',
]);
});
});
45 changes: 40 additions & 5 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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<string>();
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 };
Expand All @@ -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,
Expand Down Expand Up @@ -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<string>, 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;
Expand Down Expand Up @@ -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<string>, 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:
Expand Down