code complexity & repetition analysis tool
1use crate::Result;
2use crate::tokenizer::{Language, Token, TokenType, Tokenizer};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub enum Severity {
7 Low,
8 Moderate,
9 High,
10 VeryHigh,
11}
12
13/// Cyclomatic Complexity metrics for a file
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CyclomaticMetrics {
16 /// Overall file complexity
17 pub file_complexity: usize,
18 /// Individual function complexities (if we can detect them)
19 pub functions: Vec<FunctionComplexity>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FunctionComplexity {
24 /// Function name (if identifiable)
25 pub name: String,
26 /// Cyclomatic complexity value
27 pub complexity: usize,
28 /// Line number where function starts
29 pub line: usize,
30}
31
32impl CyclomaticMetrics {
33 /// Calculate cyclomatic complexity from source code
34 ///
35 /// Uses the simplified formula: CC = number of decision points + 1
36 /// Decision points include: if, else if, while, for, loop, match/switch, case, catch, &&, ||, ?
37 pub fn calculate(source: &str, language: Language) -> Result<Self> {
38 let tokens = Tokenizer::new(source, language).tokenize()?;
39 let decision_points = tokens.iter().filter(|t| t.token_type.is_decision_point()).count();
40 let file_complexity = if decision_points == 0 { 1 } else { decision_points + 1 };
41 let functions = Self::detect_functions(&tokens, language);
42
43 Ok(CyclomaticMetrics { file_complexity, functions })
44 }
45
46 /// Attempt to detect function boundaries and calculate per-function complexity
47 ///
48 /// Look for function patterns:
49 /// - Rust: "fn" identifier "(" ... ")" "{"
50 /// - JS/TS: "function" identifier "(" ... ")" "{"
51 /// - Go: "func" identifier "(" ... ")" "{"
52 /// - Java/C++: type identifier "(" ... ")" "{"
53 fn detect_functions(tokens: &[Token], _language: Language) -> Vec<FunctionComplexity> {
54 let mut functions = Vec::new();
55 let mut i = 0;
56
57 while i < tokens.len() {
58 let is_function_keyword = if let TokenType::Identifier(name) = &tokens[i].token_type {
59 name == "fn" || name == "func" || name == "function"
60 } else {
61 false
62 };
63
64 if is_function_keyword {
65 let mut name = "anonymous".to_string();
66 let line = tokens[i].line;
67
68 if i + 1 < tokens.len()
69 && let TokenType::Identifier(id) = &tokens[i + 1].token_type
70 {
71 name = id.clone();
72 }
73
74 let body_start = Self::find_next_token(tokens, i, TokenType::LeftBrace);
75
76 if let Some(body_start_idx) = body_start
77 && let Some(body_end_idx) = Self::find_matching_brace(tokens, body_start_idx)
78 {
79 let decision_points = tokens[body_start_idx..=body_end_idx]
80 .iter()
81 .filter(|t| t.token_type.is_decision_point())
82 .count();
83
84 let complexity = if decision_points == 0 { 1 } else { decision_points + 1 };
85
86 functions.push(FunctionComplexity { name, complexity, line });
87
88 i = body_end_idx + 1;
89 continue;
90 }
91 }
92
93 i += 1;
94 }
95
96 functions
97 }
98
99 /// Find the next token of a specific type
100 fn find_next_token(tokens: &[Token], start: usize, token_type: TokenType) -> Option<usize> {
101 tokens[start..]
102 .iter()
103 .position(|t| std::mem::discriminant(&t.token_type) == std::mem::discriminant(&token_type))
104 .map(|pos| start + pos)
105 }
106
107 /// Find the matching closing brace for an opening brace
108 fn find_matching_brace(tokens: &[Token], open_idx: usize) -> Option<usize> {
109 let mut depth = 0;
110
111 for (offset, token) in tokens[open_idx..].iter().enumerate() {
112 match token.token_type {
113 TokenType::LeftBrace => depth += 1,
114 TokenType::RightBrace => {
115 depth -= 1;
116 if depth == 0 {
117 return Some(open_idx + offset);
118 }
119 }
120 _ => {}
121 }
122 }
123
124 None
125 }
126
127 /// Get severity level based on complexity threshold
128 /// Standard thresholds from literature:
129 /// 1-10: Simple, low risk
130 /// 11-20: More complex, moderate risk
131 /// 21-50: Complex, high risk
132 /// 50+: Very complex, very high risk
133 pub fn severity(&self) -> Severity {
134 match self.file_complexity {
135 1..=10 => Severity::Low,
136 11..=20 => Severity::Moderate,
137 21..=50 => Severity::High,
138 _ => Severity::VeryHigh,
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_simple_function() {
149 let source = r#"
150fn simple() {
151 let x = 5;
152 return x;
153}
154"#;
155 let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
156 assert_eq!(metrics.file_complexity, 1);
157 assert_eq!(metrics.severity(), Severity::Low);
158 }
159
160 #[test]
161 fn test_single_if() {
162 let source = r#"
163fn check(x: i32) {
164 if x > 5 {
165 println!("big");
166 }
167}
168"#;
169 let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
170 assert_eq!(metrics.file_complexity, 2);
171 }
172
173 #[test]
174 fn test_multiple_decision_points() {
175 let source = r#"
176fn complex(x: i32, y: i32) {
177 if x > 0 && y > 0 {
178 while x < 10 {
179 x += 1;
180 }
181 } else if x < 0 {
182 for i in 0..5 {
183 println!("{}", i);
184 }
185 }
186}
187"#;
188 let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
189 assert_eq!(metrics.file_complexity, 6);
190 }
191
192 #[test]
193 fn test_ternary_operator() {
194 let source = r#"
195let x = condition ? true_value : false_value;
196let y = a && b ? c : d;
197"#;
198 let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
199 assert_eq!(metrics.file_complexity, 4);
200 }
201
202 #[test]
203 fn test_switch_case() {
204 let source = r#"
205switch (x) {
206 case 1:
207 break;
208 case 2:
209 break;
210 default:
211 break;
212}
213"#;
214 let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
215 assert!(metrics.file_complexity >= 4);
216 }
217
218 #[test]
219 fn test_function_detection_rust() {
220 let source = r#"
221fn simple() {
222 let x = 5;
223}
224
225fn complex() {
226 if true {
227 while false {
228 loop { break; }
229 }
230 }
231}
232"#;
233 let metrics = CyclomaticMetrics::calculate(source, Language::Rust).unwrap();
234
235 if !metrics.functions.is_empty() {
236 for func in &metrics.functions {
237 assert!(func.complexity >= 1);
238 assert!(!func.name.is_empty());
239 }
240 }
241 }
242
243 #[test]
244 fn test_javascript_function() {
245 let source = r#"
246function hello() {
247 if (x > 0) {
248 return true;
249 }
250 return false;
251}
252"#;
253 let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
254 assert!(!metrics.functions.is_empty());
255 assert_eq!(metrics.file_complexity, 2);
256 }
257
258 #[test]
259 fn test_severity_levels() {
260 assert_eq!(
261 CyclomaticMetrics { file_complexity: 5, functions: vec![] }.severity(),
262 Severity::Low
263 );
264 assert_eq!(
265 CyclomaticMetrics { file_complexity: 15, functions: vec![] }.severity(),
266 Severity::Moderate
267 );
268 assert_eq!(
269 CyclomaticMetrics { file_complexity: 25, functions: vec![] }.severity(),
270 Severity::High
271 );
272 assert_eq!(
273 CyclomaticMetrics { file_complexity: 100, functions: vec![] }.severity(),
274 Severity::VeryHigh
275 );
276 }
277
278 #[test]
279 fn test_logical_operators() {
280 let source = r#"
281if (a && b && c) { }
282if (x || y || z) { }
283"#;
284 let metrics = CyclomaticMetrics::calculate(source, Language::JavaScript).unwrap();
285 assert_eq!(metrics.file_complexity, 7);
286 }
287}