use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static LEADING_ZERO_NUMBER: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"(\$\(\(|\(\(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=\s+|[\[\(]\s*[^)]*?(-eq|-ne|-lt|-le|-gt|-ge)\s+)0[0-9]+").unwrap()
});
#[allow(clippy::expect_used)] static NUM_WITH_LEADING_ZERO: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"0[0-9]+").expect("valid regex pattern"));
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;
}
for mat in LEADING_ZERO_NUMBER.find_iter(line) {
let matched = mat.as_str();
if let Some(num_match) = NUM_WITH_LEADING_ZERO.find(matched) {
let num_str = num_match.as_str();
let has_invalid_octal = num_str.contains('8') || num_str.contains('9');
let severity = if has_invalid_octal {
Severity::Error } else {
Severity::Warning };
let message = if has_invalid_octal {
format!(
"'{}' is not a valid octal number (contains 8 or 9)",
num_str
)
} else {
format!(
"'{}' is interpreted as octal ({}₁₀). Remove leading 0 for decimal",
num_str,
i32::from_str_radix(&num_str[1..], 8).unwrap_or(0)
)
};
let start_col = mat.start() + num_match.start() + 1;
let end_col = mat.start() + num_match.end() + 1;
let diagnostic = Diagnostic::new(
"SC2080",
severity,
message,
Span::new(line_num, start_col, line_num, end_col),
);
result.add(diagnostic);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2080_invalid_octal() {
let code = r#"[ $x -eq 08 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].severity, Severity::Error);
}
#[test]
fn test_sc2080_octal_nine() {
let code = r#"result=$((09 + 1))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].severity, Severity::Error);
}
#[test]
fn test_sc2080_valid_octal_warning() {
let code = r#"result=$((010 + 5))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].severity, Severity::Warning);
}
#[test]
fn test_sc2080_decimal_ok() {
let code = r#"result=$((10 + 5))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2080_hex_ok() {
let code = r#"result=$((0x10 + 5))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2080_zero_ok() {
let code = r#"result=$((0 + 5))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2080_comment_ok() {
let code = r#"# result=$((08 + 1))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2080_string_ok() {
let code = r#"version="01.08.2024""#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2080_multiple() {
let code = r#"[ $a -eq 08 ] && [ $b -eq 09 ]"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_sc2080_double_paren() {
let code = r#"(( x = 077 ))"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
}