code complexity & repetition analysis tool
at main 374 lines 12 kB view raw
1use crate::Result; 2use crate::tokenizer::{Language, TokenType, Tokenizer}; 3use serde::{Deserialize, Serialize}; 4use std::collections::HashMap; 5use std::path::{Path, PathBuf}; 6 7#[derive(Debug, Clone, Copy, PartialEq, Eq)] 8enum LineKind { 9 Code, 10 Comment, 11 Blank, 12} 13 14/// Lines of Code metrics for a single file 15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 16pub struct LocMetrics { 17 /// Total number of lines in the file 18 pub physical: usize, 19 /// Number of non-blank, non-comment lines 20 pub logical: usize, 21 /// Number of comment lines 22 pub comments: usize, 23 /// Number of blank lines 24 pub blank: usize, 25} 26 27impl LocMetrics { 28 pub fn calculate(source: &str, language: Language) -> Result<Self> { 29 let tokens = Tokenizer::new(source, language).tokenize()?; 30 let physical = if source.is_empty() { 0 } else { source.split('\n').count() }; 31 let mut line_types = vec![LineKind::Blank; physical]; 32 33 for token in &tokens { 34 let line_idx = token.line.saturating_sub(1); 35 if line_idx >= line_types.len() { 36 continue; 37 } 38 39 match token.token_type { 40 _ if token.token_type.is_significant() => { 41 line_types[line_idx] = LineKind::Code; 42 } 43 TokenType::Comment => { 44 if line_types[line_idx] != LineKind::Code { 45 line_types[line_idx] = LineKind::Comment; 46 } 47 } 48 _ => {} 49 } 50 } 51 52 for (idx, line) in source.lines().enumerate() { 53 if line.trim().is_empty() && idx < line_types.len() { 54 line_types[idx] = LineKind::Blank; 55 } 56 } 57 58 let comments = line_types.iter().filter(|&&t| t == LineKind::Comment).count(); 59 let blank = line_types.iter().filter(|&&t| t == LineKind::Blank).count(); 60 let logical = physical - comments - blank; 61 62 Ok(LocMetrics { physical, logical, comments, blank }) 63 } 64 65 /// Add two LocMetrics together 66 fn add(&self, other: &Self) -> Self { 67 Self { 68 physical: self.physical + other.physical, 69 logical: self.logical + other.logical, 70 comments: self.comments + other.comments, 71 blank: self.blank + other.blank, 72 } 73 } 74} 75 76/// Ranking criteria for LOC analysis 77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 78pub enum RankBy { 79 /// Rank by logical lines of code 80 Logical, 81 /// Rank by physical lines of code 82 Physical, 83 /// Rank by comment lines 84 Comments, 85 /// Rank by blank lines 86 Blank, 87} 88 89impl RankBy { 90 /// Get the value from LocMetrics based on ranking criteria 91 pub fn value_from(&self, metrics: &LocMetrics) -> usize { 92 match self { 93 Self::Logical => metrics.logical, 94 Self::Physical => metrics.physical, 95 Self::Comments => metrics.comments, 96 Self::Blank => metrics.blank, 97 } 98 } 99} 100 101/// LOC metrics for a single file with path information 102#[derive(Debug, Clone, Serialize, Deserialize)] 103pub struct FileLocReport { 104 /// File path 105 pub path: PathBuf, 106 /// LOC metrics 107 pub metrics: LocMetrics, 108} 109 110/// Aggregated LOC metrics for a directory 111#[derive(Debug, Clone, Serialize, Deserialize)] 112pub struct DirectoryLocMetrics { 113 /// Directory path 114 pub path: PathBuf, 115 /// Total LOC metrics for all files in this directory 116 pub total: LocMetrics, 117 /// Files in this directory 118 pub files: Vec<FileLocReport>, 119} 120 121/// Complete LOC analysis report with ranking capabilities 122#[derive(Debug, Clone, Serialize, Deserialize)] 123pub struct LocReport { 124 /// Per-file reports 125 pub files: Vec<FileLocReport>, 126 /// Per-directory aggregation (if enabled) 127 pub directories: Option<Vec<DirectoryLocMetrics>>, 128 /// Summary statistics 129 pub summary: LocSummary, 130} 131 132/// Summary statistics for LOC report 133#[derive(Debug, Clone, Serialize, Deserialize)] 134pub struct LocSummary { 135 /// Total number of files analyzed 136 pub total_files: usize, 137 /// Total physical lines of code 138 pub total_physical: usize, 139 /// Total logical lines of code 140 pub total_logical: usize, 141 /// Total comment lines 142 pub total_comments: usize, 143 /// Total blank lines 144 pub total_blank: usize, 145} 146 147impl LocReport { 148 /// Create a new LOC report from file reports 149 pub fn new(mut files: Vec<FileLocReport>, rank_by: RankBy, rank_dirs: bool) -> Self { 150 files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics))); 151 152 let directories = if rank_dirs { Some(Self::aggregate_by_directory(&files, rank_by)) } else { None }; 153 let summary = LocSummary::from_files(&files); 154 155 Self { files, directories, summary } 156 } 157 158 /// Aggregate files by directory 159 fn aggregate_by_directory(files: &[FileLocReport], rank_by: RankBy) -> Vec<DirectoryLocMetrics> { 160 let mut dir_map: HashMap<PathBuf, Vec<FileLocReport>> = HashMap::new(); 161 162 for file in files { 163 let dir = file.path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf(); 164 dir_map.entry(dir).or_default().push(file.clone()); 165 } 166 167 let mut directories: Vec<DirectoryLocMetrics> = dir_map 168 .into_iter() 169 .map(|(path, files)| { 170 let total = files.iter().fold( 171 LocMetrics { physical: 0, logical: 0, comments: 0, blank: 0 }, 172 |acc, f| acc.add(&f.metrics), 173 ); 174 175 let mut sorted_files = files; 176 sorted_files.sort_by(|a, b| rank_by.value_from(&b.metrics).cmp(&rank_by.value_from(&a.metrics))); 177 178 DirectoryLocMetrics { path, total, files: sorted_files } 179 }) 180 .collect(); 181 182 directories.sort_by(|a, b| rank_by.value_from(&b.total).cmp(&rank_by.value_from(&a.total))); 183 184 directories 185 } 186 187 /// Serialize to JSON 188 pub fn to_json(&self) -> serde_json::Result<String> { 189 serde_json::to_string_pretty(self) 190 } 191} 192 193impl LocSummary { 194 fn from_files(files: &[FileLocReport]) -> Self { 195 let total_files = files.len(); 196 let total_physical = files.iter().map(|f| f.metrics.physical).sum(); 197 let total_logical = files.iter().map(|f| f.metrics.logical).sum(); 198 let total_comments = files.iter().map(|f| f.metrics.comments).sum(); 199 let total_blank = files.iter().map(|f| f.metrics.blank).sum(); 200 201 Self { total_files, total_physical, total_logical, total_comments, total_blank } 202 } 203} 204 205#[cfg(test)] 206mod tests { 207 use super::*; 208 209 #[test] 210 fn test_empty_file() { 211 let source = ""; 212 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 213 assert_eq!(metrics.physical, 0); 214 assert_eq!(metrics.logical, 0); 215 } 216 217 #[test] 218 fn test_simple_code() { 219 let source = r#" 220fn main() { 221 println!("Hello"); 222} 223"#; 224 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 225 assert_eq!(metrics.physical, 5); 226 assert!(metrics.logical >= 2); 227 assert!(metrics.blank >= 1); 228 } 229 230 #[test] 231 fn test_comments() { 232 let source = r#" 233// This is a comment 234/* Multi-line 235 comment */ 236let x = 5; // inline comment 237"#; 238 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 239 assert!(metrics.comments >= 2); 240 assert!(metrics.logical >= 1); 241 } 242 243 #[test] 244 fn test_blank_lines() { 245 let source = r#" 246 247 248fn test() {} 249 250 251"#; 252 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 253 assert!(metrics.blank >= 4); 254 assert!(metrics.logical >= 1); 255 } 256 257 #[test] 258 fn test_javascript() { 259 let source = r#" 260function hello() { 261 console.log("Hello"); 262} 263"#; 264 let metrics = LocMetrics::calculate(source, Language::JavaScript).unwrap(); 265 assert_eq!(metrics.physical, 5); 266 assert!(metrics.logical >= 2); 267 } 268 269 #[test] 270 fn test_all_comments() { 271 let source = r#" 272// Comment 1 273// Comment 2 274/* Comment 3 */ 275"#; 276 let metrics = LocMetrics::calculate(source, Language::Rust).unwrap(); 277 assert!(metrics.comments >= 3); 278 assert_eq!(metrics.logical, 0); 279 } 280 281 #[test] 282 fn test_loc_metrics_add() { 283 let m1 = LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 }; 284 let m2 = LocMetrics { physical: 20, logical: 15, comments: 3, blank: 2 }; 285 let result = m1.add(&m2); 286 287 assert_eq!(result.physical, 30); 288 assert_eq!(result.logical, 23); 289 assert_eq!(result.comments, 4); 290 assert_eq!(result.blank, 3); 291 } 292 293 #[test] 294 fn test_rank_by_value_from() { 295 let metrics = LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }; 296 297 assert_eq!(RankBy::Physical.value_from(&metrics), 100); 298 assert_eq!(RankBy::Logical.value_from(&metrics), 80); 299 assert_eq!(RankBy::Comments.value_from(&metrics), 10); 300 assert_eq!(RankBy::Blank.value_from(&metrics), 10); 301 } 302 303 #[test] 304 fn test_loc_report_new() { 305 let files = vec![ 306 FileLocReport { 307 path: PathBuf::from("test1.rs"), 308 metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }, 309 }, 310 FileLocReport { 311 path: PathBuf::from("test2.rs"), 312 metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 }, 313 }, 314 ]; 315 316 let report = LocReport::new(files, RankBy::Logical, false); 317 318 assert_eq!(report.summary.total_files, 2); 319 assert_eq!(report.summary.total_physical, 150); 320 assert_eq!(report.summary.total_logical, 120); 321 assert_eq!(report.summary.total_comments, 15); 322 assert_eq!(report.summary.total_blank, 15); 323 324 assert_eq!(report.files[0].metrics.logical, 80); 325 assert_eq!(report.files[1].metrics.logical, 40); 326 } 327 328 #[test] 329 fn test_loc_report_with_directories() { 330 let files = vec![ 331 FileLocReport { 332 path: PathBuf::from("src/main.rs"), 333 metrics: LocMetrics { physical: 100, logical: 80, comments: 10, blank: 10 }, 334 }, 335 FileLocReport { 336 path: PathBuf::from("src/lib.rs"), 337 metrics: LocMetrics { physical: 50, logical: 40, comments: 5, blank: 5 }, 338 }, 339 FileLocReport { 340 path: PathBuf::from("tests/test.rs"), 341 metrics: LocMetrics { physical: 30, logical: 25, comments: 3, blank: 2 }, 342 }, 343 ]; 344 345 let report = LocReport::new(files, RankBy::Logical, true); 346 347 assert!(report.directories.is_some()); 348 let dirs = report.directories.unwrap(); 349 assert_eq!(dirs.len(), 2); 350 351 assert_eq!(dirs[0].path, PathBuf::from("src")); 352 assert_eq!(dirs[0].total.logical, 120); 353 assert_eq!(dirs[0].files.len(), 2); 354 355 assert_eq!(dirs[1].path, PathBuf::from("tests")); 356 assert_eq!(dirs[1].total.logical, 25); 357 assert_eq!(dirs[1].files.len(), 1); 358 } 359 360 #[test] 361 fn test_loc_report_to_json() { 362 let files = vec![FileLocReport { 363 path: PathBuf::from("test.rs"), 364 metrics: LocMetrics { physical: 10, logical: 8, comments: 1, blank: 1 }, 365 }]; 366 367 let report = LocReport::new(files, RankBy::Logical, false); 368 let json = report.to_json().unwrap(); 369 370 assert!(json.contains("files")); 371 assert!(json.contains("summary")); 372 assert!(json.contains("test.rs")); 373 } 374}