forge_core/analysis/
complexity.rs1use crate::cfg::TestCfg;
7use crate::types::BlockId;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct ComplexityMetrics {
12 pub cyclomatic_complexity: usize,
15 pub decision_points: usize,
17 pub max_nesting_depth: usize,
19 pub lines_of_code: usize,
21}
22
23impl ComplexityMetrics {
24 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; let cc = if nodes == 0 {
34 1
35 } else {
36 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 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 pub fn is_complex(&self, threshold: usize) -> bool {
66 self.cyclomatic_complexity > threshold
67 }
68}
69
70#[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
90fn count_edges(cfg: &TestCfg) -> usize {
92 cfg.successors.values().map(|v| v.len()).sum()
93}
94
95fn 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
110fn count_decision_points(cfg: &TestCfg) -> usize {
113 cfg.successors
114 .values()
115 .filter(|succs| succs.len() > 1)
116 .count()
117}
118
119fn 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
149pub fn analyze_source_complexity(source: &str) -> ComplexityMetrics {
151 let lines_of_code = source.lines().count();
152
153 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 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 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 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); assert!(metrics.lines_of_code > 0);
246 }
247}