use std::fs::File;
use std::io::{BufWriter, Write, stdout};
use std::path::PathBuf;
use std::process;
use std::time::Instant;
use clap::{Parser, Subcommand};
use cargo_coupling::{
CompiledConfig, IssueThresholds, VolatilityAnalyzer, analyze_workspace_with_config,
cli_output::{
CheckConfig, generate_check_output, generate_hotspots_output, generate_impact_output,
generate_json_output, parse_grade, parse_severity,
},
generate_ai_output_with_thresholds, generate_report_with_thresholds,
generate_summary_with_thresholds, load_compiled_config,
web::{ServerConfig, start_server},
};
#[derive(Parser, Debug)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Coupling(Args),
}
#[derive(Parser, Debug)]
struct Args {
#[arg(default_value = "./src")]
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
summary: bool,
#[arg(long)]
ai: bool,
#[arg(long, default_value = "6")]
git_months: usize,
#[arg(long)]
no_git: bool,
#[arg(long)]
exclude_tests: bool,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
timing: bool,
#[arg(long, short = 'j', value_name = "N")]
jobs: Option<usize>,
#[arg(long)]
max_deps: Option<usize>,
#[arg(long)]
max_dependents: Option<usize>,
#[arg(long)]
web: bool,
#[arg(long, default_value = "3000")]
port: u16,
#[arg(long)]
no_open: bool,
#[arg(long)]
api_endpoint: Option<String>,
#[arg(long, value_name = "N", num_args = 0..=1, require_equals = true, default_missing_value = "5")]
hotspots: Option<usize>,
#[arg(long, value_name = "MODULE")]
impact: Option<String>,
#[arg(long, value_name = "ITEM")]
trace: Option<String>,
#[arg(long)]
check: bool,
#[arg(long, value_name = "GRADE", requires = "check")]
min_grade: Option<String>,
#[arg(long, value_name = "N", requires = "check")]
max_critical: Option<usize>,
#[arg(long, value_name = "N", requires = "check")]
max_circular: Option<usize>,
#[arg(long, value_name = "SEVERITY", requires = "check")]
fail_on: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
all: bool,
#[arg(long, visible_alias = "jp")]
japanese: bool,
}
fn main() {
if let Err(e) = run() {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let Commands::Coupling(args) = cli.command;
let available_cores = std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1);
let num_threads = args.jobs.unwrap_or(available_cores);
if num_threads != available_cores || args.jobs.is_some() {
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build_global()
.unwrap_or_else(|e| eprintln!("Warning: Could not set thread count: {}", e));
}
if args.verbose || args.timing {
eprintln!(
"Using {} thread(s) for parallel processing ({} CPU cores available)",
num_threads, available_cores
);
}
let total_start = Instant::now();
let config_path = args.config.as_ref().unwrap_or(&args.path);
let mut config = match load_compiled_config(config_path) {
Ok(config) => {
if args.verbose && config.has_volatility_overrides() {
eprintln!("Loaded configuration from .coupling.toml");
}
config
}
Err(e) => {
if args.verbose {
eprintln!("Note: No config file loaded: {}", e);
}
CompiledConfig::empty()
}
};
if args.exclude_tests {
config.set_exclude_tests(true);
}
if args.verbose && config.exclude_tests {
eprintln!("Test code will be excluded from analysis");
}
if args.verbose && config.prelude_module_count() > 0 {
eprintln!(
"Prelude modules configured: {} pattern(s)",
config.prelude_module_count()
);
}
eprintln!("Analyzing project at '{}'...", args.path.display());
let analysis_start = Instant::now();
let mut metrics = analyze_workspace_with_config(&args.path, &config)?;
let analysis_time = analysis_start.elapsed();
if !args.no_git {
if args.verbose {
eprintln!("Analyzing git history ({} months)...", args.git_months);
}
let mut volatility = VolatilityAnalyzer::new(args.git_months);
match volatility.analyze(&args.path) {
Ok(()) => {
if args.verbose {
let stats = volatility.statistics();
eprintln!(
"Git analysis: {} files, {} total changes",
stats.total_files, stats.total_changes
);
}
metrics.file_changes = volatility.file_changes;
metrics.update_volatility_from_git();
}
Err(e) => {
if args.verbose {
eprintln!("Warning: Git analysis failed: {}", e);
}
}
}
}
if config.has_volatility_overrides() {
let mut override_count = 0;
for coupling in &mut metrics.couplings {
if let Some(override_vol) = config.get_volatility_override(&coupling.target) {
coupling.volatility = override_vol;
override_count += 1;
}
}
if args.verbose && override_count > 0 {
eprintln!(
"Applied {} volatility overrides from config",
override_count
);
}
}
if args.timing {
eprintln!(
"Analysis complete: {} files, {} modules (took {:.2?})\n",
metrics.total_files,
metrics.module_count(),
analysis_time
);
} else {
eprintln!(
"Analysis complete: {} files, {} modules\n",
metrics.total_files,
metrics.module_count()
);
}
let thresholds = IssueThresholds {
max_dependencies: args.max_deps.unwrap_or(config.thresholds.max_dependencies),
max_dependents: args
.max_dependents
.unwrap_or(config.thresholds.max_dependents),
strict_mode: !args.all, japanese: args.japanese,
exclude_tests: config.exclude_tests,
prelude_module_count: config.prelude_module_count(),
..IssueThresholds::default()
};
if args.verbose {
eprintln!(
"Thresholds: max_deps={}, max_dependents={}",
thresholds.max_dependencies, thresholds.max_dependents
);
}
if args.web {
let server_config = ServerConfig {
port: args.port,
open_browser: !args.no_open,
api_endpoint: args.api_endpoint.clone(),
};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(start_server(metrics, thresholds, server_config))
.map_err(|e| -> Box<dyn std::error::Error> { e })?;
return Ok(());
}
let output: Box<dyn Write> = match &args.output {
Some(path) => {
let file = File::create(path)?;
Box::new(BufWriter::new(file))
}
None => Box::new(stdout()),
};
let mut writer = output;
if args.json {
generate_json_output(&metrics, &thresholds, &mut writer)?;
return Ok(());
}
if args.check {
let check_config = CheckConfig {
min_grade: args.min_grade.as_ref().and_then(|s| parse_grade(s)),
max_critical: args.max_critical,
max_circular: args.max_circular,
fail_on: args.fail_on.as_ref().and_then(|s| parse_severity(s)),
};
let exit_code = generate_check_output(&metrics, &thresholds, &check_config, &mut writer)?;
process::exit(exit_code);
}
if let Some(limit) = args.hotspots {
generate_hotspots_output(&metrics, &thresholds, limit, args.verbose, &mut writer)?;
return Ok(());
}
if let Some(module_name) = &args.impact {
let found = generate_impact_output(&metrics, module_name, &mut writer)?;
if !found {
process::exit(1);
}
return Ok(());
}
if let Some(item_name) = &args.trace {
let found =
cargo_coupling::cli_output::generate_trace_output(&metrics, item_name, &mut writer)?;
if !found {
process::exit(1);
}
return Ok(());
}
if args.ai {
generate_ai_output_with_thresholds(&metrics, &thresholds, &mut writer)?;
} else if args.summary {
generate_summary_with_thresholds(&metrics, &thresholds, &mut writer)?;
} else {
generate_report_with_thresholds(&metrics, &thresholds, &mut writer)?;
}
if let Some(path) = &args.output {
eprintln!("Report written to: {}", path.display());
}
if args.timing {
let total_time = total_start.elapsed();
let files_per_sec = metrics.total_files as f64 / total_time.as_secs_f64();
eprintln!(
"Total time: {:.2?} ({:.1} files/sec)",
total_time, files_per_sec
);
}
Ok(())
}