#![forbid(unsafe_code)]
#![allow(clippy::multiple_crate_versions)]
use clap::Parser;
use rust_doctor::cli::{Cli, Command, FailOn};
use rust_doctor::diagnostics::ScanResult;
use rust_doctor::{config, deps, discovery, fixer, output, plan, sarif, scan};
use std::process::ExitCode;
fn main() -> ExitCode {
let cli = Cli::parse();
if matches!(cli.command, Some(Command::Setup)) {
return match rust_doctor::setup::run_setup() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: {e}");
ExitCode::FAILURE
}
};
}
if cli.install_deps {
deps::print_status();
let all_ok = deps::install_missing_tools();
return if all_ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
};
}
if let Some(code) = handle_mcp_flag(&cli) {
return code;
}
let (_target_dir, project_info, file_config) = match bootstrap_project(&cli) {
Ok(result) => result,
Err(e) => {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
};
let effective_config = if cli.no_project_config {
None
} else {
file_config.as_ref()
};
let resolved = config::resolve_config(&cli, effective_config);
let scan_result = match run_scan(&cli, &project_info, &resolved) {
Ok(result) => result,
Err(e) => {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
};
if cli.fix {
let applied = fixer::apply_fixes(&scan_result.diagnostics, &cli.directory);
if applied > 0 {
eprintln!("Applied {applied} fix(es).");
} else {
eprintln!("No machine-applicable fixes available.");
}
}
if let Err(e) = emit_output(&cli, &scan_result, &resolved) {
eprintln!("Error: {e}");
return ExitCode::FAILURE;
}
if cli.plan {
let items = plan::generate_plan(&scan_result);
let plan_text = plan::format_plan_markdown(&items, &scan_result);
eprintln!("\n{plan_text}");
}
if let Some(code) = check_score_gate(&scan_result, resolved.score_fail_below) {
return code;
}
if check_fail_on_gate(&scan_result, resolved.fail_on) {
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn handle_mcp_flag(cli: &Cli) -> Option<ExitCode> {
#[cfg(feature = "mcp")]
if cli.mcp {
return Some(match rust_doctor::mcp::run_mcp_server() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: MCP server failed: {e}");
ExitCode::FAILURE
}
});
}
#[cfg(not(feature = "mcp"))]
if cli.mcp {
eprintln!("Error: MCP support not compiled in. Rebuild with `--features mcp`.");
return Some(ExitCode::FAILURE);
}
None
}
fn bootstrap_project(
cli: &Cli,
) -> Result<
(
std::path::PathBuf,
discovery::ProjectInfo,
Option<config::FileConfig>,
),
rust_doctor::error::BootstrapError,
> {
discovery::bootstrap_project(&cli.directory, cli.offline)
}
fn run_scan(
cli: &Cli,
project_info: &discovery::ProjectInfo,
resolved: &config::ResolvedConfig,
) -> Result<ScanResult, rust_doctor::error::ScanError> {
let suppress_spinner = cli.score || cli.json || cli.sarif;
scan::scan_project(
project_info,
resolved,
cli.offline,
&cli.project,
suppress_spinner,
)
}
fn emit_output(
cli: &Cli,
scan_result: &ScanResult,
resolved: &config::ResolvedConfig,
) -> Result<(), Box<dyn std::error::Error>> {
if cli.score {
output::render_score(scan_result);
} else if cli.json {
output::render_json(scan_result)?;
} else if cli.sarif {
let sarif_json = sarif::render_sarif(scan_result)?;
println!("{sarif_json}");
} else {
output::render_terminal(scan_result, resolved.verbose);
}
Ok(())
}
fn check_score_gate(scan_result: &ScanResult, threshold: Option<u32>) -> Option<ExitCode> {
if let Some(threshold) = threshold {
if scan_result.score < threshold {
eprintln!(
"Score {} is below the configured threshold of {}",
scan_result.score, threshold
);
return Some(ExitCode::FAILURE);
}
}
None
}
const fn check_fail_on_gate(scan_result: &ScanResult, fail_on: FailOn) -> bool {
match fail_on {
FailOn::Error => scan_result.error_count > 0,
FailOn::Warning => scan_result.error_count > 0 || scan_result.warning_count > 0,
FailOn::Info => {
scan_result.error_count > 0
|| scan_result.warning_count > 0
|| scan_result.info_count > 0
}
FailOn::None => false,
}
}