rumdl_lib/output/formatters/
concise.rs1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5
6pub struct ConciseFormatter;
8
9impl Default for ConciseFormatter {
10 fn default() -> Self {
11 Self
12 }
13}
14
15impl ConciseFormatter {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21impl OutputFormatter for ConciseFormatter {
22 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
23 let mut output = String::new();
24
25 for warning in warnings {
26 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
27
28 let line = format!(
30 "{}:{}:{}: [{}] {}",
31 file_path, warning.line, warning.column, rule_name, warning.message
32 );
33
34 output.push_str(&line);
35 output.push('\n');
36 }
37
38 if output.ends_with('\n') {
40 output.pop();
41 }
42
43 output
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::rule::{Fix, Severity};
51
52 #[test]
53 fn test_concise_formatter_default() {
54 let _formatter = ConciseFormatter;
55 }
57
58 #[test]
59 fn test_concise_formatter_new() {
60 let _formatter = ConciseFormatter::new();
61 }
63
64 #[test]
65 fn test_format_warnings_empty() {
66 let formatter = ConciseFormatter::new();
67 let warnings = vec![];
68 let output = formatter.format_warnings(&warnings, "test.md");
69 assert_eq!(output, "");
70 }
71
72 #[test]
73 fn test_format_single_warning() {
74 let formatter = ConciseFormatter::new();
75 let warnings = vec![LintWarning {
76 line: 10,
77 column: 5,
78 end_line: 10,
79 end_column: 15,
80 rule_name: Some("MD001".to_string()),
81 message: "Heading levels should only increment by one level at a time".to_string(),
82 severity: Severity::Warning,
83 fix: None,
84 }];
85
86 let output = formatter.format_warnings(&warnings, "README.md");
87 assert_eq!(
88 output,
89 "README.md:10:5: [MD001] Heading levels should only increment by one level at a time"
90 );
91 }
92
93 #[test]
94 fn test_format_warning_with_fix() {
95 let formatter = ConciseFormatter::new();
96 let warnings = vec![LintWarning {
97 line: 15,
98 column: 1,
99 end_line: 15,
100 end_column: 10,
101 rule_name: Some("MD022".to_string()),
102 message: "Headings should be surrounded by blank lines".to_string(),
103 severity: Severity::Warning,
104 fix: Some(Fix {
105 range: 100..110,
106 replacement: "\n# Heading\n".to_string(),
107 }),
108 }];
109
110 let output = formatter.format_warnings(&warnings, "doc.md");
111 assert_eq!(
113 output,
114 "doc.md:15:1: [MD022] Headings should be surrounded by blank lines"
115 );
116 }
117
118 #[test]
119 fn test_format_multiple_warnings() {
120 let formatter = ConciseFormatter::new();
121 let warnings = vec![
122 LintWarning {
123 line: 5,
124 column: 1,
125 end_line: 5,
126 end_column: 10,
127 rule_name: Some("MD001".to_string()),
128 message: "First warning".to_string(),
129 severity: Severity::Warning,
130 fix: None,
131 },
132 LintWarning {
133 line: 10,
134 column: 3,
135 end_line: 10,
136 end_column: 20,
137 rule_name: Some("MD013".to_string()),
138 message: "Second warning".to_string(),
139 severity: Severity::Error,
140 fix: Some(Fix {
141 range: 50..60,
142 replacement: "fixed".to_string(),
143 }),
144 },
145 ];
146
147 let output = formatter.format_warnings(&warnings, "test.md");
148 let expected = "test.md:5:1: [MD001] First warning\ntest.md:10:3: [MD013] Second warning";
149 assert_eq!(output, expected);
150 }
151
152 #[test]
153 fn test_format_warning_unknown_rule() {
154 let formatter = ConciseFormatter::new();
155 let warnings = vec![LintWarning {
156 line: 1,
157 column: 1,
158 end_line: 1,
159 end_column: 5,
160 rule_name: None,
161 message: "Unknown rule warning".to_string(),
162 severity: Severity::Warning,
163 fix: None,
164 }];
165
166 let output = formatter.format_warnings(&warnings, "file.md");
167 assert_eq!(output, "file.md:1:1: [unknown] Unknown rule warning");
168 }
169
170 #[test]
171 fn test_edge_cases() {
172 let formatter = ConciseFormatter::new();
173
174 let warnings = vec![LintWarning {
176 line: 99999,
177 column: 12345,
178 end_line: 100000,
179 end_column: 12350,
180 rule_name: Some("MD999".to_string()),
181 message: "Edge case warning".to_string(),
182 severity: Severity::Error,
183 fix: None,
184 }];
185
186 let output = formatter.format_warnings(&warnings, "large.md");
187 assert_eq!(output, "large.md:99999:12345: [MD999] Edge case warning");
188 }
189
190 #[test]
191 fn test_special_characters_in_message() {
192 let formatter = ConciseFormatter::new();
193 let warnings = vec![LintWarning {
194 line: 1,
195 column: 1,
196 end_line: 1,
197 end_column: 5,
198 rule_name: Some("MD001".to_string()),
199 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
200 severity: Severity::Warning,
201 fix: None,
202 }];
203
204 let output = formatter.format_warnings(&warnings, "test.md");
205 assert_eq!(
206 output,
207 "test.md:1:1: [MD001] Warning with \"quotes\" and 'apostrophes' and \n newline"
208 );
209 }
210
211 #[test]
212 fn test_special_characters_in_file_path() {
213 let formatter = ConciseFormatter::new();
214 let warnings = vec![LintWarning {
215 line: 1,
216 column: 1,
217 end_line: 1,
218 end_column: 5,
219 rule_name: Some("MD001".to_string()),
220 message: "Test".to_string(),
221 severity: Severity::Warning,
222 fix: None,
223 }];
224
225 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
226 assert_eq!(output, "path/with spaces/and-dashes.md:1:1: [MD001] Test");
227 }
228
229 #[test]
230 fn test_concise_format_consistency() {
231 let formatter = ConciseFormatter::new();
232
233 let warnings = vec![
235 LintWarning {
236 line: 1,
237 column: 1,
238 end_line: 1,
239 end_column: 5,
240 rule_name: Some("MD001".to_string()),
241 message: "Test 1".to_string(),
242 severity: Severity::Warning,
243 fix: None,
244 },
245 LintWarning {
246 line: 2,
247 column: 2,
248 end_line: 2,
249 end_column: 6,
250 rule_name: Some("MD002".to_string()),
251 message: "Test 2".to_string(),
252 severity: Severity::Error,
253 fix: Some(Fix {
254 range: 10..20,
255 replacement: "fix".to_string(),
256 }),
257 },
258 ];
259
260 let output = formatter.format_warnings(&warnings, "test.md");
261 let lines: Vec<&str> = output.lines().collect();
262
263 assert_eq!(lines.len(), 2);
264
265 for line in lines {
267 assert!(line.contains(":"));
268 assert!(line.contains(" [MD"));
269 assert!(line.contains("] "));
270 }
271 }
272
273 #[test]
274 fn test_severity_ignored() {
275 let formatter = ConciseFormatter::new();
276
277 let warnings = vec![
279 LintWarning {
280 line: 1,
281 column: 1,
282 end_line: 1,
283 end_column: 5,
284 rule_name: Some("MD001".to_string()),
285 message: "Warning severity".to_string(),
286 severity: Severity::Warning,
287 fix: None,
288 },
289 LintWarning {
290 line: 2,
291 column: 1,
292 end_line: 2,
293 end_column: 5,
294 rule_name: Some("MD002".to_string()),
295 message: "Error severity".to_string(),
296 severity: Severity::Error,
297 fix: None,
298 },
299 ];
300
301 let output = formatter.format_warnings(&warnings, "test.md");
302 let lines: Vec<&str> = output.lines().collect();
303
304 assert!(lines[0].starts_with("test.md:1:1: [MD001]"));
306 assert!(lines[1].starts_with("test.md:2:1: [MD002]"));
307 }
308}