solar_interface/diagnostics/emitter/
json.rs1use super::{Emitter, human::HumanBufferEmitter, io_panic};
2use crate::{
3 Span,
4 diagnostics::{CodeSuggestion, Diag, Level, MultiSpan, SpanLabel, SubDiagnostic},
5 source_map::{LineInfo, SourceFile, SourceMap},
6};
7use anstream::ColorChoice;
8use serde::Serialize;
9use solar_config::HumanEmitterKind;
10use std::{borrow::Cow, io, sync::Arc};
11
12pub struct JsonEmitter {
14 writer: Box<dyn io::Write + Send>,
15 pretty: bool,
16 rustc_like: bool,
17
18 human_emitter: HumanBufferEmitter,
19}
20
21impl Emitter for JsonEmitter {
22 fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
23 self.emit_diagnostic_ref(diagnostic);
24 }
25
26 fn emit_diagnostic_ref(&mut self, diagnostic: &Diag) {
27 if self.rustc_like {
28 let diagnostic = self.diagnostic(diagnostic);
29 self.emit(&EmitTyped::Diagnostic(diagnostic))
30 } else {
31 let diagnostic = self.solc_diagnostic(diagnostic);
32 self.emit(&diagnostic)
33 }
34 .unwrap_or_else(|e| io_panic(e));
35 }
36
37 fn source_map(&self) -> Option<&Arc<SourceMap>> {
38 Emitter::source_map(&self.human_emitter)
39 }
40}
41
42impl JsonEmitter {
43 pub fn new(writer: Box<dyn io::Write + Send>, source_map: Arc<SourceMap>) -> Self {
45 Self {
46 writer,
47 pretty: false,
48 rustc_like: false,
49 human_emitter: HumanBufferEmitter::new(ColorChoice::Never).source_map(Some(source_map)),
50 }
51 }
52
53 pub fn pretty(mut self, pretty: bool) -> Self {
55 self.pretty = pretty;
56 self
57 }
58
59 pub fn rustc_like(mut self, yes: bool) -> Self {
63 self.rustc_like = yes;
64 self
65 }
66
67 pub fn ui_testing(mut self, yes: bool) -> Self {
69 self.human_emitter = self.human_emitter.ui_testing(yes);
70 self
71 }
72
73 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
75 self.human_emitter = self.human_emitter.human_kind(kind);
76 self
77 }
78
79 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
81 self.human_emitter = self.human_emitter.terminal_width(width);
82 self
83 }
84
85 fn source_map(&self) -> &Arc<SourceMap> {
86 Emitter::source_map(self).unwrap()
87 }
88
89 fn diagnostic(&mut self, diagnostic: &Diag) -> Diagnostic {
90 let children = diagnostic
92 .children
93 .iter()
94 .map(|sub| self.sub_diagnostic(sub))
95 .chain(diagnostic.suggestions.iter().map(|sugg| self.suggestion_to_diagnostic(sugg)))
96 .collect();
97
98 Diagnostic {
99 message: diagnostic.label().into_owned(),
100 code: diagnostic
101 .id()
102 .map(|code| DiagnosticCode { code: code.to_string(), explanation: None }),
103 level: diagnostic.level.to_str(),
104 spans: self.spans(&diagnostic.span),
105 children,
106 rendered: Some(self.emit_diagnostic_to_buffer(diagnostic)),
107 }
108 }
109
110 fn sub_diagnostic(&self, diagnostic: &SubDiagnostic) -> Diagnostic {
111 Diagnostic {
112 message: diagnostic.label().into_owned(),
113 code: None,
114 level: diagnostic.level.to_str(),
115 spans: self.spans(&diagnostic.span),
116 children: vec![],
117 rendered: None,
118 }
119 }
120
121 fn suggestion_to_diagnostic(&self, sugg: &CodeSuggestion) -> Diagnostic {
122 let spans = sugg
124 .substitutions
125 .iter()
126 .flat_map(|sub| sub.parts.iter())
127 .map(|part| self.span_with_suggestion(part.span, part.snippet.to_string()))
128 .collect();
129
130 Diagnostic {
131 message: sugg.msg.as_str().to_string(),
132 code: None,
133 level: "help",
134 spans,
135 children: vec![],
136 rendered: None,
137 }
138 }
139
140 fn spans(&self, msp: &MultiSpan) -> Vec<DiagnosticSpan> {
141 msp.span_labels().iter().map(|label| self.span(label)).collect()
142 }
143
144 fn span(&self, label: &SpanLabel) -> DiagnosticSpan {
145 let sm = &**self.source_map();
146 let span = label.span;
147 let start = sm.lookup_char_pos(span.lo());
148 let end = sm.lookup_char_pos(span.hi());
149 DiagnosticSpan {
150 file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
151 byte_start: start.file.original_relative_byte_pos(span.lo()).0,
152 byte_end: start.file.original_relative_byte_pos(span.hi()).0,
153 line_start: start.line,
154 line_end: end.line,
155 column_start: start.col.0 + 1,
156 column_end: end.col.0 + 1,
157 is_primary: label.is_primary,
158 text: self.span_lines(span),
159 label: label.label.as_ref().map(|msg| msg.as_str().into()),
160 suggested_replacement: None,
161 }
162 }
163
164 fn span_with_suggestion(&self, span: Span, replacement: String) -> DiagnosticSpan {
165 let sm = &**self.source_map();
166 let start = sm.lookup_char_pos(span.lo());
167 let end = sm.lookup_char_pos(span.hi());
168 DiagnosticSpan {
169 file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
170 byte_start: start.file.original_relative_byte_pos(span.lo()).0,
171 byte_end: start.file.original_relative_byte_pos(span.hi()).0,
172 line_start: start.line,
173 line_end: end.line,
174 column_start: start.col.0 + 1,
175 column_end: end.col.0 + 1,
176 is_primary: true,
177 text: self.span_lines(span),
178 label: None,
179 suggested_replacement: Some(replacement),
180 }
181 }
182
183 fn span_lines(&self, span: Span) -> Vec<DiagnosticSpanLine> {
184 let Ok(f) = self.source_map().span_to_lines(span) else { return Vec::new() };
185 let sf = &*f.file;
186 f.data.iter().map(|line| self.span_line(sf, line)).collect()
187 }
188
189 fn span_line(&self, sf: &SourceFile, line: &LineInfo) -> DiagnosticSpanLine {
190 DiagnosticSpanLine {
191 text: sf.get_line(line.line_index).map_or_else(String::new, |l| l.to_string()),
192 highlight_start: line.start_col.0 + 1,
193 highlight_end: line.end_col.0 + 1,
194 }
195 }
196
197 pub fn solc_diagnostic<'a>(
199 &mut self,
200 diagnostic: &'a crate::diagnostics::Diag,
201 ) -> SolcDiagnostic<'a> {
202 let primary = diagnostic.span.primary_span();
203 let file = primary
204 .map(|span| {
205 let sm = &**self.source_map();
206 let start = sm.lookup_char_pos(span.lo());
207 sm.filename_for_diagnostics(&start.file.name).to_string()
208 })
209 .unwrap_or_default();
210
211 let severity = to_severity(diagnostic.level);
212
213 SolcDiagnostic {
214 source_location: primary
215 .is_some()
216 .then(|| self.solc_span(&diagnostic.span, &file, None)),
217 secondary_source_locations: diagnostic
218 .children
219 .iter()
220 .map(|sub| self.solc_span(&sub.span, &file, Some(sub.label())))
221 .collect(),
222 r#type: Cow::Borrowed(match severity {
223 Severity::Error => match diagnostic.level {
224 Level::Bug => "InternalCompilerError",
225 Level::Fatal => "FatalError",
226 Level::Error => "Exception",
227 _ => unreachable!(),
228 },
229 Severity::Warning => "Warning",
230 Severity::Info => "Info",
231 }),
232 component: Cow::Borrowed("general"),
233 severity,
234 error_code: diagnostic.id().map(Cow::Borrowed),
235 message: diagnostic.label(),
236 formatted_message: Some(Cow::Owned(self.emit_diagnostic_to_buffer(diagnostic))),
237 }
238 }
239
240 fn solc_span<'a>(
241 &self,
242 span: &MultiSpan,
243 file: &str,
244 message: Option<Cow<'a, str>>,
245 ) -> SourceLocation<'a> {
246 let sm = &**self.source_map();
247 let sp = span.primary_span();
248 SourceLocation {
249 file: sp
250 .map(|span| {
251 let start = sm.lookup_char_pos(span.lo());
252 Cow::Owned(sm.filename_for_diagnostics(&start.file.name).to_string())
253 })
254 .unwrap_or_else(|| Cow::Owned(file.to_owned())),
255 start: sp
256 .map(|span| {
257 let start = sm.lookup_char_pos(span.lo());
258 start.file.original_relative_byte_pos(span.lo()).0
259 })
260 .unwrap_or(0),
261 end: sp
262 .map(|span| {
263 let end = sm.lookup_char_pos(span.hi());
264 end.file.original_relative_byte_pos(span.hi()).0
265 })
266 .unwrap_or(0),
267 message,
268 }
269 }
270
271 fn emit_diagnostic_to_buffer(&mut self, diagnostic: &crate::diagnostics::Diag) -> String {
272 self.human_emitter.emit_diagnostic_ref(diagnostic);
273 std::mem::take(self.human_emitter.buffer_mut())
274 }
275
276 fn emit<T: ?Sized + Serialize>(&mut self, value: &T) -> io::Result<()> {
277 if self.pretty {
278 serde_json::to_writer_pretty(&mut *self.writer, value)
279 } else {
280 serde_json::to_writer(&mut *self.writer, value)
281 }?;
282 self.writer.write_all(b"\n")?;
283 self.writer.flush()
284 }
285}
286
287#[derive(Serialize)]
290#[serde(tag = "$message_type", rename_all = "snake_case")]
291enum EmitTyped {
292 Diagnostic(Diagnostic),
293}
294
295#[derive(Serialize)]
296struct Diagnostic {
297 message: String,
299 code: Option<DiagnosticCode>,
300 level: &'static str,
302 spans: Vec<DiagnosticSpan>,
303 children: Vec<Self>,
305 rendered: Option<String>,
307}
308
309#[derive(Serialize)]
310struct DiagnosticSpan {
311 file_name: String,
312 byte_start: u32,
313 byte_end: u32,
314 line_start: usize,
316 line_end: usize,
317 column_start: usize,
319 column_end: usize,
320 is_primary: bool,
323 text: Vec<DiagnosticSpanLine>,
325 label: Option<String>,
327 suggested_replacement: Option<String>,
330}
331
332#[derive(Serialize)]
333struct DiagnosticSpanLine {
334 text: String,
335
336 highlight_start: usize,
338
339 highlight_end: usize,
340}
341
342#[derive(Serialize)]
343struct DiagnosticCode {
344 code: String,
346 explanation: Option<&'static str>,
348}
349
350#[derive(Debug, Serialize)]
353#[serde(rename_all = "camelCase")]
354pub struct SolcDiagnostic<'a> {
355 #[serde(borrow)]
356 pub source_location: Option<SourceLocation<'a>>,
357 #[serde(borrow)]
358 pub secondary_source_locations: Vec<SourceLocation<'a>>,
359 #[serde(borrow)]
360 pub r#type: Cow<'a, str>,
361 #[serde(borrow)]
362 pub component: Cow<'a, str>,
363 pub severity: Severity,
364 #[serde(borrow)]
365 pub error_code: Option<Cow<'a, str>>,
366 #[serde(borrow)]
367 pub message: Cow<'a, str>,
368 #[serde(borrow)]
369 pub formatted_message: Option<Cow<'a, str>>,
370}
371
372impl SolcDiagnostic<'_> {
373 pub fn is_error(&self) -> bool {
375 matches!(self.severity, Severity::Error)
376 }
377}
378
379#[derive(Debug, Serialize)]
380pub struct SourceLocation<'a> {
381 #[serde(borrow)]
382 pub file: Cow<'a, str>,
383 pub start: u32,
384 pub end: u32,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
387 #[serde(borrow)]
388 pub message: Option<Cow<'a, str>>,
389}
390
391#[derive(Debug, Serialize)]
392#[serde(rename_all = "lowercase")]
393pub enum Severity {
394 Error,
395 Warning,
396 Info,
397}
398
399fn to_severity(level: Level) -> Severity {
400 match level {
401 Level::Bug | Level::Fatal | Level::Error => Severity::Error,
402 Level::Warning => Severity::Warning,
403 Level::Note
404 | Level::OnceNote
405 | Level::FailureNote
406 | Level::Help
407 | Level::OnceHelp
408 | Level::Allow => Severity::Info,
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn solc_diagnostic_serializes_borrowed_strings() {
418 let diagnostic = SolcDiagnostic {
419 source_location: Some(SourceLocation {
420 file: Cow::Borrowed("input.sol"),
421 start: 0,
422 end: 1,
423 message: Some(Cow::Borrowed("borrowed \"message\"")),
424 }),
425 secondary_source_locations: Vec::new(),
426 r#type: Cow::Borrowed("Exception\nquoted"),
427 component: Cow::Borrowed("general"),
428 severity: Severity::Error,
429 error_code: Some(Cow::Borrowed("1234")),
430 message: Cow::Borrowed("borrowed message"),
431 formatted_message: None,
432 };
433
434 let json = serde_json::to_string(&diagnostic).unwrap();
435 assert!(json.contains(r#""type":"Exception\nquoted""#));
436 assert!(json.contains(r#""message":"borrowed \"message\"""#));
437
438 assert!(matches!(diagnostic.r#type, Cow::Borrowed(_)));
439 assert!(matches!(diagnostic.component, Cow::Borrowed("general")));
440 }
441}