use std::process::ExitCode;
use crate::cli::{Cli, FailOn};
pub const EXIT_SCAN_ERROR: u8 = 2;
pub const EXIT_GATE_FAILURE: u8 = 3;
use crate::diagnostics::ScanResult;
use crate::{config, deps, discovery, fixer, output, plan, sarif, scan};
pub fn handle_setup() -> ExitCode {
match crate::setup::run_setup() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Error: {e}");
ExitCode::FAILURE
}
}
}
pub fn handle_mcp_flag(cli: &Cli) -> Option<ExitCode> {
#[cfg(feature = "mcp")]
if cli.mcp {
return Some(match crate::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
}
pub fn bootstrap_project(
cli: &Cli,
) -> Result<
(
std::path::PathBuf,
discovery::ProjectInfo,
Option<config::FileConfig>,
),
crate::error::BootstrapError,
> {
discovery::bootstrap_project(&cli.directory, cli.offline)
}
pub fn run_scan(
cli: &Cli,
project_info: &discovery::ProjectInfo,
resolved: &config::ResolvedConfig,
) -> Result<ScanResult, crate::error::ScanError> {
let suppress_spinner = cli.score || cli.json || cli.sarif;
scan::scan_project(
project_info,
resolved,
cli.offline,
&cli.project,
suppress_spinner,
)
}
pub 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(())
}
pub 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::from(EXIT_GATE_FAILURE));
}
}
None
}
pub fn handle_install_deps() -> ExitCode {
deps::print_status();
let all_ok = deps::install_missing_tools();
if all_ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
pub fn apply_fixes_if_requested(cli: &Cli, scan_result: &ScanResult) {
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.");
}
}
}
pub fn emit_plan_if_requested(cli: &Cli, scan_result: &ScanResult) {
if cli.plan {
let items = plan::generate_plan(scan_result);
let plan_text = plan::format_plan_markdown(&items, scan_result);
eprintln!("\n{plan_text}");
}
}
pub fn check_fail_on_gate(scan_result: &ScanResult, fail_on: FailOn) -> Option<ExitCode> {
let should_fail = 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,
};
if should_fail {
Some(ExitCode::from(EXIT_GATE_FAILURE))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::{DimensionScores, ScoreLabel};
use std::time::Duration;
fn make_scan_result(score: u32, errors: usize, warnings: usize, infos: usize) -> ScanResult {
ScanResult {
diagnostics: vec![],
score,
score_label: ScoreLabel::Great,
dimension_scores: DimensionScores {
security: 100,
reliability: 100,
maintainability: 100,
performance: 100,
dependencies: 100,
},
source_file_count: 10,
elapsed: Duration::from_secs(1),
skipped_passes: vec![],
error_count: errors,
warning_count: warnings,
info_count: infos,
pass_timings: vec![],
}
}
#[test]
fn test_score_gate_below_threshold_fails() {
let result = make_scan_result(75, 0, 0, 0);
assert!(check_score_gate(&result, Some(80)).is_some());
}
#[test]
fn test_score_gate_above_threshold_passes() {
let result = make_scan_result(85, 0, 0, 0);
assert!(check_score_gate(&result, Some(80)).is_none());
}
#[test]
fn test_score_gate_exact_threshold_passes() {
let result = make_scan_result(80, 0, 0, 0);
assert!(check_score_gate(&result, Some(80)).is_none());
}
#[test]
fn test_score_gate_no_threshold_passes() {
let result = make_scan_result(10, 0, 0, 0);
assert!(check_score_gate(&result, None).is_none());
}
#[test]
fn test_fail_on_error_with_errors() {
let result = make_scan_result(50, 1, 0, 0);
assert!(check_fail_on_gate(&result, FailOn::Error).is_some());
}
#[test]
fn test_fail_on_error_without_errors() {
let result = make_scan_result(50, 0, 5, 3);
assert!(check_fail_on_gate(&result, FailOn::Error).is_none());
}
#[test]
fn test_fail_on_warning_with_warnings() {
let result = make_scan_result(50, 0, 1, 0);
assert!(check_fail_on_gate(&result, FailOn::Warning).is_some());
}
#[test]
fn test_fail_on_warning_with_errors_too() {
let result = make_scan_result(50, 1, 0, 0);
assert!(check_fail_on_gate(&result, FailOn::Warning).is_some());
}
#[test]
fn test_fail_on_info_with_info() {
let result = make_scan_result(50, 0, 0, 1);
assert!(check_fail_on_gate(&result, FailOn::Info).is_some());
}
#[test]
fn test_fail_on_none_never_fails() {
let result = make_scan_result(50, 10, 20, 30);
assert!(check_fail_on_gate(&result, FailOn::None).is_none());
}
}