Skip to main content

codelens_core/analyzer/
complexity.rs

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