mdwright_lint/stdlib/
list_tightness_flipped.rs1use crate::diagnostic::Diagnostic;
17use crate::rule::LintRule;
18use mdwright_document::Document;
19use mdwright_document::ListGroup;
20
21pub struct ListTightnessFlipped;
22
23impl LintRule for ListTightnessFlipped {
24 fn name(&self) -> &str {
25 "list-tightness-flipped"
26 }
27
28 fn description(&self) -> &str {
29 "list tightness from the tree disagrees with tightness from source bytes"
30 }
31
32 fn explain(&self) -> &str {
33 include_str!("explain/list_tightness_flipped.md")
34 }
35
36 fn is_default(&self) -> bool {
37 false
38 }
39
40 fn is_advisory(&self) -> bool {
41 true
42 }
43
44 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
45 let source = doc.source();
46 for (group, tree_tight) in doc.list_tightness_view() {
47 let source_tight = source_view_tight(group, source);
48 if tree_tight == source_tight {
49 continue;
50 }
51 let message = if tree_tight {
52 "list reads as tight from parsed items but source has a blank line between items".to_owned()
53 } else {
54 "list reads as loose from parsed items but source has no blank line between items".to_owned()
55 };
56 let start = group.raw_range.start;
57 let local = 0..1;
58 if let Some(d) = Diagnostic::at(doc, start, local, message, None) {
59 out.push(d);
60 }
61 }
62 }
63}
64
65fn source_view_tight(group: &ListGroup, source: &str) -> bool {
69 let bytes = source.as_bytes();
70 for pair in group.items.windows(2) {
71 let [a, b] = pair else { continue };
72 let gap_start = a.raw_range.end.min(bytes.len());
73 let gap_end = b.raw_range.start.min(bytes.len());
74 let Some(gap) = bytes.get(gap_start..gap_end) else {
75 continue;
76 };
77 if has_blank_line(gap) {
78 return false;
79 }
80 }
81 true
82}
83
84fn has_blank_line(bytes: &[u8]) -> bool {
87 let mut i = 0;
88 while i < bytes.len() {
89 let line_start = i;
90 while bytes.get(i).is_some_and(|b| *b != b'\n') {
91 i = i.saturating_add(1);
92 }
93 let line = bytes.get(line_start..i).unwrap_or(&[]);
94 if line_start > 0 && line.iter().all(|b| matches!(*b, b' ' | b'\t' | b'\r')) {
98 return true;
99 }
100 i = i.saturating_add(1);
101 }
102 false
103}