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 {
121 range: 100..110,
122 replacement: "## Heading".to_string(),
123 }),
124 }];
125
126 let output = formatter.format_warnings(&warnings, "README.md");
127
128 let json: Value = serde_json::from_str(&output).unwrap();
130 assert_eq!(json["fixable"], true);
131 }
132
133 #[test]
134 fn test_format_multiple_warnings() {
135 let formatter = JsonLinesFormatter::new();
136 let warnings = vec![
137 LintWarning {
138 line: 5,
139 column: 1,
140 end_line: 5,
141 end_column: 10,
142 rule_name: Some("MD001".to_string()),
143 message: "First warning".to_string(),
144 severity: Severity::Warning,
145 fix: None,
146 },
147 LintWarning {
148 line: 10,
149 column: 3,
150 end_line: 10,
151 end_column: 20,
152 rule_name: Some("MD013".to_string()),
153 message: "Second warning".to_string(),
154 severity: Severity::Error,
155 fix: Some(Fix {
156 range: 50..60,
157 replacement: "fixed".to_string(),
158 }),
159 },
160 ];
161
162 let output = formatter.format_warnings(&warnings, "test.md");
163 let lines: Vec<&str> = output.lines().collect();
164
165 assert_eq!(lines.len(), 2);
167
168 let json1: Value = serde_json::from_str(lines[0]).unwrap();
170 assert_eq!(json1["line"], 5);
171 assert_eq!(json1["rule"], "MD001");
172 assert_eq!(json1["fixable"], false);
173
174 let json2: Value = serde_json::from_str(lines[1]).unwrap();
175 assert_eq!(json2["line"], 10);
176 assert_eq!(json2["rule"], "MD013");
177 assert_eq!(json2["fixable"], true);
178 }
179
180 #[test]
181 fn test_format_warning_unknown_rule() {
182 let formatter = JsonLinesFormatter::new();
183 let warnings = vec![LintWarning {
184 line: 1,
185 column: 1,
186 end_line: 1,
187 end_column: 5,
188 rule_name: None,
189 message: "Unknown rule warning".to_string(),
190 severity: Severity::Warning,
191 fix: None,
192 }];
193
194 let output = formatter.format_warnings(&warnings, "file.md");
195 let json: Value = serde_json::from_str(&output).unwrap();
196
197 assert_eq!(json["rule"], "unknown");
198 }
199
200 #[test]
201 fn test_edge_cases() {
202 let formatter = JsonLinesFormatter::new();
203
204 let warnings = vec![LintWarning {
206 line: 99999,
207 column: 12345,
208 end_line: 100000,
209 end_column: 12350,
210 rule_name: Some("MD999".to_string()),
211 message: "Edge case warning".to_string(),
212 severity: Severity::Error,
213 fix: None,
214 }];
215
216 let output = formatter.format_warnings(&warnings, "large.md");
217 let json: Value = serde_json::from_str(&output).unwrap();
218
219 assert_eq!(json["line"], 99999);
220 assert_eq!(json["column"], 12345);
221 }
222
223 #[test]
224 fn test_special_characters_in_message() {
225 let formatter = JsonLinesFormatter::new();
226 let warnings = vec![LintWarning {
227 line: 1,
228 column: 1,
229 end_line: 1,
230 end_column: 5,
231 rule_name: Some("MD001".to_string()),
232 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
233 severity: Severity::Warning,
234 fix: None,
235 }];
236
237 let output = formatter.format_warnings(&warnings, "test.md");
238 let json: Value = serde_json::from_str(&output).unwrap();
239
240 assert_eq!(
242 json["message"],
243 "Warning with \"quotes\" and 'apostrophes' and \n newline"
244 );
245 }
246
247 #[test]
248 fn test_special_characters_in_file_path() {
249 let formatter = JsonLinesFormatter::new();
250 let warnings = vec![LintWarning {
251 line: 1,
252 column: 1,
253 end_line: 1,
254 end_column: 5,
255 rule_name: Some("MD001".to_string()),
256 message: "Test".to_string(),
257 severity: Severity::Warning,
258 fix: None,
259 }];
260
261 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
262 let json: Value = serde_json::from_str(&output).unwrap();
263
264 assert_eq!(json["file"], "path/with spaces/and-dashes.md");
265 }
266
267 #[test]
268 fn test_json_lines_format() {
269 let formatter = JsonLinesFormatter::new();
270 let warnings = vec![
271 LintWarning {
272 line: 1,
273 column: 1,
274 end_line: 1,
275 end_column: 5,
276 rule_name: Some("MD001".to_string()),
277 message: "First".to_string(),
278 severity: Severity::Warning,
279 fix: None,
280 },
281 LintWarning {
282 line: 2,
283 column: 1,
284 end_line: 2,
285 end_column: 5,
286 rule_name: Some("MD002".to_string()),
287 message: "Second".to_string(),
288 severity: Severity::Warning,
289 fix: None,
290 },
291 LintWarning {
292 line: 3,
293 column: 1,
294 end_line: 3,
295 end_column: 5,
296 rule_name: Some("MD003".to_string()),
297 message: "Third".to_string(),
298 severity: Severity::Warning,
299 fix: None,
300 },
301 ];
302
303 let output = formatter.format_warnings(&warnings, "test.md");
304
305 for line in output.lines() {
307 assert!(serde_json::from_str::<Value>(line).is_ok());
308 }
309
310 assert_eq!(output.lines().count(), 3);
312 }
313
314 #[test]
315 fn test_severity_levels() {
316 let formatter = JsonLinesFormatter::new();
317
318 let warnings = vec![
320 LintWarning {
321 line: 1,
322 column: 1,
323 end_line: 1,
324 end_column: 5,
325 rule_name: Some("MD001".to_string()),
326 message: "Warning severity".to_string(),
327 severity: Severity::Warning,
328 fix: None,
329 },
330 LintWarning {
331 line: 2,
332 column: 1,
333 end_line: 2,
334 end_column: 5,
335 rule_name: Some("MD002".to_string()),
336 message: "Error severity".to_string(),
337 severity: Severity::Error,
338 fix: None,
339 },
340 LintWarning {
341 line: 3,
342 column: 1,
343 end_line: 3,
344 end_column: 5,
345 rule_name: Some("MD003".to_string()),
346 message: "Info severity".to_string(),
347 severity: Severity::Info,
348 fix: None,
349 },
350 ];
351
352 let output = formatter.format_warnings(&warnings, "test.md");
353 let lines: Vec<&str> = output.lines().collect();
354
355 let json0: Value = serde_json::from_str(lines[0]).unwrap();
356 let json1: Value = serde_json::from_str(lines[1]).unwrap();
357 let json2: Value = serde_json::from_str(lines[2]).unwrap();
358
359 assert_eq!(json0["severity"], "warning");
360 assert_eq!(json1["severity"], "error");
361 assert_eq!(json2["severity"], "info");
362 }
363
364 #[test]
365 fn test_json_field_order() {
366 let formatter = JsonLinesFormatter::new();
367 let warnings = vec![LintWarning {
368 line: 1,
369 column: 1,
370 end_line: 1,
371 end_column: 5,
372 rule_name: Some("MD001".to_string()),
373 message: "Test".to_string(),
374 severity: Severity::Warning,
375 fix: None,
376 }];
377
378 let output = formatter.format_warnings(&warnings, "test.md");
379
380 let json: Value = serde_json::from_str(&output).unwrap();
382 assert!(json.get("file").is_some());
383 assert!(json.get("line").is_some());
384 assert!(json.get("column").is_some());
385 assert!(json.get("rule").is_some());
386 assert!(json.get("message").is_some());
387 assert!(json.get("severity").is_some());
388 assert!(json.get("fixable").is_some());
389 }
390
391 #[test]
392 fn test_unicode_in_json() {
393 let formatter = JsonLinesFormatter::new();
394 let warnings = vec![LintWarning {
395 line: 1,
396 column: 1,
397 end_line: 1,
398 end_column: 5,
399 rule_name: Some("MD001".to_string()),
400 message: "Unicode: 你好 émoji 🎉".to_string(),
401 severity: Severity::Warning,
402 fix: None,
403 }];
404
405 let output = formatter.format_warnings(&warnings, "测试.md");
406 let json: Value = serde_json::from_str(&output).unwrap();
407
408 assert_eq!(json["message"], "Unicode: 你好 émoji 🎉");
409 assert_eq!(json["file"], "测试.md");
410 }
411}