Skip to main content

miette/handlers/
json.rs

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