use super::types::{AnalysisDefFingerprint, DependencyEdge, DependencyStrength};
use metrics::gauge;
use std::collections::VecDeque;
use std::fmt;
use std::path::{Path, PathBuf};
use thread_utilities::{RapidMap, RapidSet};
#[derive(Debug)]
pub enum GraphError {
CyclicDependency(PathBuf),
}
impl fmt::Display for GraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GraphError::CyclicDependency(path) => write!(
f,
"Cyclic dependency detected involving file: {}\n\
Hint: Use `thread deps --cycles` to visualize the cycle",
path.display()
),
}
}
}
impl std::error::Error for GraphError {}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub nodes: RapidMap<PathBuf, AnalysisDefFingerprint>,
pub edges: Vec<DependencyEdge>,
forward_adj: RapidMap<PathBuf, Vec<usize>>,
reverse_adj: RapidMap<PathBuf, Vec<usize>>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: thread_utilities::get_map(),
edges: Vec::new(),
forward_adj: thread_utilities::get_map(),
reverse_adj: thread_utilities::get_map(),
}
}
pub fn add_node(&mut self, file: &Path) {
self.ensure_node(file);
}
pub fn add_edge(&mut self, edge: DependencyEdge) {
let idx = self.edges.len();
self.ensure_node(&edge.from);
self.ensure_node(&edge.to);
self.forward_adj
.entry(edge.from.clone())
.or_default()
.push(idx);
self.reverse_adj
.entry(edge.to.clone())
.or_default()
.push(idx);
self.edges.push(edge);
gauge!("graph_nodes").set(self.nodes.len() as f64);
gauge!("graph_edges").set(self.edges.len() as f64);
}
pub fn get_dependencies(&self, file: &Path) -> Vec<&DependencyEdge> {
self.forward_adj
.get(file)
.map(|indices| indices.iter().map(|&i| &self.edges[i]).collect())
.unwrap_or_default()
}
pub fn get_dependents(&self, file: &Path) -> Vec<&DependencyEdge> {
self.reverse_adj
.get(file)
.map(|indices| indices.iter().map(|&i| &self.edges[i]).collect())
.unwrap_or_default()
}
pub fn find_affected_files(&self, changed_files: &RapidSet<PathBuf>) -> RapidSet<PathBuf> {
let mut affected = thread_utilities::get_set();
let mut visited = thread_utilities::get_set();
let mut queue: VecDeque<PathBuf> = changed_files.iter().cloned().collect();
while let Some(file) = queue.pop_front() {
if !visited.insert(file.clone()) {
continue;
}
affected.insert(file.clone());
for edge in self.get_dependents(&file) {
if edge.effective_strength() == DependencyStrength::Strong {
queue.push_back(edge.from.clone());
}
}
}
affected
}
pub fn topological_sort(&self, files: &RapidSet<PathBuf>) -> Result<Vec<PathBuf>, GraphError> {
let mut sorted = Vec::new();
let mut visited = thread_utilities::get_set();
let mut temp_mark = thread_utilities::get_set();
for file in files {
if !visited.contains(file) {
self.visit_node(file, files, &mut visited, &mut temp_mark, &mut sorted)?;
}
}
Ok(sorted)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn contains_node(&self, file: &Path) -> bool {
self.nodes.contains_key(file)
}
pub fn validate(&self) -> Result<(), GraphError> {
for edge in &self.edges {
if !self.nodes.contains_key(&edge.from) {
return Err(GraphError::CyclicDependency(edge.from.clone()));
}
if !self.nodes.contains_key(&edge.to) {
return Err(GraphError::CyclicDependency(edge.to.clone()));
}
}
Ok(())
}
pub fn clear(&mut self) {
self.nodes.clear();
self.edges.clear();
self.forward_adj.clear();
self.reverse_adj.clear();
}
fn ensure_node(&mut self, file: &Path) {
self.nodes
.entry(file.to_path_buf())
.or_insert_with(|| AnalysisDefFingerprint::new(b""));
}
fn visit_node(
&self,
file: &Path,
subset: &RapidSet<PathBuf>,
visited: &mut RapidSet<PathBuf>,
temp_mark: &mut RapidSet<PathBuf>,
sorted: &mut Vec<PathBuf>,
) -> Result<(), GraphError> {
let file_buf = file.to_path_buf();
if temp_mark.contains(&file_buf) {
return Err(GraphError::CyclicDependency(file_buf));
}
if visited.contains(&file_buf) {
return Ok(());
}
temp_mark.insert(file_buf.clone());
for edge in self.get_dependencies(file) {
if subset.contains(&edge.to) {
self.visit_node(&edge.to, subset, visited, temp_mark, sorted)?;
}
}
temp_mark.remove(&file_buf);
visited.insert(file_buf.clone());
sorted.push(file_buf);
Ok(())
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::types::DependencyType;
#[test]
fn test_graph_new_is_empty() {
let graph = DependencyGraph::new();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_graph_default_is_empty() {
let graph = DependencyGraph::default();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_graph_add_edge_creates_nodes() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
));
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
assert!(graph.contains_node(Path::new("a.rs")));
assert!(graph.contains_node(Path::new("b.rs")));
}
#[test]
fn test_graph_add_multiple_edges() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("c.rs"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("b.rs"),
PathBuf::from("c.rs"),
DependencyType::Import,
));
assert_eq!(graph.node_count(), 3);
assert_eq!(graph.edge_count(), 3);
}
#[test]
fn test_graph_add_edge_no_duplicate_nodes() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("c.rs"),
DependencyType::Import,
));
assert_eq!(graph.node_count(), 3);
}
#[test]
fn test_get_dependencies_empty_graph() {
let graph = DependencyGraph::new();
let deps = graph.get_dependencies(Path::new("nonexistent.rs"));
assert!(deps.is_empty());
}
#[test]
fn test_get_dependencies_returns_forward_edges() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("config.rs"),
DependencyType::Import,
));
let deps = graph.get_dependencies(Path::new("main.rs"));
assert_eq!(deps.len(), 2);
let dep_targets: RapidSet<_> = deps.iter().map(|e| &e.to).collect();
assert!(dep_targets.contains(&PathBuf::from("utils.rs")));
assert!(dep_targets.contains(&PathBuf::from("config.rs")));
}
#[test]
fn test_get_dependencies_leaf_node_has_none() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
let deps = graph.get_dependencies(Path::new("utils.rs"));
assert!(deps.is_empty());
}
#[test]
fn test_get_dependents_empty_graph() {
let graph = DependencyGraph::new();
let deps = graph.get_dependents(Path::new("nonexistent.rs"));
assert!(deps.is_empty());
}
#[test]
fn test_get_dependents_returns_reverse_edges() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("lib.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
let dependents = graph.get_dependents(Path::new("utils.rs"));
assert_eq!(dependents.len(), 2);
let dependent_sources: RapidSet<_> = dependents.iter().map(|e| &e.from).collect();
assert!(dependent_sources.contains(&PathBuf::from("main.rs")));
assert!(dependent_sources.contains(&PathBuf::from("lib.rs")));
}
#[test]
fn test_get_dependents_root_node_has_none() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
let dependents = graph.get_dependents(Path::new("main.rs"));
assert!(dependents.is_empty());
}
#[test]
fn test_find_affected_files_single_change() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("main.rs"),
PathBuf::from("utils.rs"),
DependencyType::Import,
));
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("utils.rs")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert!(affected.contains(&PathBuf::from("utils.rs")));
assert!(affected.contains(&PathBuf::from("main.rs")));
assert_eq!(affected.len(), 2);
}
#[test]
fn test_find_affected_files_transitive() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("C")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert_eq!(affected.len(), 3);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(affected.contains(&PathBuf::from("C")));
}
#[test]
fn test_find_affected_files_diamond_dependency() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("D")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert_eq!(affected.len(), 4);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(affected.contains(&PathBuf::from("C")));
assert!(affected.contains(&PathBuf::from("D")));
}
#[test]
fn test_find_affected_files_isolated_node() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("B")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(!affected.contains(&PathBuf::from("C")));
assert!(!affected.contains(&PathBuf::from("D")));
}
#[test]
fn test_find_affected_files_weak_dependency_not_followed() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import, ));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("B"),
DependencyType::Export, ));
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("B")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(
!affected.contains(&PathBuf::from("C")),
"Weak dependencies should not propagate invalidation"
);
}
#[test]
fn test_find_affected_files_empty_changed_set() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
let changed = thread_utilities::get_set();
let affected = graph.find_affected_files(&changed);
assert!(affected.is_empty());
}
#[test]
fn test_find_affected_files_unknown_file() {
let graph = DependencyGraph::new();
let changed: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("nonexistent.rs")].into_iter().collect();
let affected = graph.find_affected_files(&changed);
assert_eq!(affected.len(), 1);
assert!(affected.contains(&PathBuf::from("nonexistent.rs")));
}
#[test]
fn test_find_affected_files_multiple_changes() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
let changed: thread_utilities::RapidSet<PathBuf> = [PathBuf::from("C"), PathBuf::from("D")]
.into_iter()
.collect();
let affected = graph.find_affected_files(&changed);
assert_eq!(affected.len(), 4);
}
#[test]
fn test_topological_sort_linear_chain() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")]
.into_iter()
.collect();
let sorted = graph.topological_sort(&files).unwrap();
assert_eq!(sorted.len(), 3);
let pos_a = sorted.iter().position(|p| p == Path::new("A")).unwrap();
let pos_b = sorted.iter().position(|p| p == Path::new("B")).unwrap();
let pos_c = sorted.iter().position(|p| p == Path::new("C")).unwrap();
assert!(pos_c < pos_b, "C must come before B");
assert!(pos_b < pos_a, "B must come before A");
}
#[test]
fn test_topological_sort_diamond() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> = [
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
]
.into_iter()
.collect();
let sorted = graph.topological_sort(&files).unwrap();
assert_eq!(sorted.len(), 4);
let pos_a = sorted.iter().position(|p| p == Path::new("A")).unwrap();
let pos_b = sorted.iter().position(|p| p == Path::new("B")).unwrap();
let pos_c = sorted.iter().position(|p| p == Path::new("C")).unwrap();
let pos_d = sorted.iter().position(|p| p == Path::new("D")).unwrap();
assert!(pos_d < pos_b);
assert!(pos_d < pos_c);
assert!(pos_b < pos_a);
assert!(pos_c < pos_a);
}
#[test]
fn test_topological_sort_disconnected() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> = [
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
]
.into_iter()
.collect();
let sorted = graph.topological_sort(&files).unwrap();
assert_eq!(sorted.len(), 4);
let pos_a = sorted.iter().position(|p| p == Path::new("A")).unwrap();
let pos_b = sorted.iter().position(|p| p == Path::new("B")).unwrap();
let pos_c = sorted.iter().position(|p| p == Path::new("C")).unwrap();
let pos_d = sorted.iter().position(|p| p == Path::new("D")).unwrap();
assert!(pos_b < pos_a);
assert!(pos_d < pos_c);
}
#[test]
fn test_topological_sort_single_node() {
let graph = DependencyGraph::new();
let files: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("only.rs")].into_iter().collect();
let sorted = graph.topological_sort(&files).unwrap();
assert_eq!(sorted, vec![PathBuf::from("only.rs")]);
}
#[test]
fn test_topological_sort_empty_set() {
let graph = DependencyGraph::new();
let files = thread_utilities::get_set();
let sorted = graph.topological_sort(&files).unwrap();
assert!(sorted.is_empty());
}
#[test]
fn test_topological_sort_subset_of_graph() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B")]
.into_iter()
.collect();
let sorted = graph.topological_sort(&files).unwrap();
assert_eq!(sorted.len(), 2);
let pos_a = sorted.iter().position(|p| p == Path::new("A")).unwrap();
let pos_b = sorted.iter().position(|p| p == Path::new("B")).unwrap();
assert!(pos_b < pos_a);
}
#[test]
fn test_topological_sort_detects_simple_cycle() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B")]
.into_iter()
.collect();
let result = graph.topological_sort(&files);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
GraphError::CyclicDependency(path) => {
assert!(
path == Path::new("A") || path == Path::new("B"),
"Cycle should involve A or B, got: {}",
path.display()
);
}
}
}
#[test]
fn test_topological_sort_detects_longer_cycle() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("A"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> =
[PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")]
.into_iter()
.collect();
let result = graph.topological_sort(&files);
assert!(result.is_err());
}
#[test]
fn test_topological_sort_self_loop() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("A"),
DependencyType::Import,
));
let files: thread_utilities::RapidSet<PathBuf> = [PathBuf::from("A")].into_iter().collect();
let result = graph.topological_sort(&files);
assert!(result.is_err());
}
#[test]
fn test_validate_valid_graph() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
));
assert!(graph.validate().is_ok());
}
#[test]
fn test_validate_empty_graph() {
let graph = DependencyGraph::new();
assert!(graph.validate().is_ok());
}
#[test]
fn test_graph_clear() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
DependencyType::Import,
));
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
graph.clear();
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_graph_error_display() {
let err = GraphError::CyclicDependency(PathBuf::from("src/module.rs"));
let display = format!("{}", err);
assert!(display.contains("src/module.rs"));
assert!(display.contains("Cyclic dependency"));
}
#[test]
fn test_graph_error_is_std_error() {
let err = GraphError::CyclicDependency(PathBuf::from("a.rs"));
let _: &dyn std::error::Error = &err;
}
}