use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
use regex::Regex;
static PATTERN: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r#"\becho\s+(["']?[\$\w][^\|]*?["']?)\s*\|\s*(\w+)"#).unwrap()
});
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let pattern = &*PATTERN;
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
continue;
}
for cap in pattern.captures_iter(line) {
let full_match = cap.get(0).unwrap();
let var_part = cap.get(1).unwrap().as_str().trim();
let command = cap.get(2).unwrap().as_str();
let start_col = full_match.start() + 1;
let end_col = full_match.end() + 1;
let fix_text = format!("{} <<< {}", command, var_part);
let diagnostic = Diagnostic::new(
"PERF003",
Severity::Info,
format!("Useless echo piped to {}. Consider `{}`", command, fix_text),
Span::new(line_num + 1, start_col, line_num + 1, end_col),
)
.with_fix(Fix::new(fix_text));
result.add(diagnostic);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perf003_detects_echo_pipe_grep() {
let script = r#"echo "$VAR" | grep pattern"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "PERF003");
assert_eq!(result.diagnostics[0].severity, Severity::Info);
}
#[test]
fn test_perf003_provides_fix() {
let script = r#"echo "$VAR" | grep pattern"#;
let result = check(script);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert!(fix.replacement.contains("<<<"));
}
#[test]
fn test_perf003_no_false_positive_comment() {
let script = r#"# echo "$VAR" | grep pattern"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_perf003_detects_unquoted_var() {
let script = "echo $VAR | wc -l";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_perf003_no_false_positive_pipe_to_file() {
let script = "echo hello > file.txt";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
}