1use std::collections::HashMap;
32use serde::{Deserialize, Serialize};
33use scribe_core::Result;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ComplexityMetrics {
38 pub cyclomatic_complexity: usize,
40
41 pub max_nesting_depth: usize,
43
44 pub function_count: usize,
46
47 pub logical_lines: usize,
49
50 pub comment_lines: usize,
52
53 pub blank_lines: usize,
55
56 pub total_lines: usize,
58
59 pub cognitive_complexity: usize,
61
62 pub maintainability_index: f64,
64
65 pub average_function_length: f64,
67
68 pub code_density: f64,
70
71 pub comment_ratio: f64,
73
74 pub language_metrics: LanguageSpecificMetrics,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct LanguageSpecificMetrics {
81 pub language: String,
83
84 pub complexity_factors: HashMap<String, f64>,
86
87 pub import_count: usize,
89
90 pub export_count: usize,
92
93 pub api_surface_area: usize,
95}
96
97#[derive(Debug)]
99pub struct ComplexityAnalyzer {
100 config: ComplexityConfig,
102}
103
104#[derive(Debug, Clone)]
106pub struct ComplexityConfig {
107 pub enable_cognitive_complexity: bool,
109
110 pub enable_maintainability_index: bool,
112
113 pub enable_language_specific: bool,
115
116 pub thresholds: ComplexityThresholds,
118}
119
120#[derive(Debug, Clone)]
122pub struct ComplexityThresholds {
123 pub cyclomatic_warning: usize,
125
126 pub nesting_warning: usize,
128
129 pub function_length_warning: usize,
131
132 pub maintainability_warning: f64,
134}
135
136impl Default for ComplexityConfig {
137 fn default() -> Self {
138 Self {
139 enable_cognitive_complexity: true,
140 enable_maintainability_index: true,
141 enable_language_specific: true,
142 thresholds: ComplexityThresholds::default(),
143 }
144 }
145}
146
147impl Default for ComplexityThresholds {
148 fn default() -> Self {
149 Self {
150 cyclomatic_warning: 10, nesting_warning: 4, function_length_warning: 50, maintainability_warning: 20.0, }
155 }
156}
157
158impl ComplexityAnalyzer {
159 pub fn new() -> Self {
161 Self {
162 config: ComplexityConfig::default(),
163 }
164 }
165
166 pub fn with_config(config: ComplexityConfig) -> Self {
168 Self { config }
169 }
170
171 pub fn analyze_content(&self, content: &str, language: &str) -> Result<ComplexityMetrics> {
173 let line_metrics = self.analyze_lines(content);
174 let cyclomatic_complexity = self.calculate_cyclomatic_complexity(content, language);
175 let max_nesting_depth = self.calculate_max_nesting_depth(content, language);
176 let function_count = self.count_functions(content, language);
177
178 let cognitive_complexity = if self.config.enable_cognitive_complexity {
179 self.calculate_cognitive_complexity(content, language)
180 } else {
181 0
182 };
183
184 let maintainability_index = if self.config.enable_maintainability_index {
185 self.calculate_maintainability_index(&line_metrics, cyclomatic_complexity, function_count)
186 } else {
187 0.0
188 };
189
190 let average_function_length = if function_count > 0 {
191 line_metrics.logical_lines as f64 / function_count as f64
192 } else {
193 0.0
194 };
195
196 let code_density = if line_metrics.total_lines > 0 {
197 line_metrics.logical_lines as f64 / line_metrics.total_lines as f64
198 } else {
199 0.0
200 };
201
202 let comment_ratio = if line_metrics.logical_lines > 0 {
203 line_metrics.comment_lines as f64 / line_metrics.logical_lines as f64
204 } else {
205 0.0
206 };
207
208 let language_metrics = if self.config.enable_language_specific {
209 self.analyze_language_specific(content, language)
210 } else {
211 LanguageSpecificMetrics::default(language)
212 };
213
214 Ok(ComplexityMetrics {
215 cyclomatic_complexity,
216 max_nesting_depth,
217 function_count,
218 logical_lines: line_metrics.logical_lines,
219 comment_lines: line_metrics.comment_lines,
220 blank_lines: line_metrics.blank_lines,
221 total_lines: line_metrics.total_lines,
222 cognitive_complexity,
223 maintainability_index,
224 average_function_length,
225 code_density,
226 comment_ratio,
227 language_metrics,
228 })
229 }
230
231 fn analyze_lines(&self, content: &str) -> LineMetrics {
233 let mut logical_lines = 0;
234 let mut comment_lines = 0;
235 let mut blank_lines = 0;
236 let total_lines = content.lines().count();
237
238 for line in content.lines() {
239 let trimmed = line.trim();
240
241 if trimmed.is_empty() {
242 blank_lines += 1;
243 } else if self.is_comment_line(trimmed) {
244 comment_lines += 1;
245 } else {
246 logical_lines += 1;
247 }
248 }
249
250 LineMetrics {
251 logical_lines,
252 comment_lines,
253 blank_lines,
254 total_lines,
255 }
256 }
257
258 fn is_comment_line(&self, line: &str) -> bool {
260 let trimmed = line.trim();
261
262 trimmed.starts_with("//") ||
264 trimmed.starts_with("#") ||
265 trimmed.starts_with("/*") ||
266 trimmed.starts_with("*") ||
267 trimmed.starts_with("*/") ||
268 trimmed.starts_with("<!--") ||
269 trimmed.starts_with("--") ||
270 trimmed.starts_with("%") ||
271 trimmed.starts_with(";")
272 }
273
274 fn calculate_cyclomatic_complexity(&self, content: &str, language: &str) -> usize {
276 let mut complexity = 1; match language.to_lowercase().as_str() {
280 "rust" => {
281 complexity += content.matches(" if ").count();
283 complexity += content.matches(" else ").count();
284 complexity += content.matches(" match ").count();
285 complexity += content.matches(" while ").count();
286 complexity += content.matches(" for ").count();
287 complexity += content.matches("?").count(); complexity += content.matches("&&").count();
289 complexity += content.matches("||").count();
290 }
291 "python" => {
292 complexity += content.matches(" if ").count();
293 complexity += content.matches(" elif ").count();
294 complexity += content.matches(" while ").count();
295 complexity += content.matches(" for ").count();
296 complexity += content.matches(" except ").count();
297 complexity += content.matches(" and ").count();
298 complexity += content.matches(" or ").count();
299 }
300 "javascript" | "typescript" => {
301 complexity += content.matches(" if ").count();
302 complexity += content.matches(" while ").count();
303 complexity += content.matches(" for ").count();
304 complexity += content.matches(" catch ").count();
305 complexity += content.matches("&&").count();
306 complexity += content.matches("||").count();
307 complexity += content.matches("?").count();
308 }
309 _ => {
310 complexity += content.matches(" if ").count();
312 complexity += content.matches(" while ").count();
313 complexity += content.matches(" for ").count();
314 }
315 }
316
317 complexity.max(1) }
319
320 fn get_complexity_keywords(&self, language: &str) -> Vec<&'static str> {
322 match language.to_lowercase().as_str() {
323 "rust" => vec![
324 "if", "else if", "match", "while", "for", "loop",
325 "catch", "?", "&&", "||", "break", "continue"
326 ],
327 "python" => vec![
328 "if", "elif", "while", "for", "except", "and", "or",
329 "break", "continue", "return", "yield"
330 ],
331 "javascript" | "typescript" => vec![
332 "if", "else if", "while", "for", "catch", "case",
333 "&&", "||", "?", "break", "continue", "return"
334 ],
335 "java" | "c#" => vec![
336 "if", "else if", "while", "for", "foreach", "catch",
337 "case", "&&", "||", "?", "break", "continue", "return"
338 ],
339 "go" => vec![
340 "if", "else if", "for", "switch", "case", "select",
341 "&&", "||", "break", "continue", "return"
342 ],
343 "c" | "cpp" | "c++" => vec![
344 "if", "else if", "while", "for", "switch", "case",
345 "&&", "||", "?", "break", "continue", "return"
346 ],
347 _ => vec![
348 "if", "else", "while", "for", "switch", "case",
349 "&&", "||", "?", "break", "continue", "return"
350 ],
351 }
352 }
353
354 fn calculate_max_nesting_depth(&self, content: &str, _language: &str) -> usize {
356 let mut max_depth: usize = 0;
357 let mut current_depth: usize = 0;
358
359 for line in content.lines() {
361 let opens = line.matches('{').count();
363 let closes = line.matches('}').count();
364
365 current_depth += opens;
366 max_depth = max_depth.max(current_depth);
367 current_depth = current_depth.saturating_sub(closes);
368 }
369
370 max_depth
371 }
372
373 fn get_nesting_chars(&self, language: &str) -> (Vec<char>, Vec<char>) {
375 match language.to_lowercase().as_str() {
376 "python" => {
377 (vec!['{', '[', '('], vec!['}', ']', ')'])
379 },
380 _ => {
381 (vec!['{', '[', '('], vec!['}', ']', ')'])
383 }
384 }
385 }
386
387 fn count_functions(&self, content: &str, language: &str) -> usize {
389 let function_keywords = self.get_function_keywords(language);
390 let mut count = 0;
391
392 for line in content.lines() {
393 let line = line.trim();
394
395 for keyword in &function_keywords {
396 if line.starts_with(keyword) || line.contains(&format!(" {}", keyword)) {
397 count += 1;
398 break; }
400 }
401 }
402
403 count
404 }
405
406 fn get_function_keywords(&self, language: &str) -> Vec<&'static str> {
408 match language.to_lowercase().as_str() {
409 "rust" => vec!["fn ", "pub fn ", "async fn ", "pub async fn "],
410 "python" => vec!["def ", "async def ", "class "],
411 "javascript" | "typescript" => vec!["function ", "const ", "let ", "var ", "async function "],
412 "java" => vec!["public ", "private ", "protected ", "static "],
413 "c#" => vec!["public ", "private ", "protected ", "internal ", "static "],
414 "go" => vec!["func "],
415 "c" | "cpp" | "c++" => vec!["int ", "void ", "char ", "float ", "double ", "static "],
416 _ => vec!["function ", "def ", "fn "],
417 }
418 }
419
420 fn calculate_cognitive_complexity(&self, content: &str, language: &str) -> usize {
422 let mut complexity: usize = 0;
423 let mut nesting_level: usize = 0;
424
425 for line in content.lines() {
426 let line = line.trim().to_lowercase();
427
428 if line.contains('{') {
430 nesting_level += 1;
431 }
432 if line.contains('}') {
433 nesting_level = nesting_level.saturating_sub(1);
434 }
435
436 if line.contains("if") || line.contains("while") || line.contains("for") {
438 complexity += 1 + nesting_level; }
440
441 if line.contains("else if") || line.contains("elif") {
442 complexity += 1;
443 }
444
445 if line.contains("catch") || line.contains("except") {
446 complexity += 1 + nesting_level;
447 }
448
449 if line.contains("switch") || line.contains("match") {
450 complexity += 1 + nesting_level;
451 }
452
453 if self.has_recursive_call(&line, language) {
455 complexity += 1;
456 }
457 }
458
459 complexity
460 }
461
462 fn has_recursive_call(&self, line: &str, _language: &str) -> bool {
464 line.contains("self.") || line.contains("this.") ||
466 line.contains("recursive") || line.contains("recurse")
467 }
468
469 fn calculate_maintainability_index(&self, line_metrics: &LineMetrics, cyclomatic: usize, functions: usize) -> f64 {
471 let volume = (line_metrics.logical_lines as f64).ln();
475 let complexity = cyclomatic as f64;
476 let lloc = line_metrics.logical_lines as f64;
477
478 let mi = 171.0 - 5.2 * volume - 0.23 * complexity - 16.2 * lloc.ln();
480
481 mi.max(0.0).min(100.0)
483 }
484
485 fn analyze_language_specific(&self, content: &str, language: &str) -> LanguageSpecificMetrics {
487 let mut complexity_factors = HashMap::new();
488 let import_count = self.count_imports(content, language);
489 let export_count = self.count_exports(content, language);
490 let api_surface_area = self.estimate_api_surface_area(content, language);
491
492 match language.to_lowercase().as_str() {
494 "rust" => {
495 complexity_factors.insert("ownership_complexity".to_string(), self.calculate_ownership_complexity(content));
496 complexity_factors.insert("trait_complexity".to_string(), self.count_trait_usage(content) as f64);
497 complexity_factors.insert("macro_complexity".to_string(), self.count_macro_usage(content) as f64);
498 },
499 "python" => {
500 complexity_factors.insert("decorator_complexity".to_string(), self.count_decorators(content) as f64);
501 complexity_factors.insert("comprehension_complexity".to_string(), self.count_comprehensions(content) as f64);
502 },
503 "javascript" | "typescript" => {
504 complexity_factors.insert("closure_complexity".to_string(), self.count_closures(content) as f64);
505 complexity_factors.insert("promise_complexity".to_string(), self.count_async_patterns(content) as f64);
506 },
507 _ => {
508 complexity_factors.insert("generic_complexity".to_string(), 1.0);
510 }
511 }
512
513 LanguageSpecificMetrics {
514 language: language.to_string(),
515 complexity_factors,
516 import_count,
517 export_count,
518 api_surface_area,
519 }
520 }
521
522 fn count_imports(&self, content: &str, language: &str) -> usize {
524 let import_patterns = match language.to_lowercase().as_str() {
525 "rust" => vec!["use ", "extern crate "],
526 "python" => vec!["import ", "from "],
527 "javascript" | "typescript" => vec!["import ", "require(", "const ", "let "],
528 "java" => vec!["import "],
529 "go" => vec!["import "],
530 _ => vec!["import ", "include ", "use "],
531 };
532
533 content.lines()
534 .filter(|line| {
535 let trimmed = line.trim();
536 import_patterns.iter().any(|pattern| trimmed.starts_with(pattern))
537 })
538 .count()
539 }
540
541 fn count_exports(&self, content: &str, language: &str) -> usize {
543 let export_patterns = match language.to_lowercase().as_str() {
544 "rust" => vec!["pub fn ", "pub struct ", "pub enum ", "pub trait "],
545 "python" => vec!["def ", "class "], "javascript" | "typescript" => vec!["export ", "module.exports"],
547 "java" => vec!["public class ", "public interface ", "public enum "],
548 _ => vec!["public ", "export "],
549 };
550
551 content.lines()
552 .filter(|line| {
553 let trimmed = line.trim();
554 export_patterns.iter().any(|pattern| trimmed.contains(pattern))
555 })
556 .count()
557 }
558
559 fn estimate_api_surface_area(&self, content: &str, language: &str) -> usize {
561 let public_items = self.count_exports(content, language);
563 let function_count = self.count_functions(content, language);
564
565 public_items.min(function_count)
567 }
568
569 fn calculate_ownership_complexity(&self, content: &str) -> f64 {
571 let ownership_keywords = ["&", "&mut", "Box<", "Rc<", "Arc<", "RefCell<", "Mutex<"];
572 let mut complexity = 0.0;
573
574 for line in content.lines() {
575 for keyword in &ownership_keywords {
576 complexity += line.matches(keyword).count() as f64 * 0.5;
577 }
578 }
579
580 complexity
581 }
582
583 fn count_trait_usage(&self, content: &str) -> usize {
584 content.lines()
585 .filter(|line| line.contains("trait ") || line.contains("impl "))
586 .count()
587 }
588
589 fn count_macro_usage(&self, content: &str) -> usize {
590 content.lines()
591 .filter(|line| line.contains("macro_rules!") || line.contains("!"))
592 .count()
593 }
594
595 fn count_decorators(&self, content: &str) -> usize {
596 content.lines()
597 .filter(|line| line.trim().starts_with("@"))
598 .count()
599 }
600
601 fn count_comprehensions(&self, content: &str) -> usize {
602 content.lines()
603 .filter(|line| {
604 line.contains("[") && line.contains("for ") && line.contains("in ") ||
605 line.contains("{") && line.contains("for ") && line.contains("in ")
606 })
607 .count()
608 }
609
610 fn count_closures(&self, content: &str) -> usize {
611 content.lines()
612 .filter(|line| line.contains("=>") || line.contains("function("))
613 .count()
614 }
615
616 fn count_async_patterns(&self, content: &str) -> usize {
617 content.lines()
618 .filter(|line| {
619 line.contains("async") || line.contains("await") ||
620 line.contains("Promise") || line.contains(".then(")
621 })
622 .count()
623 }
624}
625
626#[derive(Debug)]
628struct LineMetrics {
629 logical_lines: usize,
630 comment_lines: usize,
631 blank_lines: usize,
632 total_lines: usize,
633}
634
635impl Default for LanguageSpecificMetrics {
636 fn default() -> Self {
637 Self::default("unknown")
638 }
639}
640
641impl LanguageSpecificMetrics {
642 fn default(language: &str) -> Self {
643 Self {
644 language: language.to_string(),
645 complexity_factors: HashMap::new(),
646 import_count: 0,
647 export_count: 0,
648 api_surface_area: 0,
649 }
650 }
651}
652
653impl ComplexityMetrics {
654 pub fn complexity_score(&self) -> f64 {
656 let cyclomatic_score = (self.cyclomatic_complexity as f64 / 20.0).min(1.0);
658 let nesting_score = (self.max_nesting_depth as f64 / 8.0).min(1.0);
659 let cognitive_score = (self.cognitive_complexity as f64 / 15.0).min(1.0);
660 let maintainability_score = (100.0 - self.maintainability_index) / 100.0;
661
662 (cyclomatic_score * 0.3 +
664 nesting_score * 0.2 +
665 cognitive_score * 0.3 +
666 maintainability_score * 0.2).min(1.0)
667 }
668
669 pub fn exceeds_thresholds(&self, thresholds: &ComplexityThresholds) -> Vec<String> {
671 let mut warnings = Vec::new();
672
673 if self.cyclomatic_complexity > thresholds.cyclomatic_warning {
674 warnings.push(format!("High cyclomatic complexity: {}", self.cyclomatic_complexity));
675 }
676
677 if self.max_nesting_depth > thresholds.nesting_warning {
678 warnings.push(format!("Deep nesting: {}", self.max_nesting_depth));
679 }
680
681 if self.average_function_length > thresholds.function_length_warning as f64 {
682 warnings.push(format!("Long functions: avg {:.1} lines", self.average_function_length));
683 }
684
685 if self.maintainability_index < thresholds.maintainability_warning {
686 warnings.push(format!("Low maintainability: {:.1}", self.maintainability_index));
687 }
688
689 warnings
690 }
691
692 pub fn summary(&self) -> String {
694 format!(
695 "Complexity: CC={}, Depth={}, Functions={}, MI={:.1}, Cognitive={}",
696 self.cyclomatic_complexity,
697 self.max_nesting_depth,
698 self.function_count,
699 self.maintainability_index,
700 self.cognitive_complexity
701 )
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_analyzer_creation() {
711 let analyzer = ComplexityAnalyzer::new();
712 assert!(analyzer.config.enable_cognitive_complexity);
713 assert!(analyzer.config.enable_maintainability_index);
714 }
715
716 #[test]
717 fn test_simple_rust_analysis() {
718 let analyzer = ComplexityAnalyzer::new();
719 let content = r#"
720fn main() {
721 if x > 0 {
722 println!("positive");
723 } else {
724 println!("negative");
725 }
726}
727"#;
728
729 let metrics = analyzer.analyze_content(content, "rust").unwrap();
730
731 assert!(metrics.cyclomatic_complexity >= 2); assert!(metrics.function_count >= 1);
733 assert!(metrics.max_nesting_depth >= 1);
734 assert!(metrics.total_lines > 0);
735 }
736
737 #[test]
738 fn test_complex_code_analysis() {
739 let analyzer = ComplexityAnalyzer::new();
740 let content = r#"
741fn complex_function() {
742 for i in 0..10 {
743 if i % 2 == 0 {
744 while some_condition() {
745 if another_condition() {
746 match value {
747 1 => do_something(),
748 2 => do_something_else(),
749 _ => default_action(),
750 }
751 }
752 }
753 } else {
754 continue;
755 }
756 }
757}
758"#;
759
760 let metrics = analyzer.analyze_content(content, "rust").unwrap();
761
762 assert!(metrics.cyclomatic_complexity > 5); assert!(metrics.max_nesting_depth > 3); assert!(metrics.cognitive_complexity > metrics.cyclomatic_complexity); }
766
767 #[test]
768 fn test_line_analysis() {
769 let analyzer = ComplexityAnalyzer::new();
770 let content = r#"
771// This is a comment
772fn test() {
773 // Another comment
774 let x = 5;
775
776 // More comments
777 println!("Hello");
778}
779"#;
780
781 let metrics = analyzer.analyze_content(content, "rust").unwrap();
782
783 assert!(metrics.comment_lines > 0);
784 assert!(metrics.blank_lines > 0);
785 assert!(metrics.logical_lines > 0);
786 assert!(metrics.comment_ratio > 0.0);
787 assert!(metrics.code_density > 0.0);
788 }
789
790 #[test]
791 fn test_language_specific_analysis() {
792 let analyzer = ComplexityAnalyzer::new();
793
794 let rust_content = r#"
796use std::collections::HashMap;
797pub fn test() -> Result<(), Box<dyn Error>> {
798 let mut data: Vec<&str> = vec![];
799 Ok(())
800}
801"#;
802
803 let rust_metrics = analyzer.analyze_content(rust_content, "rust").unwrap();
804 assert_eq!(rust_metrics.language_metrics.language, "rust");
805 assert!(rust_metrics.language_metrics.import_count > 0);
806
807 let python_content = r#"
809import os
810from typing import List
811
812@decorator
813def test_function():
814 result = [x for x in range(10) if x % 2 == 0]
815 return result
816"#;
817
818 let python_metrics = analyzer.analyze_content(python_content, "python").unwrap();
819 assert_eq!(python_metrics.language_metrics.language, "python");
820 assert!(python_metrics.language_metrics.import_count > 0);
821 }
822
823 #[test]
824 fn test_complexity_score() {
825 let analyzer = ComplexityAnalyzer::new();
826
827 let simple_content = "fn main() { println!(\"hello\"); }";
829 let simple_metrics = analyzer.analyze_content(simple_content, "rust").unwrap();
830 let simple_score = simple_metrics.complexity_score();
831
832 let complex_content = r#"
834fn complex() {
835 for i in 0..100 {
836 if i % 2 == 0 {
837 while condition() {
838 match value {
839 1 => { if nested() { deep(); } },
840 2 => { if more_nested() { deeper(); } },
841 _ => { if even_more() { deepest(); } },
842 }
843 }
844 }
845 }
846}
847"#;
848 let complex_metrics = analyzer.analyze_content(complex_content, "rust").unwrap();
849 let complex_score = complex_metrics.complexity_score();
850
851 assert!(complex_score > simple_score);
852 assert!(simple_score >= 0.0 && simple_score <= 1.0);
853 assert!(complex_score >= 0.0 && complex_score <= 1.0);
854 }
855
856 #[test]
857 fn test_threshold_warnings() {
858 let analyzer = ComplexityAnalyzer::new();
859 let thresholds = ComplexityThresholds {
860 cyclomatic_warning: 5,
861 nesting_warning: 2,
862 function_length_warning: 10,
863 maintainability_warning: 50.0,
864 };
865
866 let complex_content = r#"
867fn complex_function() {
868 for i in 0..10 {
869 if i % 2 == 0 {
870 while some_condition() {
871 if another_condition() {
872 if yet_another() {
873 do_something();
874 }
875 }
876 }
877 }
878 }
879}
880"#;
881
882 let metrics = analyzer.analyze_content(complex_content, "rust").unwrap();
883 let warnings = metrics.exceeds_thresholds(&thresholds);
884
885 assert!(!warnings.is_empty());
886 assert!(warnings.iter().any(|w| w.contains("complexity")));
887 }
888
889 #[test]
890 fn test_metrics_summary() {
891 let analyzer = ComplexityAnalyzer::new();
892 let content = "fn test() { if x > 0 { return 1; } else { return 0; } }";
893 let metrics = analyzer.analyze_content(content, "rust").unwrap();
894
895 let summary = metrics.summary();
896 assert!(summary.contains("CC="));
897 assert!(summary.contains("Depth="));
898 assert!(summary.contains("Functions="));
899 assert!(summary.contains("MI="));
900 assert!(summary.contains("Cognitive="));
901 }
902}