1use crate::format::Formatter;
2use crate::lint::LintResult;
3use crate::types::FileResult;
4use serde::Serialize;
5
6pub struct JsonFormatter {
7 pretty: bool,
8}
9
10impl JsonFormatter {
11 pub fn new(pretty: bool) -> Self {
12 Self { pretty }
13 }
14}
15
16fn file_violations(file_result: &FileResult) -> Vec<JsonViolation> {
17 file_result
18 .violations
19 .iter()
20 .map(|violation| JsonViolation {
21 line: violation.line,
22 column: violation.column,
23 rule: violation.rule.clone(),
24 message: violation.message.clone(),
25 fixable: violation.fix.as_ref().map(|_| true),
26 })
27 .collect()
28}
29
30#[derive(Serialize)]
31struct JsonOutput {
32 files: Vec<JsonFile>,
33 total_errors: usize,
34}
35
36#[derive(Serialize)]
37struct JsonFile {
38 path: String,
39 violations: Vec<JsonViolation>,
40}
41
42#[derive(Serialize)]
43struct JsonViolation {
44 line: usize,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 column: Option<usize>,
47 rule: String,
48 message: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 fixable: Option<bool>,
51}
52
53impl Formatter for JsonFormatter {
54 fn format(&self, result: &LintResult) -> String {
55 let json_output = JsonOutput {
56 files: result
57 .file_results
58 .iter()
59 .map(|file_result| JsonFile {
60 path: file_result.path.display().to_string(),
61 violations: file_violations(file_result),
62 })
63 .collect(),
64 total_errors: result.total_errors,
65 };
66
67 if self.pretty {
68 serde_json::to_string_pretty(&json_output)
69 .unwrap_or_else(|e| format!("{{\"error\": \"Failed to serialize JSON: {}\"}}", e))
70 } else {
71 serde_json::to_string(&json_output)
72 .unwrap_or_else(|e| format!("{{\"error\": \"Failed to serialize JSON: {}\"}}", e))
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::types::Violation;
81 use std::path::PathBuf;
82
83 #[test]
84 fn test_empty_result() {
85 let formatter = JsonFormatter::new(false);
86 let result = LintResult::new();
87 let output = formatter.format(&result);
88
89 assert!(output.contains("\"total_errors\":0"));
90 assert!(output.contains("\"files\":[]"));
91 }
92
93 #[test]
94 fn test_single_violation() {
95 let formatter = JsonFormatter::new(false);
96 let mut result = LintResult::new();
97
98 result.add_file_result(
99 PathBuf::from("test.md"),
100 vec![Violation {
101 line: 5,
102 column: Some(10),
103 rule: "MD001".to_string(),
104 message: "Test message".to_string(),
105 fix: None,
106 }],
107 vec![],
108 );
109
110 let output = formatter.format(&result);
111
112 assert!(output.contains("\"line\":5"));
113 assert!(output.contains("\"column\":10"));
114 assert!(output.contains("\"rule\":\"MD001\""));
115 assert!(output.contains("\"message\":\"Test message\""));
116 assert!(output.contains("\"total_errors\":1"));
117 }
118
119 #[test]
120 fn test_pretty_print() {
121 let formatter = JsonFormatter::new(true);
122 let mut result = LintResult::new();
123
124 result.add_file_result(
125 PathBuf::from("test.md"),
126 vec![Violation {
127 line: 1,
128 column: None,
129 rule: "MD001".to_string(),
130 message: "Test".to_string(),
131 fix: None,
132 }],
133 vec![],
134 );
135
136 let output = formatter.format(&result);
137
138 assert!(output.contains(" ") || output.contains("\n"));
140 }
141
142 #[test]
143 fn test_fixable_flag() {
144 let formatter = JsonFormatter::new(false);
145 let mut result = LintResult::new();
146
147 result.add_file_result(
148 PathBuf::from("test.md"),
149 vec![Violation {
150 line: 1,
151 column: Some(1),
152 rule: "MD009".to_string(),
153 message: "Trailing spaces".to_string(),
154 fix: Some(crate::types::Fix {
155 line_start: 1,
156 line_end: 1,
157 column_start: None,
158 column_end: None,
159 replacement: "fixed".to_string(),
160 description: "Remove trailing spaces".to_string(),
161 }),
162 }],
163 vec![],
164 );
165
166 let output = formatter.format(&result);
167
168 assert!(output.contains("\"fixable\":true"));
169 }
170}