use clap::{Parser, ValueEnum};
use codedebt::{CodeDebtScanner, Severity};
use colored::*;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "codedebt")]
#[command(about = "Ultra-fast code debt detection tool")]
#[command(version)]
struct Cli {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short, long, value_enum, default_value = "low")]
severity: SeverityArg,
#[arg(short, long, value_enum, default_value = "pretty")]
format: OutputFormat,
#[arg(short = 'S', long)]
summary: bool,
#[arg(short, long)]
extensions: Option<String>,
#[arg(short, long)]
ignore: Option<String>,
#[arg(long)]
git_blame: bool,
#[arg(long)]
detect_duplicates: bool,
#[arg(long)]
file_types: bool,
#[arg(long)]
age_distribution: bool,
#[arg(long)]
max_age: Option<i64>,
#[arg(long)]
min_duplicates: Option<usize>,
}
#[derive(Clone, ValueEnum)]
enum SeverityArg {
Critical,
High,
Medium,
Low,
}
impl From<SeverityArg> for Severity {
fn from(arg: SeverityArg) -> Self {
match arg {
SeverityArg::Critical => Severity::Critical,
SeverityArg::High => Severity::High,
SeverityArg::Medium => Severity::Medium,
SeverityArg::Low => Severity::Low,
}
}
}
#[derive(Clone, ValueEnum)]
enum OutputFormat {
Pretty,
Json,
Csv,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let mut scanner = CodeDebtScanner::new();
if let Some(extensions) = cli.extensions {
let exts: Vec<String> = extensions
.split(',')
.map(|s| s.trim().to_string())
.collect();
scanner = scanner.with_file_extensions(exts);
}
if let Some(ignore_dirs) = cli.ignore {
let dirs: Vec<String> = ignore_dirs
.split(',')
.map(|s| s.trim().to_string())
.collect();
scanner = scanner.with_ignore_dirs(dirs);
}
if cli.git_blame {
scanner = scanner.with_git_blame(true);
}
if cli.detect_duplicates {
scanner = scanner.with_duplicate_detection(true);
}
let start = std::time::Instant::now();
let all_items = scanner.scan(&cli.path)?;
let duration = start.elapsed();
let mut filtered_items = scanner.filter_by_severity(&all_items, cli.severity.into());
if let Some(max_age) = cli.max_age {
if !cli.git_blame {
eprintln!("Warning: --max-age requires --git-blame to be enabled");
} else {
filtered_items = scanner.filter_by_age(&filtered_items, max_age);
}
}
if let Some(min_duplicates) = cli.min_duplicates {
if !cli.detect_duplicates {
eprintln!("Warning: --min-duplicates requires --detect-duplicates to be enabled");
} else {
filtered_items = scanner.find_duplicates(&filtered_items, min_duplicates);
}
}
match cli.format {
OutputFormat::Pretty => {
if cli.summary {
print_summary(&scanner, &filtered_items);
} else {
print_pretty(&filtered_items);
}
if cli.file_types {
print_file_type_distribution(&scanner, &all_items);
}
if cli.age_distribution {
if !cli.git_blame {
eprintln!("Warning: --age-distribution requires --git-blame to be enabled");
} else {
print_age_distribution(&scanner, &all_items);
}
}
println!(
"\n{} Scanned in {:.2}ms",
"⚡".bright_yellow(),
duration.as_secs_f64() * 1000.0
);
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&filtered_items)?);
}
OutputFormat::Csv => {
print_csv(&filtered_items);
}
}
Ok(())
}
fn print_pretty(items: &[codedebt::CodeDebtItem]) {
if items.is_empty() {
println!("{} No code debt found!", "✅".green());
return;
}
println!("{} {} code debt items found:\n", "🔍".cyan(), items.len());
for item in items {
let severity_icon = match item.severity {
Severity::Critical => "🚨".red(),
Severity::High => "⚠️ ".yellow(),
Severity::Medium => "📝".blue(),
Severity::Low => "💡".white(),
};
let severity_text = match item.severity {
Severity::Critical => "CRITICAL".red().bold(),
Severity::High => "HIGH".yellow().bold(),
Severity::Medium => "MEDIUM".blue().bold(),
Severity::Low => "LOW".white().bold(),
};
println!(
"{} {} {} {}:{}:{} {}",
severity_icon,
severity_text,
item.pattern_type.purple().bold(),
item.file_path.display().to_string().cyan(),
item.line_number.to_string().green(),
item.column.to_string().green(),
item.line_content.trim()
);
let mut details = Vec::new();
if let Some(author) = &item.author {
details.push(format!("👤 {}", author.dimmed()));
}
if let Some(age_days) = item.age_days {
let age_str = if age_days == 0 {
"today".to_string()
} else if age_days == 1 {
"1 day ago".to_string()
} else if age_days < 30 {
format!("{} days ago", age_days)
} else if age_days < 365 {
format!("{} months ago", age_days / 30)
} else {
format!("{} years ago", age_days / 365)
};
details.push(format!("📅 {}", age_str.dimmed()));
}
if item.duplicate_count > 1 {
details.push(format!(
"🔄 {} duplicates",
item.duplicate_count.to_string().yellow()
));
}
if !details.is_empty() {
println!(" {}", details.join(" • "));
}
}
}
fn print_summary(scanner: &CodeDebtScanner, items: &[codedebt::CodeDebtItem]) {
let summary = scanner.get_summary(items);
println!("{} Code Debt Summary:", "📊".cyan());
println!("{}", "=".repeat(40));
let mut total = 0;
for (pattern, count) in &summary {
println!("{:15} {:>5}", pattern.purple(), count.to_string().yellow());
total += count;
}
println!("{}", "-".repeat(40));
println!(
"{:15} {:>5}",
"TOTAL".bold(),
total.to_string().bold().yellow()
);
}
fn print_csv(items: &[codedebt::CodeDebtItem]) {
println!("file_path,line_number,column,severity,pattern_type,line_content,author,age_days,duplicate_count");
for item in items {
println!(
"{},{},{},{:?},{},\"{}\",\"{}\",{},{}",
item.file_path.display(),
item.line_number,
item.column,
item.severity,
item.pattern_type,
item.line_content.replace('"', "\"\""),
item.author.as_deref().unwrap_or(""),
item.age_days.unwrap_or(-1),
item.duplicate_count
);
}
}
fn print_file_type_distribution(scanner: &CodeDebtScanner, items: &[codedebt::CodeDebtItem]) {
let distribution = scanner.get_file_type_summary(items);
if distribution.is_empty() {
return;
}
println!("\n{} File Type Distribution:", "📊".cyan());
println!("{}", "═".repeat(40));
let mut sorted_types: Vec<_> = distribution.iter().collect();
sorted_types.sort_by(|a, b| b.1.cmp(a.1));
for (file_type, count) in sorted_types {
println!(
"{:15} {:>5}",
file_type.purple(),
count.to_string().yellow()
);
}
}
fn print_age_distribution(scanner: &CodeDebtScanner, items: &[codedebt::CodeDebtItem]) {
let distribution = scanner.get_age_distribution(items);
if distribution.is_empty() {
return;
}
println!("\n{} Age Distribution:", "📅".cyan());
println!("{}", "═".repeat(40));
let order = [
"This week",
"This month",
"Last 3 months",
"This year",
"Over a year",
"Unknown age",
];
for bucket in order.iter() {
if let Some(count) = distribution.get(*bucket) {
println!("{:15} {:>5}", bucket.purple(), count.to_string().yellow());
}
}
}