use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
use regex::Regex;
static PATTERN: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"\$\(\s*echo\s+(?P<flags>-[a-z]+\s+)?(?P<content>[^)]+)\)").unwrap()
});
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let pattern = &*PATTERN;
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if line.trim_start().starts_with('#') {
continue;
}
for cap in pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
if cap.name("flags").is_some() {
continue;
}
let content = cap.name("content").unwrap().as_str().trim();
if content.contains('|') {
continue;
}
let col = full_match.start() + 1; let end_col = full_match.end() + 1;
let span = Span::new(line_num, col, line_num, end_col);
let fix = Fix::new(content.to_string());
let diag = Diagnostic::new(
"SC2116",
Severity::Info,
format!("Useless echo; just use {} directly", content),
span,
)
.with_fix(fix);
result.add(diag);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc2116_basic_detection() {
let bash_code = "result=$(echo $var)";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC2116");
assert!(result.diagnostics[0].message.contains("Useless echo"));
}
#[test]
fn test_sc2116_autofix() {
let bash_code = "result=$(echo $var)";
let result = check(bash_code);
assert!(result.diagnostics[0].fix.is_some());
assert_eq!(
result.diagnostics[0].fix.as_ref().unwrap().replacement,
"$var"
);
}
#[test]
fn test_sc2116_false_positive_with_flags() {
let bash_code = "result=$(echo -n $var)";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2116_with_literal() {
let bash_code = "result=$(echo hello)";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(
result.diagnostics[0].fix.as_ref().unwrap().replacement,
"hello"
);
}
#[test]
fn test_sc2116_severity() {
let bash_code = "result=$(echo $var)";
let result = check(bash_code);
assert_eq!(result.diagnostics[0].severity, Severity::Info);
}
#[test]
fn test_sc2116_skip_comments() {
let bash_code = "# result=$(echo $var)";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc2116_skip_pipelines() {
let bash_code = r#"val=$(echo "$x" | cut -d. -f1)"#;
let result = check(bash_code);
assert_eq!(
result.diagnostics.len(),
0,
"Should not trigger on pipelines"
);
}
}