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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,9 @@ How a change is followed:
`Drop::drop`, `From::from`) is called by formatting, operators and
conversions that name nothing. A change to one reaches every
definition that uses the type's file.
- A path is followed through re-exports: with `pub use parts::*` in the
crate root, `crate::ports::Runner` is the `Runner` of `parts::ports`.
A trait reached that way is one the workspace declares.
- A `macro_rules!` macro is found by its name wherever it is used.
- A test that runs the package's binary (`CARGO_BIN_EXE_*`, `assert_cmd`)
runs for any change to the package's sources.
Expand Down
67 changes: 59 additions & 8 deletions src/related/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ pub struct RustIndex {
files: HashMap<PathBuf, Vec<(usize, Vec<String>)>>,
/// Library targets by crate name.
libs: HashMap<String, usize>,
/// What each file re-exports: the path, and whether all of it (`::*`).
reexports: HashMap<PathBuf, Vec<(String, bool)>>,
}

/// How many re-exports a path is followed through.
const MAX_REEXPORTS: usize = 8;

impl RustIndex {
pub fn build<'p>(root: &Path, sources: impl Iterator<Item = &'p PathBuf>) -> Self {
let mut index = RustIndex::default();
Expand Down Expand Up @@ -102,15 +107,29 @@ impl RustIndex {

/// Follow `path` (`crate::a::b::Name`) from `from` as far as modules
/// go: the file of the innermost module, and the segments left over,
/// which name something inside it. A path that leads to no module of
/// the workspace stays in `from`, whole.
/// which name something inside it. A module may be one that a module on
/// the way re-exports (`pub use other::*`). A path that leads to no
/// module of the workspace stays in `from`, whole.
pub fn follow(&self, from: &Path, path: &str) -> Vec<(PathBuf, Vec<String>)> {
let segments: Vec<String> = path.split("::").map(str::to_string).collect();
let mut found = self.modules_of(from, &segments, 0);
if found.is_empty() {
found.push((from.to_path_buf(), segments));
}
found
}

fn modules_of(
&self,
from: &Path,
segments: &[String],
depth: usize,
) -> Vec<(PathBuf, Vec<String>)> {
let mut found: Vec<(PathBuf, Vec<String>)> = Vec::new();
for (target, module) in self.owners(from) {
let mut target = *target;
let mut base = module.clone();
let mut rest = &segments[..];
let mut rest = segments;
match rest.first().map(String::as_str) {
Some("crate") => {
base.clear();
Expand Down Expand Up @@ -151,16 +170,40 @@ impl RustIndex {
}
taken += 1;
}
if let Some(file) = modules.get(&base) {
let entry = (file.clone(), rest[taken..].to_vec());
let Some(file) = modules.get(&base) else {
continue;
};
let left = &rest[taken..];
// The next segment is no module of this one. It may be a module
// this one re-exports from elsewhere; if so, the path goes on
// there and names nothing here.
let mut through: Vec<(PathBuf, Vec<String>)> = Vec::new();
if left.len() > 1 && depth < MAX_REEXPORTS {
for (spec, all) in self.reexports.get(file).into_iter().flatten() {
let mut onward: Vec<String> = spec.split("::").map(str::to_string).collect();
match all {
true => onward.extend_from_slice(left),
false if onward.last() == left.first() => {
onward.extend_from_slice(&left[1..]);
}
false => continue,
}
for entry in self.modules_of(file, &onward, depth + 1) {
if entry.1.len() < left.len() && !through.contains(&entry) {
through.push(entry);
}
}
}
}
if through.is_empty() {
through.push((file.clone(), left.to_vec()));
}
for entry in through {
if !found.contains(&entry) {
found.push(entry);
}
}
}
if found.is_empty() {
found.push((from.to_path_buf(), segments));
}
found
}

Expand Down Expand Up @@ -242,6 +285,14 @@ impl Resolvers {
),
};
for (path, facts) in ws.sources {
if facts.family == Family::Rust && !facts.reexports.is_empty() {
let reexports = facts
.reexports
.iter()
.map(|spec| (spec.clone(), facts.uses_all.contains(spec)))
.collect();
resolvers.rust.reexports.insert(path.clone(), reexports);
}
match facts.family {
Family::Elixir => {
for module in &facts.modules {
Expand Down
72 changes: 72 additions & 0 deletions tests/related.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,78 @@ fn rust_trait_methods_are_reached_through_the_trait() {
);
}

// A crate that re-exports another crate's modules whole, so that its own
// code names them as `crate::…`.
const RUST_FACADE: &[(&str, &str)] = &[
(
"app/Cargo.toml",
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nparts = { path = \"crates/parts\" }\n",
),
(
"app/src/lib.rs",
"pub mod engine;\npub mod notes;\npub mod wiring;\npub use parts::*;\n",
),
(
"app/crates/parts/Cargo.toml",
"[package]\nname = \"parts\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
),
("app/crates/parts/src/lib.rs", "pub mod ports;\n"),
(
"app/crates/parts/src/ports.rs",
"pub trait Runner {\n fn run(&self) -> u32;\n}\n\npub trait Label {\n fn text(&self) -> String;\n}\n",
),
(
"app/src/engine.rs",
"use crate::ports::Runner;\n\npub struct Engine;\n\nimpl Runner for Engine {\n fn run(&self) -> u32 {\n 1\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn engine_runs() {\n let engine = Engine;\n assert_eq!(engine.run(), 1);\n }\n}\n",
),
(
"app/src/wiring.rs",
"use crate::ports::Runner;\n\npub fn drive(runner: &dyn Runner) -> u32 {\n runner.run()\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::engine::Engine;\n\n #[test]\n fn drives_the_engine() {\n assert_eq!(drive(&Engine), 1);\n }\n}\n",
),
// Uses the same re-exported module, and nothing of the engine.
(
"app/src/notes.rs",
"use crate::ports::Label;\n\npub struct Note;\n\nimpl Label for Note {\n fn text(&self) -> String {\n String::from(\"note\")\n }\n}\n\npub fn show(label: &dyn Label) -> String {\n label.text()\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn shows_a_note() {\n assert_eq!(show(&Note), \"note\");\n }\n}\n",
),
];

#[test]
fn rust_paths_are_followed_through_a_glob_re_export_of_another_crate() {
let ws = Workspace::new(RUST_FACADE);
// `crate::ports` is `parts::ports`, brought in by `pub use parts::*`.
// `Runner` is therefore a trait of the workspace, reached by the code
// that calls `run` on one, not by everything that uses the crate.
ws.edit("app/src/engine.rs", " 1\n", " 1 + 0\n");
assert_eq!(
ws.plan("test").commands("//app:test"),
[
"cargo test --lib -- --exact engine::tests::engine_runs wiring::tests::drives_the_engine",
"cargo test --doc"
]
);

// The same when the module is re-exported by name.
let named: Vec<(&str, &str)> = RUST_FACADE
.iter()
.map(|&(path, source)| match path {
"app/src/lib.rs" => (
path,
"pub mod engine;\npub mod notes;\npub mod wiring;\npub use parts::ports;\n",
),
_ => (path, source),
})
.collect();
let ws = Workspace::new(&named);
ws.edit("app/src/engine.rs", " 1\n", " 1 + 0\n");
assert_eq!(
ws.plan("test").commands("//app:test"),
[
"cargo test --lib -- --exact engine::tests::engine_runs wiring::tests::drives_the_engine",
"cargo test --doc"
]
);
}

#[test]
fn rust_macros_are_found_by_name() {
let ws = Workspace::new(RUST_KIT);
Expand Down
Loading