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::new(100..110, "\n# Heading\n".to_string())),
105 }];
106
107 let output = formatter.format_warnings(&warnings, "doc.md");
108 assert_eq!(
110 output,
111 "doc.md:15:1: [MD022] Headings should be surrounded by blank lines"
112 );
113 }
114
115 #[test]
116 fn test_format_multiple_warnings() {
117 let formatter = ConciseFormatter::new();
118 let warnings = vec![
119 LintWarning {
120 line: 5,
121 column: 1,
122 end_line: 5,
123 end_column: 10,
124 rule_name: Some("MD001".to_string()),
125 message: "First warning".to_string(),
126 severity: Severity::Warning,
127 fix: None,
128 },
129 LintWarning {
130 line: 10,
131 column: 3,
132 end_line: 10,
133 end_column: 20,
134 rule_name: Some("MD013".to_string()),
135 message: "Second warning".to_string(),
136 severity: Severity::Error,
137 fix: Some(Fix::new(50..60, "fixed".to_string())),
138 },
139 ];
140
141 let output = formatter.format_warnings(&warnings, "test.md");
142 let expected = "test.md:5:1: [MD001] First warning\ntest.md:10:3: [MD013] Second warning";
143 assert_eq!(output, expected);
144 }
145
146 #[test]
147 fn test_format_warning_unknown_rule() {
148 let formatter = ConciseFormatter::new();
149 let warnings = vec![LintWarning {
150 line: 1,
151 column: 1,
152 end_line: 1,
153 end_column: 5,
154 rule_name: None,
155 message: "Unknown rule warning".to_string(),
156 severity: Severity::Warning,
157 fix: None,
158 }];
159
160 let output = formatter.format_warnings(&warnings, "file.md");
161 assert_eq!(output, "file.md:1:1: [unknown] Unknown rule warning");
162 }
163
164 #[test]
165 fn test_edge_cases() {
166 let formatter = ConciseFormatter::new();
167
168 let warnings = vec![LintWarning {
170 line: 99999,
171 column: 12345,
172 end_line: 100000,
173 end_column: 12350,
174 rule_name: Some("MD999".to_string()),
175 message: "Edge case warning".to_string(),
176 severity: Severity::Error,
177 fix: None,
178 }];
179
180 let output = formatter.format_warnings(&warnings, "large.md");
181 assert_eq!(output, "large.md:99999:12345: [MD999] Edge case warning");
182 }
183
184 #[test]
185 fn test_special_characters_in_message() {
186 let formatter = ConciseFormatter::new();
187 let warnings = vec![LintWarning {
188 line: 1,
189 column: 1,
190 end_line: 1,
191 end_column: 5,
192 rule_name: Some("MD001".to_string()),
193 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
194 severity: Severity::Warning,
195 fix: None,
196 }];
197
198 let output = formatter.format_warnings(&warnings, "test.md");
199 assert_eq!(
200 output,
201 "test.md:1:1: [MD001] Warning with \"quotes\" and 'apostrophes' and \n newline"
202 );
203 }
204
205 #[test]
206 fn test_special_characters_in_file_path() {
207 let formatter = ConciseFormatter::new();
208 let warnings = vec![LintWarning {
209 line: 1,
210 column: 1,
211 end_line: 1,
212 end_column: 5,
213 rule_name: Some("MD001".to_string()),
214 message: "Test".to_string(),
215 severity: Severity::Warning,
216 fix: None,
217 }];
218
219 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
220 assert_eq!(output, "path/with spaces/and-dashes.md:1:1: [MD001] Test");
221 }
222
223 #[test]
224 fn test_concise_format_consistency() {
225 let formatter = ConciseFormatter::new();
226
227 let warnings = vec![
229 LintWarning {
230 line: 1,
231 column: 1,
232 end_line: 1,
233 end_column: 5,
234 rule_name: Some("MD001".to_string()),
235 message: "Test 1".to_string(),
236 severity: Severity::Warning,
237 fix: None,
238 },
239 LintWarning {
240 line: 2,
241 column: 2,
242 end_line: 2,
243 end_column: 6,
244 rule_name: Some("MD002".to_string()),
245 message: "Test 2".to_string(),
246 severity: Severity::Error,
247 fix: Some(Fix::new(10..20, "fix".to_string())),
248 },
249 ];
250
251 let output = formatter.format_warnings(&warnings, "test.md");
252 let lines: Vec<&str> = output.lines().collect();
253
254 assert_eq!(lines.len(), 2);
255
256 for line in lines {
258 assert!(line.contains(':'));
259 assert!(line.contains(" [MD"));
260 assert!(line.contains("] "));
261 }
262 }
263
264 #[test]
265 fn test_severity_ignored() {
266 let formatter = ConciseFormatter::new();
267
268 let warnings = vec![
270 LintWarning {
271 line: 1,
272 column: 1,
273 end_line: 1,
274 end_column: 5,
275 rule_name: Some("MD001".to_string()),
276 message: "Warning severity".to_string(),
277 severity: Severity::Warning,
278 fix: None,
279 },
280 LintWarning {
281 line: 2,
282 column: 1,
283 end_line: 2,
284 end_column: 5,
285 rule_name: Some("MD002".to_string()),
286 message: "Error severity".to_string(),
287 severity: Severity::Error,
288 fix: None,
289 },
290 ];
291
292 let output = formatter.format_warnings(&warnings, "test.md");
293 let lines: Vec<&str> = output.lines().collect();
294
295 assert!(lines[0].starts_with("test.md:1:1: [MD001]"));
297 assert!(lines[1].starts_with("test.md:2:1: [MD002]"));
298 }
299}