use crate::code_graph::{CodeGraph, NodeKind, EdgeRelation};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Complexity {
Simple,
Medium,
Complex,
}
impl std::fmt::Display for Complexity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Complexity::Simple => write!(f, "simple"),
Complexity::Medium => write!(f, "medium"),
Complexity::Complex => write!(f, "complex"),
}
}
}
#[derive(Debug, Clone)]
pub struct ComplexityReport {
pub complexity: Complexity,
pub relevant_nodes: usize,
pub relevant_files: usize,
pub class_count: usize,
pub inheritance_edges: usize,
pub import_edges: usize,
pub test_count: usize,
pub summary: String,
}
pub fn assess_complexity_from_graph(
code_graph: &CodeGraph,
keywords: &[&str],
test_count: usize,
) -> ComplexityReport {
let relevant = code_graph.find_relevant_nodes(keywords);
let relevant_count = relevant.len();
let relevant_files: HashSet<&str> = relevant.iter()
.map(|n| n.file_path.as_str())
.collect();
let file_count = relevant_files.len();
let class_count = relevant.iter()
.filter(|n| n.kind == NodeKind::Class)
.count();
let relevant_ids: HashSet<&str> = relevant.iter()
.map(|n| n.id.as_str())
.collect();
let inheritance_edges = code_graph.edges.iter()
.filter(|e| {
e.relation == EdgeRelation::Inherits
&& (relevant_ids.contains(e.from.as_str()) || relevant_ids.contains(e.to.as_str()))
})
.count();
let import_edges = code_graph.edges.iter()
.filter(|e| {
e.relation == EdgeRelation::Imports
&& (relevant_ids.contains(e.from.as_str()) || relevant_ids.contains(e.to.as_str()))
})
.count();
tracing::debug!(
"Graph complexity metrics: relevant_nodes={}, files={}, classes={}, inheritance={}, imports={}, tests={}",
relevant_count, file_count, class_count, inheritance_edges, import_edges, test_count
);
let complexity = if relevant_count <= 2 && file_count <= 1 && class_count == 0 && test_count <= 1 {
Complexity::Simple
} else if relevant_count >= 6 || file_count >= 3 || inheritance_edges >= 2 || import_edges >= 4 || test_count > 3 {
Complexity::Complex
} else {
Complexity::Medium
};
let summary = format!(
"Complexity: {:?} (nodes={}, files={}, classes={}, inherit={}, imports={}, tests={})",
complexity, relevant_count, file_count, class_count, inheritance_edges, import_edges, test_count
);
tracing::info!("{}", summary);
ComplexityReport {
complexity,
relevant_nodes: relevant_count,
relevant_files: file_count,
class_count,
inheritance_edges,
import_edges,
test_count,
summary,
}
}
pub fn assess_complexity(
code_graph: &CodeGraph,
problem_statement: &str,
test_count: usize,
) -> ComplexityReport {
let keywords = CodeGraph::extract_keywords(problem_statement);
let keyword_refs: Vec<&str> = keywords.iter().map(|s| *s).collect();
assess_complexity_from_graph(code_graph, &keyword_refs, test_count)
}
pub fn is_high_risk_change(code_graph: &CodeGraph, node_ids: &[&str]) -> bool {
let total_callers: usize = node_ids.iter()
.map(|id| code_graph.get_callers(id).len())
.sum();
let max_callers = node_ids.iter()
.map(|id| code_graph.get_callers(id).len())
.max()
.unwrap_or(0);
total_callers > 10 || max_callers > 5
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskLevel {
Low, Medium, High, Critical, }
impl std::fmt::Display for RiskLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RiskLevel::Low => write!(f, "low"),
RiskLevel::Medium => write!(f, "medium"),
RiskLevel::High => write!(f, "high"),
RiskLevel::Critical => write!(f, "critical"),
}
}
}
pub fn assess_risk_level(code_graph: &CodeGraph, node_ids: &[&str]) -> RiskLevel {
let total_callers: usize = node_ids.iter()
.map(|id| code_graph.get_callers(id).len())
.sum();
match total_callers {
0..=5 => RiskLevel::Low,
6..=20 => RiskLevel::Medium,
21..=50 => RiskLevel::High,
_ => RiskLevel::Critical,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_graph::{CodeGraph, CodeNode, CodeEdge};
#[test]
fn test_empty_graph_defaults_simple() {
let graph = CodeGraph::default();
let report = assess_complexity_from_graph(&graph, &["test"], 0);
assert_eq!(report.complexity, Complexity::Simple);
}
#[test]
fn test_complex_with_many_files() {
let mut graph = CodeGraph::default();
for i in 0..5 {
let file_path = format!("file{}.py", i);
graph.nodes.push(CodeNode {
id: format!("class:{}:TestClass{}", file_path, i),
kind: NodeKind::Class,
name: format!("TestClass{}", i),
file_path,
line: Some(1),
decorators: vec![],
signature: None,
docstring: None,
line_count: 10,
is_test: false,
visibility: None,
lang: None,
body_hash: None,
end_line: None,
complexity: None,
});
}
graph.build_indexes();
let report = assess_complexity_from_graph(&graph, &["TestClass"], 0);
assert_eq!(report.complexity, Complexity::Complex);
assert!(report.relevant_files >= 3);
}
#[test]
fn test_risk_level() {
let mut graph = CodeGraph::default();
graph.nodes.push(CodeNode {
id: "func:core.py:hot_func".into(),
kind: NodeKind::Function,
name: "hot_func".into(),
file_path: "core.py".into(),
line: Some(10),
decorators: vec![],
signature: None,
docstring: None,
line_count: 20,
is_test: false,
visibility: None,
lang: None,
body_hash: None,
end_line: None,
complexity: None,
});
for i in 0..30 {
let caller_id = format!("func:caller{}.py:caller_{}", i, i);
graph.nodes.push(CodeNode {
id: caller_id.clone(),
kind: NodeKind::Function,
name: format!("caller_{}", i),
file_path: format!("caller{}.py", i),
line: Some(1),
decorators: vec![],
signature: None,
docstring: None,
line_count: 5,
is_test: false,
visibility: None,
lang: None,
body_hash: None,
end_line: None,
complexity: None,
});
graph.edges.push(CodeEdge::new(&caller_id, "func:core.py:hot_func", EdgeRelation::Calls));
}
graph.build_indexes();
let risk = assess_risk_level(&graph, &["func:core.py:hot_func"]);
assert_eq!(risk, RiskLevel::High);
}
}