1use crate::lockfile::LockfileModel;
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14
15pub fn dependency_paths(lock: &LockfileModel) -> BTreeMap<String, Vec<String>> {
20 let mut adj: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
23 for pkg in &lock.packages {
24 let entry = adj.entry(pkg.id.name.as_str()).or_default();
25 for dep in &pkg.dependencies {
26 entry.insert(dep.as_str());
27 }
28 }
29
30 let mut paths: BTreeMap<String, Vec<String>> = BTreeMap::new();
31 let mut queue: VecDeque<String> = VecDeque::new();
32
33 for pkg in lock.packages.iter().filter(|p| p.id.is_local()) {
35 let name = pkg.id.name.clone();
36 if paths.contains_key(&name) {
37 continue;
38 }
39 paths.insert(name.clone(), vec![name.clone()]);
40 queue.push_back(name);
41 }
42
43 while let Some(current) = queue.pop_front() {
44 let current_path = paths.get(¤t).cloned().unwrap_or_default();
46 let Some(deps) = adj.get(current.as_str()) else {
47 continue;
48 };
49 for dep in deps.iter() {
51 if paths.contains_key(*dep) {
52 continue; }
54 let mut next = current_path.clone();
55 next.push((*dep).to_string());
56 paths.insert((*dep).to_string(), next);
57 queue.push_back((*dep).to_string());
58 }
59 }
60
61 paths
62}
63
64pub fn format_path(path: &[String]) -> String {
66 path.join(" \u{2192} ")
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::lockfile::{Package, PackageId};
73 use std::path::PathBuf;
74
75 fn p(name: &str, deps: &[&str], local: bool) -> Package {
76 Package {
77 id: PackageId {
78 name: name.into(),
79 version: "1.0.0".into(),
80 source: if local {
81 None
82 } else {
83 Some("registry+https://github.com/rust-lang/crates.io-index".into())
84 },
85 },
86 checksum: None,
87 dependencies: deps.iter().map(|s| s.to_string()).collect(),
88 }
89 }
90
91 fn lock(packages: Vec<Package>) -> LockfileModel {
92 LockfileModel {
93 path: PathBuf::from("Cargo.lock"),
94 version: Some(3),
95 packages,
96 }
97 }
98
99 #[test]
100 fn finds_transitive_path() {
101 let model = lock(vec![
103 p("demo", &["reqwest"], true),
104 p("reqwest", &["native-tls"], false),
105 p("native-tls", &["openssl-sys"], false),
106 p("openssl-sys", &[], false),
107 ]);
108 let paths = dependency_paths(&model);
109 assert_eq!(
110 paths.get("openssl-sys").unwrap(),
111 &vec![
112 "demo".to_string(),
113 "reqwest".to_string(),
114 "native-tls".to_string(),
115 "openssl-sys".to_string()
116 ]
117 );
118 assert_eq!(
119 format_path(paths.get("openssl-sys").unwrap()),
120 "demo → reqwest → native-tls → openssl-sys"
121 );
122 }
123
124 #[test]
125 fn shortest_path_wins() {
126 let model = lock(vec![
128 p("demo", &["a", "b"], true),
129 p("a", &["b"], false),
130 p("b", &[], false),
131 ]);
132 let paths = dependency_paths(&model);
133 assert_eq!(paths.get("b").unwrap().len(), 2); }
135
136 #[test]
137 fn cycle_does_not_hang() {
138 let model = lock(vec![
139 p("demo", &["a"], true),
140 p("a", &["b"], false),
141 p("b", &["a"], false), ]);
143 let paths = dependency_paths(&model);
144 assert!(paths.contains_key("a"));
145 assert!(paths.contains_key("b"));
146 }
147}