code complexity & repetition analysis tool
at main 198 lines 7.2 kB view raw
1use anyhow::Result; 2use mccabre_core::{ 3 cloner::CloneDetector, 4 complexity::{CyclomaticMetrics, LocMetrics}, 5 config::Config, 6 loader::{FileLoader, SourceFile}, 7 reporter::{FileReport, Report}, 8}; 9use owo_colors::OwoColorize; 10use std::collections::HashMap; 11use std::path::PathBuf; 12 13use crate::highlight::Highlighter; 14 15pub fn run( 16 path: PathBuf, json: bool, threshold: Option<usize>, min_tokens: Option<usize>, config_path: Option<PathBuf>, 17 respect_gitignore: bool, highlight: bool, 18) -> Result<()> { 19 let config = if let Some(config_path) = config_path { 20 Config::from_file(config_path)? 21 } else { 22 Config::load_default()? 23 }; 24 25 let config = config.merge_with_cli(threshold, min_tokens, Some(respect_gitignore)); 26 let loader = FileLoader::new().with_gitignore(config.files.respect_gitignore); 27 let files = loader.load(&path)?; 28 29 if files.is_empty() { 30 eprintln!("{}", "No supported files found".yellow()); 31 return Ok(()); 32 } 33 34 let mut file_reports = Vec::new(); 35 36 for file in &files { 37 let loc = LocMetrics::calculate(&file.content, file.language)?; 38 let cyclomatic = CyclomaticMetrics::calculate(&file.content, file.language)?; 39 40 file_reports.push(FileReport { path: file.path.clone(), loc, cyclomatic }); 41 } 42 43 let clones = if config.clones.enabled { 44 let detector = CloneDetector::new(config.clones.min_tokens); 45 let files_for_clone_detection: Vec<_> = files 46 .iter() 47 .map(|f| (f.path.clone(), f.content.clone(), f.language)) 48 .collect(); 49 detector.detect_across_files(&files_for_clone_detection)? 50 } else { 51 Vec::new() 52 }; 53 54 let report = Report::new(file_reports, clones); 55 56 if json { 57 println!("{}", report.to_json()?); 58 } else { 59 print_pretty_report(&report, &config, &files, highlight); 60 } 61 62 Ok(()) 63} 64 65fn print_pretty_report(report: &Report, config: &Config, files: &[SourceFile], highlight: bool) { 66 println!("{}", "=".repeat(80).cyan()); 67 println!("{}", "MCCABRE CODE ANALYSIS REPORT".cyan().bold()); 68 println!("{}", "=".repeat(80).cyan()); 69 println!(); 70 71 println!("{}", "SUMMARY".green().bold()); 72 println!("{}", "-".repeat(80).cyan()); 73 println!("Total files analyzed: {}", report.summary.total_files.bold()); 74 println!( 75 "Total physical LOC: {}", 76 report.summary.total_physical_loc.bold() 77 ); 78 println!( 79 "Total logical LOC: {}", 80 report.summary.total_logical_loc.bold() 81 ); 82 println!( 83 "Average complexity: {}", 84 format!("{:.2}", report.summary.avg_complexity).bold() 85 ); 86 println!("Maximum complexity: {}", report.summary.max_complexity.bold()); 87 println!( 88 "High complexity files: {}", 89 report.summary.high_complexity_files.bold() 90 ); 91 println!("Clone groups detected: {}", report.summary.total_clones.bold()); 92 println!(); 93 94 if !report.files.is_empty() { 95 println!("{}", "FILE METRICS".green().bold()); 96 println!("{}", "-".repeat(80).cyan()); 97 98 for file in &report.files { 99 println!("{} {}", "FILE:".blue().bold(), file.path.display().bold()); 100 101 let complexity_value = file.cyclomatic.file_complexity; 102 let complexity_text = format!("Cyclomatic Complexity: {complexity_value}"); 103 104 if complexity_value > config.complexity.error_threshold { 105 println!(" {}", complexity_text.red().bold()); 106 } else if complexity_value > config.complexity.warning_threshold { 107 println!(" {}", complexity_text.yellow()); 108 } else { 109 println!(" {}", complexity_text.green()); 110 } 111 println!(" Physical LOC: {}", file.loc.physical); 112 println!(" Logical LOC: {}", file.loc.logical); 113 println!(" Comment lines: {}", file.loc.comments); 114 println!(" Blank lines: {}", file.loc.blank); 115 println!(); 116 117 if !file.cyclomatic.functions.is_empty() { 118 println!(" {}:", "Functions".magenta()); 119 for func in &file.cyclomatic.functions { 120 let func_text = format!( 121 " - {} (line {}): complexity {}", 122 func.name, func.line, func.complexity 123 ); 124 125 if func.complexity > config.complexity.error_threshold { 126 println!("{}", func_text.red()); 127 } else if func.complexity > config.complexity.warning_threshold { 128 println!("{}", func_text.yellow()); 129 } else { 130 println!("{func_text}"); 131 } 132 } 133 println!(); 134 } 135 } 136 } 137 138 if !report.clones.is_empty() { 139 println!("{}", "DETECTED CLONES".green().bold()); 140 println!("{}", "-".repeat(80).cyan()); 141 142 let file_map: HashMap<_, _> = files.iter().map(|f| (&f.path, f)).collect(); 143 let highlighter = if highlight { Some(Highlighter::new()) } else { None }; 144 145 for clone in &report.clones { 146 println!( 147 "{} {} {} {} {} {}", 148 "Clone Group".yellow(), 149 format!("#{}", clone.id).yellow().bold(), 150 "(length:".dimmed(), 151 format!("{} tokens", clone.length).bold(), 152 format!("{} occurrences)", clone.locations.len()).bold(), 153 "".dimmed() 154 ); 155 156 for loc in &clone.locations { 157 println!( 158 " {} {}:{}", 159 "-".dimmed(), 160 loc.file.display(), 161 format!("{}-{}", loc.start_line, loc.end_line).dimmed() 162 ); 163 164 if highlight && let Some(source_file) = file_map.get(&loc.file) { 165 let code_block = extract_lines(&source_file.content, loc.start_line, loc.end_line); 166 167 if let Some(ref hl) = highlighter { 168 let ext = source_file.path.extension().and_then(|e| e.to_str()).unwrap_or("txt"); 169 let highlighted = hl.highlight(&code_block, ext); 170 171 println!("{}", " ┌─────".dimmed()); 172 for line in highlighted.lines() { 173 println!("{line}"); 174 } 175 println!("{}", " └─────".dimmed()); 176 } 177 } 178 } 179 println!(); 180 } 181 } 182 183 println!("{}", "=".repeat(80).cyan()); 184} 185 186/// Extract lines from source code by line numbers (1-indexed) 187fn extract_lines(source: &str, start_line: usize, end_line: usize) -> String { 188 source 189 .lines() 190 .enumerate() 191 .filter(|(idx, _)| { 192 let line_num = idx + 1; 193 line_num >= start_line && line_num <= end_line 194 }) 195 .map(|(_, line)| line) 196 .collect::<Vec<_>>() 197 .join("\n") 198}