1use std::fmt::{self, Write};
2
3use crate::{
4 diagnostic_chain::DiagnosticChain, protocol::Diagnostic, ReportHandler, Severity, SourceCode,
5};
6
7#[derive(Debug, Clone)]
11pub struct JSONReportHandler;
12
13impl JSONReportHandler {
14 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 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 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
154 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
172impl ReportHandler for JSONReportHandler {
173 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 self.render_report(f, diagnostic)
175 }
176}
177
178#[test]
179fn 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}