Skip to main content

codelens_core/analyzer/
complexity.rs

1//! Code complexity analysis.
2
3use crate::language::Language;
4
5use super::stats::Complexity;
6
7/// Analyzes code complexity metrics.
8pub struct ComplexityAnalyzer;
9
10impl ComplexityAnalyzer {
11    pub fn new() -> Self {
12        Self
13    }
14
15    /// Analyze complexity metrics for the given content.
16    ///
17    /// Uses precompiled regex patterns cached on the Language via OnceLock.
18    pub fn analyze(&self, content: &str, lang: &Language) -> Complexity {
19        let mut complexity = Complexity::default();
20        let patterns = lang.complexity_patterns();
21
22        // Count functions
23        if let Some(ref re) = patterns.function_re {
24            complexity.functions = re.find_iter(content).count();
25        }
26
27        // Count complexity keywords (single alternation regex, one pass)
28        if let Some(ref re) = patterns.keywords_re {
29            complexity.cyclomatic = re.find_iter(content).count();
30        }
31
32        // Base complexity is 1 per function
33        complexity.cyclomatic += complexity.functions;
34
35        // Calculate max nesting depth
36        complexity.max_depth = self.calculate_max_depth(content);
37
38        // Calculate average lines per function
39        if complexity.functions > 0 {
40            let total_lines = content.lines().count();
41            complexity.avg_func_lines = total_lines as f64 / complexity.functions as f64;
42        }
43
44        complexity
45    }
46
47    /// Calculate maximum nesting depth based on braces/indentation.
48    fn calculate_max_depth(&self, content: &str) -> usize {
49        let mut max_depth: usize = 0;
50        let mut current_depth: usize = 0;
51
52        for c in content.chars() {
53            match c {
54                '{' | '(' | '[' => {
55                    current_depth += 1;
56                    max_depth = max_depth.max(current_depth);
57                }
58                '}' | ')' | ']' => {
59                    current_depth = current_depth.saturating_sub(1);
60                }
61                _ => {}
62            }
63        }
64
65        max_depth
66    }
67}
68
69impl Default for ComplexityAnalyzer {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    fn make_rust_lang() -> Language {
80        Language {
81            name: "Rust".to_string(),
82            extensions: vec![".rs".to_string()],
83            line_comments: vec!["//".to_string()],
84            block_comments: vec![("/*".to_string(), "*/".to_string())],
85            function_pattern: Some(r"(?m)^\s*(pub\s+)?(async\s+)?fn\s+\w+".to_string()),
86            complexity_keywords: vec![
87                "if".to_string(),
88                "else".to_string(),
89                "for".to_string(),
90                "while".to_string(),
91                "match".to_string(),
92            ],
93            nested_comments: true,
94            ..Default::default()
95        }
96    }
97
98    #[test]
99    fn test_count_functions() {
100        let analyzer = ComplexityAnalyzer::new();
101        let lang = make_rust_lang();
102
103        let content = r#"
104fn main() {
105    println!("hello");
106}
107
108pub fn helper() {}
109
110pub async fn async_fn() {}
111"#;
112
113        let complexity = analyzer.analyze(content, &lang);
114        assert_eq!(complexity.functions, 3);
115    }
116
117    #[test]
118    fn test_cyclomatic_complexity() {
119        let analyzer = ComplexityAnalyzer::new();
120        let lang = make_rust_lang();
121
122        let content = r#"
123fn main() {
124    if true {
125        for i in 0..10 {
126            if i > 5 {
127                println!("{}", i);
128            }
129        }
130    } else {
131        while false {}
132    }
133}
134"#;
135
136        let complexity = analyzer.analyze(content, &lang);
137        // 1 function + 2 if + 1 for + 1 else + 1 while = 6
138        assert_eq!(complexity.cyclomatic, 6);
139    }
140
141    #[test]
142    fn test_max_depth() {
143        let analyzer = ComplexityAnalyzer::new();
144        let lang = make_rust_lang();
145
146        let content = r#"
147fn main() {
148    if true {
149        for i in 0..10 {
150            match i {
151                _ => {}
152            }
153        }
154    }
155}
156"#;
157
158        let complexity = analyzer.analyze(content, &lang);
159        assert!(complexity.max_depth >= 4);
160    }
161}