use std::collections::HashMap;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use crate::metrics_report::LayerCouplingMatrix;
use crate::types::{
ArchLayer, ArchitectureMode, Component, ComponentId, ComponentKind, Dependency, DependencyKind,
SourceLocation,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
pub id: ComponentId,
pub name: String,
pub layer: Option<ArchLayer>,
pub is_cross_cutting: bool,
#[serde(default)]
pub architecture_mode: ArchitectureMode,
#[serde(default)]
pub location: SourceLocation,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<ComponentKind>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
pub kind: DependencyKind,
pub location: SourceLocation,
pub import_path: Option<String>,
}
pub struct DependencyGraph {
graph: DiGraph<GraphNode, GraphEdge>,
index: HashMap<ComponentId, NodeIndex>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
graph: DiGraph::new(),
index: HashMap::new(),
}
}
pub fn add_component(&mut self, component: &Component) -> NodeIndex {
if let Some(&idx) = self.index.get(&component.id) {
return idx;
}
let node = GraphNode {
id: component.id.clone(),
name: component.name.clone(),
layer: component.layer,
is_cross_cutting: component.is_cross_cutting,
architecture_mode: component.architecture_mode,
location: component.location.clone(),
kind: Some(component.kind.clone()),
};
let idx = self.graph.add_node(node);
self.index.insert(component.id.clone(), idx);
idx
}
pub fn ensure_node(
&mut self,
id: &ComponentId,
layer: Option<ArchLayer>,
is_cross_cutting: bool,
) -> NodeIndex {
self.ensure_node_with_mode(id, layer, is_cross_cutting, ArchitectureMode::Ddd)
}
pub fn ensure_node_with_mode(
&mut self,
id: &ComponentId,
layer: Option<ArchLayer>,
is_cross_cutting: bool,
architecture_mode: ArchitectureMode,
) -> NodeIndex {
if let Some(&idx) = self.index.get(id) {
return idx;
}
let node = GraphNode {
id: id.clone(),
name: id.0.clone(),
layer,
is_cross_cutting,
architecture_mode,
location: SourceLocation::default(),
kind: None,
};
let idx = self.graph.add_node(node);
self.index.insert(id.clone(), idx);
idx
}
pub fn add_dependency(&mut self, dep: &Dependency) {
let from_idx = self.ensure_node(&dep.from, None, false);
let to_idx = self.ensure_node(&dep.to, None, false);
let edge = GraphEdge {
kind: dep.kind.clone(),
location: dep.location.clone(),
import_path: dep.import_path.clone(),
};
self.graph.add_edge(from_idx, to_idx, edge);
}
pub fn edges_with_nodes(&self) -> Vec<(&GraphNode, &GraphNode, &GraphEdge)> {
self.graph
.edge_references()
.map(|e| {
let src = &self.graph[e.source()];
let tgt = &self.graph[e.target()];
(src, tgt, e.weight())
})
.collect()
}
pub fn find_cycles(&self) -> Vec<Vec<ComponentId>> {
let sccs = petgraph::algo::kosaraju_scc(&self.graph);
sccs.into_iter()
.filter(|scc| scc.len() > 1)
.map(|scc| scc.iter().map(|&idx| self.graph[idx].id.clone()).collect())
.collect()
}
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
pub fn nodes(&self) -> Vec<&GraphNode> {
self.graph.node_weights().collect()
}
pub fn nodes_by_layer(&self) -> HashMap<String, usize> {
let mut counts: HashMap<String, usize> = HashMap::new();
for node in self.graph.node_weights() {
if node.is_cross_cutting {
*counts.entry("cross_cutting".to_string()).or_insert(0) += 1;
} else {
let key = match node.layer {
Some(layer) => layer.to_string(),
None => "unclassified".to_string(),
};
*counts.entry(key).or_insert(0) += 1;
}
}
counts
}
pub fn layer_coupling_matrix(&self) -> LayerCouplingMatrix {
let mut matrix = LayerCouplingMatrix::new();
for edge in self.graph.edge_references() {
let src = &self.graph[edge.source()];
let tgt = &self.graph[edge.target()];
if let (Some(from_layer), Some(to_layer)) = (src.layer, tgt.layer) {
matrix.increment(&from_layer, &to_layer);
}
}
matrix
}
pub fn max_dependency_depth(&self) -> usize {
use petgraph::visit::Bfs;
let mut max_depth = 0;
for idx in self.graph.node_indices() {
let has_incoming = self
.graph
.neighbors_directed(idx, petgraph::Direction::Incoming)
.next()
.is_some();
if !has_incoming {
let mut bfs = Bfs::new(&self.graph, idx);
let mut depth = 0;
let mut current_level_end = idx;
let mut next_level_end = idx;
while let Some(node) = bfs.next(&self.graph) {
if node == current_level_end || node == idx {
for neighbor in self.graph.neighbors(node) {
next_level_end = neighbor;
}
}
for neighbor in self.graph.neighbors(node) {
next_level_end = neighbor;
}
if node == current_level_end && node != idx {
depth += 1;
current_level_end = next_level_end;
}
}
max_depth = max_depth.max(depth);
}
}
if max_depth == 0 && self.graph.edge_count() > 0 {
max_depth = 1;
}
max_depth
}
}
impl Default for DependencyGraph {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::*;
use std::path::PathBuf;
fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
Component {
id: ComponentId(id.to_string()),
name: name.to_string(),
kind: ComponentKind::Entity(EntityInfo {
name: name.to_string(),
fields: vec![],
methods: vec![],
is_active_record: false,
}),
layer,
location: SourceLocation {
file: PathBuf::from("test.go"),
line: 1,
column: 1,
},
is_cross_cutting: false,
architecture_mode: ArchitectureMode::Ddd,
}
}
fn make_dep(from: &str, to: &str) -> Dependency {
Dependency {
from: ComponentId(from.to_string()),
to: ComponentId(to.to_string()),
kind: DependencyKind::Import,
location: SourceLocation {
file: PathBuf::from("test.go"),
line: 1,
column: 1,
},
import_path: None,
}
}
#[test]
fn test_add_component_and_dependency() {
let mut graph = DependencyGraph::new();
let c1 = make_component("a", "A", Some(ArchLayer::Domain));
let c2 = make_component("b", "B", Some(ArchLayer::Infrastructure));
graph.add_component(&c1);
graph.add_component(&c2);
assert_eq!(graph.node_count(), 2);
graph.add_dependency(&make_dep("a", "b"));
let edges = graph.edges_with_nodes();
assert_eq!(edges.len(), 1);
}
#[test]
fn test_find_cycles() {
let mut graph = DependencyGraph::new();
let c1 = make_component("a", "A", None);
let c2 = make_component("b", "B", None);
graph.add_component(&c1);
graph.add_component(&c2);
graph.add_dependency(&make_dep("a", "b"));
graph.add_dependency(&make_dep("b", "a"));
let cycles = graph.find_cycles();
assert!(!cycles.is_empty(), "should detect cycle");
}
#[test]
fn test_no_duplicate_nodes() {
let mut graph = DependencyGraph::new();
let c1 = make_component("a", "A", None);
graph.add_component(&c1);
graph.add_component(&c1); assert_eq!(graph.node_count(), 1);
}
}