use std::path::{Path, PathBuf};
use serde::Serialize;
use tokio::process::Command;
use crate::error::CoreError;
const TOOL_NAME: &str = "ignition-lint";
const STDERR_PREVIEW_CAP: usize = 4000;
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.is_file()
&& std::fs::metadata(path).is_ok_and(|meta| meta.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
pub fn find_lint_tool() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|dir| dir.join(TOOL_NAME))
.find(|candidate| is_executable_file(candidate))
}
#[derive(Debug, Serialize)]
pub struct LintResult {
pub ran: bool,
pub tool: String,
pub child_exit_code: Option<i32>,
pub issues_found: usize,
pub report: Option<serde_json::Value>,
pub stdout: String,
pub stderr_preview: String,
#[serde(skip)]
pub strict: bool,
}
impl LintResult {
pub fn strict_exit_code(&self) -> Option<u8> {
if !self.strict {
return None;
}
match self.child_exit_code {
Some(code) => Some((code & 0x7f) as u8),
None => Some(1),
}
}
}
pub async fn lint_run(
paths: &[String],
strict: bool,
extra_args: &[String],
) -> Result<LintResult, CoreError> {
let Some(tool) = find_lint_tool() else {
return Err(CoreError::LintToolAbsent);
};
let mut args: Vec<&str> = vec!["--report-format", "json"];
for path in paths {
args.push("--target");
args.push(path);
}
args.extend(extra_args.iter().map(String::as_str));
let output = Command::new(&tool)
.args(&args)
.output()
.await
.map_err(|err| CoreError::InvalidInput {
reason: format!(
"ignition-lint was found at {} but could not run: {err}",
tool.display()
),
})?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr_full = String::from_utf8_lossy(&output.stderr).into_owned();
let stderr_preview = if stderr_full.len() > STDERR_PREVIEW_CAP {
let mut capped: String = stderr_full.chars().take(STDERR_PREVIEW_CAP).collect();
capped.push_str("\n… (truncated)");
capped
} else {
stderr_full
};
let report: Option<serde_json::Value> = serde_json::from_str(&stdout).ok();
let issues_found = report
.as_ref()
.and_then(|report| report.get("issues"))
.and_then(serde_json::Value::as_array)
.map_or(0, Vec::len);
Ok(LintResult {
ran: true,
tool: tool.display().to_string(),
child_exit_code: output.status.code(),
issues_found,
report,
stdout,
stderr_preview,
strict,
})
}