use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static IF_WITH_REDIRECT: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\bif\s+[^;]+>\s*[^\s;]+\s*;").unwrap()
});
static WHILE_WITH_REDIRECT: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\bwhile\s+[^;]+>\s*[^\s;]+\s*;").unwrap()
});
static FOR_WITH_REDIRECT: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\bfor\s+[^;]+>\s*[^\s;]+\s*;").unwrap()
});
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if line.trim_start().starts_with('#') {
continue;
}
if let Some(cap) = IF_WITH_REDIRECT.captures(line) {
let start_col = cap.get(0).unwrap().start() + 1;
let end_col = cap.get(0).unwrap().end() + 1;
let diagnostic = Diagnostic::new(
"SC2095",
Severity::Info,
"Redirections only apply to the condition command, not the if block. Move redirection after 'fi' to redirect entire block".to_string(),
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
if let Some(cap) = WHILE_WITH_REDIRECT.captures(line) {
let start_col = cap.get(0).unwrap().start() + 1;
let end_col = cap.get(0).unwrap().end() + 1;
let diagnostic = Diagnostic::new(
"SC2095",
Severity::Info,
"Redirections only apply to the condition command, not the loop body. Wrap loop in { } and redirect after closing brace".to_string(),
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
if let Some(cap) = FOR_WITH_REDIRECT.captures(line) {
let start_col = cap.get(0).unwrap().start() + 1;
let end_col = cap.get(0).unwrap().end() + 1;
let diagnostic = Diagnostic::new(
"SC2095",
Severity::Info,
"Redirections only apply to the for statement itself, not the loop body. Wrap loop in { } and redirect after closing brace".to_string(),
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2095_if_with_redirect() {
let code = r#"if foo > file.txt; then echo "test"; fi"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2095");
assert_eq!(result.diagnostics[0].severity, Severity::Info);
assert!(result.diagnostics[0].message.contains("condition"));
}
#[test]
fn test_sc2095_while_with_redirect() {
let code = r#"while read line > output.txt; do echo "$line"; done"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].message.contains("condition"));
}
#[test]
fn test_sc2095_for_with_redirect() {
let code = r#"for i in 1 2 3 > nums.txt; do echo $i; done"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2095_if_redirect_after_fi_ok() {
let code = r#"if foo; then echo "test"; fi > file.txt"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_while_wrapped_ok() {
let code = r#"{ while read line; do echo "$line"; done; } > output.txt"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_for_wrapped_ok() {
let code = r#"{ for i in 1 2 3; do echo $i; done; } > nums.txt"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_simple_command_ok() {
let code = r#"echo "test" > file.txt"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_if_no_redirect_ok() {
let code = r#"if foo; then echo "test"; fi"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_while_no_redirect_ok() {
let code = r#"while read line; do echo "$line"; done"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2095_multiple_issues() {
let code = r#"
if test > a.txt; then echo "1"; fi
while read x > b.txt; do echo "2"; done
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 2);
}
}