1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5use serde_json::json;
6
7pub struct JsonLinesFormatter;
9
10impl Default for JsonLinesFormatter {
11 fn default() -> Self {
12 Self
13 }
14}
15
16impl JsonLinesFormatter {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl OutputFormatter for JsonLinesFormatter {
23 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
24 let mut output = String::new();
25
26 for warning in warnings {
27 let json_obj = json!({
28 "file": file_path,
29 "line": warning.line,
30 "column": warning.column,
31 "rule": warning.rule_name.as_deref().unwrap_or("unknown"),
32 "message": warning.message,
33 "severity": warning.severity,
34 "fixable": warning.fix.is_some()
35 });
36
37 if let Ok(json_str) = serde_json::to_string(&json_obj) {
39 output.push_str(&json_str);
40 output.push('\n');
41 }
42 }
43
44 if output.ends_with('\n') {
46 output.pop();
47 }
48
49 output
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::rule::{Fix, Severity};
57 use serde_json::Value;
58
59 #[test]
60 fn test_json_lines_formatter_default() {
61 let _formatter = JsonLinesFormatter;
62 }
64
65 #[test]
66 fn test_json_lines_formatter_new() {
67 let _formatter = JsonLinesFormatter::new();
68 }
70
71 #[test]
72 fn test_format_warnings_empty() {
73 let formatter = JsonLinesFormatter::new();
74 let warnings = vec![];
75 let output = formatter.format_warnings(&warnings, "test.md");
76 assert_eq!(output, "");
77 }
78
79 #[test]
80 fn test_format_single_warning() {
81 let formatter = JsonLinesFormatter::new();
82 let warnings = vec![LintWarning {
83 line: 10,
84 column: 5,
85 end_line: 10,
86 end_column: 15,
87 rule_name: Some("MD001".to_string()),
88 message: "Heading levels should only increment by one level at a time".to_string(),
89 severity: Severity::Warning,
90 fix: None,
91 }];
92
93 let output = formatter.format_warnings(&warnings, "README.md");
94
95 let json: Value = serde_json::from_str(&output).unwrap();
97 assert_eq!(json["file"], "README.md");
98 assert_eq!(json["line"], 10);
99 assert_eq!(json["column"], 5);
100 assert_eq!(json["rule"], "MD001");
101 assert_eq!(
102 json["message"],
103 "Heading levels should only increment by one level at a time"
104 );
105 assert_eq!(json["severity"], "warning");
106 assert_eq!(json["fixable"], false);
107 }
108
109 #[test]
110 fn test_format_single_warning_with_fix() {
111 let formatter = JsonLinesFormatter::new();
112 let warnings = vec![LintWarning {
113 line: 10,
114 column: 5,
115 end_line: 10,
116 end_column: 15,
117 rule_name: Some("MD001".to_string()),
118 message: "Heading levels should only increment by one level at a time".to_string(),
119 severity: Severity::Warning,
120 fix: Some(Fix::new(100..110, "## Heading".to_string())),
121 }];
122
123 let output = formatter.format_warnings(&warnings, "README.md");
124
125 let json: Value = serde_json::from_str(&output).unwrap();
127 assert_eq!(json["fixable"], true);
128 }
129
130 #[test]
131 fn test_format_multiple_warnings() {
132 let formatter = JsonLinesFormatter::new();
133 let warnings = vec![
134 LintWarning {
135 line: 5,
136 column: 1,
137 end_line: 5,
138 end_column: 10,
139 rule_name: Some("MD001".to_string()),
140 message: "First warning".to_string(),
141 severity: Severity::Warning,
142 fix: None,
143 },
144 LintWarning {
145 line: 10,
146 column: 3,
147 end_line: 10,
148 end_column: 20,
149 rule_name: Some("MD013".to_string()),
150 message: "Second warning".to_string(),
151 severity: Severity::Error,
152 fix: Some(Fix::new(50..60, "fixed".to_string())),
153 },
154 ];
155
156 let output = formatter.format_warnings(&warnings, "test.md");
157 let lines: Vec<&str> = output.lines().collect();
158
159 assert_eq!(lines.len(), 2);
161
162 let json1: Value = serde_json::from_str(lines[0]).unwrap();
164 assert_eq!(json1["line"], 5);
165 assert_eq!(json1["rule"], "MD001");
166 assert_eq!(json1["fixable"], false);
167
168 let json2: Value = serde_json::from_str(lines[1]).unwrap();
169 assert_eq!(json2["line"], 10);
170 assert_eq!(json2["rule"], "MD013");
171 assert_eq!(json2["fixable"], true);
172 }
173
174 #[test]
175 fn test_format_warning_unknown_rule() {
176 let formatter = JsonLinesFormatter::new();
177 let warnings = vec![LintWarning {
178 line: 1,
179 column: 1,
180 end_line: 1,
181 end_column: 5,
182 rule_name: None,
183 message: "Unknown rule warning".to_string(),
184 severity: Severity::Warning,
185 fix: None,
186 }];
187
188 let output = formatter.format_warnings(&warnings, "file.md");
189 let json: Value = serde_json::from_str(&output).unwrap();
190
191 assert_eq!(json["rule"], "unknown");
192 }
193
194 #[test]
195 fn test_edge_cases() {
196 let formatter = JsonLinesFormatter::new();
197
198 let warnings = vec![LintWarning {
200 line: 99999,
201 column: 12345,
202 end_line: 100000,
203 end_column: 12350,
204 rule_name: Some("MD999".to_string()),
205 message: "Edge case warning".to_string(),
206 severity: Severity::Error,
207 fix: None,
208 }];
209
210 let output = formatter.format_warnings(&warnings, "large.md");
211 let json: Value = serde_json::from_str(&output).unwrap();
212
213 assert_eq!(json["line"], 99999);
214 assert_eq!(json["column"], 12345);
215 }
216
217 #[test]
218 fn test_special_characters_in_message() {
219 let formatter = JsonLinesFormatter::new();
220 let warnings = vec![LintWarning {
221 line: 1,
222 column: 1,
223 end_line: 1,
224 end_column: 5,
225 rule_name: Some("MD001".to_string()),
226 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
227 severity: Severity::Warning,
228 fix: None,
229 }];
230
231 let output = formatter.format_warnings(&warnings, "test.md");
232 let json: Value = serde_json::from_str(&output).unwrap();
233
234 assert_eq!(
236 json["message"],
237 "Warning with \"quotes\" and 'apostrophes' and \n newline"
238 );
239 }
240
241 #[test]
242 fn test_special_characters_in_file_path() {
243 let formatter = JsonLinesFormatter::new();
244 let warnings = vec![LintWarning {
245 line: 1,
246 column: 1,
247 end_line: 1,
248 end_column: 5,
249 rule_name: Some("MD001".to_string()),
250 message: "Test".to_string(),
251 severity: Severity::Warning,
252 fix: None,
253 }];
254
255 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
256 let json: Value = serde_json::from_str(&output).unwrap();
257
258 assert_eq!(json["file"], "path/with spaces/and-dashes.md");
259 }
260
261 #[test]
262 fn test_json_lines_format() {
263 let formatter = JsonLinesFormatter::new();
264 let warnings = vec![
265 LintWarning {
266 line: 1,
267 column: 1,
268 end_line: 1,
269 end_column: 5,
270 rule_name: Some("MD001".to_string()),
271 message: "First".to_string(),
272 severity: Severity::Warning,
273 fix: None,
274 },
275 LintWarning {
276 line: 2,
277 column: 1,
278 end_line: 2,
279 end_column: 5,
280 rule_name: Some("MD002".to_string()),
281 message: "Second".to_string(),
282 severity: Severity::Warning,
283 fix: None,
284 },
285 LintWarning {
286 line: 3,
287 column: 1,
288 end_line: 3,
289 end_column: 5,
290 rule_name: Some("MD003".to_string()),
291 message: "Third".to_string(),
292 severity: Severity::Warning,
293 fix: None,
294 },
295 ];
296
297 let output = formatter.format_warnings(&warnings, "test.md");
298
299 for line in output.lines() {
301 assert!(serde_json::from_str::<Value>(line).is_ok());
302 }
303
304 assert_eq!(output.lines().count(), 3);
306 }
307
308 #[test]
309 fn test_severity_levels() {
310 let formatter = JsonLinesFormatter::new();
311
312 let warnings = vec![
314 LintWarning {
315 line: 1,
316 column: 1,
317 end_line: 1,
318 end_column: 5,
319 rule_name: Some("MD001".to_string()),
320 message: "Warning severity".to_string(),
321 severity: Severity::Warning,
322 fix: None,
323 },
324 LintWarning {
325 line: 2,
326 column: 1,
327 end_line: 2,
328 end_column: 5,
329 rule_name: Some("MD002".to_string()),
330 message: "Error severity".to_string(),
331 severity: Severity::Error,
332 fix: None,
333 },
334 LintWarning {
335 line: 3,
336 column: 1,
337 end_line: 3,
338 end_column: 5,
339 rule_name: Some("MD003".to_string()),
340 message: "Info severity".to_string(),
341 severity: Severity::Info,
342 fix: None,
343 },
344 ];
345
346 let output = formatter.format_warnings(&warnings, "test.md");
347 let lines: Vec<&str> = output.lines().collect();
348
349 let json0: Value = serde_json::from_str(lines[0]).unwrap();
350 let json1: Value = serde_json::from_str(lines[1]).unwrap();
351 let json2: Value = serde_json::from_str(lines[2]).unwrap();
352
353 assert_eq!(json0["severity"], "warning");
354 assert_eq!(json1["severity"], "error");
355 assert_eq!(json2["severity"], "info");
356 }
357
358 #[test]
359 fn test_json_field_order() {
360 let formatter = JsonLinesFormatter::new();
361 let warnings = vec![LintWarning {
362 line: 1,
363 column: 1,
364 end_line: 1,
365 end_column: 5,
366 rule_name: Some("MD001".to_string()),
367 message: "Test".to_string(),
368 severity: Severity::Warning,
369 fix: None,
370 }];
371
372 let output = formatter.format_warnings(&warnings, "test.md");
373
374 let json: Value = serde_json::from_str(&output).unwrap();
376 assert!(json.get("file").is_some());
377 assert!(json.get("line").is_some());
378 assert!(json.get("column").is_some());
379 assert!(json.get("rule").is_some());
380 assert!(json.get("message").is_some());
381 assert!(json.get("severity").is_some());
382 assert!(json.get("fixable").is_some());
383 }
384
385 #[test]
386 fn test_unicode_in_json() {
387 let formatter = JsonLinesFormatter::new();
388 let warnings = vec![LintWarning {
389 line: 1,
390 column: 1,
391 end_line: 1,
392 end_column: 5,
393 rule_name: Some("MD001".to_string()),
394 message: "Unicode: 你好 émoji 🎉".to_string(),
395 severity: Severity::Warning,
396 fix: None,
397 }];
398
399 let output = formatter.format_warnings(&warnings, "测试.md");
400 let json: Value = serde_json::from_str(&output).unwrap();
401
402 assert_eq!(json["message"], "Unicode: 你好 émoji 🎉");
403 assert_eq!(json["file"], "测试.md");
404 }
405}