1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5use serde_json::{Value, json};
6
7#[derive(Default)]
9pub struct JsonFormatter {
10 collect_all: bool,
11}
12
13impl JsonFormatter {
14 pub fn new() -> Self {
15 Self::default()
16 }
17
18 pub fn new_collecting() -> Self {
20 Self { collect_all: true }
21 }
22}
23
24impl OutputFormatter for JsonFormatter {
25 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
26 if self.collect_all {
27 return String::new();
30 }
31
32 let json_warnings: Vec<Value> = warnings
33 .iter()
34 .map(|warning| {
35 json!({
36 "file": file_path,
37 "line": warning.line,
38 "column": warning.column,
39 "rule": warning.rule_name.as_deref().unwrap_or("unknown"),
40 "message": warning.message,
41 "severity": warning.severity,
42 "fixable": warning.fix.is_some(),
43 "fix": warning.fix.as_ref().map(|f| {
44 json!({
45 "range": {
46 "start": f.range.start,
47 "end": f.range.end
48 },
49 "replacement": f.replacement
50 })
51 })
52 })
53 })
54 .collect();
55
56 serde_json::to_string_pretty(&json_warnings).unwrap_or_default()
57 }
58}
59
60pub fn format_all_warnings_as_json(all_warnings: &[(String, Vec<LintWarning>)]) -> String {
65 let mut json_warnings = Vec::new();
66
67 for (file_path, warnings) in all_warnings {
68 for warning in warnings {
69 json_warnings.push(json!({
70 "file": file_path,
71 "line": warning.line,
72 "column": warning.column,
73 "rule": warning.rule_name.as_deref().unwrap_or("unknown"),
74 "message": warning.message,
75 "severity": warning.severity,
76 "fixable": warning.fix.is_some(),
77 "fix": warning.fix.as_ref().map(|f| {
78 json!({
79 "range": {
80 "start": f.range.start,
81 "end": f.range.end
82 },
83 "replacement": f.replacement
84 })
85 })
86 }));
87 }
88 }
89
90 serde_json::to_string_pretty(&json_warnings).unwrap_or_default()
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::rule::{Fix, Severity};
97
98 #[test]
99 fn test_json_formatter_default() {
100 let formatter = JsonFormatter::default();
101 assert!(!formatter.collect_all);
102 }
103
104 #[test]
105 fn test_json_formatter_new() {
106 let formatter = JsonFormatter::new();
107 assert!(!formatter.collect_all);
108 }
109
110 #[test]
111 fn test_json_formatter_new_collecting() {
112 let formatter = JsonFormatter::new_collecting();
113 assert!(formatter.collect_all);
114 }
115
116 #[test]
117 fn test_format_warnings_empty() {
118 let formatter = JsonFormatter::new();
119 let warnings = vec![];
120 let output = formatter.format_warnings(&warnings, "test.md");
121 assert_eq!(output, "[]");
122 }
123
124 #[test]
125 fn test_format_warnings_collecting_mode() {
126 let formatter = JsonFormatter::new_collecting();
127 let warnings = vec![LintWarning {
128 line: 1,
129 column: 1,
130 end_line: 1,
131 end_column: 5,
132 rule_name: Some("MD001".to_string()),
133 message: "Test warning".to_string(),
134 severity: Severity::Warning,
135 fix: None,
136 }];
137
138 let output = formatter.format_warnings(&warnings, "test.md");
140 assert_eq!(output, "");
141 }
142
143 #[test]
144 fn test_format_single_warning() {
145 let formatter = JsonFormatter::new();
146 let warnings = vec![LintWarning {
147 line: 10,
148 column: 5,
149 end_line: 10,
150 end_column: 15,
151 rule_name: Some("MD001".to_string()),
152 message: "Heading levels should only increment by one level at a time".to_string(),
153 severity: Severity::Warning,
154 fix: None,
155 }];
156
157 let output = formatter.format_warnings(&warnings, "README.md");
158 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
159
160 assert_eq!(parsed.len(), 1);
161 assert_eq!(parsed[0]["file"], "README.md");
162 assert_eq!(parsed[0]["line"], 10);
163 assert_eq!(parsed[0]["column"], 5);
164 assert_eq!(parsed[0]["rule"], "MD001");
165 assert_eq!(
166 parsed[0]["message"],
167 "Heading levels should only increment by one level at a time"
168 );
169 assert_eq!(parsed[0]["severity"], "warning");
170 assert_eq!(parsed[0]["fixable"], false);
171 assert!(parsed[0]["fix"].is_null());
172 }
173
174 #[test]
175 fn test_format_warning_with_fix() {
176 let formatter = JsonFormatter::new();
177 let warnings = vec![LintWarning {
178 line: 15,
179 column: 1,
180 end_line: 15,
181 end_column: 10,
182 rule_name: Some("MD022".to_string()),
183 message: "Headings should be surrounded by blank lines".to_string(),
184 severity: Severity::Error,
185 fix: Some(Fix {
186 range: 100..110,
187 replacement: "\n# Heading\n".to_string(),
188 }),
189 }];
190
191 let output = formatter.format_warnings(&warnings, "doc.md");
192 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
193
194 assert_eq!(parsed.len(), 1);
195 assert_eq!(parsed[0]["file"], "doc.md");
196 assert_eq!(parsed[0]["line"], 15);
197 assert_eq!(parsed[0]["column"], 1);
198 assert_eq!(parsed[0]["rule"], "MD022");
199 assert_eq!(parsed[0]["message"], "Headings should be surrounded by blank lines");
200 assert_eq!(parsed[0]["severity"], "error");
201 assert_eq!(parsed[0]["fixable"], true);
202 assert!(!parsed[0]["fix"].is_null());
203 assert_eq!(parsed[0]["fix"]["range"]["start"], 100);
204 assert_eq!(parsed[0]["fix"]["range"]["end"], 110);
205 assert_eq!(parsed[0]["fix"]["replacement"], "\n# Heading\n");
206 }
207
208 #[test]
209 fn test_format_multiple_warnings() {
210 let formatter = JsonFormatter::new();
211 let warnings = vec![
212 LintWarning {
213 line: 5,
214 column: 1,
215 end_line: 5,
216 end_column: 10,
217 rule_name: Some("MD001".to_string()),
218 message: "First warning".to_string(),
219 severity: Severity::Warning,
220 fix: None,
221 },
222 LintWarning {
223 line: 10,
224 column: 3,
225 end_line: 10,
226 end_column: 20,
227 rule_name: Some("MD013".to_string()),
228 message: "Second warning".to_string(),
229 severity: Severity::Error,
230 fix: Some(Fix {
231 range: 50..60,
232 replacement: "fixed".to_string(),
233 }),
234 },
235 ];
236
237 let output = formatter.format_warnings(&warnings, "test.md");
238 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
239
240 assert_eq!(parsed.len(), 2);
241 assert_eq!(parsed[0]["rule"], "MD001");
242 assert_eq!(parsed[0]["message"], "First warning");
243 assert_eq!(parsed[0]["fixable"], false);
244
245 assert_eq!(parsed[1]["rule"], "MD013");
246 assert_eq!(parsed[1]["message"], "Second warning");
247 assert_eq!(parsed[1]["fixable"], true);
248 }
249
250 #[test]
251 fn test_format_warning_unknown_rule() {
252 let formatter = JsonFormatter::new();
253 let warnings = vec![LintWarning {
254 line: 1,
255 column: 1,
256 end_line: 1,
257 end_column: 5,
258 rule_name: None,
259 message: "Unknown rule warning".to_string(),
260 severity: Severity::Warning,
261 fix: None,
262 }];
263
264 let output = formatter.format_warnings(&warnings, "file.md");
265 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
266
267 assert_eq!(parsed[0]["rule"], "unknown");
268 }
269
270 #[test]
271 fn test_format_all_warnings_as_json_empty() {
272 let all_warnings: Vec<(String, Vec<LintWarning>)> = vec![];
273 let output = format_all_warnings_as_json(&all_warnings);
274 assert_eq!(output, "[]");
275 }
276
277 #[test]
278 fn test_format_all_warnings_as_json_single_file() {
279 let warnings = vec![LintWarning {
280 line: 1,
281 column: 1,
282 end_line: 1,
283 end_column: 5,
284 rule_name: Some("MD001".to_string()),
285 message: "Test warning".to_string(),
286 severity: Severity::Warning,
287 fix: None,
288 }];
289
290 let all_warnings = vec![("test.md".to_string(), warnings)];
291 let output = format_all_warnings_as_json(&all_warnings);
292 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
293
294 assert_eq!(parsed.len(), 1);
295 assert_eq!(parsed[0]["file"], "test.md");
296 assert_eq!(parsed[0]["rule"], "MD001");
297 }
298
299 #[test]
300 fn test_format_all_warnings_as_json_multiple_files() {
301 let warnings1 = vec![
302 LintWarning {
303 line: 1,
304 column: 1,
305 end_line: 1,
306 end_column: 5,
307 rule_name: Some("MD001".to_string()),
308 message: "Warning 1".to_string(),
309 severity: Severity::Warning,
310 fix: None,
311 },
312 LintWarning {
313 line: 5,
314 column: 1,
315 end_line: 5,
316 end_column: 10,
317 rule_name: Some("MD002".to_string()),
318 message: "Warning 2".to_string(),
319 severity: Severity::Warning,
320 fix: None,
321 },
322 ];
323
324 let warnings2 = vec![LintWarning {
325 line: 10,
326 column: 1,
327 end_line: 10,
328 end_column: 20,
329 rule_name: Some("MD003".to_string()),
330 message: "Warning 3".to_string(),
331 severity: Severity::Warning,
332 fix: Some(Fix {
333 range: 100..120,
334 replacement: "fixed".to_string(),
335 }),
336 }];
337
338 let all_warnings = vec![("file1.md".to_string(), warnings1), ("file2.md".to_string(), warnings2)];
339
340 let output = format_all_warnings_as_json(&all_warnings);
341 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
342
343 assert_eq!(parsed.len(), 3);
344 assert_eq!(parsed[0]["file"], "file1.md");
345 assert_eq!(parsed[0]["rule"], "MD001");
346 assert_eq!(parsed[1]["file"], "file1.md");
347 assert_eq!(parsed[1]["rule"], "MD002");
348 assert_eq!(parsed[2]["file"], "file2.md");
349 assert_eq!(parsed[2]["rule"], "MD003");
350 assert_eq!(parsed[2]["fixable"], true);
351 }
352
353 #[test]
354 fn test_json_output_is_valid() {
355 let formatter = JsonFormatter::new();
356 let warnings = vec![LintWarning {
357 line: 1,
358 column: 1,
359 end_line: 1,
360 end_column: 5,
361 rule_name: Some("MD001".to_string()),
362 message: "Test with \"quotes\" and special chars".to_string(),
363 severity: Severity::Warning,
364 fix: None,
365 }];
366
367 let output = formatter.format_warnings(&warnings, "test.md");
368
369 let result: Result<Vec<Value>, _> = serde_json::from_str(&output);
371 assert!(result.is_ok());
372
373 assert!(output.contains("\n"));
375 assert!(output.contains(" "));
376 }
377
378 #[test]
379 fn test_edge_cases() {
380 let formatter = JsonFormatter::new();
381
382 let warnings = vec![LintWarning {
384 line: 99999,
385 column: 12345,
386 end_line: 100000,
387 end_column: 12350,
388 rule_name: Some("MD999".to_string()),
389 message: "Edge case with\nnewlines\tand tabs".to_string(),
390 severity: Severity::Error,
391 fix: Some(Fix {
392 range: 999999..1000000,
393 replacement: "Multi\nline\nreplacement".to_string(),
394 }),
395 }];
396
397 let output = formatter.format_warnings(&warnings, "large.md");
398 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
399
400 assert_eq!(parsed[0]["line"], 99999);
401 assert_eq!(parsed[0]["column"], 12345);
402 assert_eq!(parsed[0]["fix"]["range"]["start"], 999999);
403 assert_eq!(parsed[0]["fix"]["range"]["end"], 1000000);
404 assert!(parsed[0]["message"].as_str().unwrap().contains("newlines\tand tabs"));
405 assert!(
406 parsed[0]["fix"]["replacement"]
407 .as_str()
408 .unwrap()
409 .contains("Multi\nline\nreplacement")
410 );
411 }
412
413 #[test]
414 fn test_severity_levels_in_json() {
415 let formatter = JsonFormatter::new();
416 let warnings = vec![
417 LintWarning {
418 line: 1,
419 column: 1,
420 end_line: 1,
421 end_column: 5,
422 rule_name: Some("MD001".to_string()),
423 message: "Error severity".to_string(),
424 severity: Severity::Error,
425 fix: None,
426 },
427 LintWarning {
428 line: 2,
429 column: 1,
430 end_line: 2,
431 end_column: 5,
432 rule_name: Some("MD002".to_string()),
433 message: "Warning severity".to_string(),
434 severity: Severity::Warning,
435 fix: None,
436 },
437 LintWarning {
438 line: 3,
439 column: 1,
440 end_line: 3,
441 end_column: 5,
442 rule_name: Some("MD003".to_string()),
443 message: "Info severity".to_string(),
444 severity: Severity::Info,
445 fix: None,
446 },
447 ];
448
449 let output = formatter.format_warnings(&warnings, "test.md");
450 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
451
452 assert_eq!(parsed.len(), 3);
453 assert_eq!(parsed[0]["severity"], "error");
454 assert_eq!(parsed[1]["severity"], "warning");
455 assert_eq!(parsed[2]["severity"], "info");
456 }
457}