1use petgraph::algo::tarjan_scc;
4use petgraph::visit::EdgeRef;
5use thiserror::Error;
6
7use super::{AgmGraph, depends_subgraph};
8
9#[derive(Debug, Clone, PartialEq, Error)]
15#[error("cycle detected in depends graph: {}", .cycle.join(" -> "))]
16pub struct CycleError {
17 pub cycle: Vec<String>,
21}
22
23impl CycleError {
24 #[must_use]
30 pub fn new(mut path: Vec<String>) -> Self {
31 if path.len() >= 2 && path.first() != path.last() {
32 let first = path[0].clone();
33 path.push(first);
34 }
35 Self { cycle: path }
36 }
37
38 #[must_use]
40 pub fn display_path(&self) -> String {
41 self.cycle.join(" -> ")
42 }
43}
44
45#[must_use]
61pub fn detect_cycles(graph: &AgmGraph) -> Vec<Vec<String>> {
62 let sub = depends_subgraph(graph);
63 let sccs = tarjan_scc(&sub);
64
65 let mut cycles: Vec<Vec<String>> = Vec::new();
66
67 for scc in sccs {
68 if scc.len() > 1 {
69 let ids: Vec<String> = scc.iter().map(|&idx| sub[idx].clone()).collect();
71 cycles.push(ids);
72 } else if scc.len() == 1 {
73 let node_idx = scc[0];
75 let has_self_loop = sub.edges(node_idx).any(|e| e.target() == node_idx);
76 if has_self_loop {
77 cycles.push(vec![sub[node_idx].clone()]);
78 }
79 }
80 }
81
82 cycles
83}
84
85#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::graph::build::build_graph;
93 use crate::graph::test_helpers::*;
94
95 #[test]
96 fn test_detect_cycles_acyclic_returns_empty() {
97 let mut a = make_node("a");
98 let b = make_node("b");
99 let c = make_node("c");
100 a.depends = Some(vec!["b".to_owned(), "c".to_owned()]);
101 let graph = build_graph(&make_file(vec![a, b, c]));
102 assert!(detect_cycles(&graph).is_empty());
103 }
104
105 #[test]
106 fn test_detect_cycles_self_loop_detected() {
107 let mut a = make_node("a");
108 a.depends = Some(vec!["a".to_owned()]);
109 let graph = build_graph(&make_file(vec![a]));
110 let cycles = detect_cycles(&graph);
111 assert_eq!(cycles.len(), 1);
112 assert_eq!(cycles[0], vec!["a"]);
113 }
114
115 #[test]
116 fn test_detect_cycles_two_node_cycle_detected() {
117 let mut a = make_node("a");
118 let mut b = make_node("b");
119 a.depends = Some(vec!["b".to_owned()]);
120 b.depends = Some(vec!["a".to_owned()]);
121 let graph = build_graph(&make_file(vec![a, b]));
122 let cycles = detect_cycles(&graph);
123 assert_eq!(cycles.len(), 1);
124 let mut ids = cycles[0].clone();
125 ids.sort();
126 assert_eq!(ids, vec!["a", "b"]);
127 }
128
129 #[test]
130 fn test_detect_cycles_multiple_sccs_detected() {
131 let mut a = make_node("a");
133 let mut b = make_node("b");
134 let mut c = make_node("c");
135 let mut d = make_node("d");
136 a.depends = Some(vec!["b".to_owned()]);
137 b.depends = Some(vec!["a".to_owned()]);
138 c.depends = Some(vec!["d".to_owned()]);
139 d.depends = Some(vec!["c".to_owned()]);
140 let graph = build_graph(&make_file(vec![a, b, c, d]));
141 let cycles = detect_cycles(&graph);
142 assert_eq!(cycles.len(), 2);
143 }
144}