use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static DESTRUCTIVE_PATTERN: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"\b(rm\s+-rf|rm\s+-r\s+-f|rm\s+-fr)\b").unwrap());
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let has_set_e = source.lines().any(|line| {
let trimmed = line.trim();
trimmed == "set -e"
|| trimmed == "set -euo pipefail"
|| trimmed.contains("set -e ")
|| trimmed.starts_with("set -e;")
|| trimmed == "set -eu"
});
if has_set_e {
return result;
}
let destructive_pattern = &*DESTRUCTIVE_PATTERN;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
continue;
}
if let Some(m) = destructive_pattern.find(line) {
let after_cmd = &line[m.end()..];
if after_cmd.contains("||") || after_cmd.contains("&&") {
continue;
}
if trimmed.starts_with("if ") || trimmed.starts_with("then") {
continue;
}
let start_col = m.start() + 1;
let end_col = m.end() + 1;
let diagnostic = Diagnostic::new(
"REL001",
Severity::Warning,
"Destructive command without error check. Add `|| exit 1` or use `set -e`.",
Span::new(line_num + 1, start_col, line_num + 1, end_col),
);
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rel001_detects_rm_rf_without_check() {
let script = "rm -rf /var/cache/app";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "REL001");
assert_eq!(result.diagnostics[0].severity, Severity::Warning);
}
#[test]
fn test_rel001_no_flag_with_error_handler() {
let script = "rm -rf /var/cache/app || exit 1";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel001_no_flag_with_set_e() {
let script = "set -e\nrm -rf /var/cache/app";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel001_no_flag_with_and_handler() {
let script = "rm -rf /var/cache/app && echo done";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel001_no_false_positive_comment() {
let script = "# rm -rf /var/cache/app";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel001_no_fix_provided() {
let script = "rm -rf /tmp/build";
let result = check(script);
assert!(result.diagnostics[0].fix.is_none());
}
#[test]
fn test_rel001_detects_rm_r_f() {
let script = "rm -r -f /tmp/build";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_rel001_no_flag_with_set_euo() {
let script = "set -euo pipefail\nrm -rf /tmp/build";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
}