use crate::linter::{Diagnostic, LintResult, Severity, Span};
use regex::Regex;
#[allow(clippy::expect_used)]
static HEREDOC_START: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r#"<<-?\s*\\?(?:'(\w+)'|"(\w+)"|(\w+))"#).expect("valid heredoc start 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();
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
continue;
}
if let Some(caps) = HEREDOC_START.captures(line) {
if let Some(delimiter) = extract_delimiter(&caps) {
let full_match = caps.get(0).unwrap();
let after_heredoc = &line[full_match.end()..];
if after_heredoc.contains(delimiter) {
let col = full_match.start() + 1;
let span = Span::new(line_num, col, line_num, col + full_match.len());
let diag = Diagnostic::new(
"SC1041",
Severity::Error,
format!(
"Found '{delimiter}' further on the same line as the << token; \
the heredoc body starts on the next line"
),
span,
);
result.add(diag);
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sc1041_detects_delimiter_on_same_line() {
let script = "cat <<EOF hello EOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(result.diagnostics[0].code, "SC1041");
assert_eq!(result.diagnostics[0].severity, Severity::Error);
}
#[test]
fn test_sc1041_no_false_positive_normal_heredoc() {
let script = "cat <<EOF\nhello\nEOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc1041_no_false_positive_comment() {
let script = "# cat <<EOF hello EOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_sc1041_detects_with_dash_heredoc() {
let script = "cat <<-MARKER content MARKER";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_sc1041_span_points_to_heredoc_token() {
let script = "cat <<EOF text EOF";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
assert_eq!(span.start_line, 1);
assert_eq!(span.start_col, 5); }
}