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