Skip to main content

forgekit_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 ",
156        "if(",
157        "else if",
158        "match ",
159        "match{",
160        "for ",
161        "for(",
162        "while ",
163        "while(",
164        "loop ",
165        "loop{",
166        "&&",
167        "||",
168        "?",
169        "unwrap_or",
170        "ok_or",
171    ];
172
173    let mut decision_points = 0;
174    for keyword in &decision_keywords {
175        decision_points += source.matches(keyword).count();
176    }
177
178    // Estimate nesting depth by counting indentation levels
179    let max_depth = source
180        .lines()
181        .map(|line| {
182            let indent = line.len() - line.trim_start().len();
183            if indent > 0 {
184                indent / 4
185            } else {
186                0
187            }
188        })
189        .max()
190        .unwrap_or(0);
191
192    // Estimate cyclomatic complexity: decisions + 1
193    let cc = decision_points + 1;
194
195    ComplexityMetrics {
196        cyclomatic_complexity: cc,
197        decision_points,
198        max_nesting_depth: max_depth,
199        lines_of_code,
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_simple_function_complexity() {
209        let cfg = TestCfg::new(BlockId(0));
210        let metrics = ComplexityMetrics::from_cfg(&cfg, 10);
211
212        // For a simple CFG with 1 node and 0 edges:
213        // CC = E - N + 2P = 0 - 1 + 2*1 = 1
214        assert_eq!(metrics.cyclomatic_complexity, 1);
215        assert_eq!(metrics.decision_points, 0);
216    }
217
218    #[test]
219    fn test_risk_levels() {
220        let low = ComplexityMetrics {
221            cyclomatic_complexity: 5,
222            decision_points: 4,
223            max_nesting_depth: 2,
224            lines_of_code: 20,
225        };
226        assert_eq!(low.risk_level(), RiskLevel::Low);
227
228        let medium = ComplexityMetrics {
229            cyclomatic_complexity: 15,
230            decision_points: 14,
231            max_nesting_depth: 3,
232            lines_of_code: 50,
233        };
234        assert_eq!(medium.risk_level(), RiskLevel::Medium);
235
236        let high = ComplexityMetrics {
237            cyclomatic_complexity: 30,
238            decision_points: 29,
239            max_nesting_depth: 5,
240            lines_of_code: 100,
241        };
242        assert_eq!(high.risk_level(), RiskLevel::High);
243    }
244
245    #[test]
246    fn test_analyze_source_complexity() {
247        let source = r#"
248            fn example(x: i32) -> i32 {
249                if x > 0 {
250                    if x > 10 {
251                        return x * 2;
252                    }
253                } else if x < 0 {
254                    return -x;
255                }
256                x
257            }
258        "#;
259
260        let metrics = analyze_source_complexity(source);
261        assert!(metrics.cyclomatic_complexity >= 3); // if + else if
262        assert!(metrics.lines_of_code > 0);
263    }
264}