godlint-cli 0.1.2

Command-line interface for Godlint.
use std::process::ExitCode;

use godlint_core::{
    config::Severity,
    date::Date,
    rules::{Finding, evaluate},
    scan::ScanReport,
};

use crate::workspace::Workspace;

pub const USAGE: &str = "check [paths...]";

pub fn run(arguments: &[String]) -> Option<ExitCode> {
    let [command, paths @ ..] = arguments else {
        return None;
    };

    if command != "check" {
        return None;
    }

    Some(check(paths))
}

fn check(paths: &[String]) -> ExitCode {
    match run_check(paths) {
        Ok(exit_code) => exit_code,
        Err(message) => {
            eprintln!("{message}");
            ExitCode::from(2)
        }
    }
}

fn run_check(paths: &[String]) -> Result<ExitCode, String> {
    let workspace = Workspace::prepare(paths)?;
    let report = workspace.scan()?;
    let today = Date::today().ok_or_else(|| "Unable to determine the current date.".to_owned())?;
    let findings = evaluate(&report.facts, &workspace.config, today);

    Ok(report_outcome(&findings, &report, workspace.config.fail_on))
}

fn report_outcome(findings: &[Finding], report: &ScanReport, fail_on: Severity) -> ExitCode {
    if findings.is_empty() && report.issues.is_empty() {
        println!("No findings.");

        return ExitCode::SUCCESS;
    }

    print_findings(findings);
    print_issues(report);

    if !report.issues.is_empty() {
        return ExitCode::from(2);
    }

    if fails(findings, fail_on) {
        return ExitCode::from(1);
    }

    ExitCode::SUCCESS
}

fn print_findings(findings: &[Finding]) {
    for finding in findings {
        println!(
            "{}:{}:{}: {}[{}] {}",
            finding.path.display(),
            finding.line,
            finding.column,
            severity_name(finding.severity),
            finding.rule_id,
            finding.message()
        );
    }
}

fn print_issues(report: &ScanReport) {
    for issue in &report.issues {
        eprintln!("{}: {}", issue.path.display(), issue.message);
    }
}

fn fails(findings: &[Finding], fail_on: Severity) -> bool {
    fail_on != Severity::Off && findings.iter().any(|finding| finding.severity >= fail_on)
}

fn severity_name(severity: Severity) -> &'static str {
    match severity {
        Severity::Error => "error",
        Severity::Info => "info",
        Severity::Off => "off",
        Severity::Warning => "warning",
    }
}