use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
};
use dashmap::DashMap;
use crate::{
metadata::{dependencies::AssemblyDependency, identity::AssemblyIdentity},
utils::graph::{algorithms, DirectedGraph, IndexedGraph, NodeId},
Error, Result,
};
pub struct AssemblyDependencyGraph {
dependencies: Arc<DashMap<AssemblyIdentity, Vec<AssemblyDependency>>>,
dependents: Arc<DashMap<AssemblyIdentity, Vec<AssemblyIdentity>>>,
cached_topology: Arc<RwLock<Option<Vec<AssemblyIdentity>>>>,
cached_cycles: Arc<RwLock<Option<Vec<AssemblyIdentity>>>>,
assembly_count: Arc<AtomicUsize>,
}
impl AssemblyDependencyGraph {
#[must_use]
pub fn new() -> Self {
Self {
dependencies: Arc::new(DashMap::new()),
dependents: Arc::new(DashMap::new()),
cached_topology: Arc::new(RwLock::new(None)),
cached_cycles: Arc::new(RwLock::new(None)),
assembly_count: Arc::new(AtomicUsize::new(0)),
}
}
#[must_use]
pub fn get_dependencies(&self, assembly: &AssemblyIdentity) -> Vec<AssemblyDependency> {
self.dependencies
.get(assembly)
.map(|deps| deps.clone())
.unwrap_or_default()
}
#[must_use]
pub fn get_dependents(&self, assembly: &AssemblyIdentity) -> Vec<AssemblyIdentity> {
self.dependents
.get(assembly)
.map(|deps| deps.clone())
.unwrap_or_default()
}
#[must_use]
pub fn assembly_count(&self) -> usize {
self.assembly_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn dependency_count(&self) -> usize {
self.dependencies
.iter()
.map(|entry| entry.value().len())
.sum()
}
pub fn find_cycles(&self) -> Result<Option<Vec<AssemblyIdentity>>> {
{
let cached = self
.cached_cycles
.read()
.map_err(|_| Error::LockError("Failed to acquire cycle cache lock".to_string()))?;
if let Some(result) = cached.as_ref() {
return Ok(if result.is_empty() {
None
} else {
Some(result.clone())
});
}
}
let result = self.detect_cycles_dfs()?;
{
let mut cache = self.cached_cycles.write().map_err(|_| {
Error::LockError("Failed to acquire cycle cache lock for write".to_string())
})?;
*cache = Some(result.clone().unwrap_or_default());
}
Ok(result)
}
pub fn topological_order(&self) -> Result<Vec<AssemblyIdentity>> {
{
let cached = self.cached_topology.read().map_err(|_| {
Error::LockError("Failed to acquire topology cache lock".to_string())
})?;
if let Some(result) = cached.as_ref() {
return Ok(result.clone());
}
}
let result = self.scc_based_order()?;
{
let mut cache = self.cached_topology.write().map_err(|_| {
Error::LockError("Failed to acquire topology cache lock for write".to_string())
})?;
*cache = Some(result.clone());
}
Ok(result)
}
fn scc_based_order(&self) -> Result<Vec<AssemblyIdentity>> {
let indexed_graph = self.build_indexed_graph()?;
if indexed_graph.is_empty() {
return Ok(Vec::new());
}
let graph = indexed_graph.inner();
let sccs = algorithms::strongly_connected_components(graph);
let (node_to_scc, _scc_edges) = algorithms::condensation(graph, &sccs);
let mut scc_dag: DirectedGraph<usize, ()> = DirectedGraph::new();
let scc_count = sccs.len();
for i in 0..scc_count {
scc_dag.add_node(i);
}
for node_idx in 0..graph.node_count() {
let source_scc = node_to_scc[node_idx];
for successor in graph.successors(NodeId::new(node_idx)) {
let target_scc = node_to_scc[successor.index()];
if source_scc != target_scc {
let target_scc_node = NodeId::new(target_scc);
let source_scc_node = NodeId::new(source_scc);
if !scc_dag
.successors(target_scc_node)
.any(|s| s == source_scc_node)
{
scc_dag.add_edge(target_scc_node, source_scc_node, ())?;
}
}
}
}
let scc_order = algorithms::topological_sort(&scc_dag).unwrap_or_else(|| {
(0..scc_count).map(NodeId::new).collect()
});
let mut result = Vec::with_capacity(graph.node_count());
for scc_node_id in scc_order {
let scc_idx = scc_node_id.index();
for &node_id in &sccs[scc_idx] {
if let Some(identity) = indexed_graph.get_key(node_id) {
result.push(identity.clone());
}
}
}
Ok(result)
}
fn build_indexed_graph(&self) -> Result<IndexedGraph<AssemblyIdentity, ()>> {
let dep_count = self.dependency_count();
let assembly_count = self.assembly_count.load(Ordering::Relaxed);
let mut graph: IndexedGraph<AssemblyIdentity, ()> =
IndexedGraph::with_capacity(assembly_count, dep_count);
for entry in self.dependencies.iter() {
let source = entry.key().clone();
graph.add_node(source.clone());
for dep in entry.value() {
graph.add_edge(source.clone(), dep.target_identity.clone(), ())?;
}
}
Ok(graph)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.dependencies.is_empty() && self.dependents.is_empty()
}
pub fn clear(&self) {
self.dependencies.clear();
self.dependents.clear();
self.assembly_count.store(0, Ordering::Relaxed);
self.invalidate_caches();
}
pub fn add_dependency_with_source(
&self,
source_identity: &AssemblyIdentity,
dependency: AssemblyDependency,
) -> Result<()> {
let target_identity = dependency.target_identity.clone();
let mut source_is_new = false;
let mut target_is_new = false;
self.dependencies
.entry(source_identity.clone())
.and_modify(|deps| {
if !deps.iter().any(|d| d.target_identity == target_identity) {
deps.push(dependency.clone());
}
})
.or_insert_with(|| {
source_is_new = true;
vec![dependency]
});
self.dependents
.entry(target_identity.clone())
.and_modify(|deps| {
if !deps.iter().any(|d| d == source_identity) {
deps.push(source_identity.clone());
}
})
.or_insert_with(|| {
target_is_new = true;
vec![source_identity.clone()]
});
let new_assemblies =
if source_is_new && target_is_new && source_identity == &target_identity {
1
} else {
usize::from(source_is_new) + usize::from(target_is_new)
};
if new_assemblies > 0 {
self.assembly_count
.fetch_add(new_assemblies, Ordering::Relaxed);
}
self.invalidate_caches();
Ok(())
}
fn invalidate_caches(&self) {
if let Ok(mut cache) = self.cached_topology.write() {
*cache = None;
}
if let Ok(mut cache) = self.cached_cycles.write() {
*cache = None;
}
}
fn detect_cycles_dfs(&self) -> Result<Option<Vec<AssemblyIdentity>>> {
let graph = self.build_indexed_graph()?;
if graph.is_empty() {
return Ok(None);
}
Ok(graph.find_any_cycle())
}
#[must_use]
pub fn contains_assembly(&self, identity: &AssemblyIdentity) -> bool {
self.dependencies.contains_key(identity)
}
#[must_use]
pub fn all_assemblies(&self) -> Vec<AssemblyIdentity> {
self.dependencies
.iter()
.map(|entry| entry.key().clone())
.collect()
}
#[must_use]
pub fn has_dependencies(&self, identity: &AssemblyIdentity) -> bool {
self.dependencies
.get(identity)
.is_some_and(|deps| !deps.is_empty())
}
#[must_use]
pub fn has_dependents(&self, identity: &AssemblyIdentity) -> bool {
self.dependents
.get(identity)
.is_some_and(|deps| !deps.is_empty())
}
}
impl Default for AssemblyDependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
metadata::dependencies::DependencyType,
test::helpers::dependencies::{create_test_dependency, create_test_identity},
};
use std::thread;
#[test]
fn test_dependency_graph_creation() {
let graph = AssemblyDependencyGraph::new();
assert!(graph.is_empty());
assert_eq!(graph.assembly_count(), 0);
assert_eq!(graph.dependency_count(), 0);
}
#[test]
fn test_dependency_graph_add_dependency() {
let graph = AssemblyDependencyGraph::new();
let source = create_test_identity("App", 1, 0);
let dependency = create_test_dependency("mscorlib", DependencyType::Reference);
graph
.add_dependency_with_source(&source, dependency)
.unwrap();
assert!(!graph.is_empty());
assert_eq!(graph.assembly_count(), 2); assert_eq!(graph.dependency_count(), 1);
let deps = graph.get_dependencies(&source);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].target_identity.name, "mscorlib");
}
#[test]
fn test_dependency_graph_reverse_lookup() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("App", 1, 0);
let mscorlib = create_test_identity("mscorlib", 1, 0); let dependency = create_test_dependency("mscorlib", DependencyType::Reference);
graph.add_dependency_with_source(&app, dependency).unwrap();
let dependents = graph.get_dependents(&mscorlib);
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].name, "App");
}
#[test]
fn test_dependency_graph_multiple_dependencies() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("App", 1, 0);
let mscorlib_dep = create_test_dependency("mscorlib", DependencyType::Reference);
let system_dep = create_test_dependency("System", DependencyType::Reference);
let system_core_dep = create_test_dependency("System.Core", DependencyType::Reference);
graph
.add_dependency_with_source(&app, mscorlib_dep)
.unwrap();
graph.add_dependency_with_source(&app, system_dep).unwrap();
graph
.add_dependency_with_source(&app, system_core_dep)
.unwrap();
assert_eq!(graph.assembly_count(), 4); assert_eq!(graph.dependency_count(), 3);
let deps = graph.get_dependencies(&app);
assert_eq!(deps.len(), 3);
let dep_names: Vec<String> = deps
.iter()
.map(|d| d.target_identity.name.clone())
.collect();
assert!(dep_names.contains(&"mscorlib".to_string()));
assert!(dep_names.contains(&"System".to_string()));
assert!(dep_names.contains(&"System.Core".to_string()));
}
#[test]
fn test_dependency_graph_no_cycles_simple() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("App", 1, 0);
let mscorlib_dep = create_test_dependency("mscorlib", DependencyType::Reference);
graph
.add_dependency_with_source(&app, mscorlib_dep)
.unwrap();
let cycles = graph.find_cycles().unwrap();
assert!(cycles.is_none());
}
#[test]
fn test_dependency_graph_cycle_detection() {
let graph = AssemblyDependencyGraph::new();
let a = create_test_identity("A", 1, 0);
let b = create_test_identity("B", 1, 0);
let c = create_test_identity("C", 1, 0);
let b_dep = create_test_dependency("B", DependencyType::Reference);
let c_dep = create_test_dependency("C", DependencyType::Reference);
let a_dep = create_test_dependency("A", DependencyType::Reference);
graph.add_dependency_with_source(&a, b_dep).unwrap();
graph.add_dependency_with_source(&b, c_dep).unwrap();
graph.add_dependency_with_source(&c, a_dep).unwrap();
let cycles = graph.find_cycles().unwrap();
assert!(cycles.is_some());
let cycle = cycles.unwrap();
assert!(cycle.len() >= 3);
}
#[test]
fn test_dependency_graph_self_cycle() {
let graph = AssemblyDependencyGraph::new();
let a = create_test_identity("A", 1, 0);
let self_dep = create_test_dependency("A", DependencyType::Reference);
graph.add_dependency_with_source(&a, self_dep).unwrap();
let cycles = graph.find_cycles().unwrap();
assert!(cycles.is_some());
let cycle = cycles.unwrap();
assert_eq!(cycle.len(), 2); assert_eq!(cycle[0].name, "A");
assert_eq!(cycle[1].name, "A");
}
#[test]
fn test_topological_sort_simple() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("App", 1, 0);
let lib = create_test_identity("Lib", 1, 0);
let _mscorlib = create_test_identity("mscorlib", 1, 0);
let lib_dep = create_test_dependency("Lib", DependencyType::Reference);
let mscorlib_dep = create_test_dependency("mscorlib", DependencyType::Reference);
graph.add_dependency_with_source(&app, lib_dep).unwrap();
graph
.add_dependency_with_source(&lib, mscorlib_dep)
.unwrap();
let order = graph.topological_order().unwrap();
assert_eq!(order.len(), 3);
let mscorlib_pos = order.iter().position(|id| id.name == "mscorlib").unwrap();
let lib_pos = order.iter().position(|id| id.name == "Lib").unwrap();
let app_pos = order.iter().position(|id| id.name == "App").unwrap();
assert!(mscorlib_pos < lib_pos);
assert!(lib_pos < app_pos);
}
#[test]
fn test_topological_sort_with_cycle() {
let graph = AssemblyDependencyGraph::new();
let a = create_test_identity("A", 1, 0);
let b = create_test_identity("B", 1, 0);
let b_dep = create_test_dependency("B", DependencyType::Reference);
let a_dep = create_test_dependency("A", DependencyType::Reference);
graph.add_dependency_with_source(&a, b_dep).unwrap();
graph.add_dependency_with_source(&b, a_dep).unwrap();
let result = graph.topological_order();
assert!(result.is_ok());
let order = result.unwrap();
assert_eq!(order.len(), 2); }
#[test]
fn test_topological_sort_complex_dag() {
let graph = AssemblyDependencyGraph::new();
let a = create_test_identity("A", 1, 0);
let b = create_test_identity("B", 1, 0);
let c = create_test_identity("C", 1, 0);
let d = create_test_identity("D", 1, 0);
let e = create_test_identity("E", 1, 0);
let _f = create_test_identity("F", 1, 0);
graph
.add_dependency_with_source(&a, create_test_dependency("B", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&a, create_test_dependency("C", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&b, create_test_dependency("D", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&b, create_test_dependency("E", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&c, create_test_dependency("E", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&d, create_test_dependency("F", DependencyType::Reference))
.unwrap();
graph
.add_dependency_with_source(&e, create_test_dependency("F", DependencyType::Reference))
.unwrap();
let order = graph.topological_order().unwrap();
assert_eq!(order.len(), 6);
let positions: std::collections::HashMap<String, usize> = order
.iter()
.enumerate()
.map(|(i, id)| (id.name.clone(), i))
.collect();
assert!(positions["F"] < positions["D"]);
assert!(positions["F"] < positions["E"]);
assert!(positions["D"] < positions["B"]);
assert!(positions["E"] < positions["B"]);
assert!(positions["E"] < positions["C"]);
assert!(positions["B"] < positions["A"]);
assert!(positions["C"] < positions["A"]);
}
#[test]
fn test_dependency_graph_clear() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("App", 1, 0);
let dependency = create_test_dependency("mscorlib", DependencyType::Reference);
graph.add_dependency_with_source(&app, dependency).unwrap();
assert!(!graph.is_empty());
assert_eq!(graph.dependency_count(), 1);
graph.clear();
assert!(graph.is_empty());
assert_eq!(graph.assembly_count(), 0);
assert_eq!(graph.dependency_count(), 0);
}
#[test]
fn test_concurrent_graph_operations() {
let graph = Arc::new(AssemblyDependencyGraph::new());
let mut handles = vec![];
for i in 0..10 {
let graph_clone = graph.clone();
let handle = thread::spawn(move || {
let app = create_test_identity(&format!("App{}", i), 1, 0);
let dependency = create_test_dependency("mscorlib", DependencyType::Reference);
graph_clone
.add_dependency_with_source(&app, dependency)
.unwrap();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(graph.assembly_count(), 11); assert_eq!(graph.dependency_count(), 10);
let cycles = graph.find_cycles().unwrap();
assert!(cycles.is_none());
}
#[test]
fn test_duplicate_dependency_prevention() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("MyApp", 1, 0);
let dependency1 = create_test_dependency("MyLib", DependencyType::Reference);
let dependency2 = create_test_dependency("MyLib", DependencyType::Reference);
graph
.add_dependency_with_source(&app, dependency1.clone())
.unwrap();
graph.add_dependency_with_source(&app, dependency2).unwrap();
assert_eq!(graph.dependency_count(), 1);
let deps = graph.get_dependencies(&app);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].target_identity.name, "MyLib");
let target = &dependency1.target_identity;
let dependents = graph.get_dependents(target);
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0].name, "MyApp");
}
#[test]
fn test_assembly_count_with_self_reference() {
let graph = AssemblyDependencyGraph::new();
let app = create_test_identity("MyApp", 1, 0);
let mut self_dep = create_test_dependency("MyApp", DependencyType::Reference);
self_dep.target_identity = app.clone();
graph.add_dependency_with_source(&app, self_dep).unwrap();
assert_eq!(graph.assembly_count(), 1);
assert_eq!(graph.dependency_count(), 1);
}
#[test]
fn test_assembly_count_increments_correctly() {
let graph = AssemblyDependencyGraph::new();
assert_eq!(graph.assembly_count(), 0);
let app = create_test_identity("MyApp", 1, 0);
let lib1_dep = create_test_dependency("Lib1", DependencyType::Reference);
let lib1_target = lib1_dep.target_identity.clone();
graph.add_dependency_with_source(&app, lib1_dep).unwrap();
assert_eq!(graph.assembly_count(), 2);
let lib2_dep = create_test_dependency("Lib2", DependencyType::Reference);
graph.add_dependency_with_source(&app, lib2_dep).unwrap();
assert_eq!(graph.assembly_count(), 3);
let lib1_dep2 = create_test_dependency("Lib1", DependencyType::Reference);
let mut lib1_dep2_fixed = lib1_dep2.clone();
lib1_dep2_fixed.target_identity = lib1_target.clone();
graph
.add_dependency_with_source(&app, lib1_dep2_fixed)
.unwrap();
assert_eq!(graph.assembly_count(), 3); }
}