1use crate::{CsrGraph, NodeId};
11use anyhow::Result;
12use std::collections::{HashMap, HashSet};
13
14#[derive(Debug, Clone)]
16pub struct PatternMatch {
17 pub node_mapping: HashMap<u32, NodeId>,
19
20 pub pattern_name: String,
22
23 pub severity: Severity,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum Severity {
30 Low,
32 Medium,
34 High,
36 Critical,
38}
39
40#[derive(Debug, Clone)]
42pub struct Pattern {
43 pub name: String,
45
46 pub edges: Vec<(u32, u32)>,
48
49 pub min_nodes: usize,
51
52 pub max_nodes: Option<usize>,
54
55 pub severity: Severity,
57}
58
59impl Pattern {
60 #[must_use]
62 pub fn new(name: String, edges: Vec<(u32, u32)>, severity: Severity) -> Self {
63 let num_nodes =
64 edges.iter().flat_map(|(s, t)| [*s, *t]).max().map_or(0, |max| max as usize + 1);
65
66 Self { name, edges, min_nodes: num_nodes, max_nodes: None, severity }
67 }
68
69 #[must_use]
73 pub fn god_class(min_callees: usize) -> Self {
74 Self {
75 name: "God Class".to_string(),
76 edges: Vec::new(), min_nodes: min_callees + 1,
78 max_nodes: None,
79 severity: Severity::High,
80 }
81 }
82
83 #[must_use]
87 #[allow(clippy::cast_possible_truncation)] pub fn circular_dependency(cycle_length: usize) -> Self {
89 let mut edges = Vec::new();
90 for i in 0..cycle_length {
91 let next = (i + 1) % cycle_length;
92 edges.push((i as u32, next as u32));
93 }
94
95 Self {
96 name: "Circular Dependency".to_string(),
97 edges,
98 min_nodes: cycle_length,
99 max_nodes: Some(cycle_length),
100 severity: Severity::Critical,
101 }
102 }
103
104 #[must_use]
108 pub fn dead_code() -> Self {
109 Self {
110 name: "Dead Code".to_string(),
111 edges: Vec::new(),
112 min_nodes: 1,
113 max_nodes: Some(1),
114 severity: Severity::Medium,
115 }
116 }
117}
118
119pub fn find_patterns(graph: &CsrGraph, pattern: &Pattern) -> Result<Vec<PatternMatch>> {
152 match pattern.name.as_str() {
153 "God Class" => Ok(find_god_class_patterns(graph, pattern)),
154 "Circular Dependency" => Ok(find_cycle_patterns(graph, pattern)),
155 "Dead Code" => Ok(find_dead_code_patterns(graph, pattern)),
156 _ => find_generic_patterns(graph, pattern),
157 }
158}
159
160fn find_god_class_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
162 let mut matches = Vec::new();
163
164 for node_id in 0..graph.num_nodes() {
165 #[allow(clippy::cast_possible_truncation)]
166 let node = NodeId(node_id as u32);
167
168 if let Ok(neighbors) = graph.outgoing_neighbors(node) {
169 if neighbors.len() >= pattern.min_nodes - 1 {
170 let mut node_mapping = HashMap::new();
172 node_mapping.insert(0, node); matches.push(PatternMatch {
175 node_mapping,
176 pattern_name: pattern.name.clone(),
177 severity: pattern.severity,
178 });
179 }
180 }
181 }
182
183 matches
184}
185
186fn find_cycle_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
188 let cycle_length = pattern.min_nodes;
189 let mut matches = Vec::new();
190 let mut visited_cycles = HashSet::new();
191
192 for start_node in 0..graph.num_nodes() {
194 #[allow(clippy::cast_possible_truncation)]
195 let start = NodeId(start_node as u32);
196
197 let cycles = find_cycles_from_node(graph, start, cycle_length);
198
199 for cycle in cycles {
200 let mut normalized = cycle.clone();
202 normalized.sort_unstable();
203
204 if visited_cycles.insert(normalized) {
205 let mut node_mapping = HashMap::new();
207 for (pattern_id, &graph_node) in cycle.iter().enumerate() {
208 #[allow(clippy::cast_possible_truncation)]
209 node_mapping.insert(pattern_id as u32, graph_node);
210 }
211
212 matches.push(PatternMatch {
213 node_mapping,
214 pattern_name: pattern.name.clone(),
215 severity: pattern.severity,
216 });
217 }
218 }
219 }
220
221 matches
222}
223
224fn find_dead_code_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
226 let mut matches = Vec::new();
227
228 for node_id in 0..graph.num_nodes() {
229 #[allow(clippy::cast_possible_truncation)]
230 let node = NodeId(node_id as u32);
231
232 if let Ok(incoming) = graph.incoming_neighbors(node) {
233 if incoming.is_empty() {
234 let mut node_mapping = HashMap::new();
236 node_mapping.insert(0, node);
237
238 matches.push(PatternMatch {
239 node_mapping,
240 pattern_name: pattern.name.clone(),
241 severity: pattern.severity,
242 });
243 }
244 }
245 }
246
247 matches
248}
249
250#[allow(clippy::unnecessary_wraps, clippy::cast_possible_truncation)]
255fn find_generic_patterns(graph: &CsrGraph, pattern: &Pattern) -> Result<Vec<PatternMatch>> {
256 let mut matches = Vec::new();
257
258 let mut pattern_adj: HashMap<u32, HashSet<u32>> = HashMap::new();
260 for (src, dst) in &pattern.edges {
261 pattern_adj.entry(*src).or_default().insert(*dst);
262 }
263
264 for start_node in 0..graph.num_nodes() {
266 let mut mapping: HashMap<u32, NodeId> = HashMap::new();
267 let mut used_nodes: HashSet<NodeId> = HashSet::new();
268
269 if try_match_pattern(
270 graph,
271 &pattern_adj,
272 pattern.min_nodes,
273 0,
274 NodeId(start_node as u32),
275 &mut mapping,
276 &mut used_nodes,
277 ) {
278 matches.push(PatternMatch {
280 node_mapping: mapping.clone(),
281 pattern_name: pattern.name.clone(),
282 severity: pattern.severity,
283 });
284 }
285 }
286
287 Ok(matches)
288}
289
290fn edges_consistent(
292 graph: &CsrGraph,
293 pattern_adj: &HashMap<u32, HashSet<u32>>,
294 mapping: &HashMap<u32, NodeId>,
295) -> bool {
296 for (&p_src, &g_src) in mapping {
297 let Some(p_targets) = pattern_adj.get(&p_src) else {
298 continue;
299 };
300 let g_targets: HashSet<u32> =
301 graph.outgoing_neighbors(g_src).unwrap_or_default().iter().copied().collect();
302 for &p_target in p_targets {
303 if let Some(&g_target) = mapping.get(&p_target) {
304 if !g_targets.contains(&g_target.0) {
305 return false;
306 }
307 }
308 }
309 }
310 true
311}
312
313fn backtrack(
315 pattern_node: u32,
316 graph_node: NodeId,
317 mapping: &mut HashMap<u32, NodeId>,
318 used_nodes: &mut HashSet<NodeId>,
319) {
320 mapping.remove(&pattern_node);
321 used_nodes.remove(&graph_node);
322}
323
324#[allow(clippy::cast_possible_truncation)]
326fn try_match_pattern(
327 graph: &CsrGraph,
328 pattern_adj: &HashMap<u32, HashSet<u32>>,
329 pattern_size: usize,
330 pattern_node: u32,
331 graph_node: NodeId,
332 mapping: &mut HashMap<u32, NodeId>,
333 used_nodes: &mut HashSet<NodeId>,
334) -> bool {
335 if used_nodes.contains(&graph_node) {
336 return false;
337 }
338
339 mapping.insert(pattern_node, graph_node);
340 used_nodes.insert(graph_node);
341
342 if mapping.len() == pattern_size {
343 return true;
344 }
345
346 if !edges_consistent(graph, pattern_adj, mapping) {
347 backtrack(pattern_node, graph_node, mapping, used_nodes);
348 return false;
349 }
350
351 for next_pattern_node in 0..pattern_size as u32 {
353 if mapping.contains_key(&next_pattern_node) {
354 continue;
355 }
356 let found = (0..graph.num_nodes()).any(|next_graph_node| {
357 try_match_pattern(
358 graph,
359 pattern_adj,
360 pattern_size,
361 next_pattern_node,
362 NodeId(next_graph_node as u32),
363 mapping,
364 used_nodes,
365 )
366 });
367 if !found {
368 backtrack(pattern_node, graph_node, mapping, used_nodes);
369 }
370 return found;
371 }
372
373 backtrack(pattern_node, graph_node, mapping, used_nodes);
374 false
375}
376
377fn find_cycles_from_node(
379 graph: &CsrGraph,
380 start: NodeId,
381 target_length: usize,
382) -> Vec<Vec<NodeId>> {
383 let mut cycles = Vec::new();
384 let mut path = vec![start];
385 let mut visited = HashSet::new();
386 visited.insert(start.0);
387
388 dfs_find_cycles(graph, start, start, &mut path, &mut visited, target_length, &mut cycles);
389
390 cycles
391}
392
393#[allow(clippy::too_many_arguments)]
395fn dfs_find_cycles(
396 graph: &CsrGraph,
397 current: NodeId,
398 start: NodeId,
399 path: &mut Vec<NodeId>,
400 visited: &mut HashSet<u32>,
401 target_length: usize,
402 cycles: &mut Vec<Vec<NodeId>>,
403) {
404 if path.len() == target_length {
405 if let Ok(neighbors) = graph.outgoing_neighbors(current) {
407 if neighbors.contains(&start.0) {
408 cycles.push(path.clone());
409 }
410 }
411 return;
412 }
413
414 if let Ok(neighbors) = graph.outgoing_neighbors(current) {
416 for &neighbor_id in neighbors {
417 let neighbor = NodeId(neighbor_id);
418
419 if !visited.contains(&neighbor_id)
420 || (neighbor == start && path.len() == target_length - 1)
421 {
422 visited.insert(neighbor_id);
423 path.push(neighbor);
424
425 dfs_find_cycles(graph, neighbor, start, path, visited, target_length, cycles);
426
427 path.pop();
428 visited.remove(&neighbor_id);
429 }
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_god_class_pattern() {
440 let mut graph = CsrGraph::new();
442 for i in 1..=5 {
443 graph.add_edge(NodeId(0), NodeId(i), 1.0).unwrap();
444 }
445
446 let pattern = Pattern::god_class(5);
447 let matches = find_patterns(&graph, &pattern).unwrap();
448
449 assert_eq!(matches.len(), 1);
450 assert_eq!(matches[0].pattern_name, "God Class");
451 assert_eq!(matches[0].severity, Severity::High);
452 }
453
454 #[test]
455 fn test_circular_dependency_pattern() {
456 let mut graph = CsrGraph::new();
458 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
459 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
460 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
461
462 let pattern = Pattern::circular_dependency(3);
463 let matches = find_patterns(&graph, &pattern).unwrap();
464
465 assert_eq!(matches.len(), 1);
466 assert_eq!(matches[0].pattern_name, "Circular Dependency");
467 assert_eq!(matches[0].severity, Severity::Critical);
468 }
469
470 #[test]
471 fn test_dead_code_pattern() {
472 let mut graph = CsrGraph::new();
474 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
475 graph.add_edge(NodeId(2), NodeId(1), 1.0).unwrap(); graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
479
480 let pattern = Pattern::dead_code();
481 let matches = find_patterns(&graph, &pattern).unwrap();
482
483 assert!(!matches.is_empty());
485 }
486
487 #[test]
488 fn test_pattern_new() {
489 let edges = vec![(0, 1), (1, 2)];
490 let pattern = Pattern::new("Test Pattern".to_string(), edges, Severity::Medium);
491
492 assert_eq!(pattern.name, "Test Pattern");
493 assert_eq!(pattern.min_nodes, 3); assert_eq!(pattern.severity, Severity::Medium);
495 }
496
497 #[test]
498 fn test_no_circular_dependency() {
499 let mut graph = CsrGraph::new();
501 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
502 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
503
504 let pattern = Pattern::circular_dependency(3);
505 let matches = find_patterns(&graph, &pattern).unwrap();
506
507 assert_eq!(matches.len(), 0); }
509
510 #[test]
511 fn test_severity_ordering() {
512 assert!(Severity::Low < Severity::Medium);
513 assert!(Severity::Medium < Severity::High);
514 assert!(Severity::High < Severity::Critical);
515 }
516
517 #[test]
518 fn test_generic_pattern_triangle() {
519 let pattern =
521 Pattern::new("Triangle".to_string(), vec![(0, 1), (1, 2), (2, 0)], Severity::Low);
522
523 let mut graph = CsrGraph::new();
525 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
526 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
527 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
528
529 let matches = find_patterns(&graph, &pattern).unwrap();
530 assert!(!matches.is_empty());
531 }
532
533 #[test]
534 fn test_generic_pattern_line() {
535 let pattern = Pattern::new("Line".to_string(), vec![(0, 1), (1, 2)], Severity::Low);
537
538 let mut graph = CsrGraph::new();
540 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
541 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
542
543 let matches = find_patterns(&graph, &pattern).unwrap();
544 assert!(!matches.is_empty());
545 }
546
547 #[test]
548 fn test_generic_pattern_no_match() {
549 let pattern = Pattern::new("V-shape".to_string(), vec![(0, 1), (0, 2)], Severity::Low);
551
552 let mut graph = CsrGraph::new();
554 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
555
556 let matches = find_patterns(&graph, &pattern).unwrap();
557 assert_eq!(matches.len(), 0);
558 }
559
560 #[test]
561 fn test_pattern_with_max_nodes() {
562 let mut pattern = Pattern::circular_dependency(3);
563 pattern.max_nodes = Some(3);
564
565 let mut graph = CsrGraph::new();
566 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
567 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
568 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
569
570 let matches = find_patterns(&graph, &pattern).unwrap();
571 assert_eq!(matches.len(), 1);
572 }
573
574 #[test]
575 fn test_multiple_god_classes() {
576 let mut graph = CsrGraph::new();
578 for i in 1..=5 {
580 graph.add_edge(NodeId(0), NodeId(i), 1.0).unwrap();
581 }
582 for i in 7..=11 {
584 graph.add_edge(NodeId(6), NodeId(i), 1.0).unwrap();
585 }
586
587 let pattern = Pattern::god_class(5);
588 let matches = find_patterns(&graph, &pattern).unwrap();
589
590 assert_eq!(matches.len(), 2);
591 }
592
593 #[test]
594 fn test_multiple_cycles() {
595 let mut graph = CsrGraph::new();
597 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
599 graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
600 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
601 graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
603 graph.add_edge(NodeId(4), NodeId(5), 1.0).unwrap();
604 graph.add_edge(NodeId(5), NodeId(3), 1.0).unwrap();
605
606 let pattern = Pattern::circular_dependency(3);
607 let matches = find_patterns(&graph, &pattern).unwrap();
608
609 assert_eq!(matches.len(), 2);
610 }
611
612 #[test]
613 fn test_dead_code_with_callers() {
614 let mut graph = CsrGraph::new();
616 graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
617 graph.add_edge(NodeId(1), NodeId(3), 1.0).unwrap();
618
619 let pattern = Pattern::dead_code();
620 let matches = find_patterns(&graph, &pattern).unwrap();
621
622 assert!(matches.len() >= 2);
624 }
625}