use crate::lockfile::LockfileModel;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
pub fn dependency_paths(lock: &LockfileModel) -> BTreeMap<String, Vec<String>> {
let mut adj: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
for pkg in &lock.packages {
let entry = adj.entry(pkg.id.name.as_str()).or_default();
for dep in &pkg.dependencies {
entry.insert(dep.as_str());
}
}
let mut paths: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
for pkg in lock.packages.iter().filter(|p| p.id.is_local()) {
let name = pkg.id.name.clone();
if paths.contains_key(&name) {
continue;
}
paths.insert(name.clone(), vec![name.clone()]);
queue.push_back(name);
}
while let Some(current) = queue.pop_front() {
let current_path = paths.get(¤t).cloned().unwrap_or_default();
let Some(deps) = adj.get(current.as_str()) else {
continue;
};
for dep in deps.iter() {
if paths.contains_key(*dep) {
continue; }
let mut next = current_path.clone();
next.push((*dep).to_string());
paths.insert((*dep).to_string(), next);
queue.push_back((*dep).to_string());
}
}
paths
}
pub fn format_path(path: &[String]) -> String {
path.join(" \u{2192} ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lockfile::{Package, PackageId};
use std::path::PathBuf;
fn p(name: &str, deps: &[&str], local: bool) -> Package {
Package {
id: PackageId {
name: name.into(),
version: "1.0.0".into(),
source: if local {
None
} else {
Some("registry+https://github.com/rust-lang/crates.io-index".into())
},
},
checksum: None,
dependencies: deps.iter().map(|s| s.to_string()).collect(),
}
}
fn lock(packages: Vec<Package>) -> LockfileModel {
LockfileModel {
path: PathBuf::from("Cargo.lock"),
version: Some(3),
packages,
}
}
#[test]
fn finds_transitive_path() {
let model = lock(vec![
p("demo", &["reqwest"], true),
p("reqwest", &["native-tls"], false),
p("native-tls", &["openssl-sys"], false),
p("openssl-sys", &[], false),
]);
let paths = dependency_paths(&model);
assert_eq!(
paths.get("openssl-sys").unwrap(),
&vec![
"demo".to_string(),
"reqwest".to_string(),
"native-tls".to_string(),
"openssl-sys".to_string()
]
);
assert_eq!(
format_path(paths.get("openssl-sys").unwrap()),
"demo → reqwest → native-tls → openssl-sys"
);
}
#[test]
fn shortest_path_wins() {
let model = lock(vec![
p("demo", &["a", "b"], true),
p("a", &["b"], false),
p("b", &[], false),
]);
let paths = dependency_paths(&model);
assert_eq!(paths.get("b").unwrap().len(), 2); }
#[test]
fn cycle_does_not_hang() {
let model = lock(vec![
p("demo", &["a"], true),
p("a", &["b"], false),
p("b", &["a"], false), ]);
let paths = dependency_paths(&model);
assert!(paths.contains_key("a"));
assert!(paths.contains_key("b"));
}
}