1use std::fmt::{self, Write};
2
3use crate::{
4 ReportHandler, Severity, SourceCode, SpanContents, diagnostic_chain::DiagnosticChain,
5 protocol::Diagnostic,
6};
7
8#[derive(Debug, Clone)]
12pub struct JSONReportHandler;
13
14impl JSONReportHandler {
15 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 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(¬e))?;
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}