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, Dependency, DependencyKind, SourceLocation,
10};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GraphNode {
15 pub id: ComponentId,
16 pub name: String,
17 pub layer: Option<ArchLayer>,
18 pub is_cross_cutting: bool,
19 #[serde(default)]
20 pub architecture_mode: ArchitectureMode,
21 #[serde(default)]
22 pub location: SourceLocation,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GraphEdge {
28 pub kind: DependencyKind,
29 pub location: SourceLocation,
30 pub import_path: Option<String>,
31}
32
33pub struct DependencyGraph {
35 graph: DiGraph<GraphNode, GraphEdge>,
36 index: HashMap<ComponentId, NodeIndex>,
37}
38
39impl DependencyGraph {
40 pub fn new() -> Self {
41 Self {
42 graph: DiGraph::new(),
43 index: HashMap::new(),
44 }
45 }
46
47 pub fn add_component(&mut self, component: &Component) -> NodeIndex {
49 if let Some(&idx) = self.index.get(&component.id) {
50 return idx;
51 }
52 let node = GraphNode {
53 id: component.id.clone(),
54 name: component.name.clone(),
55 layer: component.layer,
56 is_cross_cutting: component.is_cross_cutting,
57 architecture_mode: component.architecture_mode,
58 location: component.location.clone(),
59 };
60 let idx = self.graph.add_node(node);
61 self.index.insert(component.id.clone(), idx);
62 idx
63 }
64
65 pub fn ensure_node(
67 &mut self,
68 id: &ComponentId,
69 layer: Option<ArchLayer>,
70 is_cross_cutting: bool,
71 ) -> NodeIndex {
72 self.ensure_node_with_mode(id, layer, is_cross_cutting, ArchitectureMode::Ddd)
73 }
74
75 pub fn ensure_node_with_mode(
77 &mut self,
78 id: &ComponentId,
79 layer: Option<ArchLayer>,
80 is_cross_cutting: bool,
81 architecture_mode: ArchitectureMode,
82 ) -> NodeIndex {
83 if let Some(&idx) = self.index.get(id) {
84 return idx;
85 }
86 let node = GraphNode {
87 id: id.clone(),
88 name: id.0.clone(),
89 layer,
90 is_cross_cutting,
91 architecture_mode,
92 location: SourceLocation::default(),
93 };
94 let idx = self.graph.add_node(node);
95 self.index.insert(id.clone(), idx);
96 idx
97 }
98
99 pub fn add_dependency(&mut self, dep: &Dependency) {
101 let from_idx = self.ensure_node(&dep.from, None, false);
102 let to_idx = self.ensure_node(&dep.to, None, false);
103 let edge = GraphEdge {
104 kind: dep.kind.clone(),
105 location: dep.location.clone(),
106 import_path: dep.import_path.clone(),
107 };
108 self.graph.add_edge(from_idx, to_idx, edge);
109 }
110
111 pub fn edges_with_nodes(&self) -> Vec<(&GraphNode, &GraphNode, &GraphEdge)> {
113 self.graph
114 .edge_references()
115 .map(|e| {
116 let src = &self.graph[e.source()];
117 let tgt = &self.graph[e.target()];
118 (src, tgt, e.weight())
119 })
120 .collect()
121 }
122
123 pub fn find_cycles(&self) -> Vec<Vec<ComponentId>> {
125 let sccs = petgraph::algo::kosaraju_scc(&self.graph);
126 sccs.into_iter()
127 .filter(|scc| scc.len() > 1)
128 .map(|scc| scc.iter().map(|&idx| self.graph[idx].id.clone()).collect())
129 .collect()
130 }
131
132 pub fn node_count(&self) -> usize {
134 self.graph.node_count()
135 }
136
137 pub fn nodes(&self) -> Vec<&GraphNode> {
139 self.graph.node_weights().collect()
140 }
141
142 pub fn nodes_by_layer(&self) -> HashMap<String, usize> {
144 let mut counts: HashMap<String, usize> = HashMap::new();
145 for node in self.graph.node_weights() {
146 if node.is_cross_cutting {
147 *counts.entry("cross_cutting".to_string()).or_insert(0) += 1;
148 } else {
149 let key = match node.layer {
150 Some(layer) => layer.to_string(),
151 None => "unclassified".to_string(),
152 };
153 *counts.entry(key).or_insert(0) += 1;
154 }
155 }
156 counts
157 }
158
159 pub fn layer_coupling_matrix(&self) -> LayerCouplingMatrix {
161 let mut matrix = LayerCouplingMatrix::new();
162 for edge in self.graph.edge_references() {
163 let src = &self.graph[edge.source()];
164 let tgt = &self.graph[edge.target()];
165 if let (Some(from_layer), Some(to_layer)) = (src.layer, tgt.layer) {
166 matrix.increment(&from_layer, &to_layer);
167 }
168 }
169 matrix
170 }
171
172 pub fn max_dependency_depth(&self) -> usize {
174 use petgraph::visit::Bfs;
175 let mut max_depth = 0;
176 for idx in self.graph.node_indices() {
178 let has_incoming = self
179 .graph
180 .neighbors_directed(idx, petgraph::Direction::Incoming)
181 .next()
182 .is_some();
183 if !has_incoming {
184 let mut bfs = Bfs::new(&self.graph, idx);
185 let mut depth = 0;
186 let mut current_level_end = idx;
187 let mut next_level_end = idx;
188 while let Some(node) = bfs.next(&self.graph) {
189 if node == current_level_end || node == idx {
190 for neighbor in self.graph.neighbors(node) {
192 next_level_end = neighbor;
193 }
194 }
195 for neighbor in self.graph.neighbors(node) {
196 next_level_end = neighbor;
197 }
198 if node == current_level_end && node != idx {
199 depth += 1;
200 current_level_end = next_level_end;
201 }
202 }
203 max_depth = max_depth.max(depth);
205 }
206 }
207 if max_depth == 0 && self.graph.edge_count() > 0 {
209 max_depth = 1;
210 }
211 max_depth
212 }
213}
214
215impl Default for DependencyGraph {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::types::*;
225 use std::path::PathBuf;
226
227 fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
228 Component {
229 id: ComponentId(id.to_string()),
230 name: name.to_string(),
231 kind: ComponentKind::Entity(EntityInfo {
232 name: name.to_string(),
233 fields: vec![],
234 methods: vec![],
235 is_active_record: false,
236 }),
237 layer,
238 location: SourceLocation {
239 file: PathBuf::from("test.go"),
240 line: 1,
241 column: 1,
242 },
243 is_cross_cutting: false,
244 architecture_mode: ArchitectureMode::Ddd,
245 }
246 }
247
248 fn make_dep(from: &str, to: &str) -> Dependency {
249 Dependency {
250 from: ComponentId(from.to_string()),
251 to: ComponentId(to.to_string()),
252 kind: DependencyKind::Import,
253 location: SourceLocation {
254 file: PathBuf::from("test.go"),
255 line: 1,
256 column: 1,
257 },
258 import_path: None,
259 }
260 }
261
262 #[test]
263 fn test_add_component_and_dependency() {
264 let mut graph = DependencyGraph::new();
265 let c1 = make_component("a", "A", Some(ArchLayer::Domain));
266 let c2 = make_component("b", "B", Some(ArchLayer::Infrastructure));
267
268 graph.add_component(&c1);
269 graph.add_component(&c2);
270 assert_eq!(graph.node_count(), 2);
271
272 graph.add_dependency(&make_dep("a", "b"));
273 let edges = graph.edges_with_nodes();
274 assert_eq!(edges.len(), 1);
275 }
276
277 #[test]
278 fn test_find_cycles() {
279 let mut graph = DependencyGraph::new();
280 let c1 = make_component("a", "A", None);
281 let c2 = make_component("b", "B", None);
282
283 graph.add_component(&c1);
284 graph.add_component(&c2);
285 graph.add_dependency(&make_dep("a", "b"));
286 graph.add_dependency(&make_dep("b", "a"));
287
288 let cycles = graph.find_cycles();
289 assert!(!cycles.is_empty(), "should detect cycle");
290 }
291
292 #[test]
293 fn test_no_duplicate_nodes() {
294 let mut graph = DependencyGraph::new();
295 let c1 = make_component("a", "A", None);
296
297 graph.add_component(&c1);
298 graph.add_component(&c1); assert_eq!(graph.node_count(), 1);
300 }
301}