mdwright_lint/stdlib/
trailing_whitespace.rs1use crate::diagnostic::{Diagnostic, Fix};
10use crate::rule::LintRule;
11use mdwright_document::Document;
12
13pub struct TrailingWhitespace;
14
15impl LintRule for TrailingWhitespace {
16 fn name(&self) -> &str {
17 "trailing-whitespace"
18 }
19
20 fn description(&self) -> &str {
21 "Trailing whitespace at end of line."
22 }
23
24 fn explain(&self) -> &str {
25 include_str!("explain/trailing_whitespace.md")
26 }
27
28 fn produces_fix(&self) -> bool {
29 true
30 }
31
32 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
33 let source = doc.source();
34 let code_blocks = doc.code_blocks();
35 let mut line_start: usize = 0;
36 for line in source.split_inclusive('\n') {
37 let line_end_incl = line_start.saturating_add(line.len());
38 let (content, newline_len) = line.strip_suffix('\n').map_or((line, 0), |c| (c, 1));
42 let content_end = line_start.saturating_add(content.len());
43 let trail = content.bytes().rev().take_while(|b| matches!(b, b' ' | b'\t')).count();
44 if trail == 0 {
45 line_start = line_end_incl;
46 continue;
47 }
48 if trail == 2 && content.bytes().rev().take(2).all(|b| b == b' ') && content.len() > 2 {
50 line_start = line_end_incl;
51 continue;
52 }
53 let span_start = content_end.saturating_sub(trail);
54 let span_end = content_end;
55 let inside_code = code_blocks
56 .iter()
57 .any(|c| c.raw_range.start <= span_start && span_start < c.raw_range.end);
58 if inside_code {
59 line_start = line_end_incl;
60 continue;
61 }
62 let local = 0..(span_end.saturating_sub(span_start));
63 if let Some(d) = Diagnostic::at(
64 doc,
65 span_start,
66 local,
67 "trailing whitespace".to_owned(),
68 Some(Fix {
69 replacement: String::new(),
70 safe: true,
71 }),
72 ) {
73 out.push(d);
74 }
75 let _ = newline_len;
77 line_start = line_end_incl;
78 }
79 }
80}