Skip to main content

mdlint/format/
json.rs

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        // Pretty print should have indentation
139        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}