use crate::cfg::TestCfg;
use crate::types::BlockId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComplexityMetrics {
pub cyclomatic_complexity: usize,
pub decision_points: usize,
pub max_nesting_depth: usize,
pub lines_of_code: usize,
}
impl ComplexityMetrics {
pub fn from_cfg(cfg: &TestCfg, lines_of_code: usize) -> Self {
let edges = count_edges(cfg);
let nodes = count_nodes(cfg);
let connected_components = 1;
let cc = if nodes == 0 {
1
} else {
let e = edges as isize;
let n = nodes as isize;
let p = connected_components as isize;
((e - n + 2 * p).max(1)) as usize
};
let decision_points = count_decision_points(cfg);
let max_depth = calculate_max_depth(cfg);
Self {
cyclomatic_complexity: cc,
decision_points,
max_nesting_depth: max_depth,
lines_of_code,
}
}
pub fn risk_level(&self) -> RiskLevel {
match self.cyclomatic_complexity {
1..=10 => RiskLevel::Low,
11..=20 => RiskLevel::Medium,
21..=50 => RiskLevel::High,
_ => RiskLevel::VeryHigh,
}
}
pub fn is_complex(&self, threshold: usize) -> bool {
self.cyclomatic_complexity > threshold
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskLevel {
Low,
Medium,
High,
VeryHigh,
}
impl RiskLevel {
pub fn as_str(&self) -> &'static str {
match self {
RiskLevel::Low => "low",
RiskLevel::Medium => "medium",
RiskLevel::High => "high",
RiskLevel::VeryHigh => "very_high",
}
}
}
fn count_edges(cfg: &TestCfg) -> usize {
cfg.successors.values().map(|v| v.len()).sum()
}
fn count_nodes(cfg: &TestCfg) -> usize {
let mut nodes: std::collections::HashSet<BlockId> = std::collections::HashSet::new();
nodes.insert(cfg.entry);
for (from, tos) in &cfg.successors {
nodes.insert(*from);
for to in tos {
nodes.insert(*to);
}
}
nodes.len()
}
fn count_decision_points(cfg: &TestCfg) -> usize {
cfg.successors
.values()
.filter(|succs| succs.len() > 1)
.count()
}
fn calculate_max_depth(cfg: &TestCfg) -> usize {
let mut max_depth = 0;
let mut visited = std::collections::HashSet::new();
fn dfs(
cfg: &TestCfg,
node: BlockId,
depth: usize,
visited: &mut std::collections::HashSet<BlockId>,
max_depth: &mut usize,
) {
if visited.contains(&node) {
return;
}
visited.insert(node);
*max_depth = (*max_depth).max(depth);
if let Some(succs) = cfg.successors.get(&node) {
for succ in succs {
dfs(cfg, *succ, depth + 1, visited, max_depth);
}
}
}
dfs(cfg, cfg.entry, 1, &mut visited, &mut max_depth);
max_depth
}
pub fn analyze_source_complexity(source: &str) -> ComplexityMetrics {
let lines_of_code = source.lines().count();
let decision_keywords = [
"if ",
"if(",
"else if",
"match ",
"match{",
"for ",
"for(",
"while ",
"while(",
"loop ",
"loop{",
"&&",
"||",
"?",
"unwrap_or",
"ok_or",
];
let mut decision_points = 0;
for keyword in &decision_keywords {
decision_points += source.matches(keyword).count();
}
let max_depth = source
.lines()
.map(|line| {
let indent = line.len() - line.trim_start().len();
if indent > 0 {
indent / 4
} else {
0
}
})
.max()
.unwrap_or(0);
let cc = decision_points + 1;
ComplexityMetrics {
cyclomatic_complexity: cc,
decision_points,
max_nesting_depth: max_depth,
lines_of_code,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_function_complexity() {
let cfg = TestCfg::new(BlockId(0));
let metrics = ComplexityMetrics::from_cfg(&cfg, 10);
assert_eq!(metrics.cyclomatic_complexity, 1);
assert_eq!(metrics.decision_points, 0);
}
#[test]
fn test_risk_levels() {
let low = ComplexityMetrics {
cyclomatic_complexity: 5,
decision_points: 4,
max_nesting_depth: 2,
lines_of_code: 20,
};
assert_eq!(low.risk_level(), RiskLevel::Low);
let medium = ComplexityMetrics {
cyclomatic_complexity: 15,
decision_points: 14,
max_nesting_depth: 3,
lines_of_code: 50,
};
assert_eq!(medium.risk_level(), RiskLevel::Medium);
let high = ComplexityMetrics {
cyclomatic_complexity: 30,
decision_points: 29,
max_nesting_depth: 5,
lines_of_code: 100,
};
assert_eq!(high.risk_level(), RiskLevel::High);
}
#[test]
fn test_analyze_source_complexity() {
let source = r#"
fn example(x: i32) -> i32 {
if x > 0 {
if x > 10 {
return x * 2;
}
} else if x < 0 {
return -x;
}
x
}
"#;
let metrics = analyze_source_complexity(source);
assert!(metrics.cyclomatic_complexity >= 3); assert!(metrics.lines_of_code > 0);
}
}