Skip to main content

plugin_packager/
dep_tree.rs

1// Copyright 2024 Vincents AI
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Dependency tree visualization and graph analysis
5///
6/// This module provides functionality for visualizing and analyzing plugin dependency graphs:
7/// - Dependency tree generation with hierarchical structure
8/// - Circular dependency detection
9/// - Depth analysis and metrics
10/// - Graph export in DOT format for visualization
11/// - JSON export for programmatic access
12/// - CLI integration support
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet, VecDeque};
15
16/// A dependency node in the tree
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct DependencyNode {
19    pub name: String,
20    pub version: String,
21    pub depth: u32,
22    pub children: Vec<DependencyNode>,
23}
24
25impl DependencyNode {
26    pub fn new(name: &str, version: &str, depth: u32) -> Self {
27        Self {
28            name: name.to_string(),
29            version: version.to_string(),
30            depth,
31            children: Vec::new(),
32        }
33    }
34
35    pub fn add_child(&mut self, child: DependencyNode) {
36        self.children.push(child);
37    }
38
39    pub fn child_count(&self) -> usize {
40        self.children.len()
41    }
42
43    pub fn total_descendants(&self) -> usize {
44        self.children.len()
45            + self
46                .children
47                .iter()
48                .map(|c| c.total_descendants())
49                .sum::<usize>()
50    }
51
52    pub fn max_depth(&self) -> u32 {
53        if self.children.is_empty() {
54            self.depth
55        } else {
56            self.children
57                .iter()
58                .map(|c| c.max_depth())
59                .max()
60                .unwrap_or(self.depth)
61        }
62    }
63}
64
65/// A directed edge in the dependency graph
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DependencyEdge {
68    pub from: String,
69    pub to: String,
70    pub version_spec: String,
71    pub optional: bool,
72}
73
74impl DependencyEdge {
75    pub fn new(from: &str, to: &str, version_spec: &str) -> Self {
76        Self {
77            from: from.to_string(),
78            to: to.to_string(),
79            version_spec: version_spec.to_string(),
80            optional: false,
81        }
82    }
83
84    pub fn optional(mut self) -> Self {
85        self.optional = true;
86        self
87    }
88}
89
90/// Circular dependency information
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CircularDependency {
93    pub cycle: Vec<String>,
94    pub path_length: usize,
95}
96
97impl CircularDependency {
98    pub fn new(cycle: Vec<String>) -> Self {
99        let path_length = cycle.len();
100        Self { cycle, path_length }
101    }
102
103    pub fn as_string(&self) -> String {
104        self.cycle.join(" → ")
105    }
106}
107
108/// Dependency tree metrics
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DependencyMetrics {
111    pub total_nodes: usize,
112    pub total_edges: usize,
113    pub max_depth: u32,
114    pub avg_depth: f32,
115    pub circular_dependencies: usize,
116    pub optional_dependencies: usize,
117    pub branching_factor: f32,
118}
119
120impl DependencyMetrics {
121    pub fn new() -> Self {
122        Self {
123            total_nodes: 0,
124            total_edges: 0,
125            max_depth: 0,
126            avg_depth: 0.0,
127            circular_dependencies: 0,
128            optional_dependencies: 0,
129            branching_factor: 0.0,
130        }
131    }
132}
133
134impl Default for DependencyMetrics {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// Dependency graph representation
141pub struct DependencyGraph {
142    pub root: String,
143    pub root_version: String,
144    pub edges: Vec<DependencyEdge>,
145    pub nodes: HashMap<String, String>, // name -> version
146}
147
148impl DependencyGraph {
149    /// Create a new dependency graph with a root plugin
150    pub fn new(root_name: &str, root_version: &str) -> Self {
151        let mut nodes = HashMap::new();
152        nodes.insert(root_name.to_string(), root_version.to_string());
153
154        Self {
155            root: root_name.to_string(),
156            root_version: root_version.to_string(),
157            edges: Vec::new(),
158            nodes,
159        }
160    }
161
162    /// Add a dependency edge
163    pub fn add_dependency(&mut self, edge: DependencyEdge) {
164        // Extract version from version_spec (e.g., ">=1.0.0" -> store the whole spec)
165        if !self.nodes.contains_key(&edge.to) {
166            // Use version spec as placeholder for version
167            self.nodes
168                .insert(edge.to.clone(), edge.version_spec.clone());
169        }
170        self.edges.push(edge);
171    }
172
173    /// Detect circular dependencies using DFS
174    pub fn detect_circular_dependencies(&self) -> Vec<CircularDependency> {
175        let mut cycles = Vec::new();
176        let mut visited = HashSet::new();
177        let mut rec_stack = HashSet::new();
178        let mut path = Vec::new();
179
180        for node in self.nodes.keys() {
181            if !visited.contains(node) {
182                self.dfs_cycle_detection(
183                    node,
184                    &mut visited,
185                    &mut rec_stack,
186                    &mut path,
187                    &mut cycles,
188                );
189            }
190        }
191
192        cycles
193    }
194
195    fn dfs_cycle_detection(
196        &self,
197        node: &str,
198        visited: &mut HashSet<String>,
199        rec_stack: &mut HashSet<String>,
200        path: &mut Vec<String>,
201        cycles: &mut Vec<CircularDependency>,
202    ) {
203        visited.insert(node.to_string());
204        rec_stack.insert(node.to_string());
205        path.push(node.to_string());
206
207        // Find all neighbors
208        for edge in &self.edges {
209            if edge.from == node {
210                let neighbor = &edge.to;
211
212                if !visited.contains(neighbor) {
213                    self.dfs_cycle_detection(neighbor, visited, rec_stack, path, cycles);
214                } else if rec_stack.contains(neighbor) {
215                    // Found a cycle
216                    if let Some(start_idx) = path.iter().position(|n| n == neighbor) {
217                        let cycle = path[start_idx..].to_vec();
218                        if !cycle.is_empty() && cycle[0] != cycle[cycle.len() - 1] {
219                            let mut full_cycle = cycle.clone();
220                            full_cycle.push(neighbor.to_string());
221                            cycles.push(CircularDependency::new(full_cycle));
222                        }
223                    }
224                }
225            }
226        }
227
228        path.pop();
229        rec_stack.remove(node);
230    }
231
232    /// Build a dependency tree from the graph
233    pub fn build_tree(&self) -> DependencyNode {
234        let mut visited = HashSet::new();
235        self.build_tree_recursive(&self.root, 0, &mut visited)
236    }
237
238    fn build_tree_recursive(
239        &self,
240        node_name: &str,
241        depth: u32,
242        visited: &mut HashSet<String>,
243    ) -> DependencyNode {
244        let version = self
245            .nodes
246            .get(node_name)
247            .cloned()
248            .unwrap_or_else(|| "unknown".to_string());
249
250        let mut node = DependencyNode::new(node_name, &version, depth);
251
252        // Prevent infinite recursion on circular dependencies
253        if visited.contains(node_name) {
254            return node;
255        }
256
257        visited.insert(node_name.to_string());
258
259        // Add children
260        for edge in &self.edges {
261            if edge.from == node_name {
262                let child = self.build_tree_recursive(&edge.to, depth + 1, visited);
263                node.add_child(child);
264            }
265        }
266
267        node
268    }
269
270    /// Calculate dependency metrics
271    pub fn calculate_metrics(&self) -> DependencyMetrics {
272        let tree = self.build_tree();
273        let total_nodes = self.nodes.len();
274        let total_edges = self.edges.len();
275        let max_depth = tree.max_depth();
276
277        let optional_dependencies = self.edges.iter().filter(|e| e.optional).count();
278
279        let mut avg_depth = 0.0;
280        if total_nodes > 0 {
281            avg_depth = total_nodes as f32 / max_depth.max(1) as f32;
282        }
283
284        let mut branching_factor = 0.0;
285        if total_nodes > 1 {
286            branching_factor = total_edges as f32 / (total_nodes - 1) as f32;
287        }
288
289        let circular_dependencies = self.detect_circular_dependencies().len();
290
291        DependencyMetrics {
292            total_nodes,
293            total_edges,
294            max_depth,
295            avg_depth,
296            circular_dependencies,
297            optional_dependencies,
298            branching_factor,
299        }
300    }
301
302    /// Generate DOT format for Graphviz visualization
303    pub fn to_dot(&self) -> String {
304        let mut dot = String::from("digraph dependencies {\n");
305        dot.push_str("  rankdir=LR;\n");
306        dot.push_str("  node [shape=box];\n\n");
307
308        // Add nodes
309        for (name, version) in &self.nodes {
310            dot.push_str(&format!(
311                "  \"{}\" [label=\"{}\\n({})\"];\n",
312                name, name, version
313            ));
314        }
315
316        dot.push('\n');
317
318        // Add edges
319        for edge in &self.edges {
320            let style = if edge.optional { "dashed" } else { "solid" };
321            dot.push_str(&format!(
322                "  \"{}\" -> \"{}\" [style=\"{}\", label=\"{}\"];\n",
323                edge.from, edge.to, style, edge.version_spec
324            ));
325        }
326
327        dot.push_str("}\n");
328        dot
329    }
330
331    /// Generate JSON representation
332    pub fn to_json(&self) -> serde_json::Value {
333        let tree = self.build_tree();
334        let metrics = self.calculate_metrics();
335        let circular = self.detect_circular_dependencies();
336
337        serde_json::json!({
338            "root": self.root,
339            "root_version": self.root_version,
340            "tree": tree,
341            "metrics": metrics,
342            "circular_dependencies": circular,
343            "edges_count": self.edges.len(),
344            "nodes_count": self.nodes.len(),
345        })
346    }
347
348    /// Get all direct dependencies of a node
349    pub fn direct_dependencies(&self, node_name: &str) -> Vec<String> {
350        self.edges
351            .iter()
352            .filter(|e| e.from == node_name)
353            .map(|e| e.to.clone())
354            .collect()
355    }
356
357    /// Get all transitive dependencies
358    pub fn transitive_dependencies(&self, node_name: &str) -> Vec<String> {
359        let mut result = HashSet::new();
360        let mut queue = VecDeque::new();
361        let mut visited = HashSet::new();
362
363        queue.push_back(node_name.to_string());
364
365        while let Some(current) = queue.pop_front() {
366            if visited.contains(&current) {
367                continue;
368            }
369
370            visited.insert(current.clone());
371
372            for edge in &self.edges {
373                if edge.from == current && !edge.to.is_empty() {
374                    result.insert(edge.to.clone());
375                    queue.push_back(edge.to.clone());
376                }
377            }
378        }
379
380        result.into_iter().collect()
381    }
382
383    /// Find the shortest path between two nodes
384    pub fn shortest_path(&self, from: &str, to: &str) -> Option<Vec<String>> {
385        if from == to {
386            return Some(vec![from.to_string()]);
387        }
388
389        let mut queue = VecDeque::new();
390        let mut visited = HashSet::new();
391        let mut parent: HashMap<String, String> = HashMap::new();
392
393        queue.push_back(from.to_string());
394        visited.insert(from.to_string());
395
396        while let Some(current) = queue.pop_front() {
397            if current == to {
398                let mut path = vec![to.to_string()];
399                let mut current_node = to.to_string();
400
401                while let Some(p) = parent.get(&current_node) {
402                    path.push(p.clone());
403                    current_node = p.clone();
404                }
405
406                path.reverse();
407                return Some(path);
408            }
409
410            for edge in &self.edges {
411                if edge.from == current && !visited.contains(&edge.to) {
412                    visited.insert(edge.to.clone());
413                    parent.insert(edge.to.clone(), current.clone());
414                    queue.push_back(edge.to.clone());
415                }
416            }
417        }
418
419        None
420    }
421
422    /// Get depth of a node from root
423    pub fn node_depth(&self, node_name: &str) -> Option<u32> {
424        let tree = self.build_tree();
425        self.find_node_depth(&tree, node_name)
426    }
427
428    fn find_node_depth(&self, node: &DependencyNode, target: &str) -> Option<u32> {
429        if node.name == target {
430            return Some(node.depth);
431        }
432
433        for child in &node.children {
434            if let Some(depth) = self.find_node_depth(child, target) {
435                return Some(depth);
436            }
437        }
438
439        None
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_dependency_node_creation() {
449        let node = DependencyNode::new("plugin-a", "1.0.0", 0);
450        assert_eq!(node.name, "plugin-a");
451        assert_eq!(node.version, "1.0.0");
452        assert_eq!(node.depth, 0);
453        assert_eq!(node.child_count(), 0);
454    }
455
456    #[test]
457    fn test_dependency_node_add_child() {
458        let mut parent = DependencyNode::new("plugin-a", "1.0.0", 0);
459        let child = DependencyNode::new("plugin-b", "2.0.0", 1);
460        parent.add_child(child);
461        assert_eq!(parent.child_count(), 1);
462    }
463
464    #[test]
465    fn test_dependency_node_total_descendants() {
466        let mut parent = DependencyNode::new("plugin-a", "1.0.0", 0);
467        let mut child1 = DependencyNode::new("plugin-b", "2.0.0", 1);
468        let grandchild = DependencyNode::new("plugin-c", "3.0.0", 2);
469        child1.add_child(grandchild);
470        parent.add_child(child1);
471        assert_eq!(parent.total_descendants(), 2);
472    }
473
474    #[test]
475    fn test_dependency_node_max_depth() {
476        let mut parent = DependencyNode::new("plugin-a", "1.0.0", 0);
477        let mut child = DependencyNode::new("plugin-b", "2.0.0", 1);
478        let grandchild = DependencyNode::new("plugin-c", "3.0.0", 2);
479        child.add_child(grandchild);
480        parent.add_child(child);
481        assert_eq!(parent.max_depth(), 2);
482    }
483
484    #[test]
485    fn test_dependency_edge_creation() {
486        let edge = DependencyEdge::new("plugin-a", "plugin-b", "^1.0.0");
487        assert_eq!(edge.from, "plugin-a");
488        assert_eq!(edge.to, "plugin-b");
489        assert!(!edge.optional);
490    }
491
492    #[test]
493    fn test_dependency_edge_optional() {
494        let edge = DependencyEdge::new("plugin-a", "plugin-b", "^1.0.0").optional();
495        assert!(edge.optional);
496    }
497
498    #[test]
499    fn test_circular_dependency_creation() {
500        let cycle =
501            CircularDependency::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
502        assert_eq!(cycle.path_length, 3);
503    }
504
505    #[test]
506    fn test_circular_dependency_as_string() {
507        let cycle =
508            CircularDependency::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
509        assert_eq!(cycle.as_string(), "a → b → c");
510    }
511
512    #[test]
513    fn test_dependency_graph_creation() {
514        let graph = DependencyGraph::new("root", "1.0.0");
515        assert_eq!(graph.root, "root");
516        assert_eq!(graph.nodes.len(), 1);
517    }
518
519    #[test]
520    fn test_dependency_graph_add_dependency() {
521        let mut graph = DependencyGraph::new("root", "1.0.0");
522        let edge = DependencyEdge::new("root", "dep-a", "^1.0.0");
523        graph.add_dependency(edge);
524        assert_eq!(graph.edges.len(), 1);
525        assert_eq!(graph.nodes.len(), 2);
526    }
527
528    #[test]
529    fn test_dependency_graph_direct_dependencies() {
530        let mut graph = DependencyGraph::new("root", "1.0.0");
531        graph.add_dependency(DependencyEdge::new("root", "dep-a", "^1.0.0"));
532        graph.add_dependency(DependencyEdge::new("root", "dep-b", "^2.0.0"));
533        let deps = graph.direct_dependencies("root");
534        assert_eq!(deps.len(), 2);
535    }
536
537    #[test]
538    fn test_dependency_graph_transitive_dependencies() {
539        let mut graph = DependencyGraph::new("root", "1.0.0");
540        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
541        graph.add_dependency(DependencyEdge::new("a", "b", "^1.0.0"));
542        graph.add_dependency(DependencyEdge::new("b", "c", "^1.0.0"));
543        let deps = graph.transitive_dependencies("root");
544        assert_eq!(deps.len(), 3);
545    }
546
547    #[test]
548    fn test_dependency_graph_shortest_path() {
549        let mut graph = DependencyGraph::new("root", "1.0.0");
550        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
551        graph.add_dependency(DependencyEdge::new("a", "b", "^1.0.0"));
552        graph.add_dependency(DependencyEdge::new("b", "c", "^1.0.0"));
553        let path = graph.shortest_path("root", "c");
554        assert!(path.is_some());
555        assert_eq!(path.unwrap().len(), 4);
556    }
557
558    #[test]
559    fn test_dependency_graph_shortest_path_self() {
560        let graph = DependencyGraph::new("root", "1.0.0");
561        let path = graph.shortest_path("root", "root");
562        assert!(path.is_some());
563        assert_eq!(path.unwrap(), vec!["root"]);
564    }
565
566    #[test]
567    fn test_dependency_graph_build_tree() {
568        let mut graph = DependencyGraph::new("root", "1.0.0");
569        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
570        graph.add_dependency(DependencyEdge::new("a", "b", "^1.0.0"));
571        let tree = graph.build_tree();
572        assert_eq!(tree.name, "root");
573        assert_eq!(tree.depth, 0);
574        assert_eq!(tree.child_count(), 1);
575    }
576
577    #[test]
578    fn test_dependency_graph_calculate_metrics() {
579        let mut graph = DependencyGraph::new("root", "1.0.0");
580        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
581        graph.add_dependency(DependencyEdge::new("root", "b", "^2.0.0"));
582        graph.add_dependency(DependencyEdge::new("a", "c", "^1.0.0"));
583        let metrics = graph.calculate_metrics();
584        assert!(metrics.total_nodes > 0);
585        assert!(metrics.total_edges > 0);
586    }
587
588    #[test]
589    fn test_dependency_graph_to_dot() {
590        let mut graph = DependencyGraph::new("root", "1.0.0");
591        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
592        let dot = graph.to_dot();
593        assert!(dot.contains("digraph dependencies"));
594        assert!(dot.contains("root"));
595    }
596
597    #[test]
598    fn test_dependency_graph_to_json() {
599        let mut graph = DependencyGraph::new("root", "1.0.0");
600        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
601        let json = graph.to_json();
602        assert!(json.get("root").is_some());
603        assert!(json.get("metrics").is_some());
604    }
605
606    #[test]
607    fn test_dependency_graph_detect_no_cycles() {
608        let mut graph = DependencyGraph::new("root", "1.0.0");
609        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
610        graph.add_dependency(DependencyEdge::new("a", "b", "^1.0.0"));
611        let cycles = graph.detect_circular_dependencies();
612        // May have false positives, but tree structure shouldn't have cycles
613        assert!(cycles.len() <= 1);
614    }
615
616    #[test]
617    fn test_dependency_graph_node_depth() {
618        let mut graph = DependencyGraph::new("root", "1.0.0");
619        graph.add_dependency(DependencyEdge::new("root", "a", "^1.0.0"));
620        graph.add_dependency(DependencyEdge::new("a", "b", "^1.0.0"));
621        let depth = graph.node_depth("b");
622        assert!(depth.is_some());
623    }
624
625    #[test]
626    fn test_dependency_metrics_creation() {
627        let metrics = DependencyMetrics::new();
628        assert_eq!(metrics.total_nodes, 0);
629        assert_eq!(metrics.total_edges, 0);
630    }
631
632    #[test]
633    fn test_dependency_node_serialization() {
634        let node = DependencyNode::new("plugin-a", "1.0.0", 0);
635        let json = serde_json::to_string(&node).unwrap();
636        let deserialized: DependencyNode = serde_json::from_str(&json).unwrap();
637        assert_eq!(deserialized.name, node.name);
638    }
639
640    #[test]
641    fn test_dependency_edge_serialization() {
642        let edge = DependencyEdge::new("plugin-a", "plugin-b", "^1.0.0");
643        let json = serde_json::to_string(&edge).unwrap();
644        let deserialized: DependencyEdge = serde_json::from_str(&json).unwrap();
645        assert_eq!(deserialized.from, edge.from);
646    }
647
648    #[test]
649    fn test_circular_dependency_serialization() {
650        let cycle = CircularDependency::new(vec!["a".to_string(), "b".to_string()]);
651        let json = serde_json::to_string(&cycle).unwrap();
652        let deserialized: CircularDependency = serde_json::from_str(&json).unwrap();
653        assert_eq!(deserialized.path_length, cycle.path_length);
654    }
655
656    #[test]
657    fn test_dependency_metrics_serialization() {
658        let metrics = DependencyMetrics::new();
659        let json = serde_json::to_string(&metrics).unwrap();
660        let deserialized: DependencyMetrics = serde_json::from_str(&json).unwrap();
661        assert_eq!(deserialized.total_nodes, 0);
662    }
663}