codeprism_analysis/
complexity.rs1use anyhow::Result;
4use serde_json::Value;
5use std::collections::HashSet;
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct ComplexityMetrics {
11 pub cyclomatic: usize,
12 pub cognitive: usize,
13 pub halstead_volume: f64,
14 pub halstead_difficulty: f64,
15 pub halstead_effort: f64,
16 pub maintainability_index: f64,
17 pub lines_of_code: usize,
18}
19
20pub struct ComplexityAnalyzer;
22
23impl ComplexityAnalyzer {
24 pub fn new() -> Self {
25 Self
26 }
27
28 pub fn analyze_file_complexity(
30 &self,
31 file_path: &Path,
32 metrics: &[String],
33 threshold_warnings: bool,
34 ) -> Result<Value> {
35 let content = std::fs::read_to_string(file_path)?;
36 let lines_count = content.lines().count();
37
38 let complexity_metrics = self.calculate_all_metrics(&content, lines_count);
39
40 let mut result = serde_json::json!({
41 "file": file_path.display().to_string(),
42 "lines_of_code": lines_count,
43 "metrics": {}
44 });
45
46 if metrics.contains(&"cyclomatic".to_string()) || metrics.contains(&"all".to_string()) {
47 result["metrics"]["cyclomatic_complexity"] = complexity_metrics.cyclomatic.into();
48 if threshold_warnings && complexity_metrics.cyclomatic > 10 {
49 result["warnings"] = serde_json::json!([{
50 "type": "high_cyclomatic_complexity",
51 "message": format!("Cyclomatic complexity ({}) exceeds recommended threshold (10)", complexity_metrics.cyclomatic)
52 }]);
53 }
54 }
55
56 if metrics.contains(&"cognitive".to_string()) || metrics.contains(&"all".to_string()) {
57 result["metrics"]["cognitive_complexity"] = complexity_metrics.cognitive.into();
58 if threshold_warnings && complexity_metrics.cognitive > 15 {
59 let warnings = result["warnings"].as_array().cloned().unwrap_or_default();
60 let mut new_warnings = warnings;
61 new_warnings.push(serde_json::json!({
62 "type": "high_cognitive_complexity",
63 "message": format!("Cognitive complexity ({}) exceeds recommended threshold (15)", complexity_metrics.cognitive)
64 }));
65 result["warnings"] = new_warnings.into();
66 }
67 }
68
69 if metrics.contains(&"halstead".to_string()) || metrics.contains(&"all".to_string()) {
70 result["metrics"]["halstead"] = serde_json::json!({
71 "volume": complexity_metrics.halstead_volume,
72 "difficulty": complexity_metrics.halstead_difficulty,
73 "effort": complexity_metrics.halstead_effort
74 });
75 }
76
77 if metrics.contains(&"maintainability".to_string()) || metrics.contains(&"all".to_string())
78 {
79 result["metrics"]["maintainability_index"] =
80 complexity_metrics.maintainability_index.into();
81 if threshold_warnings && complexity_metrics.maintainability_index < 50.0 {
82 let warnings = result["warnings"].as_array().cloned().unwrap_or_default();
83 let mut new_warnings = warnings;
84 new_warnings.push(serde_json::json!({
85 "type": "low_maintainability",
86 "message": format!("Maintainability index ({:.1}) is below recommended threshold (50.0)", complexity_metrics.maintainability_index)
87 }));
88 result["warnings"] = new_warnings.into();
89 }
90 }
91
92 Ok(result)
93 }
94
95 pub fn calculate_all_metrics(&self, content: &str, lines_count: usize) -> ComplexityMetrics {
97 let cyclomatic = self.calculate_cyclomatic_complexity(content);
98 let cognitive = self.calculate_cognitive_complexity(content);
99 let (halstead_volume, halstead_difficulty, halstead_effort) =
100 self.calculate_halstead_metrics(content);
101 let maintainability_index = self.calculate_maintainability_index(content, lines_count);
102
103 ComplexityMetrics {
104 cyclomatic,
105 cognitive,
106 halstead_volume,
107 halstead_difficulty,
108 halstead_effort,
109 maintainability_index,
110 lines_of_code: lines_count,
111 }
112 }
113
114 pub fn calculate_cyclomatic_complexity(&self, content: &str) -> usize {
116 let mut complexity = 1; let decision_keywords = [
120 "if", "else if", "elif", "while", "for", "foreach", "switch", "case", "catch",
121 "except", "?", "&&", "||", "and", "or",
122 ];
123
124 for keyword in &decision_keywords {
125 complexity += content.matches(keyword).count();
126 }
127
128 complexity
129 }
130
131 pub fn calculate_cognitive_complexity(&self, content: &str) -> usize {
133 let mut complexity = 0;
134 let mut nesting_level: usize = 0;
135
136 let lines = content.lines();
137 for line in lines {
138 let trimmed = line.trim();
139
140 if trimmed.contains('{')
142 || trimmed.starts_with("if ")
143 || trimmed.starts_with("for ")
144 || trimmed.starts_with("while ")
145 || trimmed.starts_with("try ")
146 || trimmed.starts_with("def ")
147 || trimmed.starts_with("function ")
148 {
149 nesting_level += 1;
150 }
151
152 if trimmed.contains('}') {
154 nesting_level = nesting_level.saturating_sub(1usize);
155 }
156
157 if trimmed.contains("if ") || trimmed.contains("elif ") || trimmed.contains("else if") {
159 complexity += 1 + nesting_level;
160 }
161 if trimmed.contains("while ") || trimmed.contains("for ") {
162 complexity += 1 + nesting_level;
163 }
164 if trimmed.contains("catch ") || trimmed.contains("except ") {
165 complexity += 1 + nesting_level;
166 }
167 }
168
169 complexity
170 }
171
172 pub fn calculate_halstead_metrics(&self, content: &str) -> (f64, f64, f64) {
174 let operators = [
176 "=", "+", "-", "*", "/", "==", "!=", "<", ">", "<=", ">=", "&&", "||",
177 ];
178 let mut unique_operators = HashSet::new();
179 let mut total_operators = 0;
180
181 for op in &operators {
182 let count = content.matches(op).count();
183 if count > 0 {
184 unique_operators.insert(op);
185 total_operators += count;
186 }
187 }
188
189 let words: Vec<&str> = content.split_whitespace().collect();
191 let mut unique_operands = HashSet::new();
192 let mut total_operands = 0;
193
194 for word in words {
195 if word.chars().any(|c| c.is_alphanumeric()) {
196 unique_operands.insert(word);
197 total_operands += 1;
198 }
199 }
200
201 let n1 = unique_operators.len().max(1) as f64; let n2 = unique_operands.len().max(1) as f64; let big_n1 = total_operators.max(1) as f64; let big_n2 = total_operands.max(1) as f64; let vocabulary = n1 + n2;
207 let length = big_n1 + big_n2;
208
209 let safe_vocabulary = vocabulary.max(2.0);
211 let volume = length * safe_vocabulary.log2();
212
213 let difficulty = (n1 / 2.0) * (big_n2 / n2);
215 let effort = difficulty * volume;
216
217 (volume, difficulty, effort)
218 }
219
220 pub fn calculate_maintainability_index(&self, content: &str, lines_count: usize) -> f64 {
222 let (volume, difficulty, _effort) = self.calculate_halstead_metrics(content);
223 let cyclomatic = self.calculate_cyclomatic_complexity(content) as f64;
224 let loc = lines_count.max(1) as f64; let safe_volume = volume.max(1.0);
228 let safe_loc = loc.max(1.0);
229
230 let volume_penalty = safe_volume.ln() * 8.0; let complexity_penalty = cyclomatic * 5.0; let loc_penalty = safe_loc.ln() * 20.0; let difficulty_penalty = difficulty * 2.0; let mi = 171.0 - volume_penalty - complexity_penalty - loc_penalty - difficulty_penalty;
239
240 mi.clamp(0.0, 100.0)
242 }
243}
244
245impl Default for ComplexityAnalyzer {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_cyclomatic_complexity() {
257 let analyzer = ComplexityAnalyzer::new();
258
259 let simple_code = "function test() { return 1; }";
260 assert_eq!(analyzer.calculate_cyclomatic_complexity(simple_code), 1);
261
262 let complex_code = "if (x) { if (y) { while (z) { for (i in items) { } } } }";
263 assert!(analyzer.calculate_cyclomatic_complexity(complex_code) > 1);
264 }
265
266 #[test]
267 fn test_cognitive_complexity() {
268 let analyzer = ComplexityAnalyzer::new();
269
270 let simple_code = "function test() { return 1; }";
271 assert_eq!(analyzer.calculate_cognitive_complexity(simple_code), 0);
272
273 let nested_code = "if (x) {\n if (y) {\n while (z) {\n }\n }\n}";
274 assert!(analyzer.calculate_cognitive_complexity(nested_code) > 0);
275 }
276
277 #[test]
278 fn test_halstead_metrics() {
279 let analyzer = ComplexityAnalyzer::new();
280
281 let code = "x = a + b * c";
282 let (volume, difficulty, effort) = analyzer.calculate_halstead_metrics(code);
283
284 assert!(volume > 0.0);
285 assert!(difficulty > 0.0);
286 assert!(effort > 0.0);
287 }
288
289 #[test]
290 fn test_maintainability_index() {
291 let analyzer = ComplexityAnalyzer::new();
292
293 let simple_code = "function test() { return 1; }";
294 let mi = analyzer.calculate_maintainability_index(simple_code, 1);
295
296 assert!((0.0..=100.0).contains(&mi));
297 }
298}