Skip to main content

boundary_core/
graph.rs

1use std::collections::HashMap;
2
3use petgraph::graph::{DiGraph, NodeIndex};
4use petgraph::visit::EdgeRef;
5use serde::{Deserialize, Serialize};
6
7use crate::metrics_report::LayerCouplingMatrix;
8use crate::types::{
9    ArchLayer, ArchitectureMode, Component, ComponentId, ComponentKind, Dependency, DependencyKind,
10    SourceLocation,
11};
12
13/// Node in the dependency graph
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct GraphNode {
16    pub id: ComponentId,
17    pub name: String,
18    pub layer: Option<ArchLayer>,
19    pub is_cross_cutting: bool,
20    #[serde(default)]
21    pub architecture_mode: ArchitectureMode,
22    #[serde(default)]
23    pub location: SourceLocation,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub kind: Option<ComponentKind>,
26    #[serde(default)]
27    pub is_external: bool,
28}
29
30/// Edge in the dependency graph
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct GraphEdge {
33    pub kind: DependencyKind,
34    pub location: SourceLocation,
35    pub import_path: Option<String>,
36}
37
38/// Directed dependency graph of architectural components.
39pub struct DependencyGraph {
40    graph: DiGraph<GraphNode, GraphEdge>,
41    index: HashMap<ComponentId, NodeIndex>,
42}
43
44impl DependencyGraph {
45    pub fn new() -> Self {
46        Self {
47            graph: DiGraph::new(),
48            index: HashMap::new(),
49        }
50    }
51
52    /// Add a component as a node. Returns the node index.
53    pub fn add_component(&mut self, component: &Component) -> NodeIndex {
54        if let Some(&idx) = self.index.get(&component.id) {
55            return idx;
56        }
57        let node = GraphNode {
58            id: component.id.clone(),
59            name: component.name.clone(),
60            layer: component.layer,
61            is_cross_cutting: component.is_cross_cutting,
62            architecture_mode: component.architecture_mode,
63            location: component.location.clone(),
64            kind: Some(component.kind.clone()),
65            is_external: false,
66        };
67        let idx = self.graph.add_node(node);
68        self.index.insert(component.id.clone(), idx);
69        idx
70    }
71
72    /// Ensure a component ID exists as a node (create a minimal node if needed).
73    pub fn ensure_node(
74        &mut self,
75        id: &ComponentId,
76        layer: Option<ArchLayer>,
77        is_cross_cutting: bool,
78    ) -> NodeIndex {
79        self.ensure_node_with_mode(id, layer, is_cross_cutting, ArchitectureMode::Ddd)
80    }
81
82    /// Ensure a component ID exists as a node with a specific architecture mode.
83    pub fn ensure_node_with_mode(
84        &mut self,
85        id: &ComponentId,
86        layer: Option<ArchLayer>,
87        is_cross_cutting: bool,
88        architecture_mode: ArchitectureMode,
89    ) -> NodeIndex {
90        if let Some(&idx) = self.index.get(id) {
91            return idx;
92        }
93        let node = GraphNode {
94            id: id.clone(),
95            name: id.0.clone(),
96            layer,
97            is_cross_cutting,
98            architecture_mode,
99            location: SourceLocation::default(),
100            kind: None,
101            is_external: false,
102        };
103        let idx = self.graph.add_node(node);
104        self.index.insert(id.clone(), idx);
105        idx
106    }
107
108    /// Add a dependency as an edge.
109    pub fn add_dependency(&mut self, dep: &Dependency) {
110        let from_idx = self.ensure_node(&dep.from, None, false);
111        let to_idx = self.ensure_node(&dep.to, None, false);
112        let edge = GraphEdge {
113            kind: dep.kind.clone(),
114            location: dep.location.clone(),
115            import_path: dep.import_path.clone(),
116        };
117        self.graph.add_edge(from_idx, to_idx, edge);
118    }
119
120    /// Iterate over all edges with their source and target nodes.
121    pub fn edges_with_nodes(&self) -> Vec<(&GraphNode, &GraphNode, &GraphEdge)> {
122        self.graph
123            .edge_references()
124            .map(|e| {
125                let src = &self.graph[e.source()];
126                let tgt = &self.graph[e.target()];
127                (src, tgt, e.weight())
128            })
129            .collect()
130    }
131
132    /// Find cycles using DFS. Returns groups of component IDs that form cycles.
133    pub fn find_cycles(&self) -> Vec<Vec<ComponentId>> {
134        let sccs = petgraph::algo::kosaraju_scc(&self.graph);
135        sccs.into_iter()
136            .filter(|scc| scc.len() > 1)
137            .map(|scc| scc.iter().map(|&idx| self.graph[idx].id.clone()).collect())
138            .collect()
139    }
140
141    /// Count ports and adapters in the graph.
142    pub fn node_count(&self) -> usize {
143        self.graph.node_count()
144    }
145
146    /// Get all nodes
147    pub fn nodes(&self) -> Vec<&GraphNode> {
148        self.graph.node_weights().collect()
149    }
150
151    /// Mark a node as external (not from analyzed source files).
152    pub fn mark_external(&mut self, id: &ComponentId) {
153        if let Some(&idx) = self.index.get(id) {
154            self.graph[idx].is_external = true;
155        }
156    }
157
158    /// Count nodes grouped by layer, skipping external nodes.
159    pub fn nodes_by_layer(&self) -> HashMap<String, usize> {
160        let mut counts: HashMap<String, usize> = HashMap::new();
161        for node in self.graph.node_weights() {
162            if node.is_external {
163                continue;
164            }
165            if node.is_cross_cutting {
166                *counts.entry("cross_cutting".to_string()).or_insert(0) += 1;
167            } else {
168                let key = match node.layer {
169                    Some(layer) => layer.to_string(),
170                    None => "unclassified".to_string(),
171                };
172                *counts.entry(key).or_insert(0) += 1;
173            }
174        }
175        counts
176    }
177
178    /// Build a layer coupling matrix from edge data.
179    pub fn layer_coupling_matrix(&self) -> LayerCouplingMatrix {
180        let mut matrix = LayerCouplingMatrix::new();
181        for edge in self.graph.edge_references() {
182            let src = &self.graph[edge.source()];
183            let tgt = &self.graph[edge.target()];
184            if let (Some(from_layer), Some(to_layer)) = (src.layer, tgt.layer) {
185                matrix.increment(&from_layer, &to_layer);
186            }
187        }
188        matrix
189    }
190
191    /// Calculate max dependency depth using BFS from each root node.
192    pub fn max_dependency_depth(&self) -> usize {
193        use petgraph::visit::Bfs;
194        let mut max_depth = 0;
195        // Find root nodes (no incoming edges)
196        for idx in self.graph.node_indices() {
197            let has_incoming = self
198                .graph
199                .neighbors_directed(idx, petgraph::Direction::Incoming)
200                .next()
201                .is_some();
202            if !has_incoming {
203                let mut bfs = Bfs::new(&self.graph, idx);
204                let mut depth = 0;
205                let mut current_level_end = idx;
206                let mut next_level_end = idx;
207                while let Some(node) = bfs.next(&self.graph) {
208                    if node == current_level_end || node == idx {
209                        // Check neighbors to find end of next level
210                        for neighbor in self.graph.neighbors(node) {
211                            next_level_end = neighbor;
212                        }
213                    }
214                    for neighbor in self.graph.neighbors(node) {
215                        next_level_end = neighbor;
216                    }
217                    if node == current_level_end && node != idx {
218                        depth += 1;
219                        current_level_end = next_level_end;
220                    }
221                }
222                // Simple approach: count edges in longest path from root
223                max_depth = max_depth.max(depth);
224            }
225        }
226        // Fallback: use edge count as rough depth estimate
227        if max_depth == 0 && self.graph.edge_count() > 0 {
228            max_depth = 1;
229        }
230        max_depth
231    }
232}
233
234impl Default for DependencyGraph {
235    fn default() -> Self {
236        Self::new()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::types::*;
244    use std::path::PathBuf;
245
246    fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
247        Component {
248            id: ComponentId(id.to_string()),
249            name: name.to_string(),
250            kind: ComponentKind::Entity(EntityInfo {
251                name: name.to_string(),
252                fields: vec![],
253                methods: vec![],
254                is_active_record: false,
255                is_anemic_domain_model: false,
256            }),
257            layer,
258            location: SourceLocation {
259                file: PathBuf::from("test.go"),
260                line: 1,
261                column: 1,
262            },
263            is_cross_cutting: false,
264            architecture_mode: ArchitectureMode::Ddd,
265        }
266    }
267
268    fn make_dep(from: &str, to: &str) -> Dependency {
269        Dependency {
270            from: ComponentId(from.to_string()),
271            to: ComponentId(to.to_string()),
272            kind: DependencyKind::Import,
273            location: SourceLocation {
274                file: PathBuf::from("test.go"),
275                line: 1,
276                column: 1,
277            },
278            import_path: None,
279        }
280    }
281
282    #[test]
283    fn test_add_component_and_dependency() {
284        let mut graph = DependencyGraph::new();
285        let c1 = make_component("a", "A", Some(ArchLayer::Domain));
286        let c2 = make_component("b", "B", Some(ArchLayer::Infrastructure));
287
288        graph.add_component(&c1);
289        graph.add_component(&c2);
290        assert_eq!(graph.node_count(), 2);
291
292        graph.add_dependency(&make_dep("a", "b"));
293        let edges = graph.edges_with_nodes();
294        assert_eq!(edges.len(), 1);
295    }
296
297    #[test]
298    fn test_find_cycles() {
299        let mut graph = DependencyGraph::new();
300        let c1 = make_component("a", "A", None);
301        let c2 = make_component("b", "B", None);
302
303        graph.add_component(&c1);
304        graph.add_component(&c2);
305        graph.add_dependency(&make_dep("a", "b"));
306        graph.add_dependency(&make_dep("b", "a"));
307
308        let cycles = graph.find_cycles();
309        assert!(!cycles.is_empty(), "should detect cycle");
310    }
311
312    #[test]
313    fn test_no_duplicate_nodes() {
314        let mut graph = DependencyGraph::new();
315        let c1 = make_component("a", "A", None);
316
317        graph.add_component(&c1);
318        graph.add_component(&c1); // duplicate
319        assert_eq!(graph.node_count(), 1);
320    }
321}