code complexity & repetition analysis tool
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}