use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
use regex::Regex;
static PATTERN: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"/tmp/[a-zA-Z_][a-zA-Z0-9_.\-]*").unwrap());
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let pattern = &*PATTERN;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
continue;
}
if line.contains("mktemp") {
continue;
}
if trimmed.starts_with("trap ") {
continue;
}
for m in pattern.find_iter(line) {
let path = m.as_str();
if path == "/tmp/." || path == "/tmp/.." {
continue;
}
let start_col = m.start() + 1;
let end_col = m.end() + 1;
let diagnostic = Diagnostic::new(
"REL005",
Severity::Warning,
format!(
"Predictable temp file `{}`. Use `mktemp` for unique, secure temp files.",
path
),
Span::new(line_num + 1, start_col, line_num + 1, end_col),
)
.with_fix(Fix::new("$(mktemp)".to_string()));
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rel005_detects_hardcoded_tmp() {
let script = "tmpfile=/tmp/output.txt";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "REL005");
assert_eq!(result.diagnostics[0].severity, Severity::Warning);
}
#[test]
fn test_rel005_provides_fix() {
let script = "tmpfile=/tmp/output.txt";
let result = check(script);
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement, "$(mktemp)");
}
#[test]
fn test_rel005_no_flag_with_mktemp() {
let script = "tmpfile=$(mktemp /tmp/myapp.XXXXXX)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel005_no_false_positive_comment() {
let script = "# tmpfile=/tmp/output.txt";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel005_detects_redirect_to_tmp() {
let script = "echo data > /tmp/myapp.log";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_rel005_no_flag_trap_cleanup() {
let script = "trap 'rm -f /tmp/mylock' EXIT";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_rel005_detects_multiple_tmp_files() {
let script = "a=/tmp/foo\nb=/tmp/bar";
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
}
}