vtcode_core/code/code_quality/metrics/
complexity.rs1use std::fs;
2use std::path::Path;
3use vtcode_commons::walk::build_walker_single_threaded;
4
5#[derive(Debug, Clone)]
7pub struct ComplexityResult {
8 pub cyclomatic_complexity: f64,
9 pub cognitive_complexity: f64,
10 pub lines_of_code: usize,
11 pub maintainability_index: f64,
12}
13
14pub struct ComplexityAnalyzer;
16
17impl ComplexityAnalyzer {
18 pub fn new() -> Self {
19 Self
20 }
21
22 pub fn analyze_file(&self, _file_path: &Path, source: &str) -> ComplexityResult {
24 let lines_of_code = source.lines().count();
26
27 let cyclomatic_complexity = self.calculate_cyclomatic_complexity(source);
29
30 let cognitive_complexity = self.calculate_cognitive_complexity(source);
32
33 let maintainability_index = self.calculate_maintainability_index(cyclomatic_complexity, lines_of_code, source);
35
36 ComplexityResult {
37 cyclomatic_complexity,
38 cognitive_complexity,
39 lines_of_code,
40 maintainability_index,
41 }
42 }
43
44 pub fn analyze_directory(&self, dir_path: &Path) -> Vec<ComplexityResult> {
46 let mut results = Vec::new();
47
48 for entry in build_walker_single_threaded(dir_path)
50 .follow_links(true)
51 .build()
52 .filter_map(|e| e.ok())
53 {
54 if entry.file_type().is_some_and(|ft| ft.is_file())
55 && let Some(ext) = entry.path().extension()
56 {
57 let source_extensions = ["rs", "js", "ts", "py", "java", "cpp", "c", "go"];
59 if source_extensions.contains(&ext.to_str().unwrap_or(""))
60 && let Ok(content) = fs::read_to_string(entry.path())
61 {
62 let result = self.analyze_file(entry.path(), &content);
63 results.push(result);
64 }
65 }
66 }
67
68 results
69 }
70
71 fn calculate_cyclomatic_complexity(&self, source: &str) -> f64 {
73 let mut complexity = 1.0; for line in source.lines() {
77 let trimmed = line.trim();
78 if trimmed.starts_with("if ")
79 || trimmed.starts_with("while ")
80 || trimmed.starts_with("for ")
81 || trimmed.starts_with("match ")
82 || trimmed.contains(" && ")
83 || trimmed.contains(" || ")
84 || trimmed.starts_with("case ")
85 || trimmed.starts_with("catch ")
86 {
87 complexity += 1.0;
88 }
89 }
90
91 complexity
92 }
93
94 fn calculate_cognitive_complexity(&self, source: &str) -> f64 {
96 let mut complexity = 0.0;
97
98 let mut nesting_level: i32 = 0;
100 for line in source.lines() {
101 let trimmed = line.trim();
102
103 if trimmed.starts_with("if ")
105 || trimmed.starts_with("while ")
106 || trimmed.starts_with("for ")
107 || trimmed.starts_with("match ")
108 {
109 nesting_level += 1;
110 complexity += nesting_level as f64;
111 }
112
113 if trimmed == "}" || trimmed == "end" {
115 nesting_level = nesting_level.saturating_sub(1);
116 }
117 }
118
119 complexity
120 }
121
122 fn calculate_maintainability_index(&self, cyclomatic_complexity: f64, lines_of_code: usize, source: &str) -> f64 {
124 let halstead_volume = source.chars().count() as f64;
126
127 let mi = 171.0 - 5.2 * halstead_volume.ln() - 0.23 * cyclomatic_complexity - 16.2 * (lines_of_code as f64).ln();
129
130 mi.clamp(0.0, 100.0)
132 }
133}
134
135impl Default for ComplexityAnalyzer {
136 fn default() -> Self {
137 Self::new()
138 }
139}