code_ranker_graph/
cycles.rs1use crate::level_graph::CycleGroup;
7use code_ranker_plugin_api::{attrs::AttrValue, graph::Graph};
8use std::collections::HashMap;
9use std::collections::HashSet;
10
11pub fn annotate_cycles(graph: &mut Graph, flow_kinds: &HashSet<String>) -> Vec<CycleGroup> {
15 let n = graph.nodes.len();
16 if n == 0 {
17 return Vec::new();
18 }
19
20 let id_to_idx: HashMap<&str, usize> = graph
21 .nodes
22 .iter()
23 .enumerate()
24 .map(|(i, node)| (node.id.as_str(), i))
25 .collect();
26
27 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
28 for edge in &graph.edges {
29 if !flow_kinds.contains(&edge.kind) {
30 continue;
31 }
32 if let (Some(&fi), Some(&ti)) = (
33 id_to_idx.get(edge.source.as_str()),
34 id_to_idx.get(edge.target.as_str()),
35 ) && fi != ti
36 {
37 adj[fi].push(ti);
38 }
39 }
40
41 let sccs = kosaraju_sccs(n, &adj);
42
43 let mut node_kind: Vec<Option<&'static str>> = vec![None; n];
44 let mut groups: Vec<CycleGroup> = Vec::new();
45 for scc in &sccs {
46 if scc.len() < 2 {
47 continue;
48 }
49 if spans_multiple_crates(scc, graph) {
53 continue;
54 }
55 let kind = classify_scc(scc);
56 for &idx in scc {
57 node_kind[idx] = Some(kind);
58 }
59 groups.push(CycleGroup {
60 kind: kind.to_string(),
61 nodes: scc.iter().map(|&i| graph.nodes[i].id.clone()).collect(),
62 });
63 }
64
65 for (i, node) in graph.nodes.iter_mut().enumerate() {
66 match node_kind[i] {
67 Some(k) => {
68 node.attrs
69 .insert("cycle".to_string(), AttrValue::Str(k.to_string()));
70 }
71 None => {
72 node.attrs.remove("cycle");
73 }
74 }
75 }
76 groups
77}
78
79fn crate_of(node: &code_ranker_plugin_api::node::Node) -> Option<&str> {
85 if let Some(AttrValue::Str(c)) = node.attrs.get("crate") {
86 return Some(c.as_str());
87 }
88 node.id.rfind("/src/").map(|i| &node.id[..i])
89}
90
91fn spans_multiple_crates(scc: &[usize], graph: &Graph) -> bool {
95 let mut crates = Vec::with_capacity(scc.len());
96 for &i in scc {
97 match crate_of(&graph.nodes[i]) {
98 Some(c) => crates.push(c),
99 None => return false,
100 }
101 }
102 crates.iter().any(|c| *c != crates[0])
103}
104
105fn classify_scc(scc: &[usize]) -> &'static str {
106 if scc.len() == 2 { "mutual" } else { "chain" }
107}
108
109fn kosaraju_sccs(n: usize, adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
112 let mut visited = vec![false; n];
113 let mut finish_order = Vec::with_capacity(n);
114 for i in 0..n {
115 if !visited[i] {
116 dfs_finish(i, adj, &mut visited, &mut finish_order);
117 }
118 }
119 let mut radj: Vec<Vec<usize>> = vec![Vec::new(); n];
120 for (u, neighbors) in adj.iter().enumerate() {
121 for &v in neighbors {
122 radj[v].push(u);
123 }
124 }
125 let mut visited2 = vec![false; n];
126 let mut sccs: Vec<Vec<usize>> = Vec::new();
127 for &start in finish_order.iter().rev() {
128 if !visited2[start] {
129 let mut scc = Vec::new();
130 dfs_collect(start, &radj, &mut visited2, &mut scc);
131 sccs.push(scc);
132 }
133 }
134 sccs
135}
136
137fn dfs_finish(start: usize, adj: &[Vec<usize>], visited: &mut [bool], order: &mut Vec<usize>) {
138 let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
139 visited[start] = true;
140 while let Some((u, ni)) = stack.last_mut() {
141 let u = *u;
142 if *ni < adj[u].len() {
143 let v = adj[u][*ni];
144 *ni += 1;
145 if !visited[v] {
146 visited[v] = true;
147 stack.push((v, 0));
148 }
149 } else {
150 stack.pop();
151 order.push(u);
152 }
153 }
154}
155
156fn dfs_collect(start: usize, adj: &[Vec<usize>], visited: &mut [bool], scc: &mut Vec<usize>) {
157 let mut stack = vec![start];
158 visited[start] = true;
159 while let Some(u) = stack.pop() {
160 scc.push(u);
161 for &v in &adj[u] {
162 if !visited[v] {
163 visited[v] = true;
164 stack.push(v);
165 }
166 }
167 }
168}
169
170#[cfg(test)]
171#[path = "cycles_test.rs"]
172mod tests;