use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
#[allow(clippy::expect_used)]
static HEREDOC_STRIP: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r#"<<-\s*\\?(?:'(\w+)'|"(\w+)"|(\w+))"#).expect("valid heredoc strip regex")
});
fn extract_delimiter<'a>(caps: &'a regex::Captures<'a>) -> Option<&'a str> {
caps.get(1)
.or_else(|| caps.get(2))
.or_else(|| caps.get(3))
.map(|m| m.as_str())
}
pub fn check(source: &str) -> LintResult {
let mut result = LintResult::new();
let lines: Vec<&str> = source.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
i += 1;
continue;
}
if let Some(caps) = HEREDOC_STRIP.captures(line) {
if let Some(delimiter) = extract_delimiter(&caps) {
let mut j = i + 1;
while j < lines.len() {
let body_line = lines[j];
if body_line.trim() == delimiter {
break;
}
if !body_line.is_empty() && body_line.starts_with(' ') {
let line_num = j + 1;
let span = Span::new(line_num, 1, line_num, 1);
let diag = Diagnostic::new(
"SC1040",
Severity::Warning,
"When using <<-, you can only indent with tabs",
span,
);
result.add(diag);
}
j += 1;
}
i = j + 1;
} else {
i += 1;
}
} else {
i += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc1040_detects_space_indentation() {
let script = "cat <<-EOF\n hello world\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC1040");
assert_eq!(result.diagnostics[0].severity, Severity::Warning);
}
#[test]
fn test_sc1040_no_flag_for_tab_indentation() {
let script = "cat <<-EOF\n\thello world\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc1040_no_flag_for_regular_heredoc() {
let script = "cat <<EOF\n hello world\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc1040_multiple_space_lines() {
let script = "cat <<-END\n line1\n line2\n line3\nEND";
let result = check(script);
assert_eq!(result.diagnostics.len(), 3);
}
#[test]
fn test_sc1040_mixed_tabs_and_spaces() {
let script = "cat <<-EOF\n\tgood line\n bad line\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].span.start_line, 3);
}
#[test]
fn test_sc1040_no_false_positive_comment() {
let script = "# cat <<-EOF\n not a heredoc\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
}