code complexity & repetition analysis tool
at main 287 lines 8.3 kB view raw
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}