mdlint/lint/rules/
md009.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::{Fix, Violation};
4use serde_json::Value;
5
6pub struct MD009;
7
8impl Rule for MD009 {
9 fn name(&self) -> &'static str {
10 "MD009"
11 }
12
13 fn description(&self) -> &'static str {
14 "Trailing spaces"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["whitespace"]
19 }
20
21 #[allow(clippy::cast_possible_truncation)] fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
23 let br_spaces = config
24 .and_then(|c| c.get("br_spaces"))
25 .and_then(serde_json::Value::as_u64)
26 .unwrap_or(2) as usize;
27
28 let strict = config
29 .and_then(|c| c.get("strict"))
30 .and_then(serde_json::Value::as_bool)
31 .unwrap_or(false);
32
33 let mut violations = Vec::new();
34
35 for (line_num, line) in parser.lines().iter().enumerate() {
36 let trimmed = line.trim_end();
37 let trailing_spaces = line.len() - trimmed.len();
38
39 if trailing_spaces > 0 {
40 if !strict && trailing_spaces == br_spaces {
42 continue;
43 }
44
45 violations.push(Violation {
46 line: line_num + 1,
47 column: Some(trimmed.len() + 1),
48 rule: self.name().to_owned(),
49 message: format!("Trailing spaces ({trailing_spaces} spaces)"),
50 fix: Some(Fix {
51 line_start: line_num + 1,
52 line_end: line_num + 1,
53 column_start: Some(trimmed.len() + 1),
54 column_end: Some(line.len()),
55 replacement: String::new(),
56 description: "Remove trailing spaces".to_owned(),
57 }),
58 });
59 }
60 }
61
62 violations
63 }
64
65 fn fixable(&self) -> bool {
66 true
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use crate::fix::Fixer;
74
75 fn apply_fixes(content: &str, violations: &[Violation]) -> String {
76 let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
77 Fixer::new()
78 .apply_fixes_to_content(content, &fixes)
79 .unwrap()
80 }
81
82 #[test]
83 fn test_no_trailing_spaces() {
84 let content = "Line 1\nLine 2\nLine 3";
85 let parser = MarkdownParser::new(content);
86 let rule = MD009;
87 let violations = rule.check(&parser, None);
88
89 assert_eq!(violations.len(), 0);
90 }
91
92 #[test]
93 fn test_trailing_spaces() {
94 let content = "Line 1 \nLine 2\nLine 3 ";
95 let parser = MarkdownParser::new(content);
96 let rule = MD009;
97 let violations = rule.check(&parser, None);
98
99 assert_eq!(violations.len(), 1); assert_eq!(violations[0].line, 3);
101 assert_eq!(violations[0].column, Some(7));
102 }
103
104 #[test]
105 fn test_strict_mode() {
106 let content = "Line 1 \nLine 2";
107 let parser = MarkdownParser::new(content);
108 let rule = MD009;
109 let config = serde_json::json!({ "strict": true });
110 let violations = rule.check(&parser, Some(&config));
111
112 assert_eq!(violations.len(), 1);
113 assert_eq!(violations[0].line, 1);
114 }
115
116 #[test]
117 fn test_custom_br_spaces() {
118 let content = "Line 1 \nLine 2";
119 let parser = MarkdownParser::new(content);
120 let rule = MD009;
121 let config = serde_json::json!({ "br_spaces": 3 });
122 let violations = rule.check(&parser, Some(&config));
123
124 assert_eq!(violations.len(), 0); }
126
127 #[test]
128 fn test_fix_removes_trailing_spaces() {
129 let content = "Line 1 \nLine 2\n";
130 let parser = MarkdownParser::new(content);
131 let rule = MD009;
132 let violations = rule.check(&parser, None);
133 let fixed = apply_fixes(content, &violations);
134 assert_eq!(fixed, "Line 1\nLine 2\n");
135 }
136}