use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
static ARITH_EXPR: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\$\(\(([^)]+)\)\)").unwrap()
});
static BRACED_VAR: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap()
});
fn should_skip_braced_var(matched: &str) -> bool {
matched.contains('[') || matched.contains('#')
}
fn create_braces_diagnostic(
var_name: &str,
abs_start: usize,
abs_end: usize,
line_num: usize,
) -> Diagnostic {
Diagnostic::new(
"SC2137",
Severity::Info,
format!(
"Braces are unnecessary in arithmetic. Use ${} instead of ${{{}}}",
var_name, var_name
),
Span::new(line_num, abs_start + 1, line_num, abs_end + 1),
)
}
fn process_braced_var(
var_cap: regex::Captures<'_>,
arith_start: usize,
line_num: usize,
result: &mut LintResult,
) {
let full_match = match var_cap.get(0) {
Some(m) => m,
None => return,
};
let matched = full_match.as_str();
if should_skip_braced_var(matched) {
return;
}
let var_name = match var_cap.get(1) {
Some(m) => m.as_str(),
None => return,
};
let var_pos = full_match.start();
let abs_start = arith_start + var_pos;
let abs_end = abs_start + matched.len();
let diagnostic = create_braces_diagnostic(var_name, abs_start, abs_end, line_num);
result.add(diagnostic);
}
fn check_arithmetic_expression(
arith_content: &str,
arith_start: usize,
line_num: usize,
result: &mut LintResult,
) {
for var_cap in BRACED_VAR.captures_iter(arith_content) {
process_braced_var(var_cap, arith_start, line_num, result);
}
}
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 arith_mat in ARITH_EXPR.find_iter(line) {
let arith_content = &line[arith_mat.start()..arith_mat.end()];
check_arithmetic_expression(arith_content, arith_mat.start(), line_num, &mut result);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2137_braced_variable() {
let code = "echo $(( ${var} + 1 ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].message.contains("$var"));
}
#[test]
fn test_sc2137_simple_variable_ok() {
let code = "echo $(( $var + 1 ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2137_no_dollar_ok() {
let code = "echo $(( var + 1 ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2137_array_index_ok() {
let code = "echo $(( ${arr[i]} + 1 ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2137_length_ok() {
let code = "len=$(( ${#str} ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2137_multiple_braced() {
let code = "result=$(( ${x} * ${y} ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 2);
}
#[test]
fn test_sc2137_comment_ok() {
let code = "# echo $(( ${var} + 1 ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2137_complex_expression() {
let code = "val=$(( ${a} + ${b} * ${c} ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 3);
}
#[test]
fn test_sc2137_mixed_ok_and_bad() {
let code = "result=$(( $x + ${y} ))";
let result = check(code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc2137_multiline() {
let code = r#"
x=$(( ${foo} ))
y=$(( ${bar} + 1 ))
"#;
let result = check(code);
assert_eq!(result.diagnostics.len(), 2);
}
}