1 use std::fmt::{self, Write};
2 
3 use crate::{
4     diagnostic_chain::DiagnosticChain, protocol::Diagnostic, ReportHandler, Severity, SourceCode,
5 };
6 
7 /**
8 [`ReportHandler`] that renders JSON output. It's a machine-readable output.
9 */
10 #[derive(Debug, Clone)]
11 pub struct JSONReportHandler;
12 
13 impl JSONReportHandler {
14     /// Create a new [`JSONReportHandler`]. There are no customization
15     /// options.
new() -> Self16     pub const fn new() -> Self {
17         Self
18     }
19 }
20 
21 impl Default for JSONReportHandler {
default() -> Self22     fn default() -> Self {
23         Self::new()
24     }
25 }
26 
27 struct Escape<'a>(&'a str);
28 
29 impl fmt::Display for Escape<'_> {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result30     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 
escape(input: &'_ str) -> Escape<'_>52 const fn escape(input: &'_ str) -> Escape<'_> {
53     Escape(input)
54 }
55 
56 impl 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.
render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result60     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 
_render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), parent_src: Option<&dyn SourceCode>, ) -> fmt::Result68     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         let src = diagnostic.source_code().or(parent_src);
110         if let Some(src) = src {
111             self.render_snippets(f, diagnostic, src)?;
112         }
113         if let Some(labels) = diagnostic.labels() {
114             write!(f, r#""labels": ["#)?;
115             let mut add_comma = false;
116             for label in labels {
117                 if add_comma {
118                     write!(f, ",")?;
119                 } else {
120                     add_comma = true;
121                 }
122                 write!(f, "{{")?;
123                 if let Some(label_name) = label.label() {
124                     write!(f, r#""label": "{}","#, escape(label_name))?;
125                 }
126                 write!(f, r#""span": {{"#)?;
127                 write!(f, r#""offset": {},"#, label.offset())?;
128                 write!(f, r#""length": {}"#, label.len())?;
129 
130                 write!(f, "}}}}")?;
131             }
132             write!(f, "],")?;
133         } else {
134             write!(f, r#""labels": [],"#)?;
135         }
136         if let Some(relateds) = diagnostic.related() {
137             write!(f, r#""related": ["#)?;
138             let mut add_comma = false;
139             for related in relateds {
140                 if add_comma {
141                     write!(f, ",")?;
142                 } else {
143                     add_comma = true;
144                 }
145                 self._render_report(f, related, src)?;
146             }
147             write!(f, "]")?;
148         } else {
149             write!(f, r#""related": []"#)?;
150         }
151         write!(f, "}}")
152     }
153 
render_snippets( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), source: &dyn SourceCode, ) -> fmt::Result154     fn render_snippets(
155         &self,
156         f: &mut impl fmt::Write,
157         diagnostic: &(dyn Diagnostic),
158         source: &dyn SourceCode,
159     ) -> fmt::Result {
160         if let Some(mut labels) = diagnostic.labels() {
161             if let Some(label) = labels.next() {
162                 if let Ok(span_content) = source.read_span(label.inner(), 0, 0) {
163                     let filename = span_content.name().unwrap_or_default();
164                     return write!(f, r#""filename": "{}","#, escape(filename));
165                 }
166             }
167         }
168         write!(f, r#""filename": "","#)
169     }
170 }
171 
172 impl ReportHandler for JSONReportHandler {
debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result173     fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
174         self.render_report(f, diagnostic)
175     }
176 }
177 
178 #[test]
test_escape()179 fn test_escape() {
180     assert_eq!(escape("a\nb").to_string(), r"a\nb");
181     assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette");
182 }
183