use crate::cop::{Cop, CopConfig};
use crate::correction::Correction;
use crate::diagnostic::Diagnostic;
use crate::parse::source::SourceFile;
pub struct TrailingWhitespace;
fn trailing_whitespace_start(line: &[u8]) -> Option<usize> {
let mut end = line.len();
let mut found = false;
while end > 0 {
if matches!(line[end - 1], b' ' | b'\t') {
end -= 1; found = true; continue;
}
if end >= 3 && line[end - 3..end] == [0xE3, 0x80, 0x80] {
end -= 3; found = true; continue;
}
if end >= 2 && line[end - 2..end] == [0xC2, 0xA0] {
end -= 2; found = true; continue;
}
break;
}
found.then_some(end)
}
fn strip_cr(line: &[u8]) -> &[u8] {
line.strip_suffix(b"\r").unwrap_or(line)
}
fn before_shift(s: &str, idx: usize) -> bool {
if idx == 0 { return false; }
let before = &s.as_bytes()[..idx];
let Some(&b) = before.iter().rev().find(|&&b| b != b' ' && b != b'\t') else { return false; };
matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b')' | b']' | b'}' | b'@' | b'$')
}
fn strip_heredoc_prefix(rest: &str) -> &str {
rest.strip_prefix('~').or_else(|| rest.strip_prefix('-')).unwrap_or(rest).trim_start()
}
fn take_ident(rest: &str) -> String {
rest.chars()
.skip_while(|c| *c == '\'' || *c == '"' || *c == '`')
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect()
}
fn parse_heredoc_ident(rest: &str) -> Option<Vec<u8>> {
let rest = strip_heredoc_prefix(rest);
if rest.is_empty() || rest.as_bytes()[0].is_ascii_digit() {
return None;
}
let ident = take_ident(rest);
if ident.is_empty() {
None
} else {
Some(ident.into_bytes())
}
}
fn heredoc_opener(line: &[u8]) -> Option<Vec<u8>> {
let s = std::str::from_utf8(line).ok()?;
let idx = s.find("<<")?;
if before_shift(s, idx) { return None; }
parse_heredoc_ident(&s[idx + 2..])
}
fn utf8_col(prefix: &[u8]) -> usize {
prefix.iter().filter(|&&b| (b & 0xC0) != 0x80).count()
}
fn report_trailing(
cop: &dyn Cop, source: &SourceFile, line_no: usize, start: usize, stripped: &[u8],
byte_offset: usize,
diagnostics: &mut Vec<Diagnostic>, corrections: &mut Option<&mut Vec<Correction>>,
) {
let mut diag = cop.diagnostic(
source, line_no, utf8_col(&stripped[..start]),
"Trailing whitespace detected.".into(),
);
if let Some(corr) = corrections {
corr.push(Correction {
start: byte_offset + start, end: byte_offset + stripped.len(),
replacement: String::new(), cop_name: cop.name(), cop_index: 0,
});
diag.corrected = true;
}
diagnostics.push(diag);
}
fn in_heredoc_skip(
stripped: &[u8], allow: bool, terms: &mut Vec<Vec<u8>>,
) -> bool {
let Some(term) = terms.last() else { return false; };
if stripped
.iter()
.copied()
.skip_while(|&b| b == b' ' || b == b'\t')
.collect::<Vec<u8>>()
== *term
{
terms.pop();
false
} else {
allow
}
}
fn process_line(
cop: &dyn Cop, source: &SourceFile, line_no: usize, line: &[u8], allow: bool,
terms: &mut Vec<Vec<u8>>, saw_nonblank: &mut bool, byte_offset: usize,
diagnostics: &mut Vec<Diagnostic>, corrections: &mut Option<&mut Vec<Correction>>,
) -> bool {
let stripped = strip_cr(line);
if in_heredoc_skip(stripped, allow, terms) { return true; }
if stripped == b"__END__" && terms.is_empty() && *saw_nonblank { return false; }
if !stripped.iter().all(|&b| b == b' ' || b == b'\t') { *saw_nonblank = true; }
if let Some(start) = trailing_whitespace_start(stripped) {
report_trailing(cop, source, line_no, start, stripped, byte_offset, diagnostics, corrections);
}
if let Some(term) = heredoc_opener(stripped) { terms.push(term); }
true
}
impl Cop for TrailingWhitespace {
fn name(&self) -> &'static str { "Layout/TrailingWhitespace" }
fn supports_autocorrect(&self) -> bool { true }
fn uses_line_phase(&self) -> bool {
true
}
fn check_lines(
&self, source: &SourceFile, config: &CopConfig,
diagnostics: &mut Vec<Diagnostic>, mut corrections: Option<&mut Vec<Correction>>,
) {
let allow = config.get_bool("AllowInHeredoc", false);
let mut terms = Vec::new();
let mut saw_nonblank = false;
let mut byte_offset = 0usize;
for (i, line) in source.lines().enumerate() {
if !process_line(
self, source, i + 1, line, allow, &mut terms, &mut saw_nonblank, byte_offset,
diagnostics, &mut corrections,
) { break; }
byte_offset += line.len() + 1;
}
}
}