Skip to main content

miette/handlers/
json.rs

1use std::fmt::{self, Write};
2
3use crate::{Severity, protocol::Diagnostic, source_impls::SpanScanner};
4
5/**
6Renders diagnostics as machine-readable JSON.
7*/
8#[derive(Debug, Clone)]
9pub struct JSONReportHandler;
10
11impl JSONReportHandler {
12    /// Create a new [`JSONReportHandler`]. There are no customization
13    /// options.
14    #[must_use]
15    pub const fn new() -> Self {
16        Self
17    }
18}
19
20impl Default for JSONReportHandler {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26struct Escape<'a>(&'a str);
27
28impl fmt::Display for Escape<'_> {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        for c in self.0.chars() {
31            let escape = match c {
32                '\\' => Some(r"\\"),
33                '"' => Some(r#"\""#),
34                '\r' => Some(r"\r"),
35                '\n' => Some(r"\n"),
36                '\t' => Some(r"\t"),
37                '\u{08}' => Some(r"\b"),
38                '\u{0c}' => Some(r"\f"),
39                _ => None,
40            };
41            if let Some(escape) = escape {
42                f.write_str(escape)?;
43            } else {
44                f.write_char(c)?;
45            }
46        }
47        Ok(())
48    }
49}
50
51const fn escape(input: &'_ str) -> Escape<'_> {
52    Escape(input)
53}
54
55impl JSONReportHandler {
56    /// Render a [`Diagnostic`].
57    ///
58    /// # Errors
59    ///
60    /// Returns an error when writing the rendered report fails.
61    #[expect(clippy::unused_self, reason = "keeps a consistent renderer API")]
62    pub fn render_report(
63        &self,
64        f: &mut impl fmt::Write,
65        diagnostic: &dyn Diagnostic,
66    ) -> fmt::Result {
67        write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?;
68        if let Some(code) = diagnostic.code() {
69            write!(f, r#""code": "{}","#, escape(&code))?;
70        }
71        let severity = match diagnostic.severity() {
72            Some(Severity::Error) | None => "error",
73            Some(Severity::Warning) => "warning",
74            Some(Severity::Advice) => "advice",
75        };
76        write!(f, r#""severity": "{severity:}","#)?;
77        if let Some(url) = diagnostic.url() {
78            write!(f, r#""url": "{url}","#)?;
79        }
80        if let Some(help) = diagnostic.help() {
81            write!(f, r#""help": "{}","#, escape(&help))?;
82        }
83        if let Some(note) = diagnostic.note() {
84            write!(f, r#""note": "{}","#, escape(&note))?;
85        }
86        let source = diagnostic.source_code();
87        if let Some(source) = source {
88            write!(f, r#""filename": "{}","#, escape(source.name().unwrap_or_default()))?;
89        }
90        {
91            write!(f, r#""labels": ["#)?;
92            let mut scanner = source.map(|source| SpanScanner::new(source.data(), 0, 0));
93            let mut add_comma = false;
94            for label in diagnostic.labels() {
95                if add_comma {
96                    write!(f, ",")?;
97                } else {
98                    add_comma = true;
99                }
100                write!(f, "{{")?;
101                if let Some(label_name) = label.label() {
102                    write!(f, r#""label": "{}","#, escape(label_name))?;
103                }
104                write!(f, r#""span": {{"#)?;
105                write!(f, r#""offset": {},"#, label.offset())?;
106                write!(f, r#""length": {},"#, label.len())?;
107
108                if let Some(location) =
109                    scanner.as_mut().and_then(|scanner| scanner.read_span(*label.inner()))
110                {
111                    write!(f, r#""line": {},"#, location.line() + 1)?;
112                    write!(f, r#""column": {}"#, location.column() + 1)?;
113                } else {
114                    write!(f, r#""line": null,"column": null"#)?;
115                }
116
117                write!(f, "}}}}")?;
118            }
119            write!(f, "]")?;
120        }
121        write!(f, "}}")
122    }
123}
124
125#[test]
126fn test_escape() {
127    assert_eq!(escape("a\nb").to_string(), r"a\nb");
128    assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette");
129}