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 mut result = String::with_capacity(content.len() + 1);
90 result.push_str(content);
91 result.push('\n');
92 Ok(result)
93 }
94
95 fn as_any(&self) -> &dyn std::any::Any {
96 self
97 }
98
99 fn from_config(_config: &crate::config::Config) -> Box<dyn Rule>
100 where
101 Self: Sized,
102 {
103 Box::new(MD047SingleTrailingNewline)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::lint_context::LintContext;
111
112 #[test]
113 fn test_valid_trailing_newline() {
114 let rule = MD047SingleTrailingNewline;
115 let content = "Line 1\nLine 2\n";
116 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
117 let result = rule.check(&ctx).unwrap();
118 assert!(result.is_empty());
119 }
120
121 #[test]
122 fn test_missing_trailing_newline() {
123 let rule = MD047SingleTrailingNewline;
124 let content = "Line 1\nLine 2";
125 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
126 let result = rule.check(&ctx).unwrap();
127 assert_eq!(result.len(), 1);
128 let fixed = rule.fix(&ctx).unwrap();
129 assert_eq!(fixed, "Line 1\nLine 2\n");
130 }
131
132 #[test]
133 fn test_multiple_trailing_newlines() {
134 let rule = MD047SingleTrailingNewline;
136 let content = "Line 1\nLine 2\n\n\n";
137 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
138 let result = rule.check(&ctx).unwrap();
139 assert!(result.is_empty());
140 }
141
142 #[test]
143 fn test_normalized_lf_content() {
144 let rule = MD047SingleTrailingNewline;
147 let content = "Line 1\nLine 2";
148 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
149 let result = rule.check(&ctx).unwrap();
150 assert_eq!(result.len(), 1);
151
152 let fixed = rule.fix(&ctx).unwrap();
153 assert_eq!(fixed, "Line 1\nLine 2\n");
155 assert!(fixed.ends_with('\n'), "Should end with LF");
156 }
157
158 #[test]
159 fn test_blank_file() {
160 let rule = MD047SingleTrailingNewline;
161 let content = "";
162 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
163 let result = rule.check(&ctx).unwrap();
164 assert!(result.is_empty());
165 }
166
167 #[test]
168 fn test_file_with_only_newlines() {
169 let rule = MD047SingleTrailingNewline;
171 let content = "\n\n\n";
172 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
173 let result = rule.check(&ctx).unwrap();
174 assert!(result.is_empty());
175 }
176}