use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static TEST_WITH_OR: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\[\s+[^\]]*\s+-o\s+[^\]]*\]|test\s+[^\n]*\s+-o\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 line.contains("[[") {
continue;
}
if line.contains("set -o") || line.contains("shopt -o") {
continue;
}
for mat in TEST_WITH_OR.find_iter(line) {
let start_col = mat.start() + 1;
let end_col = mat.end() + 1;
let diagnostic = Diagnostic::new(
"SC2056",
Severity::Warning,
"You probably wanted || here, not -o (which is obsolete). Use [ cond ] || [ cond ]"
.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_sc2056_or_operator_in_test() {
let code = r#"[ $a -eq 1 -o $b -eq 2 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2056");
}
#[test]
fn test_sc2056_file_tests_with_or() {
let code = r#"[ ! -f file -o ! -r file ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2056_test_command() {
let code = r#"test $x -lt 0 -o $x -gt 10"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2056_chained_tests_ok() {
let code = r#"[ $a -eq 1 ] || [ $b -eq 2 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2056_double_bracket_ok() {
let code = r#"[[ $a -eq 1 || $b -eq 2 ]]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2056_set_option_ok() {
let code = r#"set -o errexit"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2056_shopt_ok() {
let code = r#"shopt -o noclobber"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2056_comment_ok() {
let code = r#"# [ $a -eq 1 -o $b -eq 2 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2056_multiple_conditions() {
let code = r#"[ $a -eq 1 -o $b -eq 2 -o $c -eq 3 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2056_if_statement() {
let code = r#"if [ $status -ne 0 -o $ready = "no" ]; then"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
}