syncable_cli/analyzer/hadolint/formatter/
json.rs

1//! JSON formatter for hadolint-rs.
2//!
3//! Outputs lint results in JSON format for CI/CD pipeline integration.
4//! Compatible with the original hadolint JSON output.
5
6use crate::analyzer::hadolint::formatter::Formatter;
7use crate::analyzer::hadolint::lint::LintResult;
8use crate::analyzer::hadolint::types::Severity;
9use serde::Serialize;
10use std::io::Write;
11
12/// JSON output formatter.
13#[derive(Debug, Clone, Default)]
14pub struct JsonFormatter {
15    /// Pretty-print the JSON output.
16    pub pretty: bool,
17}
18
19impl JsonFormatter {
20    /// Create a new JSON formatter.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Create a JSON formatter with pretty-printing enabled.
26    pub fn pretty() -> Self {
27        Self { pretty: true }
28    }
29}
30
31/// JSON representation of a lint failure.
32#[derive(Debug, Serialize)]
33struct JsonFailure {
34    line: u32,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    column: Option<u32>,
37    code: String,
38    message: String,
39    level: String,
40    file: String,
41}
42
43impl Formatter for JsonFormatter {
44    fn format<W: Write>(&self, result: &LintResult, filename: &str, writer: &mut W) -> std::io::Result<()> {
45        let failures: Vec<JsonFailure> = result
46            .failures
47            .iter()
48            .map(|f| JsonFailure {
49                line: f.line,
50                column: f.column,
51                code: f.code.to_string(),
52                message: f.message.clone(),
53                level: match f.severity {
54                    Severity::Error => "error",
55                    Severity::Warning => "warning",
56                    Severity::Info => "info",
57                    Severity::Style => "style",
58                    Severity::Ignore => "ignore",
59                }
60                .to_string(),
61                file: filename.to_string(),
62            })
63            .collect();
64
65        let json = if self.pretty {
66            serde_json::to_string_pretty(&failures)
67        } else {
68            serde_json::to_string(&failures)
69        }
70        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
71
72        writeln!(writer, "{}", json)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::analyzer::hadolint::types::CheckFailure;
80
81    #[test]
82    fn test_json_output() {
83        let mut result = LintResult::new();
84        result.failures.push(CheckFailure::new(
85            "DL3008",
86            Severity::Warning,
87            "Pin versions in apt get install",
88            5,
89        ));
90
91        let formatter = JsonFormatter::new();
92        let output = formatter.format_to_string(&result, "Dockerfile");
93
94        assert!(output.contains("DL3008"));
95        assert!(output.contains("warning"));
96        assert!(output.contains("Pin versions"));
97    }
98}