Skip to main content

forge_core/analysis/
complexity.rs

1//! Cyclomatic complexity and other code metrics calculation
2//!
3//! Analyzes function CFG to calculate complexity metrics.
4
5// Note: This module provides complexity analysis without needing Result types
6use crate::cfg::TestCfg;
7use crate::types::BlockId;
8
9/// Complexity metrics for a function
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct ComplexityMetrics {
12    /// Cyclomatic complexity (McCabe metric)
13    /// CC = E - N + 2P where E=edges, N=nodes, P=connected components
14    pub cyclomatic_complexity: usize,
15    /// Number of decision points (if, while, for, match, etc.)
16    pub decision_points: usize,
17    /// Maximum nesting depth
18    pub max_nesting_depth: usize,
19    /// Number of lines of code (approximate)
20    pub lines_of_code: usize,
21}
22
23impl ComplexityMetrics {
24    /// Calculate complexity from a CFG
25    pub fn from_cfg(cfg: &TestCfg, lines_of_code: usize) -> Self {
26        let edges = count_edges(cfg);
27        let nodes = count_nodes(cfg);
28        let connected_components = 1; // Assuming single entry point
29        
30        // Cyclomatic complexity: CC = E - N + 2P
31        // For a connected graph with a single entry/exit, this gives the number
32        // of linearly independent paths through the code
33        let cc = if nodes == 0 {
34            1
35        } else {
36            // Use isize to handle negative values properly
37            let e = edges as isize;
38            let n = nodes as isize;
39            let p = connected_components as isize;
40            ((e - n + 2 * p).max(1)) as usize
41        };
42        
43        let decision_points = count_decision_points(cfg);
44        let max_depth = calculate_max_depth(cfg);
45        
46        Self {
47            cyclomatic_complexity: cc,
48            decision_points,
49            max_nesting_depth: max_depth,
50            lines_of_code,
51        }
52    }
53    
54    /// Returns a risk assessment based on complexity
55    pub fn risk_level(&self) -> RiskLevel {
56        match self.cyclomatic_complexity {
57            1..=10 => RiskLevel::Low,
58            11..=20 => RiskLevel::Medium,
59            21..=50 => RiskLevel::High,
60            _ => RiskLevel::VeryHigh,
61        }
62    }
63    
64    /// Check if complexity exceeds threshold
65    pub fn is_complex(&self, threshold: usize) -> bool {
66        self.cyclomatic_complexity > threshold
67    }
68}
69
70/// Risk level assessment
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum RiskLevel {
73    Low,
74    Medium,
75    High,
76    VeryHigh,
77}
78
79impl RiskLevel {
80    pub fn as_str(&self) -> &'static str {
81        match self {
82            RiskLevel::Low => "low",
83            RiskLevel::Medium => "medium",
84            RiskLevel::High => "high",
85            RiskLevel::VeryHigh => "very_high",
86        }
87    }
88}
89
90/// Count total edges in CFG
91fn count_edges(cfg: &TestCfg) -> usize {
92    cfg.successors.values().map(|v| v.len()).sum()
93}
94
95/// Count total nodes (blocks) in CFG
96fn count_nodes(cfg: &TestCfg) -> usize {
97    let mut nodes: std::collections::HashSet<BlockId> = std::collections::HashSet::new();
98    nodes.insert(cfg.entry);
99    
100    for (from, tos) in &cfg.successors {
101        nodes.insert(*from);
102        for to in tos {
103            nodes.insert(*to);
104        }
105    }
106    
107    nodes.len()
108}
109
110/// Count decision points (branches) in CFG
111/// Each node with more than one outgoing edge is a decision
112fn count_decision_points(cfg: &TestCfg) -> usize {
113    cfg.successors
114        .values()
115        .filter(|succs| succs.len() > 1)
116        .count()
117}
118
119/// Calculate maximum nesting depth using DFS
120fn calculate_max_depth(cfg: &TestCfg) -> usize {
121    let mut max_depth = 0;
122    let mut visited = std::collections::HashSet::new();
123    
124    fn dfs(
125        cfg: &TestCfg,
126        node: BlockId,
127        depth: usize,
128        visited: &mut std::collections::HashSet<BlockId>,
129        max_depth: &mut usize,
130    ) {
131        if visited.contains(&node) {
132            return;
133        }
134        visited.insert(node);
135        
136        *max_depth = (*max_depth).max(depth);
137        
138        if let Some(succs) = cfg.successors.get(&node) {
139            for succ in succs {
140                dfs(cfg, *succ, depth + 1, visited, max_depth);
141            }
142        }
143    }
144    
145    dfs(cfg, cfg.entry, 1, &mut visited, &mut max_depth);
146    max_depth
147}
148
149/// Analyze source code to estimate complexity without full CFG
150pub fn analyze_source_complexity(source: &str) -> ComplexityMetrics {
151    let lines_of_code = source.lines().count();
152    
153    // Count decision keywords
154    let decision_keywords = [
155        "if ", "if(", "else if", "match ", "match{",
156        "for ", "for(", "while ", "while(", "loop ", "loop{",
157        "&&", "||", "?", "unwrap_or", "ok_or",
158    ];
159    
160    let mut decision_points = 0;
161    for keyword in &decision_keywords {
162        decision_points += source.matches(keyword).count();
163    }
164    
165    // Estimate nesting depth by counting indentation levels
166    let max_depth = source
167        .lines()
168        .filter_map(|line| {
169            let indent = line.len() - line.trim_start().len();
170            if indent > 0 { Some(indent / 4) } else { Some(0) }
171        })
172        .max()
173        .unwrap_or(0);
174    
175    // Estimate cyclomatic complexity: decisions + 1
176    let cc = decision_points + 1;
177    
178    ComplexityMetrics {
179        cyclomatic_complexity: cc,
180        decision_points,
181        max_nesting_depth: max_depth,
182        lines_of_code,
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    
190    #[test]
191    fn test_simple_function_complexity() {
192        let cfg = TestCfg::new(BlockId(0));
193        let metrics = ComplexityMetrics::from_cfg(&cfg, 10);
194        
195        // For a simple CFG with 1 node and 0 edges:
196        // CC = E - N + 2P = 0 - 1 + 2*1 = 1
197        assert_eq!(metrics.cyclomatic_complexity, 1);
198        assert_eq!(metrics.decision_points, 0);
199    }
200    
201    #[test]
202    fn test_risk_levels() {
203        let low = ComplexityMetrics {
204            cyclomatic_complexity: 5,
205            decision_points: 4,
206            max_nesting_depth: 2,
207            lines_of_code: 20,
208        };
209        assert_eq!(low.risk_level(), RiskLevel::Low);
210        
211        let medium = ComplexityMetrics {
212            cyclomatic_complexity: 15,
213            decision_points: 14,
214            max_nesting_depth: 3,
215            lines_of_code: 50,
216        };
217        assert_eq!(medium.risk_level(), RiskLevel::Medium);
218        
219        let high = ComplexityMetrics {
220            cyclomatic_complexity: 30,
221            decision_points: 29,
222            max_nesting_depth: 5,
223            lines_of_code: 100,
224        };
225        assert_eq!(high.risk_level(), RiskLevel::High);
226    }
227    
228    #[test]
229    fn test_analyze_source_complexity() {
230        let source = r#"
231            fn example(x: i32) -> i32 {
232                if x > 0 {
233                    if x > 10 {
234                        return x * 2;
235                    }
236                } else if x < 0 {
237                    return -x;
238                }
239                x
240            }
241        "#;
242        
243        let metrics = analyze_source_complexity(source);
244        assert!(metrics.cyclomatic_complexity >= 3); // if + else if
245        assert!(metrics.lines_of_code > 0);
246    }
247}