rumdl_lib/rules/
md047_single_trailing_newline.rs1use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
2
3#[derive(Debug, Default, Clone)]
8pub struct MD047SingleTrailingNewline;
9
10impl Rule for MD047SingleTrailingNewline {
11 fn name(&self) -> &'static str {
12 "MD047"
13 }
14
15 fn description(&self) -> &'static str {
16 "Files should end with a single newline character"
17 }
18
19 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
20 ctx.content.is_empty()
22 }
23
24 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
25 let content = ctx.content;
26 let mut warnings = Vec::new();
27
28 if content.is_empty() {
30 return Ok(warnings);
31 }
32
33 let has_trailing_newline = content.ends_with('\n');
36
37 if !has_trailing_newline {
39 let lines = &ctx.lines;
40 let last_line_num = lines.len();
41 let last_line_content = lines.last().map(|s| s.content(content)).unwrap_or("");
42
43 let (start_line, start_col, end_line, end_col) = (
46 last_line_num,
47 last_line_content.len() + 1,
48 last_line_num,
49 last_line_content.len() + 1,
50 );
51
52 warnings.push(LintWarning {
53 rule_name: Some(self.name().to_string()),
54 message: String::from("File should end with a single newline character"),
55 line: start_line,
56 column: start_col,
57 end_line,
58 end_column: end_col,
59 severity: Severity::Warning,
60 fix: Some(Fix {
61 range: content.len()..content.len(),
63 replacement: "\n".to_string(),
65 }),
66 });
67 }
68
69 Ok(warnings)
70 }
71
72 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
73 let content = ctx.content;
74
75 if content.is_empty() {
77 return Ok(String::new());
78 }
79
80 let has_trailing_newline = content.ends_with('\n');
83
84 if has_trailing_newline {
85 return Ok(content.to_string());
86 }
87
88 let last_line_num = ctx.lines.len();
90 if ctx.inline_config().is_rule_disabled(self.name(), last_line_num) {
91 return Ok(content.to_string());
92 }
93
94 let mut result = String::with_capacity(content.len() + 1);
96 result.push_str(content);
97 result.push('\n');
98 Ok(result)
99 }
100
101 fn as_any(&self) -> &dyn std::any::Any {
102 self
103 }
104
105 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
106 where
107 Self: Sized,
108 {
109 Box::new(MD047SingleTrailingNewline)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::lint_context::LintContext;
117
118 #[test]
119 fn test_valid_trailing_newline() {
120 let rule = MD047SingleTrailingNewline;
121 let content = "Line 1\nLine 2\n";
122 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
123 let result = rule.check(&ctx).unwrap();
124 assert!(result.is_empty());
125 }
126
127 #[test]
128 fn test_missing_trailing_newline() {
129 let rule = MD047SingleTrailingNewline;
130 let content = "Line 1\nLine 2";
131 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
132 let result = rule.check(&ctx).unwrap();
133 assert_eq!(result.len(), 1);
134 let fixed = rule.fix(&ctx).unwrap();
135 assert_eq!(fixed, "Line 1\nLine 2\n");
136 }
137
138 #[test]
139 fn test_multiple_trailing_newlines() {
140 let rule = MD047SingleTrailingNewline;
142 let content = "Line 1\nLine 2\n\n\n";
143 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
144 let result = rule.check(&ctx).unwrap();
145 assert!(result.is_empty());
146 }
147
148 #[test]
149 fn test_normalized_lf_content() {
150 let rule = MD047SingleTrailingNewline;
153 let content = "Line 1\nLine 2";
154 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
155 let result = rule.check(&ctx).unwrap();
156 assert_eq!(result.len(), 1);
157
158 let fixed = rule.fix(&ctx).unwrap();
159 assert_eq!(fixed, "Line 1\nLine 2\n");
161 assert!(fixed.ends_with('\n'), "Should end with LF");
162 }
163
164 #[test]
165 fn test_blank_file() {
166 let rule = MD047SingleTrailingNewline;
167 let content = "";
168 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
169 let result = rule.check(&ctx).unwrap();
170 assert!(result.is_empty());
171 }
172
173 #[test]
174 fn test_file_with_only_newlines() {
175 let rule = MD047SingleTrailingNewline;
177 let content = "\n\n\n";
178 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
179 let result = rule.check(&ctx).unwrap();
180 assert!(result.is_empty());
181 }
182}