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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct GraphEdge {
33 pub kind: DependencyKind,
34 pub location: SourceLocation,
35 pub import_path: Option<String>,
36}
37
38pub 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 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 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 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 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 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 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 pub fn node_count(&self) -> usize {
143 self.graph.node_count()
144 }
145
146 pub fn nodes(&self) -> Vec<&GraphNode> {
148 self.graph.node_weights().collect()
149 }
150
151 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 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 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 pub fn max_dependency_depth(&self) -> usize {
193 use petgraph::visit::Bfs;
194 let mut max_depth = 0;
195 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 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 max_depth = max_depth.max(depth);
224 }
225 }
226 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); assert_eq!(graph.node_count(), 1);
320 }
321}