codeprism_analysis/
complexity.rs

1//! Code complexity analysis module
2
3use anyhow::Result;
4use serde_json::Value;
5use std::collections::HashSet;
6use std::path::Path;
7
8/// Complexity metrics for code analysis
9#[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
20/// Complexity analyzer for code analysis
21pub struct ComplexityAnalyzer;
22
23impl ComplexityAnalyzer {
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Analyze complexity for a given file
29    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    /// Calculate all complexity metrics for content
96    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    /// Calculate cyclomatic complexity (simplified)
115    pub fn calculate_cyclomatic_complexity(&self, content: &str) -> usize {
116        let mut complexity = 1; // Base complexity
117
118        // Count decision points (simplified heuristic)
119        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    /// Calculate cognitive complexity (simplified)
132    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            // Increment nesting for certain constructs
141            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            // Decrement nesting
153            if trimmed.contains('}') {
154                nesting_level = nesting_level.saturating_sub(1usize);
155            }
156
157            // Add complexity based on constructs
158            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    /// Calculate Halstead complexity metrics (simplified)
173    pub fn calculate_halstead_metrics(&self, content: &str) -> (f64, f64, f64) {
174        // Simplified Halstead calculation
175        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        // Rough operand estimation (identifiers, literals)
190        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; // Minimum 1 operator
202        let n2 = unique_operands.len().max(1) as f64; // Minimum 1 operand
203        let big_n1 = total_operators.max(1) as f64; // Minimum 1 operator usage
204        let big_n2 = total_operands.max(1) as f64; // Minimum 1 operand usage
205
206        let vocabulary = n1 + n2;
207        let length = big_n1 + big_n2;
208
209        // Ensure vocabulary is at least 2 to avoid log2(1) = 0
210        let safe_vocabulary = vocabulary.max(2.0);
211        let volume = length * safe_vocabulary.log2();
212
213        // Safe difficulty calculation
214        let difficulty = (n1 / 2.0) * (big_n2 / n2);
215        let effort = difficulty * volume;
216
217        (volume, difficulty, effort)
218    }
219
220    /// Calculate maintainability index (simplified)
221    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; // Minimum 1 line
225
226        // Ensure volume is meaningful for logarithm
227        let safe_volume = volume.max(1.0);
228        let safe_loc = loc.max(1.0);
229
230        // Adjusted maintainability index formula to be more sensitive
231        // Based on the standard formula but with adjusted coefficients for this simplified implementation
232        // Higher volume, complexity, and difficulty should decrease maintainability more significantly
233        let volume_penalty = safe_volume.ln() * 8.0; // Increased from 5.2
234        let complexity_penalty = cyclomatic * 5.0; // Increased from 0.23
235        let loc_penalty = safe_loc.ln() * 20.0; // Increased from 16.2
236        let difficulty_penalty = difficulty * 2.0; // Add difficulty factor
237
238        let mi = 171.0 - volume_penalty - complexity_penalty - loc_penalty - difficulty_penalty;
239
240        // Ensure result is in valid range
241        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}