use crate::{CsrGraph, NodeId};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct PatternMatch {
pub node_mapping: HashMap<u32, NodeId>,
pub pattern_name: String,
pub severity: Severity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct Pattern {
pub name: String,
pub edges: Vec<(u32, u32)>,
pub min_nodes: usize,
pub max_nodes: Option<usize>,
pub severity: Severity,
}
impl Pattern {
#[must_use]
pub fn new(name: String, edges: Vec<(u32, u32)>, severity: Severity) -> Self {
let num_nodes =
edges.iter().flat_map(|(s, t)| [*s, *t]).max().map_or(0, |max| max as usize + 1);
Self { name, edges, min_nodes: num_nodes, max_nodes: None, severity }
}
#[must_use]
pub fn god_class(min_callees: usize) -> Self {
Self {
name: "God Class".to_string(),
edges: Vec::new(), min_nodes: min_callees + 1,
max_nodes: None,
severity: Severity::High,
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn circular_dependency(cycle_length: usize) -> Self {
let mut edges = Vec::new();
for i in 0..cycle_length {
let next = (i + 1) % cycle_length;
edges.push((i as u32, next as u32));
}
Self {
name: "Circular Dependency".to_string(),
edges,
min_nodes: cycle_length,
max_nodes: Some(cycle_length),
severity: Severity::Critical,
}
}
#[must_use]
pub fn dead_code() -> Self {
Self {
name: "Dead Code".to_string(),
edges: Vec::new(),
min_nodes: 1,
max_nodes: Some(1),
severity: Severity::Medium,
}
}
}
pub fn find_patterns(graph: &CsrGraph, pattern: &Pattern) -> Result<Vec<PatternMatch>> {
match pattern.name.as_str() {
"God Class" => Ok(find_god_class_patterns(graph, pattern)),
"Circular Dependency" => Ok(find_cycle_patterns(graph, pattern)),
"Dead Code" => Ok(find_dead_code_patterns(graph, pattern)),
_ => find_generic_patterns(graph, pattern),
}
}
fn find_god_class_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
let mut matches = Vec::new();
for node_id in 0..graph.num_nodes() {
#[allow(clippy::cast_possible_truncation)]
let node = NodeId(node_id as u32);
if let Ok(neighbors) = graph.outgoing_neighbors(node) {
if neighbors.len() >= pattern.min_nodes - 1 {
let mut node_mapping = HashMap::new();
node_mapping.insert(0, node);
matches.push(PatternMatch {
node_mapping,
pattern_name: pattern.name.clone(),
severity: pattern.severity,
});
}
}
}
matches
}
fn find_cycle_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
let cycle_length = pattern.min_nodes;
let mut matches = Vec::new();
let mut visited_cycles = HashSet::new();
for start_node in 0..graph.num_nodes() {
#[allow(clippy::cast_possible_truncation)]
let start = NodeId(start_node as u32);
let cycles = find_cycles_from_node(graph, start, cycle_length);
for cycle in cycles {
let mut normalized = cycle.clone();
normalized.sort_unstable();
if visited_cycles.insert(normalized) {
let mut node_mapping = HashMap::new();
for (pattern_id, &graph_node) in cycle.iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
node_mapping.insert(pattern_id as u32, graph_node);
}
matches.push(PatternMatch {
node_mapping,
pattern_name: pattern.name.clone(),
severity: pattern.severity,
});
}
}
}
matches
}
fn find_dead_code_patterns(graph: &CsrGraph, pattern: &Pattern) -> Vec<PatternMatch> {
let mut matches = Vec::new();
for node_id in 0..graph.num_nodes() {
#[allow(clippy::cast_possible_truncation)]
let node = NodeId(node_id as u32);
if let Ok(incoming) = graph.incoming_neighbors(node) {
if incoming.is_empty() {
let mut node_mapping = HashMap::new();
node_mapping.insert(0, node);
matches.push(PatternMatch {
node_mapping,
pattern_name: pattern.name.clone(),
severity: pattern.severity,
});
}
}
}
matches
}
#[allow(clippy::unnecessary_wraps, clippy::cast_possible_truncation)]
fn find_generic_patterns(graph: &CsrGraph, pattern: &Pattern) -> Result<Vec<PatternMatch>> {
let mut matches = Vec::new();
let mut pattern_adj: HashMap<u32, HashSet<u32>> = HashMap::new();
for (src, dst) in &pattern.edges {
pattern_adj.entry(*src).or_default().insert(*dst);
}
for start_node in 0..graph.num_nodes() {
let mut mapping: HashMap<u32, NodeId> = HashMap::new();
let mut used_nodes: HashSet<NodeId> = HashSet::new();
if try_match_pattern(
graph,
&pattern_adj,
pattern.min_nodes,
0,
NodeId(start_node as u32),
&mut mapping,
&mut used_nodes,
) {
matches.push(PatternMatch {
node_mapping: mapping.clone(),
pattern_name: pattern.name.clone(),
severity: pattern.severity,
});
}
}
Ok(matches)
}
fn edges_consistent(
graph: &CsrGraph,
pattern_adj: &HashMap<u32, HashSet<u32>>,
mapping: &HashMap<u32, NodeId>,
) -> bool {
for (&p_src, &g_src) in mapping.iter() {
let Some(p_targets) = pattern_adj.get(&p_src) else {
continue;
};
let g_targets: HashSet<u32> =
graph.outgoing_neighbors(g_src).unwrap_or_default().iter().copied().collect();
for &p_target in p_targets {
if let Some(&g_target) = mapping.get(&p_target) {
if !g_targets.contains(&g_target.0) {
return false;
}
}
}
}
true
}
fn backtrack(
pattern_node: u32,
graph_node: NodeId,
mapping: &mut HashMap<u32, NodeId>,
used_nodes: &mut HashSet<NodeId>,
) {
mapping.remove(&pattern_node);
used_nodes.remove(&graph_node);
}
#[allow(clippy::cast_possible_truncation)]
fn try_match_pattern(
graph: &CsrGraph,
pattern_adj: &HashMap<u32, HashSet<u32>>,
pattern_size: usize,
pattern_node: u32,
graph_node: NodeId,
mapping: &mut HashMap<u32, NodeId>,
used_nodes: &mut HashSet<NodeId>,
) -> bool {
if used_nodes.contains(&graph_node) {
return false;
}
mapping.insert(pattern_node, graph_node);
used_nodes.insert(graph_node);
if mapping.len() == pattern_size {
return true;
}
if !edges_consistent(graph, pattern_adj, mapping) {
backtrack(pattern_node, graph_node, mapping, used_nodes);
return false;
}
for next_pattern_node in 0..pattern_size as u32 {
if mapping.contains_key(&next_pattern_node) {
continue;
}
let found = (0..graph.num_nodes()).any(|next_graph_node| {
try_match_pattern(
graph,
pattern_adj,
pattern_size,
next_pattern_node,
NodeId(next_graph_node as u32),
mapping,
used_nodes,
)
});
if !found {
backtrack(pattern_node, graph_node, mapping, used_nodes);
}
return found;
}
backtrack(pattern_node, graph_node, mapping, used_nodes);
false
}
fn find_cycles_from_node(
graph: &CsrGraph,
start: NodeId,
target_length: usize,
) -> Vec<Vec<NodeId>> {
let mut cycles = Vec::new();
let mut path = vec![start];
let mut visited = HashSet::new();
visited.insert(start.0);
dfs_find_cycles(graph, start, start, &mut path, &mut visited, target_length, &mut cycles);
cycles
}
#[allow(clippy::too_many_arguments)]
fn dfs_find_cycles(
graph: &CsrGraph,
current: NodeId,
start: NodeId,
path: &mut Vec<NodeId>,
visited: &mut HashSet<u32>,
target_length: usize,
cycles: &mut Vec<Vec<NodeId>>,
) {
if path.len() == target_length {
if let Ok(neighbors) = graph.outgoing_neighbors(current) {
if neighbors.contains(&start.0) {
cycles.push(path.clone());
}
}
return;
}
if let Ok(neighbors) = graph.outgoing_neighbors(current) {
for &neighbor_id in neighbors {
let neighbor = NodeId(neighbor_id);
if !visited.contains(&neighbor_id)
|| (neighbor == start && path.len() == target_length - 1)
{
visited.insert(neighbor_id);
path.push(neighbor);
dfs_find_cycles(graph, neighbor, start, path, visited, target_length, cycles);
path.pop();
visited.remove(&neighbor_id);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_god_class_pattern() {
let mut graph = CsrGraph::new();
for i in 1..=5 {
graph.add_edge(NodeId(0), NodeId(i), 1.0).unwrap();
}
let pattern = Pattern::god_class(5);
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].pattern_name, "God Class");
assert_eq!(matches[0].severity, Severity::High);
}
#[test]
fn test_circular_dependency_pattern() {
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
let pattern = Pattern::circular_dependency(3);
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].pattern_name, "Circular Dependency");
assert_eq!(matches[0].severity, Severity::Critical);
}
#[test]
fn test_dead_code_pattern() {
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(2), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
let pattern = Pattern::dead_code();
let matches = find_patterns(&graph, &pattern).unwrap();
assert!(!matches.is_empty());
}
#[test]
fn test_pattern_new() {
let edges = vec![(0, 1), (1, 2)];
let pattern = Pattern::new("Test Pattern".to_string(), edges, Severity::Medium);
assert_eq!(pattern.name, "Test Pattern");
assert_eq!(pattern.min_nodes, 3); assert_eq!(pattern.severity, Severity::Medium);
}
#[test]
fn test_no_circular_dependency() {
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
let pattern = Pattern::circular_dependency(3);
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 0); }
#[test]
fn test_severity_ordering() {
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn test_generic_pattern_triangle() {
let pattern =
Pattern::new("Triangle".to_string(), vec![(0, 1), (1, 2), (2, 0)], Severity::Low);
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
let matches = find_patterns(&graph, &pattern).unwrap();
assert!(!matches.is_empty());
}
#[test]
fn test_generic_pattern_line() {
let pattern = Pattern::new("Line".to_string(), vec![(0, 1), (1, 2)], Severity::Low);
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
let matches = find_patterns(&graph, &pattern).unwrap();
assert!(!matches.is_empty());
}
#[test]
fn test_generic_pattern_no_match() {
let pattern = Pattern::new("V-shape".to_string(), vec![(0, 1), (0, 2)], Severity::Low);
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 0);
}
#[test]
fn test_pattern_with_max_nodes() {
let mut pattern = Pattern::circular_dependency(3);
pattern.max_nodes = Some(3);
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 1);
}
#[test]
fn test_multiple_god_classes() {
let mut graph = CsrGraph::new();
for i in 1..=5 {
graph.add_edge(NodeId(0), NodeId(i), 1.0).unwrap();
}
for i in 7..=11 {
graph.add_edge(NodeId(6), NodeId(i), 1.0).unwrap();
}
let pattern = Pattern::god_class(5);
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 2);
}
#[test]
fn test_multiple_cycles() {
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(2), 1.0).unwrap();
graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
graph.add_edge(NodeId(3), NodeId(4), 1.0).unwrap();
graph.add_edge(NodeId(4), NodeId(5), 1.0).unwrap();
graph.add_edge(NodeId(5), NodeId(3), 1.0).unwrap();
let pattern = Pattern::circular_dependency(3);
let matches = find_patterns(&graph, &pattern).unwrap();
assert_eq!(matches.len(), 2);
}
#[test]
fn test_dead_code_with_callers() {
let mut graph = CsrGraph::new();
graph.add_edge(NodeId(2), NodeId(0), 1.0).unwrap();
graph.add_edge(NodeId(1), NodeId(3), 1.0).unwrap();
let pattern = Pattern::dead_code();
let matches = find_patterns(&graph, &pattern).unwrap();
assert!(matches.len() >= 2);
}
}