mod clippy; mod eslint; mod ruff; use std::path::Path; use std::time::Duration; use compliance_core::models::ScanType; use compliance_core::traits::{ScanOutput, Scanner}; use compliance_core::CoreError; /// Timeout for each individual lint command pub(crate) const LINT_TIMEOUT: Duration = Duration::from_secs(120); pub struct LintScanner; impl Scanner for LintScanner { fn name(&self) -> &str { "lint" } fn scan_type(&self) -> ScanType { ScanType::Lint } #[tracing::instrument(skip_all)] async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { let mut all_findings = Vec::new(); // Detect which languages are present and run appropriate linters if has_rust_project(repo_path) { match clippy::run_clippy(repo_path, repo_id).await { Ok(findings) => all_findings.extend(findings), Err(e) => tracing::warn!("Clippy failed: {e}"), } } if has_js_project(repo_path) { match eslint::run_eslint(repo_path, repo_id).await { Ok(findings) => all_findings.extend(findings), Err(e) => tracing::warn!("ESLint failed: {e}"), } } if has_python_project(repo_path) { match ruff::run_ruff(repo_path, repo_id).await { Ok(findings) => all_findings.extend(findings), Err(e) => tracing::warn!("Ruff failed: {e}"), } } Ok(ScanOutput { findings: all_findings, sbom_entries: Vec::new(), }) } } fn has_rust_project(repo_path: &Path) -> bool { repo_path.join("Cargo.toml").exists() } fn has_js_project(repo_path: &Path) -> bool { // Only run if eslint is actually installed in the project repo_path.join("package.json").exists() && repo_path.join("node_modules/.bin/eslint").exists() } fn has_python_project(repo_path: &Path) -> bool { repo_path.join("pyproject.toml").exists() || repo_path.join("setup.py").exists() || repo_path.join("requirements.txt").exists() } /// Run a command with a timeout, returning its output or an error pub(crate) async fn run_with_timeout( child: tokio::process::Child, scanner_name: &str, ) -> Result { let result = tokio::time::timeout(LINT_TIMEOUT, child.wait_with_output()).await; match result { Ok(Ok(output)) => Ok(output), Ok(Err(e)) => Err(CoreError::Scanner { scanner: scanner_name.to_string(), source: Box::new(e), }), Err(_) => { // Process is dropped here which sends SIGKILL on Unix Err(CoreError::Scanner { scanner: scanner_name.to_string(), source: Box::new(std::io::Error::new( std::io::ErrorKind::TimedOut, format!("{scanner_name} timed out after {}s", LINT_TIMEOUT.as_secs()), )), }) } } }