xbp 10.57.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! `xbp audit` — language-agnostic static analysis CLI surface.

use crate::cli::commands::{AuditCmd, AuditErrorsCmd, AuditSubCommand};
use std::env;
use std::path::PathBuf;
use xbp_analysis::adapters::config::load_from_root;
use xbp_analysis::adapters::fix::SafeFixApplier;
use xbp_analysis::adapters::output::render_report;
use xbp_analysis::application::engine::AnalysisEngine;
use xbp_analysis::domain::request::{AnalysisRequest, OutputFormat};
use xbp_analysis::domain::types::{LanguageId, Severity};
use xbp_analysis::ports::inbound::FixService;

pub async fn run_audit(cmd: AuditCmd) -> Result<(), String> {
    match cmd.command {
        AuditSubCommand::Errors(errors) => run_audit_errors(errors).await,
    }
}

async fn run_audit_errors(cmd: AuditErrorsCmd) -> Result<(), String> {
    let root = cmd
        .path
        .clone()
        .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));

    let mut config = load_from_root(&root)?;
    if !cmd.enable.is_empty() {
        config.rules.enabled = cmd.enable.clone();
    }
    if !cmd.disable.is_empty() {
        config.rules.disabled.extend(cmd.disable.clone());
    }

    let format = OutputFormat::parse(&cmd.format)
        .ok_or_else(|| {
            format!(
                "unknown format `{}` (use terminal, json, sarif, github-actions)",
                cmd.format
            )
        })?;

    let mut request = AnalysisRequest::new(&root);
    request.config = config;
    request.format = format;
    request.apply_fixes = cmd.fix;
    request.workspace = cmd.workspace;
    if let Some(lang) = cmd.language {
        request.language_filter = Some(LanguageId::new(lang));
    }
    if !cmd.files.is_empty() {
        let mut paths = Vec::new();
        for p in &cmd.files {
            let abs = if p.is_absolute() {
                p.clone()
            } else {
                root.join(p)
            };
            if abs.is_dir() {
                // Let discovery under that dir
                request.root = abs;
            } else {
                paths.push(abs);
            }
        }
        request.paths = paths;
    }

    let engine = AnalysisEngine::new();
    let report = engine.analyze(&request)?;

    if cmd.fix {
        let fixer = SafeFixApplier::default_fs();
        let applied = fixer.apply_safe_fixes(&report.findings, &[])?;
        if applied.is_empty() {
            eprintln!("note: no safe autofixes available for these findings (by design)");
        } else {
            for a in applied {
                eprintln!("fixed {}: {}", a.path.display(), a.description);
            }
        }
    }

    let rendered = render_report(&report, format)?;
    println!("{rendered}");

    if let Some(fail) = cmd.fail_on.as_deref().and_then(Severity::parse) {
        let bad = report.findings.iter().any(|f| f.severity >= fail);
        if bad {
            return Err(format!(
                "audit failed: {} finding(s) at or above severity `{fail}`",
                report
                    .findings
                    .iter()
                    .filter(|f| f.severity >= fail)
                    .count()
            ));
        }
    }

    Ok(())
}