1use std::fmt::{self, Write};
2
3use crate::{
4 ReportHandler, Severity, SourceCode, diagnostic_chain::DiagnosticChain, protocol::Diagnostic,
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 if let Some(note) = diagnostic.note() {
110 write!(f, r#""note": "{}","#, escape(¬e.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}