solar_interface/diagnostics/emitter/
json.rs

1use super::{human::HumanBufferEmitter, io_panic, Emitter};
2use crate::{
3    diagnostics::{Level, MultiSpan, SpanLabel},
4    source_map::{LineInfo, SourceFile},
5    SourceMap, Span,
6};
7use anstream::ColorChoice;
8use serde::Serialize;
9use std::{io, sync::Arc};
10
11/// Diagnostic emitter that emits diagnostics as JSON.
12pub struct JsonEmitter {
13    writer: Box<dyn io::Write + Send>,
14    pretty: bool,
15    rustc_like: bool,
16
17    human_emitter: HumanBufferEmitter,
18}
19
20impl Emitter for JsonEmitter {
21    fn emit_diagnostic(&mut self, diagnostic: &crate::diagnostics::Diagnostic) {
22        if self.rustc_like {
23            let diagnostic = self.diagnostic(diagnostic);
24            self.emit(&EmitTyped::Diagnostic(diagnostic))
25        } else {
26            let diagnostic = self.solc_diagnostic(diagnostic);
27            self.emit(&diagnostic)
28        }
29        .unwrap_or_else(|e| io_panic(e));
30    }
31
32    fn source_map(&self) -> Option<&Arc<SourceMap>> {
33        Emitter::source_map(&self.human_emitter)
34    }
35}
36
37impl JsonEmitter {
38    /// Creates a new `JsonEmitter` that writes to given writer.
39    pub fn new(writer: Box<dyn io::Write + Send>, source_map: Arc<SourceMap>) -> Self {
40        Self {
41            writer,
42            pretty: false,
43            rustc_like: false,
44            human_emitter: HumanBufferEmitter::new(ColorChoice::Never).source_map(Some(source_map)),
45        }
46    }
47
48    /// Sets whether to pretty print the JSON.
49    pub fn pretty(mut self, pretty: bool) -> Self {
50        self.pretty = pretty;
51        self
52    }
53
54    /// Sets whether to emit diagnostics in a format that is compatible with rustc.
55    ///
56    /// Mainly used in UI testing.
57    pub fn rustc_like(mut self, yes: bool) -> Self {
58        self.rustc_like = yes;
59        self
60    }
61
62    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
63    pub fn ui_testing(mut self, yes: bool) -> Self {
64        self.human_emitter = self.human_emitter.ui_testing(yes);
65        self
66    }
67
68    fn source_map(&self) -> &Arc<SourceMap> {
69        Emitter::source_map(self).unwrap()
70    }
71
72    fn diagnostic(&mut self, diagnostic: &crate::diagnostics::Diagnostic) -> Diagnostic {
73        Diagnostic {
74            message: diagnostic.label().into_owned(),
75            code: diagnostic.id().map(|code| DiagnosticCode { code, explanation: None }),
76            level: diagnostic.level.to_str(),
77            spans: self.spans(&diagnostic.span),
78            children: diagnostic.children.iter().map(|sub| self.sub_diagnostic(sub)).collect(),
79            rendered: Some(self.emit_diagnostic_to_buffer(diagnostic)),
80        }
81    }
82
83    fn sub_diagnostic(&self, diagnostic: &crate::diagnostics::SubDiagnostic) -> Diagnostic {
84        Diagnostic {
85            message: diagnostic.label().into_owned(),
86            code: None,
87            level: diagnostic.level.to_str(),
88            spans: self.spans(&diagnostic.span),
89            children: vec![],
90            rendered: None,
91        }
92    }
93
94    fn spans(&self, msp: &MultiSpan) -> Vec<DiagnosticSpan> {
95        msp.span_labels().iter().map(|label| self.span(label)).collect()
96    }
97
98    fn span(&self, label: &SpanLabel) -> DiagnosticSpan {
99        let sm = &**self.source_map();
100        let span = label.span;
101        let start = sm.lookup_char_pos(span.lo());
102        let end = sm.lookup_char_pos(span.hi());
103        DiagnosticSpan {
104            file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
105            byte_start: start.file.original_relative_byte_pos(span.lo()).0,
106            byte_end: start.file.original_relative_byte_pos(span.hi()).0,
107            line_start: start.line,
108            line_end: end.line,
109            column_start: start.col.0 + 1,
110            column_end: end.col.0 + 1,
111            is_primary: label.is_primary,
112            text: self.span_lines(span),
113            label: label.label.as_ref().map(|msg| msg.as_str().into()),
114        }
115    }
116
117    fn span_lines(&self, span: Span) -> Vec<DiagnosticSpanLine> {
118        let Ok(f) = self.source_map().span_to_lines(span) else { return Vec::new() };
119        let sf = &*f.file;
120        f.lines.iter().map(|line| self.span_line(sf, line)).collect()
121    }
122
123    fn span_line(&self, sf: &SourceFile, line: &LineInfo) -> DiagnosticSpanLine {
124        DiagnosticSpanLine {
125            text: sf.get_line(line.line_index).map_or_else(String::new, |l| l.to_string()),
126            highlight_start: line.start_col.0 + 1,
127            highlight_end: line.end_col.0 + 1,
128        }
129    }
130
131    fn solc_diagnostic(&mut self, diagnostic: &crate::diagnostics::Diagnostic) -> SolcDiagnostic {
132        let primary = diagnostic.span.primary_span();
133        let file = primary
134            .map(|span| {
135                let sm = &**self.source_map();
136                let start = sm.lookup_char_pos(span.lo());
137                sm.filename_for_diagnostics(&start.file.name).to_string()
138            })
139            .unwrap_or_default();
140
141        let severity = to_severity(diagnostic.level);
142
143        SolcDiagnostic {
144            source_location: primary
145                .is_some()
146                .then(|| self.solc_span(&diagnostic.span, &file, None)),
147            secondary_source_locations: diagnostic
148                .children
149                .iter()
150                .map(|sub| self.solc_span(&sub.span, &file, Some(sub.label().into_owned())))
151                .collect(),
152            r#type: match severity {
153                Severity::Error => match diagnostic.level {
154                    Level::Bug => "InternalCompilerError",
155                    Level::Fatal => "FatalError",
156                    Level::Error => "Exception",
157                    _ => unreachable!(),
158                },
159                Severity::Warning => "Warning",
160                Severity::Info => "Info",
161            }
162            .into(),
163            component: "general".into(),
164            severity,
165            error_code: diagnostic.id(),
166            message: diagnostic.label().into_owned(),
167            formatted_message: Some(self.emit_diagnostic_to_buffer(diagnostic)),
168        }
169    }
170
171    fn solc_span(&self, span: &MultiSpan, file: &str, message: Option<String>) -> SourceLocation {
172        let sm = &**self.source_map();
173        let sp = span.primary_span();
174        SourceLocation {
175            file: sp
176                .map(|span| {
177                    let start = sm.lookup_char_pos(span.lo());
178                    sm.filename_for_diagnostics(&start.file.name).to_string()
179                })
180                .unwrap_or_else(|| file.into()),
181            start: sp
182                .map(|span| {
183                    let start = sm.lookup_char_pos(span.lo());
184                    start.file.original_relative_byte_pos(span.lo()).0
185                })
186                .unwrap_or(0),
187            end: sp
188                .map(|span| {
189                    let end = sm.lookup_char_pos(span.hi());
190                    end.file.original_relative_byte_pos(span.hi()).0
191                })
192                .unwrap_or(0),
193            message,
194        }
195    }
196
197    fn emit_diagnostic_to_buffer(&mut self, diagnostic: &crate::diagnostics::Diagnostic) -> String {
198        self.human_emitter.emit_diagnostic(diagnostic);
199        std::mem::take(self.human_emitter.buffer_mut())
200    }
201
202    fn emit<T: ?Sized + Serialize>(&mut self, value: &T) -> io::Result<()> {
203        if self.pretty {
204            serde_json::to_writer_pretty(&mut *self.writer, value)
205        } else {
206            serde_json::to_writer(&mut *self.writer, value)
207        }?;
208        self.writer.write_all(b"\n")?;
209        self.writer.flush()
210    }
211}
212
213// Rustc-like JSON format.
214
215#[derive(Serialize)]
216#[serde(tag = "$message_type", rename_all = "snake_case")]
217enum EmitTyped {
218    Diagnostic(Diagnostic),
219}
220
221#[derive(Serialize)]
222struct Diagnostic {
223    /// The primary error message.
224    message: String,
225    code: Option<DiagnosticCode>,
226    /// "error", "warning", "note", "help".
227    level: &'static str,
228    spans: Vec<DiagnosticSpan>,
229    /// Associated diagnostic messages.
230    children: Vec<Diagnostic>,
231    /// The message as the compiler would render it.
232    rendered: Option<String>,
233}
234
235#[derive(Serialize)]
236struct DiagnosticSpan {
237    file_name: String,
238    byte_start: u32,
239    byte_end: u32,
240    /// 1-based.
241    line_start: usize,
242    line_end: usize,
243    /// 1-based, character offset.
244    column_start: usize,
245    column_end: usize,
246    /// Is this a "primary" span -- meaning the point, or one of the points,
247    /// where the error occurred?
248    is_primary: bool,
249    /// Source text from the start of line_start to the end of line_end.
250    text: Vec<DiagnosticSpanLine>,
251    /// Label that should be placed at this location (if any)
252    label: Option<String>,
253    // /// If we are suggesting a replacement, this will contain text
254    // /// that should be sliced in atop this span.
255    // suggested_replacement: Option<String>,
256}
257
258#[derive(Serialize)]
259struct DiagnosticSpanLine {
260    text: String,
261
262    /// 1-based, character offset in self.text.
263    highlight_start: usize,
264
265    highlight_end: usize,
266}
267
268#[derive(Serialize)]
269struct DiagnosticCode {
270    /// The code itself.
271    code: String,
272    /// An explanation for the code.
273    explanation: Option<&'static str>,
274}
275
276// Solc JSON format.
277
278#[derive(Serialize)]
279#[serde(rename_all = "camelCase")]
280struct SolcDiagnostic {
281    source_location: Option<SourceLocation>,
282    secondary_source_locations: Vec<SourceLocation>,
283    r#type: String,
284    component: String,
285    severity: Severity,
286    error_code: Option<String>,
287    message: String,
288    formatted_message: Option<String>,
289}
290
291#[derive(Serialize)]
292struct SourceLocation {
293    file: String,
294    start: u32,
295    end: u32,
296    // Some if it's a secondary source location.
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    message: Option<String>,
299}
300
301#[derive(Serialize)]
302#[serde(rename_all = "lowercase")]
303enum Severity {
304    Error,
305    Warning,
306    Info,
307}
308
309fn to_severity(level: Level) -> Severity {
310    match level {
311        Level::Bug | Level::Fatal | Level::Error => Severity::Error,
312        Level::Warning => Severity::Warning,
313        #[rustfmt::skip]
314        Level::Note | Level::OnceNote | Level::FailureNote |
315        Level::Help | Level::OnceHelp |
316        Level::Allow => Severity::Info,
317    }
318}