miette/handlers/
json.rs

1use std::fmt::{self, Write};
2
3use crate::{
4    ReportHandler, Severity, SourceCode, diagnostic_chain::DiagnosticChain, protocol::Diagnostic,
5};
6
7/**
8[`ReportHandler`] that renders JSON output. It's a machine-readable output.
9*/
10#[derive(Debug, Clone)]
11pub struct JSONReportHandler;
12
13impl JSONReportHandler {
14    /// Create a new [`JSONReportHandler`]. There are no customization
15    /// options.
16    pub const fn new() -> Self {
17        Self
18    }
19}
20
21impl Default for JSONReportHandler {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27struct Escape<'a>(&'a str);
28
29impl fmt::Display for Escape<'_> {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        for c in self.0.chars() {
32            let escape = match c {
33                '\\' => Some(r"\\"),
34                '"' => Some(r#"\""#),
35                '\r' => Some(r"\r"),
36                '\n' => Some(r"\n"),
37                '\t' => Some(r"\t"),
38                '\u{08}' => Some(r"\b"),
39                '\u{0c}' => Some(r"\f"),
40                _ => None,
41            };
42            if let Some(escape) = escape {
43                f.write_str(escape)?;
44            } else {
45                f.write_char(c)?;
46            }
47        }
48        Ok(())
49    }
50}
51
52const fn escape(input: &'_ str) -> Escape<'_> {
53    Escape(input)
54}
55
56impl JSONReportHandler {
57    /// Render a [`Diagnostic`]. This function is mostly internal and meant to
58    /// be called by the toplevel [`ReportHandler`] handler, but is made public
59    /// to make it easier (possible) to test in isolation from global state.
60    pub fn render_report(
61        &self,
62        f: &mut impl fmt::Write,
63        diagnostic: &dyn Diagnostic,
64    ) -> fmt::Result {
65        self._render_report(f, diagnostic, None)
66    }
67
68    fn _render_report(
69        &self,
70        f: &mut impl fmt::Write,
71        diagnostic: &dyn Diagnostic,
72        parent_src: Option<&dyn SourceCode>,
73    ) -> fmt::Result {
74        write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?;
75        if let Some(code) = diagnostic.code() {
76            write!(f, r#""code": "{}","#, escape(&code.to_string()))?;
77        }
78        let severity = match diagnostic.severity() {
79            Some(Severity::Error) | None => "error",
80            Some(Severity::Warning) => "warning",
81            Some(Severity::Advice) => "advice",
82        };
83        write!(f, r#""severity": "{severity:}","#)?;
84        if let Some(cause_iter) = diagnostic
85            .diagnostic_source()
86            .map(DiagnosticChain::from_diagnostic)
87            .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
88        {
89            write!(f, r#""causes": ["#)?;
90            let mut add_comma = false;
91            for error in cause_iter {
92                if add_comma {
93                    write!(f, ",")?;
94                } else {
95                    add_comma = true;
96                }
97                write!(f, r#""{}""#, escape(&error.to_string()))?;
98            }
99            write!(f, "],")?;
100        } else {
101            write!(f, r#""causes": [],"#)?;
102        }
103        if let Some(url) = diagnostic.url() {
104            write!(f, r#""url": "{}","#, &url.to_string())?;
105        }
106        if let Some(help) = diagnostic.help() {
107            write!(f, r#""help": "{}","#, escape(&help.to_string()))?;
108        }
109        if let Some(note) = diagnostic.note() {
110            write!(f, r#""note": "{}","#, escape(&note.to_string()))?;
111        }
112        let src = diagnostic.source_code().or(parent_src);
113        if let Some(src) = src {
114            self.render_snippets(f, diagnostic, src)?;
115        }
116        match diagnostic.labels() {
117            Some(labels) => {
118                write!(f, r#""labels": ["#)?;
119                let mut add_comma = false;
120                for label in labels {
121                    if add_comma {
122                        write!(f, ",")?;
123                    } else {
124                        add_comma = true;
125                    }
126                    write!(f, "{{")?;
127                    if let Some(label_name) = label.label() {
128                        write!(f, r#""label": "{}","#, escape(label_name))?;
129                    }
130                    write!(f, r#""span": {{"#)?;
131                    write!(f, r#""offset": {},"#, label.offset())?;
132                    write!(f, r#""length": {},"#, label.len())?;
133
134                    if let Some(Ok(location)) = diagnostic
135                        .source_code()
136                        .or(parent_src)
137                        .map(|src| src.read_span(label.inner(), 0, 0))
138                    {
139                        write!(f, r#""line": {},"#, location.line() + 1)?;
140                        write!(f, r#""column": {}"#, location.column() + 1)?;
141                    } else {
142                        write!(f, r#""line": null,"column": null"#)?;
143                    }
144
145                    write!(f, "}}}}")?;
146                }
147                write!(f, "],")?;
148            }
149            _ => {
150                write!(f, r#""labels": [],"#)?;
151            }
152        }
153        match diagnostic.related() {
154            Some(relates) => {
155                write!(f, r#""related": ["#)?;
156                let mut add_comma = false;
157                for related in relates {
158                    if add_comma {
159                        write!(f, ",")?;
160                    } else {
161                        add_comma = true;
162                    }
163                    self._render_report(f, related, src)?;
164                }
165                write!(f, "]")?;
166            }
167            _ => {
168                write!(f, r#""related": []"#)?;
169            }
170        }
171        write!(f, "}}")
172    }
173
174    fn render_snippets(
175        &self,
176        f: &mut impl fmt::Write,
177        diagnostic: &dyn Diagnostic,
178        source: &dyn SourceCode,
179    ) -> fmt::Result {
180        if let Some(mut labels) = diagnostic.labels() {
181            if let Some(label) = labels.next() {
182                if let Ok(span_content) = source.read_span(label.inner(), 0, 0) {
183                    let filename = span_content.name().unwrap_or_default();
184                    return write!(f, r#""filename": "{}","#, escape(filename));
185                }
186            }
187        }
188        write!(f, r#""filename": "","#)
189    }
190}
191
192impl ReportHandler for JSONReportHandler {
193    fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        self.render_report(f, diagnostic)
195    }
196}
197
198#[test]
199fn test_escape() {
200    assert_eq!(escape("a\nb").to_string(), r"a\nb");
201    assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette");
202}