Skip to main content

solar_interface/diagnostics/emitter/
json.rs

1use 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
12/// Diagnostic emitter that emits diagnostics as JSON.
13pub 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    /// Creates a new `JsonEmitter` that writes to given writer.
44    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    /// Sets whether to pretty print the JSON.
54    pub fn pretty(mut self, pretty: bool) -> Self {
55        self.pretty = pretty;
56        self
57    }
58
59    /// Sets whether to emit diagnostics in a format that is compatible with rustc.
60    ///
61    /// Mainly used in UI testing.
62    pub fn rustc_like(mut self, yes: bool) -> Self {
63        self.rustc_like = yes;
64        self
65    }
66
67    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
68    pub fn ui_testing(mut self, yes: bool) -> Self {
69        self.human_emitter = self.human_emitter.ui_testing(yes);
70        self
71    }
72
73    /// Sets the human emitter kind for rendered messages.
74    pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
75        self.human_emitter = self.human_emitter.human_kind(kind);
76        self
77    }
78
79    /// Sets the terminal width for formatting.
80    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        // Unlike the human emitter, all suggestions are preserved as separate diagnostic children.
91        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        // Collect all spans from all substitutions
123        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    /// Converts a diagnostic to a solc-compatible JSON diagnostic.
198    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// Rustc-like JSON format.
288
289#[derive(Serialize)]
290#[serde(tag = "$message_type", rename_all = "snake_case")]
291enum EmitTyped {
292    Diagnostic(Diagnostic),
293}
294
295#[derive(Serialize)]
296struct Diagnostic {
297    /// The primary error message.
298    message: String,
299    code: Option<DiagnosticCode>,
300    /// "error", "warning", "note", "help".
301    level: &'static str,
302    spans: Vec<DiagnosticSpan>,
303    /// Associated diagnostic messages.
304    children: Vec<Self>,
305    /// The message as the compiler would render it.
306    rendered: Option<String>,
307}
308
309#[derive(Serialize)]
310struct DiagnosticSpan {
311    file_name: String,
312    byte_start: u32,
313    byte_end: u32,
314    /// 1-based.
315    line_start: usize,
316    line_end: usize,
317    /// 1-based, character offset.
318    column_start: usize,
319    column_end: usize,
320    /// Is this a "primary" span -- meaning the point, or one of the points,
321    /// where the error occurred?
322    is_primary: bool,
323    /// Source text from the start of line_start to the end of line_end.
324    text: Vec<DiagnosticSpanLine>,
325    /// Label that should be placed at this location (if any)
326    label: Option<String>,
327    /// If we are suggesting a replacement, this will contain text
328    /// that should be sliced in atop this span.
329    suggested_replacement: Option<String>,
330}
331
332#[derive(Serialize)]
333struct DiagnosticSpanLine {
334    text: String,
335
336    /// 1-based, character offset in self.text.
337    highlight_start: usize,
338
339    highlight_end: usize,
340}
341
342#[derive(Serialize)]
343struct DiagnosticCode {
344    /// The code itself.
345    code: String,
346    /// An explanation for the code.
347    explanation: Option<&'static str>,
348}
349
350// Solc JSON format.
351
352#[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    /// Returns `true` if this diagnostic has error severity.
374    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    // Some if it's a secondary source location.
386    #[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}