fob_graph/
dependency_chain.rs1use serde::{Deserialize, Serialize};
8use std::collections::VecDeque;
9
10use super::ModuleId;
11
12const MAX_DEPENDENCY_CHAIN_DEPTH: usize = 50;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct DependencyChain {
25 pub path: Vec<ModuleId>,
27 pub depth: usize,
29}
30
31impl DependencyChain {
32 pub fn new(path: Vec<ModuleId>) -> Self {
34 let depth = if !path.is_empty() { path.len() - 1 } else { 0 };
35
36 Self { path, depth }
37 }
38
39 pub fn entry_point(&self) -> Option<&ModuleId> {
41 self.path.first()
42 }
43
44 pub fn target(&self) -> Option<&ModuleId> {
46 self.path.last()
47 }
48
49 pub fn has_cycle(&self) -> bool {
51 use std::collections::HashSet;
52 let mut seen = HashSet::new();
53 for module in &self.path {
54 if !seen.insert(module) {
55 return true;
56 }
57 }
58 false
59 }
60
61 pub fn format_chain(&self) -> String {
65 self.path
66 .iter()
67 .map(|id| id.path_string())
68 .collect::<Vec<_>>()
69 .join(" -> ")
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ChainAnalysis {
76 pub target: ModuleId,
78 pub chains: Vec<DependencyChain>,
80 pub min_depth: Option<usize>,
82 pub max_depth: Option<usize>,
84 pub avg_depth: f64,
86 pub entry_point_count: usize,
88}
89
90impl ChainAnalysis {
91 pub fn from_chains(target: ModuleId, chains: Vec<DependencyChain>) -> Self {
93 let min_depth = chains.iter().map(|c| c.depth).min();
94 let max_depth = chains.iter().map(|c| c.depth).max();
95 let avg_depth = if chains.is_empty() {
96 0.0
97 } else {
98 chains.iter().map(|c| c.depth).sum::<usize>() as f64 / chains.len() as f64
99 };
100
101 let mut entry_points = std::collections::HashSet::new();
102 for chain in &chains {
103 if let Some(entry) = chain.entry_point() {
104 entry_points.insert(entry.clone());
105 }
106 }
107
108 Self {
109 target,
110 chains,
111 min_depth,
112 max_depth,
113 avg_depth,
114 entry_point_count: entry_points.len(),
115 }
116 }
117
118 pub fn is_reachable(&self) -> bool {
120 !self.chains.is_empty()
121 }
122
123 pub fn shortest_chain(&self) -> Option<&DependencyChain> {
125 self.chains.iter().min_by_key(|c| c.depth)
126 }
127
128 pub fn circular_chains(&self) -> Vec<&DependencyChain> {
130 self.chains.iter().filter(|c| c.has_cycle()).collect()
131 }
132}
133
134pub(crate) fn find_chains<F>(
138 entry_points: &[ModuleId],
139 target: &ModuleId,
140 mut get_dependencies: F,
141) -> Vec<DependencyChain>
142where
143 F: FnMut(&ModuleId) -> Vec<ModuleId>,
144{
145 let mut chains = Vec::new();
146 let mut queue: VecDeque<Vec<ModuleId>> = VecDeque::new();
147
148 for entry in entry_points {
150 queue.push_back(vec![entry.clone()]);
151 }
152
153 let mut visited_paths = std::collections::HashSet::new();
155
156 while let Some(current_path) = queue.pop_front() {
157 let current_module = match current_path.last() {
159 Some(module) => module,
160 None => continue, };
162
163 let path_sig = current_path
165 .iter()
166 .map(|id| id.path_string())
167 .collect::<Vec<_>>()
168 .join("->");
169
170 if !visited_paths.insert(path_sig) {
171 continue;
173 }
174
175 if current_module == target {
178 chains.push(DependencyChain::new(current_path.clone()));
179 }
181
182 if current_path.len() > MAX_DEPENDENCY_CHAIN_DEPTH {
184 continue;
185 }
186
187 for dep in get_dependencies(current_module) {
189 let mut new_path = current_path.clone();
190 new_path.push(dep);
191 queue.push_back(new_path);
192 }
193 }
194
195 chains
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn mock_module_id(name: &str) -> ModuleId {
203 ModuleId::new_virtual(format!("virtual:{}", name))
204 }
205
206 #[test]
207 fn test_empty_dependency_path_edge_case() {
208 let entry = mock_module_id("entry");
211 let target = mock_module_id("target");
212
213 let get_dependencies = |_id: &ModuleId| -> Vec<ModuleId> { vec![] };
215
216 let chains = find_chains(&[entry], &target, get_dependencies);
218
219 assert_eq!(chains.len(), 0);
222 }
223
224 #[test]
225 fn test_dependency_chain_creation() {
226 let path = vec![
227 mock_module_id("entry"),
228 mock_module_id("utils"),
229 mock_module_id("target"),
230 ];
231
232 let chain = DependencyChain::new(path.clone());
233
234 assert_eq!(chain.depth, 2);
235 assert_eq!(chain.entry_point(), Some(&path[0]));
236 assert_eq!(chain.target(), Some(&path[2]));
237 assert!(!chain.has_cycle());
238 }
239
240 #[test]
241 fn test_dependency_chain_cycle_detection() {
242 let a = mock_module_id("a");
243 let b = mock_module_id("b");
244
245 let path = vec![a.clone(), b.clone(), a.clone()];
247 let chain = DependencyChain::new(path);
248
249 assert!(chain.has_cycle());
250 }
251
252 #[test]
253 fn test_dependency_chain_no_cycle() {
254 let path = vec![
255 mock_module_id("a"),
256 mock_module_id("b"),
257 mock_module_id("c"),
258 ];
259 let chain = DependencyChain::new(path);
260
261 assert!(!chain.has_cycle());
262 }
263
264 #[test]
265 fn test_chain_analysis() {
266 let target = mock_module_id("target");
267
268 let chains = vec![
269 DependencyChain::new(vec![mock_module_id("entry1"), target.clone()]),
270 DependencyChain::new(vec![
271 mock_module_id("entry2"),
272 mock_module_id("middle"),
273 target.clone(),
274 ]),
275 ];
276
277 let analysis = ChainAnalysis::from_chains(target.clone(), chains);
278
279 assert!(analysis.is_reachable());
280 assert_eq!(analysis.min_depth, Some(1));
281 assert_eq!(analysis.max_depth, Some(2));
282 assert_eq!(analysis.avg_depth, 1.5);
283 assert_eq!(analysis.entry_point_count, 2);
284 }
285
286 #[test]
287 fn test_chain_analysis_unreachable() {
288 let target = mock_module_id("unreachable");
289 let analysis = ChainAnalysis::from_chains(target, vec![]);
290
291 assert!(!analysis.is_reachable());
292 assert_eq!(analysis.min_depth, None);
293 assert_eq!(analysis.max_depth, None);
294 assert_eq!(analysis.avg_depth, 0.0);
295 assert_eq!(analysis.entry_point_count, 0);
296 }
297
298 #[test]
299 fn test_find_chains_basic() {
300 let entry = mock_module_id("entry");
302 let a = mock_module_id("a");
303 let target = mock_module_id("target");
304
305 let get_deps = |module: &ModuleId| -> Vec<ModuleId> {
306 if module == &entry {
307 vec![a.clone()]
308 } else if module == &a {
309 vec![target.clone()]
310 } else {
311 vec![]
312 }
313 };
314
315 let chains = find_chains(std::slice::from_ref(&entry), &target, get_deps);
316
317 assert_eq!(chains.len(), 1);
318 assert_eq!(chains[0].path.len(), 3);
319 assert_eq!(chains[0].depth, 2);
320 }
321
322 #[test]
323 fn test_find_chains_multiple_paths() {
324 let entry = mock_module_id("entry");
328 let a = mock_module_id("a");
329 let b = mock_module_id("b");
330 let target = mock_module_id("target");
331
332 let get_deps = |module: &ModuleId| -> Vec<ModuleId> {
333 if module == &entry {
334 vec![a.clone(), b.clone()]
335 } else if module == &a || module == &b {
336 vec![target.clone()]
337 } else {
338 vec![]
339 }
340 };
341
342 let chains = find_chains(std::slice::from_ref(&entry), &target, get_deps);
343
344 assert_eq!(chains.len(), 2);
345 assert!(chains.iter().all(|c| c.path.len() == 3));
346 }
347
348 #[test]
349 fn test_format_chain() {
350 let path = vec![
351 mock_module_id("entry"),
352 mock_module_id("utils"),
353 mock_module_id("target"),
354 ];
355
356 let chain = DependencyChain::new(path);
357 let formatted = chain.format_chain();
358
359 assert!(formatted.contains("entry"));
360 assert!(formatted.contains("utils"));
361 assert!(formatted.contains("target"));
362 assert!(formatted.contains("->"));
363 }
364}