forgekit_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 ",
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 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 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 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); assert!(metrics.lines_of_code > 0);
263 }
264}