main-spex 0.21.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use super::super::finding::Location;
use super::super::scoped_file::ScopedFile;
use super::{Hit, Outcome};

#[cfg(test)]
#[path = "script_test.rs"]
mod script_test;

const SCRIPT_BATCH_SIZE: usize = 1000;
const SPEX_DIR_ENV: &str = "SPEX_DIR";

pub(in crate::commands::check) fn run_per_file_script(
    program: &Path,
    package_path: &Path,
    spex_dir: &Path,
    files: &[&ScopedFile],
) -> Outcome {
    let mut hits = Vec::new();
    for batch in files.chunks(SCRIPT_BATCH_SIZE) {
        hits.extend(run_script(
            program,
            package_path,
            spex_dir,
            batch.iter().map(|file| file.rel.as_os_str()),
        )?);
    }
    Ok(hits)
}

pub(in crate::commands::check) fn run_script_once(
    program: &Path,
    cwd: &Path,
    spex_dir: &Path,
) -> Outcome {
    run_script(program, cwd, spex_dir, std::iter::empty::<&OsStr>())
}

fn run_script<I, S>(program: &Path, cwd: &Path, spex_dir: &Path, args: I) -> Outcome
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let output = Command::new(program)
        .args(args)
        .current_dir(cwd)
        .env(SPEX_DIR_ENV, spex_dir)
        .output()
        .map_err(|err| format!("could not run script `{}`: {err}", program.display()))?;

    Ok(if output.status.success() {
        Vec::new()
    } else {
        script_hits(&output)
    })
}

fn script_hits(output: &Output) -> Vec<Hit> {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let hits: Vec<Hit> = stdout
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(parse_script_line)
        .collect();
    if hits.is_empty() {
        vec![Hit::new(None, script_fallback(output))]
    } else {
        hits
    }
}

fn parse_script_line(line: &str) -> Hit {
    match line.split_once('\t') {
        Some((location, message)) => {
            let location = location.trim();
            Hit::new(
                (!location.is_empty()).then(|| Location::parse(location)),
                message.trim(),
            )
        }
        None => Hit::new(None, line.trim()),
    }
}

pub(in crate::commands::check) fn locate_script(
    script_dir: &Path,
    reference: &str,
) -> core::result::Result<PathBuf, String> {
    resolve_script(script_dir, reference)
        .ok_or_else(|| format!("script `{reference}` not found in {}", script_dir.display()))
}

pub(in crate::commands::check) fn resolve_script(
    script_dir: &Path,
    reference: &str,
) -> Option<PathBuf> {
    let script = script_dir.join(reference);
    script.is_file().then_some(script)
}

fn script_fallback(output: &Output) -> String {
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !stderr.trim().is_empty() {
        return stderr.trim().to_string();
    }
    let code = output
        .status
        .code()
        .map_or_else(|| "signal".to_string(), |code| code.to_string());
    format!("script exited with status {code}")
}