use std::env;
use std::mem::take;
use std::path::PathBuf;
use slopguard_core::baseline::{
self, find_baseline_file, load, project_root, Baseline, BASELINE_FILE,
};
use slopguard_core::finding::ScanResult;
use slopguard_core::scanner::count_severities;
use crate::{collect_findings, AppError, CollectOpts};
pub struct BaselineOpts {
pub paths: Vec<PathBuf>,
pub output: Option<PathBuf>,
pub config_path: Option<PathBuf>,
pub cli_disable: Vec<String>,
pub cli_enable: Vec<String>,
pub no_cache: bool,
pub cache_dir: Option<PathBuf>,
pub no_ai: bool,
}
fn recompute_stats(result: &mut ScanResult, baseline_filtered: usize) {
let (errors, warnings) = count_severities(&result.findings);
result.stats.errors = errors;
result.stats.warnings = warnings;
result.stats.total = errors + warnings;
result.stats.baseline_filtered = baseline_filtered;
}
fn resolve_baseline(
no_baseline: bool,
baseline_path: Option<PathBuf>,
) -> Result<Option<(Baseline, PathBuf)>, AppError> {
if no_baseline {
return Ok(None);
}
let path = match baseline_path {
Some(explicit) => explicit,
None => {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match find_baseline_file(&cwd) {
Some(found) => found,
None => return Ok(None),
}
}
};
let baseline = load(&path)?;
let root = path
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
Ok(Some((baseline, root)))
}
pub fn apply_baseline(
result: &mut ScanResult,
no_baseline: bool,
baseline_path: Option<PathBuf>,
) -> Result<(), AppError> {
if let Some((bl, root)) = resolve_baseline(no_baseline, baseline_path)? {
let (kept, filtered) = baseline::filter(take(&mut result.findings), &bl, &root);
result.findings = kept;
recompute_stats(result, filtered);
}
Ok(())
}
pub fn run_baseline(opts: BaselineOpts) -> Result<(), AppError> {
let BaselineOpts {
paths,
output,
config_path,
cli_disable,
cli_enable,
no_cache,
cache_dir,
no_ai,
} = opts;
let (result, _config) = collect_findings(CollectOpts {
paths,
config_path,
cli_disable,
cli_enable,
rule_filter: None,
no_cache,
cache_dir,
no_ai,
diff: false,
diff_base: None,
})?;
let out_path = match output {
Some(explicit) => explicit,
None => {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
project_root(&cwd).join(BASELINE_FILE)
}
};
let root = out_path
.parent()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let baseline = baseline::build(&result.findings, &root);
baseline::write(&baseline, &out_path)?;
println!(
"Wrote {} findings to {}",
result.findings.len(),
out_path.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::Path;
use tempfile::tempdir;
use slopguard_core::baseline::BaselineError;
use super::resolve_baseline;
use crate::AppError;
#[test]
fn no_baseline_flag_skips_resolution() {
let resolved = resolve_baseline(true, Some(Path::new("nope.json").to_path_buf()))
.expect("--no-baseline should not read any file");
assert!(resolved.is_none());
}
#[test]
fn missing_explicit_baseline_is_an_error() {
let dir = tempdir().unwrap();
let missing = dir.path().join("nope.json");
match resolve_baseline(false, Some(missing)) {
Err(AppError::Baseline(BaselineError::Io { .. })) => {}
other => panic!("expected an I/O baseline error, got {other:?}"),
}
}
}