use petgraph::algo::tarjan_scc;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::{EdgeFiltered, EdgeRef};
use std::collections::HashMap;
use crate::graph::edge::EdgeData;
use crate::graph::node::NodeData;
#[derive(Debug, Clone)]
pub struct Scc {
pub index: usize,
pub nodes: Vec<NodeIndex>,
pub is_cyclic: bool,
pub hint: DeployabilityHint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DeployabilityHint {
Independent,
AcyclicDependency,
CyclicCluster,
SelfLoop,
}
impl std::fmt::Display for DeployabilityHint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeployabilityHint::Independent => write!(f, "independent"),
DeployabilityHint::AcyclicDependency => write!(f, "acyclic_dependency"),
DeployabilityHint::CyclicCluster => write!(f, "cyclic_cluster"),
DeployabilityHint::SelfLoop => write!(f, "self_loop"),
}
}
}
#[derive(Debug, Clone)]
pub struct SccAnalysis {
pub components: Vec<Scc>,
pub node_to_component: HashMap<NodeIndex, usize>,
}
impl SccAnalysis {
pub fn analyze(graph: &DiGraph<NodeData, EdgeData>) -> Self {
let dep_view = EdgeFiltered::from_fn(
graph,
|edge: petgraph::graph::EdgeReference<'_, EdgeData>| {
edge.weight().kind.participates_in_scc()
},
);
let scc_groups = tarjan_scc(&dep_view);
let mut components = Vec::with_capacity(scc_groups.len());
let mut node_to_component = HashMap::new();
for (index, nodes) in scc_groups.into_iter().enumerate() {
let has_self_loop = nodes.iter().any(|&node| {
graph
.edges_directed(node, petgraph::Direction::Outgoing)
.any(|edge| edge.weight().kind.participates_in_scc() && edge.target() == node)
});
let is_cyclic = nodes.len() > 1 || has_self_loop;
let hint = if nodes.len() > 1 {
DeployabilityHint::CyclicCluster
} else if has_self_loop {
DeployabilityHint::SelfLoop
} else if nodes.len() == 1 {
DeployabilityHint::AcyclicDependency
} else {
DeployabilityHint::Independent
};
for &node in &nodes {
node_to_component.insert(node, index);
}
components.push(Scc {
index,
nodes,
is_cyclic,
hint,
});
}
Self::classify_independence(graph, &mut components, &node_to_component);
Self {
components,
node_to_component,
}
}
fn classify_independence(
graph: &DiGraph<NodeData, EdgeData>,
components: &mut [Scc],
node_to_component: &HashMap<NodeIndex, usize>,
) {
let mut component_deps: HashMap<usize, Vec<usize>> = HashMap::new();
for edge_idx in graph.edge_indices() {
let Some(weight) = graph.edge_weight(edge_idx) else {
continue;
};
if !weight.kind.participates_in_scc() {
continue;
}
let Some((source, target)) = graph.edge_endpoints(edge_idx) else {
continue;
};
let source_comp = node_to_component.get(&source);
let target_comp = node_to_component.get(&target);
if let (Some(&s), Some(&t)) = (source_comp, target_comp)
&& s != t
{
component_deps.entry(s).or_default().push(t);
}
}
for comp in components.iter_mut() {
if comp.hint == DeployabilityHint::AcyclicDependency {
let has_external_deps = component_deps
.get(&comp.index)
.map(|deps| !deps.is_empty())
.unwrap_or(false);
if !has_external_deps {
comp.hint = DeployabilityHint::Independent;
}
}
}
}
pub fn component_of(&self, node: NodeIndex) -> Option<usize> {
self.node_to_component.get(&node).copied()
}
pub fn mutually_dependent(&self, a: NodeIndex, b: NodeIndex) -> bool {
self.component_of(a) == self.component_of(b)
}
pub fn has_cycles(&self) -> bool {
self.components.iter().any(|c| c.is_cyclic)
}
pub fn cyclic_components(&self) -> impl Iterator<Item = &Scc> {
self.components.iter().filter(|c| c.is_cyclic)
}
pub fn acyclic_components(&self) -> impl Iterator<Item = &Scc> {
self.components.iter().filter(|c| !c.is_cyclic)
}
pub fn hint_counts(&self) -> HashMap<DeployabilityHint, usize> {
let mut counts = HashMap::new();
for comp in &self.components {
*counts.entry(comp.hint).or_insert(0) += 1;
}
counts
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::edge::EdgeKind;
use crate::graph::node::{FileNode, SymbolNode};
use crate::language::LangId;
use crate::model::{LineColumn, SourceRange, SymbolId, Visibility, ids::FileId};
fn make_source_range() -> SourceRange {
SourceRange {
byte_start: 0,
byte_end: 10,
start: LineColumn { line: 1, column: 0 },
end: LineColumn {
line: 1,
column: 10,
},
}
}
fn make_file_node(id: u32, path: &str) -> NodeData {
NodeData::File(FileNode {
id: FileId::new(id.max(1)).unwrap(),
path: std::path::PathBuf::from(path),
language: LangId::Rust,
snapshot_id: crate::model::ids::SnapshotId::new(1).unwrap(),
})
}
fn make_symbol_node(id: u32, name: &str, file_id: u32) -> NodeData {
NodeData::Symbol(SymbolNode {
id: SymbolId::new(id).unwrap(),
name: name.to_string(),
kind: crate::model::SymbolKind::Function,
file_id: FileId::new(file_id.max(1)).unwrap(),
visibility: Some(Visibility::Public),
source_range: make_source_range(),
})
}
fn make_edge(kind: EdgeKind) -> EdgeData {
EdgeData::new(kind)
}
#[test]
fn scc_single_node_no_edges() {
let mut graph = DiGraph::new();
let node = graph.add_node(make_symbol_node(1, "foo", 0));
let analysis = SccAnalysis::analyze(&graph);
assert_eq!(analysis.components.len(), 1);
assert_eq!(analysis.components[0].nodes, vec![node]);
assert!(!analysis.components[0].is_cyclic);
assert_eq!(analysis.components[0].hint, DeployabilityHint::Independent);
}
#[test]
fn scc_linear_chain() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(b, c, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert_eq!(analysis.components.len(), 3);
assert!(!analysis.has_cycles());
assert!(analysis.cyclic_components().next().is_none());
}
#[test]
fn scc_simple_cycle() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(b, c, make_edge(EdgeKind::Reference));
graph.add_edge(c, a, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert!(analysis.has_cycles());
let cyclic: Vec<_> = analysis.cyclic_components().collect();
assert_eq!(cyclic.len(), 1);
assert_eq!(cyclic[0].nodes.len(), 3);
assert_eq!(cyclic[0].hint, DeployabilityHint::CyclicCluster);
}
#[test]
fn scc_ownership_edges_excluded() {
let mut graph = DiGraph::new();
let file = graph.add_node(make_file_node(0, "test.rs"));
let sym = graph.add_node(make_symbol_node(1, "func", 0));
graph.add_edge(file, sym, make_edge(EdgeKind::Ownership));
let analysis = SccAnalysis::analyze(&graph);
assert_eq!(analysis.components.len(), 2);
assert!(!analysis.has_cycles());
}
#[test]
fn scc_self_loop_detected() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
graph.add_edge(a, a, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert!(analysis.has_cycles());
let comp = &analysis.components[0];
assert!(comp.is_cyclic);
assert_eq!(comp.hint, DeployabilityHint::SelfLoop);
}
#[test]
fn scc_flow_self_loop_not_cyclic() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
graph.add_edge(a, a, make_edge(EdgeKind::Flow));
let analysis = SccAnalysis::analyze(&graph);
assert!(!analysis.has_cycles());
let comp = &analysis.components[0];
assert!(!comp.is_cyclic);
assert_eq!(comp.hint, DeployabilityHint::Independent);
}
#[test]
fn scc_flow_edge_not_external_dependency() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Flow));
let analysis = SccAnalysis::analyze(&graph);
assert_eq!(analysis.components.len(), 2);
assert!(!analysis.has_cycles());
for comp in &analysis.components {
assert_eq!(comp.hint, DeployabilityHint::Independent);
}
}
#[test]
fn scc_multiple_cycles() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
let d = graph.add_node(make_symbol_node(4, "d", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(b, a, make_edge(EdgeKind::Reference));
graph.add_edge(c, d, make_edge(EdgeKind::Reference));
graph.add_edge(d, c, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert!(analysis.has_cycles());
let cyclic: Vec<_> = analysis.cyclic_components().collect();
assert_eq!(cyclic.len(), 2);
let counts = analysis.hint_counts();
assert_eq!(counts.get(&DeployabilityHint::CyclicCluster), Some(&2));
}
#[test]
fn scc_mutual_dependence_check() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(b, a, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert!(analysis.mutually_dependent(a, b));
assert!(!analysis.mutually_dependent(a, c));
assert!(!analysis.mutually_dependent(b, c));
}
#[test]
fn scc_component_lookup() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
let comp_a = analysis.component_of(a);
let comp_b = analysis.component_of(b);
assert!(comp_a.is_some());
assert!(comp_b.is_some());
}
#[test]
fn scc_topological_order_dependencies_first() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(b, c, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
let indices: Vec<_> = analysis
.components
.iter()
.map(|c| {
c.nodes
.first()
.map(|n| n.index())
.expect("Component has nodes")
})
.collect();
assert_eq!(indices.len(), 3);
}
#[test]
fn scc_diamond_structure() {
let mut graph = DiGraph::new();
let a = graph.add_node(make_symbol_node(1, "a", 0));
let b = graph.add_node(make_symbol_node(2, "b", 0));
let c = graph.add_node(make_symbol_node(3, "c", 0));
let d = graph.add_node(make_symbol_node(4, "d", 0));
graph.add_edge(a, b, make_edge(EdgeKind::Reference));
graph.add_edge(a, c, make_edge(EdgeKind::Reference));
graph.add_edge(b, d, make_edge(EdgeKind::Reference));
graph.add_edge(c, d, make_edge(EdgeKind::Reference));
let analysis = SccAnalysis::analyze(&graph);
assert!(!analysis.has_cycles());
assert_eq!(analysis.components.len(), 4);
}
#[test]
fn deployability_hint_display() {
assert_eq!(format!("{}", DeployabilityHint::Independent), "independent");
assert_eq!(
format!("{}", DeployabilityHint::CyclicCluster),
"cyclic_cluster"
);
assert_eq!(format!("{}", DeployabilityHint::SelfLoop), "self_loop");
assert_eq!(
format!("{}", DeployabilityHint::AcyclicDependency),
"acyclic_dependency"
);
}
}