use crate::linter::LintResult;
use crate::linter::{Diagnostic, Severity, Span};
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with('#') {
continue;
}
let code_only = if let Some(pos) = trimmed.find('#') {
&trimmed[..pos]
} else {
trimmed
};
let code_only = code_only.trim();
if code_only.contains("for ") && code_only.contains("$(seq") {
let span = Span::new(line_num + 1, 1, line_num + 1, line.len());
let diag = Diagnostic::new(
"BASH009",
Severity::Info,
"Inefficient loop using $(seq ...) - use bash brace expansion {start..end} or C-style for loop for better performance and portability",
span,
);
result.add(diag);
}
if code_only.contains("cat ") && code_only.contains("| while read") {
let span = Span::new(line_num + 1, 1, line_num + 1, line.len());
let diag = Diagnostic::new(
"BASH009",
Severity::Info,
"Inefficient pattern 'cat file | while read' - use 'while read; do ... done < file' to avoid spawning cat process",
span,
);
result.add(diag);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_BASH009_detects_seq_in_loop() {
let script = r#"#!/bin/bash
for i in $(seq 1 10); do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "BASH009");
assert_eq!(diag.severity, Severity::Info);
assert!(diag.message.contains("seq"));
assert!(diag.message.contains("brace expansion"));
}
#[test]
fn test_BASH009_passes_brace_expansion() {
let script = r#"#!/bin/bash
for i in {1..10}; do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Brace expansion should pass");
}
#[test]
fn test_BASH009_passes_c_style_loop() {
let script = r#"#!/bin/bash
for ((i=1; i<=10; i++)); do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "C-style loop should pass");
}
#[test]
fn test_BASH009_detects_cat_while_read() {
let script = r#"#!/bin/bash
cat file.txt | while read line; do
echo "$line"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "BASH009");
assert!(diag.message.contains("cat"));
assert!(diag.message.contains("while read"));
}
#[test]
fn test_BASH009_passes_while_read_redirect() {
let script = r#"#!/bin/bash
while read line; do
echo "$line"
done < file.txt
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Input redirection should pass");
}
#[test]
fn test_BASH009_detects_seq_with_step() {
let script = r#"#!/bin/bash
for i in $(seq 1 2 10); do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "BASH009");
}
#[test]
fn test_BASH009_ignores_comments() {
let script = r#"#!/bin/bash
# for i in $(seq 1 10); do
for i in {1..10}; do
echo "$i"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0, "Comments should be ignored");
}
#[test]
fn test_BASH009_detects_multiple_violations() {
let script = r#"#!/bin/bash
for i in $(seq 1 10); do
echo "$i"
done
cat data.txt | while read line; do
echo "$line"
done
"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 2);
assert_eq!(result.diagnostics[0].code, "BASH009");
assert_eq!(result.diagnostics[1].code, "BASH009");
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(10))]
#[test]
fn prop_bash009_never_panics(s in ".*") {
let _ = check(&s);
}
#[test]
fn prop_bash009_detects_seq(
start in 1u8..50,
end in 51u8..100,
) {
let script = format!("for i in $(seq {} {}); do echo $i; done", start, end);
let result = check(&script);
prop_assert_eq!(result.diagnostics.len(), 1);
prop_assert_eq!(result.diagnostics[0].code.as_str(), "BASH009");
}
#[test]
fn prop_bash009_passes_braces(
start in 1u8..50,
end in 51u8..100,
) {
let script = format!("for i in {{{}..{}}}; do echo $i; done", start, end);
let result = check(&script);
prop_assert_eq!(result.diagnostics.len(), 0);
}
}
}