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