rustc_errors/
json.rs

1//! A JSON emitter for errors.
2//!
3//! This works by converting errors to a simplified structural format (see the
4//! structs at the start of the file) and then serializing them. These should
5//! contain as much information about the error as possible.
6//!
7//! The format of the JSON output should be considered *unstable*. For now the
8//! structs at the end of this file (Diagnostic*) specify the error format.
9
10// FIXME: spec the JSON output properly.
11
12use std::error::Report;
13use std::io::{self, Write};
14use std::path::Path;
15use std::sync::{Arc, Mutex};
16use std::vec;
17
18use derive_setters::Setters;
19use rustc_data_structures::sync::IntoDynSyncSend;
20use rustc_error_messages::FluentArgs;
21use rustc_lint_defs::Applicability;
22use rustc_span::Span;
23use rustc_span::hygiene::ExpnData;
24use rustc_span::source_map::{FilePathMapping, SourceMap};
25use serde::Serialize;
26use termcolor::{ColorSpec, WriteColor};
27
28use crate::diagnostic::IsLint;
29use crate::emitter::{
30    ColorConfig, Destination, Emitter, HumanEmitter, HumanReadableErrorType, OutputTheme,
31    should_show_source_code,
32};
33use crate::registry::Registry;
34use crate::translation::{Translate, to_fluent_args};
35use crate::{
36    CodeSuggestion, FluentBundle, LazyFallbackBundle, MultiSpan, SpanLabel, Subdiag, Suggestions,
37    TerminalUrl,
38};
39
40#[cfg(test)]
41mod tests;
42
43#[derive(Setters)]
44pub struct JsonEmitter {
45    #[setters(skip)]
46    dst: IntoDynSyncSend<Box<dyn Write + Send>>,
47    #[setters(skip)]
48    sm: Option<Arc<SourceMap>>,
49    fluent_bundle: Option<Arc<FluentBundle>>,
50    #[setters(skip)]
51    fallback_bundle: LazyFallbackBundle,
52    #[setters(skip)]
53    pretty: bool,
54    ui_testing: bool,
55    ignored_directories_in_source_blocks: Vec<String>,
56    #[setters(skip)]
57    json_rendered: HumanReadableErrorType,
58    color_config: ColorConfig,
59    diagnostic_width: Option<usize>,
60    macro_backtrace: bool,
61    track_diagnostics: bool,
62    terminal_url: TerminalUrl,
63}
64
65impl JsonEmitter {
66    pub fn new(
67        dst: Box<dyn Write + Send>,
68        sm: Option<Arc<SourceMap>>,
69        fallback_bundle: LazyFallbackBundle,
70        pretty: bool,
71        json_rendered: HumanReadableErrorType,
72        color_config: ColorConfig,
73    ) -> JsonEmitter {
74        JsonEmitter {
75            dst: IntoDynSyncSend(dst),
76            sm,
77            fluent_bundle: None,
78            fallback_bundle,
79            pretty,
80            ui_testing: false,
81            ignored_directories_in_source_blocks: Vec::new(),
82            json_rendered,
83            color_config,
84            diagnostic_width: None,
85            macro_backtrace: false,
86            track_diagnostics: false,
87            terminal_url: TerminalUrl::No,
88        }
89    }
90
91    fn emit(&mut self, val: EmitTyped<'_>) -> io::Result<()> {
92        if self.pretty {
93            serde_json::to_writer_pretty(&mut *self.dst, &val)?
94        } else {
95            serde_json::to_writer(&mut *self.dst, &val)?
96        };
97        self.dst.write_all(b"\n")?;
98        self.dst.flush()
99    }
100}
101
102#[derive(Serialize)]
103#[serde(tag = "$message_type", rename_all = "snake_case")]
104enum EmitTyped<'a> {
105    Diagnostic(Diagnostic),
106    Artifact(ArtifactNotification<'a>),
107    FutureIncompat(FutureIncompatReport<'a>),
108    UnusedExtern(UnusedExterns<'a>),
109}
110
111impl Translate for JsonEmitter {
112    fn fluent_bundle(&self) -> Option<&FluentBundle> {
113        self.fluent_bundle.as_deref()
114    }
115
116    fn fallback_fluent_bundle(&self) -> &FluentBundle {
117        &self.fallback_bundle
118    }
119}
120
121impl Emitter for JsonEmitter {
122    fn emit_diagnostic(&mut self, diag: crate::DiagInner, registry: &Registry) {
123        let data = Diagnostic::from_errors_diagnostic(diag, self, registry);
124        let result = self.emit(EmitTyped::Diagnostic(data));
125        if let Err(e) = result {
126            panic!("failed to print diagnostics: {e:?}");
127        }
128    }
129
130    fn emit_artifact_notification(&mut self, path: &Path, artifact_type: &str) {
131        let data = ArtifactNotification { artifact: path, emit: artifact_type };
132        let result = self.emit(EmitTyped::Artifact(data));
133        if let Err(e) = result {
134            panic!("failed to print notification: {e:?}");
135        }
136    }
137
138    fn emit_future_breakage_report(&mut self, diags: Vec<crate::DiagInner>, registry: &Registry) {
139        let data: Vec<FutureBreakageItem<'_>> = diags
140            .into_iter()
141            .map(|mut diag| {
142                // Allowed or expected lints don't normally (by definition) emit a lint
143                // but future incompat lints are special and are emitted anyway.
144                //
145                // So to avoid ICEs and confused users we "upgrade" the lint level for
146                // those `FutureBreakageItem` to warn.
147                if matches!(diag.level, crate::Level::Allow | crate::Level::Expect(..)) {
148                    diag.level = crate::Level::Warning;
149                }
150                FutureBreakageItem {
151                    diagnostic: EmitTyped::Diagnostic(Diagnostic::from_errors_diagnostic(
152                        diag, self, registry,
153                    )),
154                }
155            })
156            .collect();
157        let report = FutureIncompatReport { future_incompat_report: data };
158        let result = self.emit(EmitTyped::FutureIncompat(report));
159        if let Err(e) = result {
160            panic!("failed to print future breakage report: {e:?}");
161        }
162    }
163
164    fn emit_unused_externs(&mut self, lint_level: rustc_lint_defs::Level, unused_externs: &[&str]) {
165        let lint_level = lint_level.as_str();
166        let data = UnusedExterns { lint_level, unused_extern_names: unused_externs };
167        let result = self.emit(EmitTyped::UnusedExtern(data));
168        if let Err(e) = result {
169            panic!("failed to print unused externs: {e:?}");
170        }
171    }
172
173    fn source_map(&self) -> Option<&SourceMap> {
174        self.sm.as_deref()
175    }
176
177    fn should_show_explain(&self) -> bool {
178        !self.json_rendered.short()
179    }
180}
181
182// The following data types are provided just for serialisation.
183
184#[derive(Serialize)]
185struct Diagnostic {
186    /// The primary error message.
187    message: String,
188    code: Option<DiagnosticCode>,
189    /// "error: internal compiler error", "error", "warning", "note", "help".
190    level: &'static str,
191    spans: Vec<DiagnosticSpan>,
192    /// Associated diagnostic messages.
193    children: Vec<Diagnostic>,
194    /// The message as rustc would render it.
195    rendered: Option<String>,
196}
197
198#[derive(Serialize)]
199struct DiagnosticSpan {
200    file_name: String,
201    byte_start: u32,
202    byte_end: u32,
203    /// 1-based.
204    line_start: usize,
205    line_end: usize,
206    /// 1-based, character offset.
207    column_start: usize,
208    column_end: usize,
209    /// Is this a "primary" span -- meaning the point, or one of the points,
210    /// where the error occurred?
211    is_primary: bool,
212    /// Source text from the start of line_start to the end of line_end.
213    text: Vec<DiagnosticSpanLine>,
214    /// Label that should be placed at this location (if any)
215    label: Option<String>,
216    /// If we are suggesting a replacement, this will contain text
217    /// that should be sliced in atop this span.
218    suggested_replacement: Option<String>,
219    /// If the suggestion is approximate
220    suggestion_applicability: Option<Applicability>,
221    /// Macro invocations that created the code at this span, if any.
222    expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
223}
224
225#[derive(Serialize)]
226struct DiagnosticSpanLine {
227    text: String,
228
229    /// 1-based, character offset in self.text.
230    highlight_start: usize,
231
232    highlight_end: usize,
233}
234
235#[derive(Serialize)]
236struct DiagnosticSpanMacroExpansion {
237    /// span where macro was applied to generate this code; note that
238    /// this may itself derive from a macro (if
239    /// `span.expansion.is_some()`)
240    span: DiagnosticSpan,
241
242    /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]")
243    macro_decl_name: String,
244
245    /// span where macro was defined (if known)
246    def_site_span: DiagnosticSpan,
247}
248
249#[derive(Serialize)]
250struct DiagnosticCode {
251    /// The error code (e.g. "E1234"), if the diagnostic has one. Or the lint
252    /// name, if it's a lint without an error code.
253    code: String,
254    /// An explanation for the code.
255    explanation: Option<&'static str>,
256}
257
258#[derive(Serialize)]
259struct ArtifactNotification<'a> {
260    /// The path of the artifact.
261    artifact: &'a Path,
262    /// What kind of artifact we're emitting.
263    emit: &'a str,
264}
265
266#[derive(Serialize)]
267struct FutureBreakageItem<'a> {
268    // Always EmitTyped::Diagnostic, but we want to make sure it gets serialized
269    // with "$message_type".
270    diagnostic: EmitTyped<'a>,
271}
272
273#[derive(Serialize)]
274struct FutureIncompatReport<'a> {
275    future_incompat_report: Vec<FutureBreakageItem<'a>>,
276}
277
278// NOTE: Keep this in sync with the equivalent structs in rustdoc's
279// doctest component (as well as cargo).
280// We could unify this struct the one in rustdoc but they have different
281// ownership semantics, so doing so would create wasteful allocations.
282#[derive(Serialize)]
283struct UnusedExterns<'a> {
284    /// The severity level of the unused dependencies lint
285    lint_level: &'a str,
286    /// List of unused externs by their names.
287    unused_extern_names: &'a [&'a str],
288}
289
290impl Diagnostic {
291    /// Converts from `rustc_errors::DiagInner` to `Diagnostic`.
292    fn from_errors_diagnostic(
293        diag: crate::DiagInner,
294        je: &JsonEmitter,
295        registry: &Registry,
296    ) -> Diagnostic {
297        let args = to_fluent_args(diag.args.iter());
298        let sugg_to_diag = |sugg: &CodeSuggestion| {
299            let translated_message =
300                je.translate_message(&sugg.msg, &args).map_err(Report::new).unwrap();
301            Diagnostic {
302                message: translated_message.to_string(),
303                code: None,
304                level: "help",
305                spans: DiagnosticSpan::from_suggestion(sugg, &args, je),
306                children: vec![],
307                rendered: None,
308            }
309        };
310        let sugg = match &diag.suggestions {
311            Suggestions::Enabled(suggestions) => suggestions.iter().map(sugg_to_diag),
312            Suggestions::Sealed(suggestions) => suggestions.iter().map(sugg_to_diag),
313            Suggestions::Disabled => [].iter().map(sugg_to_diag),
314        };
315
316        // generate regular command line output and store it in the json
317
318        // A threadsafe buffer for writing.
319        #[derive(Default, Clone)]
320        struct BufWriter(Arc<Mutex<Vec<u8>>>);
321
322        impl Write for BufWriter {
323            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
324                self.0.lock().unwrap().write(buf)
325            }
326            fn flush(&mut self) -> io::Result<()> {
327                self.0.lock().unwrap().flush()
328            }
329        }
330        impl WriteColor for BufWriter {
331            fn supports_color(&self) -> bool {
332                false
333            }
334
335            fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
336                Ok(())
337            }
338
339            fn reset(&mut self) -> io::Result<()> {
340                Ok(())
341            }
342        }
343
344        let translated_message = je.translate_messages(&diag.messages, &args);
345
346        let code = if let Some(code) = diag.code {
347            Some(DiagnosticCode {
348                code: code.to_string(),
349                explanation: registry.try_find_description(code).ok(),
350            })
351        } else if let Some(IsLint { name, .. }) = &diag.is_lint {
352            Some(DiagnosticCode { code: name.to_string(), explanation: None })
353        } else {
354            None
355        };
356        let level = diag.level.to_str();
357        let spans = DiagnosticSpan::from_multispan(&diag.span, &args, je);
358        let children = diag
359            .children
360            .iter()
361            .map(|c| Diagnostic::from_sub_diagnostic(c, &args, je))
362            .chain(sugg)
363            .collect();
364
365        let buf = BufWriter::default();
366        let mut dst: Destination = Box::new(buf.clone());
367        let short = je.json_rendered.short();
368        match je.color_config {
369            ColorConfig::Always | ColorConfig::Auto => dst = Box::new(termcolor::Ansi::new(dst)),
370            ColorConfig::Never => {}
371        }
372        HumanEmitter::new(dst, Arc::clone(&je.fallback_bundle))
373            .short_message(short)
374            .sm(je.sm.clone())
375            .fluent_bundle(je.fluent_bundle.clone())
376            .diagnostic_width(je.diagnostic_width)
377            .macro_backtrace(je.macro_backtrace)
378            .track_diagnostics(je.track_diagnostics)
379            .terminal_url(je.terminal_url)
380            .ui_testing(je.ui_testing)
381            .ignored_directories_in_source_blocks(je.ignored_directories_in_source_blocks.clone())
382            .theme(if let HumanReadableErrorType::Unicode = je.json_rendered {
383                OutputTheme::Unicode
384            } else {
385                OutputTheme::Ascii
386            })
387            .emit_diagnostic(diag, registry);
388        let buf = Arc::try_unwrap(buf.0).unwrap().into_inner().unwrap();
389        let buf = String::from_utf8(buf).unwrap();
390
391        Diagnostic {
392            message: translated_message.to_string(),
393            code,
394            level,
395            spans,
396            children,
397            rendered: Some(buf),
398        }
399    }
400
401    fn from_sub_diagnostic(
402        subdiag: &Subdiag,
403        args: &FluentArgs<'_>,
404        je: &JsonEmitter,
405    ) -> Diagnostic {
406        let translated_message = je.translate_messages(&subdiag.messages, args);
407        Diagnostic {
408            message: translated_message.to_string(),
409            code: None,
410            level: subdiag.level.to_str(),
411            spans: DiagnosticSpan::from_multispan(&subdiag.span, args, je),
412            children: vec![],
413            rendered: None,
414        }
415    }
416}
417
418impl DiagnosticSpan {
419    fn from_span_label(
420        span: SpanLabel,
421        suggestion: Option<(&String, Applicability)>,
422        args: &FluentArgs<'_>,
423        je: &JsonEmitter,
424    ) -> DiagnosticSpan {
425        Self::from_span_etc(
426            span.span,
427            span.is_primary,
428            span.label
429                .as_ref()
430                .map(|m| je.translate_message(m, args).unwrap())
431                .map(|m| m.to_string()),
432            suggestion,
433            je,
434        )
435    }
436
437    fn from_span_etc(
438        span: Span,
439        is_primary: bool,
440        label: Option<String>,
441        suggestion: Option<(&String, Applicability)>,
442        je: &JsonEmitter,
443    ) -> DiagnosticSpan {
444        // obtain the full backtrace from the `macro_backtrace`
445        // helper; in some ways, it'd be better to expand the
446        // backtrace ourselves, but the `macro_backtrace` helper makes
447        // some decision, such as dropping some frames, and I don't
448        // want to duplicate that logic here.
449        let backtrace = span.macro_backtrace();
450        DiagnosticSpan::from_span_full(span, is_primary, label, suggestion, backtrace, je)
451    }
452
453    fn from_span_full(
454        mut span: Span,
455        is_primary: bool,
456        label: Option<String>,
457        suggestion: Option<(&String, Applicability)>,
458        mut backtrace: impl Iterator<Item = ExpnData>,
459        je: &JsonEmitter,
460    ) -> DiagnosticSpan {
461        let empty_source_map;
462        let sm = match &je.sm {
463            Some(s) => s,
464            None => {
465                span = rustc_span::DUMMY_SP;
466                empty_source_map = Arc::new(SourceMap::new(FilePathMapping::empty()));
467                empty_source_map
468                    .new_source_file(std::path::PathBuf::from("empty.rs").into(), String::new());
469                &empty_source_map
470            }
471        };
472        let start = sm.lookup_char_pos(span.lo());
473        // If this goes from the start of a line to the end and the replacement
474        // is an empty string, increase the length to include the newline so we don't
475        // leave an empty line
476        if start.col.0 == 0
477            && let Some((suggestion, _)) = suggestion
478            && suggestion.is_empty()
479            && let Ok(after) = sm.span_to_next_source(span)
480            && after.starts_with('\n')
481        {
482            span = span.with_hi(span.hi() + rustc_span::BytePos(1));
483        }
484        let end = sm.lookup_char_pos(span.hi());
485        let backtrace_step = backtrace.next().map(|bt| {
486            let call_site = Self::from_span_full(bt.call_site, false, None, None, backtrace, je);
487            let def_site_span = Self::from_span_full(
488                sm.guess_head_span(bt.def_site),
489                false,
490                None,
491                None,
492                [].into_iter(),
493                je,
494            );
495            Box::new(DiagnosticSpanMacroExpansion {
496                span: call_site,
497                macro_decl_name: bt.kind.descr(),
498                def_site_span,
499            })
500        });
501
502        DiagnosticSpan {
503            file_name: sm.filename_for_diagnostics(&start.file.name).to_string(),
504            byte_start: start.file.original_relative_byte_pos(span.lo()).0,
505            byte_end: start.file.original_relative_byte_pos(span.hi()).0,
506            line_start: start.line,
507            line_end: end.line,
508            column_start: start.col.0 + 1,
509            column_end: end.col.0 + 1,
510            is_primary,
511            text: DiagnosticSpanLine::from_span(span, je),
512            suggested_replacement: suggestion.map(|x| x.0.clone()),
513            suggestion_applicability: suggestion.map(|x| x.1),
514            expansion: backtrace_step,
515            label,
516        }
517    }
518
519    fn from_multispan(
520        msp: &MultiSpan,
521        args: &FluentArgs<'_>,
522        je: &JsonEmitter,
523    ) -> Vec<DiagnosticSpan> {
524        msp.span_labels()
525            .into_iter()
526            .map(|span_str| Self::from_span_label(span_str, None, args, je))
527            .collect()
528    }
529
530    fn from_suggestion(
531        suggestion: &CodeSuggestion,
532        args: &FluentArgs<'_>,
533        je: &JsonEmitter,
534    ) -> Vec<DiagnosticSpan> {
535        suggestion
536            .substitutions
537            .iter()
538            .flat_map(|substitution| {
539                substitution.parts.iter().map(move |suggestion_inner| {
540                    let span_label =
541                        SpanLabel { span: suggestion_inner.span, is_primary: true, label: None };
542                    DiagnosticSpan::from_span_label(
543                        span_label,
544                        Some((&suggestion_inner.snippet, suggestion.applicability)),
545                        args,
546                        je,
547                    )
548                })
549            })
550            .collect()
551    }
552}
553
554impl DiagnosticSpanLine {
555    fn line_from_source_file(
556        sf: &rustc_span::SourceFile,
557        index: usize,
558        h_start: usize,
559        h_end: usize,
560    ) -> DiagnosticSpanLine {
561        DiagnosticSpanLine {
562            text: sf.get_line(index).map_or_else(String::new, |l| l.into_owned()),
563            highlight_start: h_start,
564            highlight_end: h_end,
565        }
566    }
567
568    /// Creates a list of DiagnosticSpanLines from span - each line with any part
569    /// of `span` gets a DiagnosticSpanLine, with the highlight indicating the
570    /// `span` within the line.
571    fn from_span(span: Span, je: &JsonEmitter) -> Vec<DiagnosticSpanLine> {
572        je.sm
573            .as_ref()
574            .and_then(|sm| {
575                let lines = sm.span_to_lines(span).ok()?;
576                // We can't get any lines if the source is unavailable.
577                if !should_show_source_code(
578                    &je.ignored_directories_in_source_blocks,
579                    &sm,
580                    &lines.file,
581                ) {
582                    return None;
583                }
584
585                let sf = &*lines.file;
586                let span_lines = lines
587                    .lines
588                    .iter()
589                    .map(|line| {
590                        DiagnosticSpanLine::line_from_source_file(
591                            sf,
592                            line.line_index,
593                            line.start_col.0 + 1,
594                            line.end_col.0 + 1,
595                        )
596                    })
597                    .collect();
598                Some(span_lines)
599            })
600            .unwrap_or_default()
601    }
602}