1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5use serde_json::{Value, json};
6use std::borrow::Cow;
7
8#[derive(Default)]
10pub struct JsonFormatter {
11 collect_all: bool,
12}
13
14impl JsonFormatter {
15 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn new_collecting() -> Self {
21 Self { collect_all: true }
22 }
23}
24
25impl OutputFormatter for JsonFormatter {
26 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
27 if self.collect_all {
28 return String::new();
31 }
32
33 let json_warnings: Vec<Value> = warnings
34 .iter()
35 .map(|warning| warning_to_json(warning, file_path))
36 .collect();
37
38 serde_json::to_string_pretty(&json_warnings).unwrap_or_default()
39 }
40}
41
42fn warning_to_json(warning: &LintWarning, file_path: &str) -> Value {
43 let mut value = json!({
44 "file": file_path,
45 "line": warning.line,
46 "column": warning.column,
47 "rule": warning.rule_name.as_deref().unwrap_or("unknown"),
48 "message": warning.message,
49 "severity": warning.severity,
50 "fixable": warning.fix.is_some(),
51 });
52 if let Some(fix) = &warning.fix {
53 value["fix"] = fix_to_json(fix);
54 }
55 value
56}
57
58fn fix_to_json(fix: &crate::rule::Fix) -> serde_json::Value {
59 let mut obj = json!({
60 "range": {
61 "start": fix.range.start,
62 "end": fix.range.end,
63 },
64 "replacement": fix.replacement,
65 });
66 if !fix.additional_edits.is_empty() {
67 obj["additional_edits"] = serde_json::Value::Array(fix.additional_edits.iter().map(fix_to_json).collect());
68 }
69 obj
70}
71
72pub fn remap_fix_ranges_to_original(
77 warnings: &mut [LintWarning],
78 line_endings: &crate::utils::NormalizedLineEndingMap,
79) {
80 fn remap_fix(fix: &mut crate::rule::Fix, line_endings: &crate::utils::NormalizedLineEndingMap) {
81 let start = fix.range.start;
82 let end = fix.range.end;
83 if let Cow::Owned(replacement) = line_endings.original_text_at(&fix.replacement, start) {
84 fix.replacement = replacement;
85 }
86 fix.range.start = line_endings.original_offset(start);
87 fix.range.end = line_endings.original_offset(end);
88 for additional in &mut fix.additional_edits {
89 remap_fix(additional, line_endings);
90 }
91 }
92
93 for warning in warnings {
94 if let Some(fix) = &mut warning.fix {
95 remap_fix(fix, line_endings);
96 }
97 }
98}
99
100pub fn format_all_warnings_as_json(all_warnings: &[(String, Vec<LintWarning>)]) -> String {
105 let mut json_warnings = Vec::new();
106
107 for (file_path, warnings) in all_warnings {
108 for warning in warnings {
109 json_warnings.push(warning_to_json(warning, file_path));
110 }
111 }
112
113 serde_json::to_string_pretty(&json_warnings).unwrap_or_default()
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::rule::{Fix, Severity};
120
121 #[test]
122 fn test_json_formatter_default() {
123 let formatter = JsonFormatter::default();
124 assert!(!formatter.collect_all);
125 }
126
127 #[test]
128 fn test_json_formatter_new() {
129 let formatter = JsonFormatter::new();
130 assert!(!formatter.collect_all);
131 }
132
133 #[test]
134 fn test_json_formatter_new_collecting() {
135 let formatter = JsonFormatter::new_collecting();
136 assert!(formatter.collect_all);
137 }
138
139 #[test]
140 fn test_format_warnings_empty() {
141 let formatter = JsonFormatter::new();
142 let warnings = vec![];
143 let output = formatter.format_warnings(&warnings, "test.md");
144 assert_eq!(output, "[]");
145 }
146
147 #[test]
148 fn test_format_warnings_collecting_mode() {
149 let formatter = JsonFormatter::new_collecting();
150 let warnings = vec![LintWarning {
151 line: 1,
152 column: 1,
153 end_line: 1,
154 end_column: 5,
155 rule_name: Some("MD001".to_string()),
156 message: "Test warning".to_string(),
157 severity: Severity::Warning,
158 fix: None,
159 }];
160
161 let output = formatter.format_warnings(&warnings, "test.md");
163 assert_eq!(output, "");
164 }
165
166 #[test]
167 fn test_format_single_warning() {
168 let formatter = JsonFormatter::new();
169 let warnings = vec![LintWarning {
170 line: 10,
171 column: 5,
172 end_line: 10,
173 end_column: 15,
174 rule_name: Some("MD001".to_string()),
175 message: "Heading levels should only increment by one level at a time".to_string(),
176 severity: Severity::Warning,
177 fix: None,
178 }];
179
180 let output = formatter.format_warnings(&warnings, "README.md");
181 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
182
183 assert_eq!(parsed.len(), 1);
184 assert_eq!(parsed[0]["file"], "README.md");
185 assert_eq!(parsed[0]["line"], 10);
186 assert_eq!(parsed[0]["column"], 5);
187 assert_eq!(parsed[0]["rule"], "MD001");
188 assert_eq!(
189 parsed[0]["message"],
190 "Heading levels should only increment by one level at a time"
191 );
192 assert_eq!(parsed[0]["severity"], "warning");
193 assert_eq!(parsed[0]["fixable"], false);
194 assert!(parsed[0].get("fix").is_none());
195 }
196
197 #[test]
198 fn test_format_warning_with_fix() {
199 let formatter = JsonFormatter::new();
200 let warnings = vec![LintWarning {
201 line: 15,
202 column: 1,
203 end_line: 15,
204 end_column: 10,
205 rule_name: Some("MD022".to_string()),
206 message: "Headings should be surrounded by blank lines".to_string(),
207 severity: Severity::Error,
208 fix: Some(Fix::new(100..110, "\n# Heading\n".to_string())),
209 }];
210
211 let output = formatter.format_warnings(&warnings, "doc.md");
212 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
213
214 assert_eq!(parsed.len(), 1);
215 assert_eq!(parsed[0]["file"], "doc.md");
216 assert_eq!(parsed[0]["line"], 15);
217 assert_eq!(parsed[0]["column"], 1);
218 assert_eq!(parsed[0]["rule"], "MD022");
219 assert_eq!(parsed[0]["message"], "Headings should be surrounded by blank lines");
220 assert_eq!(parsed[0]["severity"], "error");
221 assert_eq!(parsed[0]["fixable"], true);
222 assert!(!parsed[0]["fix"].is_null());
223 assert_eq!(parsed[0]["fix"]["range"]["start"], 100);
224 assert_eq!(parsed[0]["fix"]["range"]["end"], 110);
225 assert_eq!(parsed[0]["fix"]["replacement"], "\n# Heading\n");
226 }
227
228 #[test]
229 fn test_format_warning_with_additional_edits() {
230 let formatter = JsonFormatter::new();
235 let warnings = vec![LintWarning {
236 line: 1,
237 column: 5,
238 end_line: 1,
239 end_column: 32,
240 rule_name: Some("MD054".to_string()),
241 message: "Inconsistent link style".to_string(),
242 severity: Severity::Warning,
243 fix: Some(Fix::with_additional_edits(
244 4..31,
245 "[docs]".to_string(),
246 vec![Fix::new(45..45, "\n[docs]: https://example.com\n".to_string())],
247 )),
248 }];
249
250 let output = formatter.format_warnings(&warnings, "doc.md");
251 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
252
253 assert_eq!(parsed[0]["fixable"], true);
254 assert_eq!(parsed[0]["fix"]["range"]["start"], 4);
255 assert_eq!(parsed[0]["fix"]["range"]["end"], 31);
256 assert_eq!(parsed[0]["fix"]["replacement"], "[docs]");
257
258 let extras = parsed[0]["fix"]["additional_edits"]
259 .as_array()
260 .expect("additional_edits should serialize as an array when non-empty");
261 assert_eq!(extras.len(), 1);
262 assert_eq!(extras[0]["range"]["start"], 45);
263 assert_eq!(extras[0]["range"]["end"], 45);
264 assert_eq!(extras[0]["replacement"], "\n[docs]: https://example.com\n");
265 }
266
267 #[test]
268 fn test_format_warning_omits_empty_additional_edits() {
269 let formatter = JsonFormatter::new();
273 let warnings = vec![LintWarning {
274 line: 1,
275 column: 1,
276 end_line: 1,
277 end_column: 5,
278 rule_name: Some("MD009".to_string()),
279 message: "Trailing whitespace".to_string(),
280 severity: Severity::Warning,
281 fix: Some(Fix::new(0..2, " ".to_string())),
282 }];
283
284 let output = formatter.format_warnings(&warnings, "doc.md");
285 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
286
287 let fix = &parsed[0]["fix"];
288 assert!(
289 fix.get("additional_edits").is_none(),
290 "additional_edits must be omitted when empty, got: {fix}"
291 );
292 }
293
294 #[test]
295 fn test_format_multiple_warnings() {
296 let formatter = JsonFormatter::new();
297 let warnings = vec![
298 LintWarning {
299 line: 5,
300 column: 1,
301 end_line: 5,
302 end_column: 10,
303 rule_name: Some("MD001".to_string()),
304 message: "First warning".to_string(),
305 severity: Severity::Warning,
306 fix: None,
307 },
308 LintWarning {
309 line: 10,
310 column: 3,
311 end_line: 10,
312 end_column: 20,
313 rule_name: Some("MD013".to_string()),
314 message: "Second warning".to_string(),
315 severity: Severity::Error,
316 fix: Some(Fix::new(50..60, "fixed".to_string())),
317 },
318 ];
319
320 let output = formatter.format_warnings(&warnings, "test.md");
321 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
322
323 assert_eq!(parsed.len(), 2);
324 assert_eq!(parsed[0]["rule"], "MD001");
325 assert_eq!(parsed[0]["message"], "First warning");
326 assert_eq!(parsed[0]["fixable"], false);
327
328 assert_eq!(parsed[1]["rule"], "MD013");
329 assert_eq!(parsed[1]["message"], "Second warning");
330 assert_eq!(parsed[1]["fixable"], true);
331 }
332
333 #[test]
334 fn test_format_warning_unknown_rule() {
335 let formatter = JsonFormatter::new();
336 let warnings = vec![LintWarning {
337 line: 1,
338 column: 1,
339 end_line: 1,
340 end_column: 5,
341 rule_name: None,
342 message: "Unknown rule warning".to_string(),
343 severity: Severity::Warning,
344 fix: None,
345 }];
346
347 let output = formatter.format_warnings(&warnings, "file.md");
348 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
349
350 assert_eq!(parsed[0]["rule"], "unknown");
351 }
352
353 #[test]
354 fn test_format_all_warnings_as_json_empty() {
355 let all_warnings: Vec<(String, Vec<LintWarning>)> = vec![];
356 let output = format_all_warnings_as_json(&all_warnings);
357 assert_eq!(output, "[]");
358 }
359
360 #[test]
361 fn test_format_all_warnings_as_json_single_file() {
362 let warnings = vec![LintWarning {
363 line: 1,
364 column: 1,
365 end_line: 1,
366 end_column: 5,
367 rule_name: Some("MD001".to_string()),
368 message: "Test warning".to_string(),
369 severity: Severity::Warning,
370 fix: None,
371 }];
372
373 let all_warnings = vec![("test.md".to_string(), warnings)];
374 let output = format_all_warnings_as_json(&all_warnings);
375 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
376
377 assert_eq!(parsed.len(), 1);
378 assert_eq!(parsed[0]["file"], "test.md");
379 assert_eq!(parsed[0]["rule"], "MD001");
380 }
381
382 #[test]
383 fn test_format_all_warnings_as_json_multiple_files() {
384 let warnings1 = vec![
385 LintWarning {
386 line: 1,
387 column: 1,
388 end_line: 1,
389 end_column: 5,
390 rule_name: Some("MD001".to_string()),
391 message: "Warning 1".to_string(),
392 severity: Severity::Warning,
393 fix: None,
394 },
395 LintWarning {
396 line: 5,
397 column: 1,
398 end_line: 5,
399 end_column: 10,
400 rule_name: Some("MD002".to_string()),
401 message: "Warning 2".to_string(),
402 severity: Severity::Warning,
403 fix: None,
404 },
405 ];
406
407 let warnings2 = vec![LintWarning {
408 line: 10,
409 column: 1,
410 end_line: 10,
411 end_column: 20,
412 rule_name: Some("MD003".to_string()),
413 message: "Warning 3".to_string(),
414 severity: Severity::Warning,
415 fix: Some(Fix::new(100..120, "fixed".to_string())),
416 }];
417
418 let all_warnings = vec![("file1.md".to_string(), warnings1), ("file2.md".to_string(), warnings2)];
419
420 let output = format_all_warnings_as_json(&all_warnings);
421 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
422
423 assert_eq!(parsed.len(), 3);
424 assert_eq!(parsed[0]["file"], "file1.md");
425 assert_eq!(parsed[0]["rule"], "MD001");
426 assert_eq!(parsed[1]["file"], "file1.md");
427 assert_eq!(parsed[1]["rule"], "MD002");
428 assert_eq!(parsed[2]["file"], "file2.md");
429 assert_eq!(parsed[2]["rule"], "MD003");
430 assert_eq!(parsed[2]["fixable"], true);
431 }
432
433 #[test]
434 fn test_json_output_is_valid() {
435 let formatter = JsonFormatter::new();
436 let warnings = vec![LintWarning {
437 line: 1,
438 column: 1,
439 end_line: 1,
440 end_column: 5,
441 rule_name: Some("MD001".to_string()),
442 message: "Test with \"quotes\" and special chars".to_string(),
443 severity: Severity::Warning,
444 fix: None,
445 }];
446
447 let output = formatter.format_warnings(&warnings, "test.md");
448
449 let result: Result<Vec<Value>, _> = serde_json::from_str(&output);
451 assert!(result.is_ok());
452
453 assert!(output.contains('\n'));
455 assert!(output.contains(" "));
456 }
457
458 #[test]
459 fn test_edge_cases() {
460 let formatter = JsonFormatter::new();
461
462 let warnings = vec![LintWarning {
464 line: 99999,
465 column: 12345,
466 end_line: 100000,
467 end_column: 12350,
468 rule_name: Some("MD999".to_string()),
469 message: "Edge case with\nnewlines\tand tabs".to_string(),
470 severity: Severity::Error,
471 fix: Some(Fix::new(999999..1000000, "Multi\nline\nreplacement".to_string())),
472 }];
473
474 let output = formatter.format_warnings(&warnings, "large.md");
475 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
476
477 assert_eq!(parsed[0]["line"], 99999);
478 assert_eq!(parsed[0]["column"], 12345);
479 assert_eq!(parsed[0]["fix"]["range"]["start"], 999999);
480 assert_eq!(parsed[0]["fix"]["range"]["end"], 1000000);
481 assert!(parsed[0]["message"].as_str().unwrap().contains("newlines\tand tabs"));
482 assert!(
483 parsed[0]["fix"]["replacement"]
484 .as_str()
485 .unwrap()
486 .contains("Multi\nline\nreplacement")
487 );
488 }
489
490 #[test]
491 fn test_severity_levels_in_json() {
492 let formatter = JsonFormatter::new();
493 let warnings = vec![
494 LintWarning {
495 line: 1,
496 column: 1,
497 end_line: 1,
498 end_column: 5,
499 rule_name: Some("MD001".to_string()),
500 message: "Error severity".to_string(),
501 severity: Severity::Error,
502 fix: None,
503 },
504 LintWarning {
505 line: 2,
506 column: 1,
507 end_line: 2,
508 end_column: 5,
509 rule_name: Some("MD002".to_string()),
510 message: "Warning severity".to_string(),
511 severity: Severity::Warning,
512 fix: None,
513 },
514 LintWarning {
515 line: 3,
516 column: 1,
517 end_line: 3,
518 end_column: 5,
519 rule_name: Some("MD003".to_string()),
520 message: "Info severity".to_string(),
521 severity: Severity::Info,
522 fix: None,
523 },
524 ];
525
526 let output = formatter.format_warnings(&warnings, "test.md");
527 let parsed: Vec<Value> = serde_json::from_str(&output).unwrap();
528
529 assert_eq!(parsed.len(), 3);
530 assert_eq!(parsed[0]["severity"], "error");
531 assert_eq!(parsed[1]["severity"], "warning");
532 assert_eq!(parsed[2]["severity"], "info");
533 }
534}