struct TarjanState {
index: usize,
indices: std::collections::HashMap<String, usize>,
lowlinks: std::collections::HashMap<String, usize>,
on_stack: std::collections::HashSet<String>,
stack: Vec<String>,
sccs: Vec<Vec<String>>,
}
impl TarjanState {
fn new() -> Self {
Self {
index: 0,
indices: std::collections::HashMap::new(),
lowlinks: std::collections::HashMap::new(),
on_stack: std::collections::HashSet::new(),
stack: Vec::new(),
sccs: Vec::new(),
}
}
fn is_scc_root(&self, v: &str) -> bool {
match (self.indices.get(v), self.lowlinks.get(v)) {
(Some(&v_index), Some(&v_lowlink)) => v_index == v_lowlink,
_ => false,
}
}
fn extract_scc(&mut self, v: &str) -> Vec<String> {
let mut scc = Vec::new();
while let Some(w) = self.stack.pop() {
self.on_stack.remove(&w);
let is_root = w == v;
scc.push(w);
if is_root {
break;
}
}
scc.sort();
scc
}
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub(crate) adjacency: std::collections::HashMap<String, Vec<String>>,
sccs: Vec<Vec<String>>,
}
impl DependencyGraph {
pub fn from_adjacency(adjacency: std::collections::HashMap<String, Vec<String>>) -> Self {
let sccs = Self::compute_sccs(&adjacency);
Self { adjacency, sccs }
}
fn compute_sccs(
adjacency: &std::collections::HashMap<String, Vec<String>>,
) -> Vec<Vec<String>> {
let mut state = TarjanState::new();
let mut nodes: Vec<_> = adjacency.keys().cloned().collect();
nodes.sort();
for node in nodes {
if !state.indices.contains_key(&node) {
Self::strongconnect(adjacency, &node, &mut state);
}
}
state.sccs
}
#[allow(clippy::excessive_nesting)]
#[allow(clippy::too_many_lines)]
fn strongconnect(
adjacency: &std::collections::HashMap<String, Vec<String>>,
start: &str,
state: &mut TarjanState,
) {
let children_of = |node: &str| -> Vec<String> {
let Some(deps) = adjacency.get(node) else {
return Vec::new();
};
let mut kids: Vec<_> = deps
.iter()
.filter(|w| adjacency.contains_key(*w))
.cloned()
.collect();
kids.sort();
kids
};
let mut work_stack = vec![(start.to_string(), children_of(start), 0usize)];
while let Some((v, children, i)) = work_stack.pop() {
if i == 0 {
if state.indices.contains_key(&v) {
continue;
}
let idx = state.index;
state.indices.insert(v.clone(), idx);
state.lowlinks.insert(v.clone(), idx);
state.index += 1;
state.stack.push(v.clone());
state.on_stack.insert(v.clone());
} else {
let w = &children[i - 1];
let w_lowlink = state.lowlinks.get(w).copied();
if let (Some(w_lowlink), Some(v_lowlink)) = (w_lowlink, state.lowlinks.get_mut(&v))
{
*v_lowlink = (*v_lowlink).min(w_lowlink);
}
}
let mut next_i = i;
let mut recurse: Option<String> = None;
while next_i < children.len() {
let w = &children[next_i];
next_i += 1;
if !state.indices.contains_key(w) {
recurse = Some(w.clone());
break;
} else if state.on_stack.contains(w) {
if let (Some(&w_index), Some(v_lowlink)) =
(state.indices.get(w), state.lowlinks.get_mut(&v))
{
*v_lowlink = (*v_lowlink).min(w_index);
}
}
}
if let Some(w) = recurse {
work_stack.push((v, children, next_i));
work_stack.push((w.clone(), children_of(&w), 0));
} else {
if state.is_scc_root(&v) {
let scc = state.extract_scc(&v);
state.sccs.push(scc);
}
}
}
}
pub fn cycle_groups(&self) -> Vec<Vec<String>> {
self.sccs
.iter()
.filter(|scc| scc.len() > 1)
.cloned()
.collect()
}
pub fn sort_leaves_first(&self) -> Vec<String> {
self.sccs.iter().flatten().cloned().collect()
}
pub fn direct_dependents(&self, package: &str) -> Vec<String> {
self.adjacency
.iter()
.filter(|(_, deps)| deps.iter().any(|dep| dep == package))
.map(|(name, _)| name.clone())
.collect()
}
pub fn sort_roots_first(&self) -> Vec<String> {
let mut result = self.sort_leaves_first();
result.reverse();
result
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod integration_tests;