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::{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 if self.rustc_like {
24 let diagnostic = self.diagnostic(diagnostic);
25 self.emit(&EmitTyped::Diagnostic(diagnostic))
26 } else {
27 let diagnostic = self.solc_diagnostic(diagnostic);
28 self.emit(&diagnostic)
29 }
30 .unwrap_or_else(|e| io_panic(e));
31 }
32
33 fn source_map(&self) -> Option<&Arc<SourceMap>> {
34 Emitter::source_map(&self.human_emitter)
35 }
36}
37
38impl JsonEmitter {
39 pub fn new(writer: Box<dyn io::Write + Send>, source_map: Arc<SourceMap>) -> Self {
41 Self {
42 writer,
43 pretty: false,
44 rustc_like: false,
45 human_emitter: HumanBufferEmitter::new(ColorChoice::Never).source_map(Some(source_map)),
46 }
47 }
48
49 pub fn pretty(mut self, pretty: bool) -> Self {
51 self.pretty = pretty;
52 self
53 }
54
55 pub fn rustc_like(mut self, yes: bool) -> Self {
59 self.rustc_like = yes;
60 self
61 }
62
63 pub fn ui_testing(mut self, yes: bool) -> Self {
65 self.human_emitter = self.human_emitter.ui_testing(yes);
66 self
67 }
68
69 pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
71 self.human_emitter = self.human_emitter.human_kind(kind);
72 self
73 }
74
75 pub fn terminal_width(mut self, width: Option<usize>) -> Self {
77 self.human_emitter = self.human_emitter.terminal_width(width);
78 self
79 }
80
81 fn source_map(&self) -> &Arc<SourceMap> {
82 Emitter::source_map(self).unwrap()
83 }
84
85 fn diagnostic(&mut self, diagnostic: &mut Diag) -> Diagnostic {
86 let children = diagnostic
88 .children
89 .iter()
90 .map(|sub| self.sub_diagnostic(sub))
91 .chain(diagnostic.suggestions.iter().map(|sugg| self.suggestion_to_diagnostic(sugg)))
92 .collect();
93
94 Diagnostic {
95 message: diagnostic.label().into_owned(),
96 code: diagnostic.id().map(|code| DiagnosticCode { code, explanation: None }),
97 level: diagnostic.level.to_str(),
98 spans: self.spans(&diagnostic.span),
99 children,
100 rendered: Some(self.emit_diagnostic_to_buffer(diagnostic)),
101 }
102 }
103
104 fn sub_diagnostic(&self, diagnostic: &SubDiagnostic) -> Diagnostic {
105 Diagnostic {
106 message: diagnostic.label().into_owned(),
107 code: None,
108 level: diagnostic.level.to_str(),
109 spans: self.spans(&diagnostic.span),
110 children: vec![],
111 rendered: None,
112 }
113 }
114
115 fn suggestion_to_diagnostic(&self, sugg: &CodeSuggestion) -> Diagnostic {
116 let spans = sugg
118 .substitutions
119 .iter()
120 .flat_map(|sub| sub.parts.iter())
121 .map(|part| self.span_with_suggestion(part.span, part.snippet.to_string()))
122 .collect();
123
124 Diagnostic {
125 message: sugg.msg.as_str().to_string(),
126 code: None,
127 level: "help",
128 spans,
129 children: vec![],
130 rendered: None,
131 }
132 }
133
134 fn spans(&self, msp: &MultiSpan) -> Vec<DiagnosticSpan> {
135 msp.span_labels().iter().map(|label| self.span(label)).collect()
136 }
137
138 fn span(&self, label: &SpanLabel) -> DiagnosticSpan {
139 let sm = &**self.source_map();
140 let span = label.span;
141 let start = sm.lookup_char_pos(span.lo());
142 let end = sm.lookup_char_pos(span.hi());
143 DiagnosticSpan {
144 file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
145 byte_start: start.file.original_relative_byte_pos(span.lo()).0,
146 byte_end: start.file.original_relative_byte_pos(span.hi()).0,
147 line_start: start.line,
148 line_end: end.line,
149 column_start: start.col.0 + 1,
150 column_end: end.col.0 + 1,
151 is_primary: label.is_primary,
152 text: self.span_lines(span),
153 label: label.label.as_ref().map(|msg| msg.as_str().into()),
154 suggested_replacement: None,
155 }
156 }
157
158 fn span_with_suggestion(&self, span: Span, replacement: String) -> DiagnosticSpan {
159 let sm = &**self.source_map();
160 let start = sm.lookup_char_pos(span.lo());
161 let end = sm.lookup_char_pos(span.hi());
162 DiagnosticSpan {
163 file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
164 byte_start: start.file.original_relative_byte_pos(span.lo()).0,
165 byte_end: start.file.original_relative_byte_pos(span.hi()).0,
166 line_start: start.line,
167 line_end: end.line,
168 column_start: start.col.0 + 1,
169 column_end: end.col.0 + 1,
170 is_primary: true,
171 text: self.span_lines(span),
172 label: None,
173 suggested_replacement: Some(replacement),
174 }
175 }
176
177 fn span_lines(&self, span: Span) -> Vec<DiagnosticSpanLine> {
178 let Ok(f) = self.source_map().span_to_lines(span) else { return Vec::new() };
179 let sf = &*f.file;
180 f.data.iter().map(|line| self.span_line(sf, line)).collect()
181 }
182
183 fn span_line(&self, sf: &SourceFile, line: &LineInfo) -> DiagnosticSpanLine {
184 DiagnosticSpanLine {
185 text: sf.get_line(line.line_index).map_or_else(String::new, |l| l.to_string()),
186 highlight_start: line.start_col.0 + 1,
187 highlight_end: line.end_col.0 + 1,
188 }
189 }
190
191 fn solc_diagnostic(&mut self, diagnostic: &mut crate::diagnostics::Diag) -> SolcDiagnostic {
192 let primary = diagnostic.span.primary_span();
193 let file = primary
194 .map(|span| {
195 let sm = &**self.source_map();
196 let start = sm.lookup_char_pos(span.lo());
197 sm.filename_for_diagnostics(&start.file.name).to_string()
198 })
199 .unwrap_or_default();
200
201 let severity = to_severity(diagnostic.level);
202
203 SolcDiagnostic {
204 source_location: primary
205 .is_some()
206 .then(|| self.solc_span(&diagnostic.span, &file, None)),
207 secondary_source_locations: diagnostic
208 .children
209 .iter()
210 .map(|sub| self.solc_span(&sub.span, &file, Some(sub.label().into_owned())))
211 .collect(),
212 r#type: match severity {
213 Severity::Error => match diagnostic.level {
214 Level::Bug => "InternalCompilerError",
215 Level::Fatal => "FatalError",
216 Level::Error => "Exception",
217 _ => unreachable!(),
218 },
219 Severity::Warning => "Warning",
220 Severity::Info => "Info",
221 }
222 .into(),
223 component: "general".into(),
224 severity,
225 error_code: diagnostic.id(),
226 message: diagnostic.label().into_owned(),
227 formatted_message: Some(self.emit_diagnostic_to_buffer(diagnostic)),
228 }
229 }
230
231 fn solc_span(&self, span: &MultiSpan, file: &str, message: Option<String>) -> SourceLocation {
232 let sm = &**self.source_map();
233 let sp = span.primary_span();
234 SourceLocation {
235 file: sp
236 .map(|span| {
237 let start = sm.lookup_char_pos(span.lo());
238 sm.filename_for_diagnostics(&start.file.name).to_string()
239 })
240 .unwrap_or_else(|| file.into()),
241 start: sp
242 .map(|span| {
243 let start = sm.lookup_char_pos(span.lo());
244 start.file.original_relative_byte_pos(span.lo()).0
245 })
246 .unwrap_or(0),
247 end: sp
248 .map(|span| {
249 let end = sm.lookup_char_pos(span.hi());
250 end.file.original_relative_byte_pos(span.hi()).0
251 })
252 .unwrap_or(0),
253 message,
254 }
255 }
256
257 fn emit_diagnostic_to_buffer(&mut self, diagnostic: &mut crate::diagnostics::Diag) -> String {
258 self.human_emitter.emit_diagnostic(diagnostic);
259 std::mem::take(self.human_emitter.buffer_mut())
260 }
261
262 fn emit<T: ?Sized + Serialize>(&mut self, value: &T) -> io::Result<()> {
263 if self.pretty {
264 serde_json::to_writer_pretty(&mut *self.writer, value)
265 } else {
266 serde_json::to_writer(&mut *self.writer, value)
267 }?;
268 self.writer.write_all(b"\n")?;
269 self.writer.flush()
270 }
271}
272
273#[derive(Serialize)]
276#[serde(tag = "$message_type", rename_all = "snake_case")]
277enum EmitTyped {
278 Diagnostic(Diagnostic),
279}
280
281#[derive(Serialize)]
282struct Diagnostic {
283 message: String,
285 code: Option<DiagnosticCode>,
286 level: &'static str,
288 spans: Vec<DiagnosticSpan>,
289 children: Vec<Diagnostic>,
291 rendered: Option<String>,
293}
294
295#[derive(Serialize)]
296struct DiagnosticSpan {
297 file_name: String,
298 byte_start: u32,
299 byte_end: u32,
300 line_start: usize,
302 line_end: usize,
303 column_start: usize,
305 column_end: usize,
306 is_primary: bool,
309 text: Vec<DiagnosticSpanLine>,
311 label: Option<String>,
313 suggested_replacement: Option<String>,
316}
317
318#[derive(Serialize)]
319struct DiagnosticSpanLine {
320 text: String,
321
322 highlight_start: usize,
324
325 highlight_end: usize,
326}
327
328#[derive(Serialize)]
329struct DiagnosticCode {
330 code: String,
332 explanation: Option<&'static str>,
334}
335
336#[derive(Serialize)]
339#[serde(rename_all = "camelCase")]
340struct SolcDiagnostic {
341 source_location: Option<SourceLocation>,
342 secondary_source_locations: Vec<SourceLocation>,
343 r#type: String,
344 component: String,
345 severity: Severity,
346 error_code: Option<String>,
347 message: String,
348 formatted_message: Option<String>,
349}
350
351#[derive(Serialize)]
352struct SourceLocation {
353 file: String,
354 start: u32,
355 end: u32,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 message: Option<String>,
359}
360
361#[derive(Serialize)]
362#[serde(rename_all = "lowercase")]
363enum Severity {
364 Error,
365 Warning,
366 Info,
367}
368
369fn to_severity(level: Level) -> Severity {
370 match level {
371 Level::Bug | Level::Fatal | Level::Error => Severity::Error,
372 Level::Warning => Severity::Warning,
373 Level::Note
374 | Level::OnceNote
375 | Level::FailureNote
376 | Level::Help
377 | Level::OnceHelp
378 | Level::Allow => Severity::Info,
379 }
380}