slicec/
diagnostic_emitter.rs

1// Copyright (c) ZeroC, Inc.
2
3use crate::diagnostics::{Diagnostic, DiagnosticLevel};
4use crate::slice_file::{SliceFile, Span};
5use crate::slice_options::{DiagnosticFormat, SliceOptions};
6use serde::ser::SerializeStruct;
7use serde::Serializer;
8use std::io::{Result, Write};
9use std::path::Path;
10
11#[derive(Debug)]
12pub struct DiagnosticEmitter<'a, T: Write> {
13    /// Reference to the output that diagnostics should be emitted to.
14    output: &'a mut T,
15    /// Can specify `json` to serialize errors as JSON or `human` to pretty-print them.
16    diagnostic_format: DiagnosticFormat,
17    /// If true, diagnostic output will not be styled with colors (only used in `human` format).
18    disable_color: bool,
19    /// Provides the emitter access to the slice files that were compiled so it can extract snippets from them.
20    files: &'a [SliceFile],
21}
22
23impl<'a, T: Write> DiagnosticEmitter<'a, T> {
24    pub fn new(output: &'a mut T, slice_options: &SliceOptions, files: &'a [SliceFile]) -> Self {
25        DiagnosticEmitter {
26            output,
27            diagnostic_format: slice_options.diagnostic_format,
28            disable_color: slice_options.disable_color,
29            files,
30        }
31    }
32
33    pub fn emit_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) -> Result<()> {
34        // Disable colors if the user requested no colors.
35        if self.disable_color {
36            console::set_colors_enabled(false);
37            console::set_colors_enabled_stderr(false);
38        }
39
40        // Emit the diagnostics in whatever form the user requested.
41        match self.diagnostic_format {
42            DiagnosticFormat::Human => self.emit_diagnostics_in_human(diagnostics),
43            DiagnosticFormat::Json => self.emit_diagnostics_in_json(diagnostics),
44        }
45    }
46
47    fn emit_diagnostics_in_human(&mut self, diagnostics: Vec<Diagnostic>) -> Result<()> {
48        for diagnostic in diagnostics {
49            // Style the prefix. Note that for `Notes` we do not insert a newline since they should be "attached"
50            // to the previously emitted diagnostic.
51            let code = diagnostic.code();
52            let prefix = match diagnostic.level() {
53                DiagnosticLevel::Error => console::style(format!("error [{code}]")).red().bold(),
54                DiagnosticLevel::Warning => console::style(format!("warning [{code}]")).yellow().bold(),
55                DiagnosticLevel::Allowed => continue,
56            };
57
58            // Emit the message with the prefix.
59            writeln!(self.output, "{prefix}: {}", console::style(diagnostic.message()).bold())?;
60
61            // If the diagnostic contains a span, show a snippet containing the offending code.
62            if let Some(span) = diagnostic.span() {
63                self.emit_snippet(span)?;
64            }
65
66            // If the diagnostic contains notes, display them.
67            for note in diagnostic.notes() {
68                writeln!(
69                    self.output,
70                    "{}: {}",
71                    console::style("note").blue().bold(),
72                    console::style(&note.message).bold(),
73                )?;
74
75                if let Some(span) = &note.span {
76                    self.emit_snippet(span)?;
77                }
78            }
79        }
80        Ok(())
81    }
82
83    fn emit_diagnostics_in_json(&mut self, diagnostics: Vec<Diagnostic>) -> Result<()> {
84        // Write each diagnostic as a single line of JSON.
85        for diagnostic in diagnostics {
86            let severity = match diagnostic.level() {
87                DiagnosticLevel::Error => "error",
88                DiagnosticLevel::Warning => "warning",
89                DiagnosticLevel::Allowed => continue,
90            };
91
92            let mut serializer = serde_json::Serializer::new(&mut *self.output);
93            let mut state = serializer.serialize_struct("Diagnostic", 5)?;
94            state.serialize_field("message", &diagnostic.message())?;
95            state.serialize_field("severity", severity)?;
96            state.serialize_field("span", &diagnostic.span())?;
97            state.serialize_field("notes", diagnostic.notes())?;
98            state.serialize_field("error_code", diagnostic.code())?;
99            state.end()?;
100            writeln!(self.output)?; // Separate each diagnostic by a newline character.
101        }
102        Ok(())
103    }
104
105    fn emit_snippet(&mut self, span: &Span) -> Result<()> {
106        // Display the file name and line row and column where the error began.
107        writeln!(
108            self.output,
109            " {} {}:{}:{}",
110            console::style("-->").blue().bold(),
111            Path::new(&span.file).display(),
112            span.start.row,
113            span.start.col,
114        )?;
115
116        // Display the line of code where the error occurred.
117        let file = self.files.iter().find(|f| f.relative_path == span.file).unwrap();
118        writeln!(self.output, "{}", file.get_snippet(span.start, span.end))?;
119
120        Ok(())
121    }
122}
123
124pub fn emit_totals(total_warnings: usize, total_errors: usize) -> Result<()> {
125    // Totals are always printed to stdout.
126    let stdout = &mut console::Term::stdout();
127
128    if total_warnings > 0 {
129        let warnings = console::style("Warnings").yellow().bold();
130        writeln!(stdout, "{warnings}: Compilation generated {total_warnings} warning(s)")?;
131    }
132    if total_errors > 0 {
133        let failed = console::style("Failed").red().bold();
134        writeln!(stdout, "{failed}: Compilation failed with {total_errors} error(s)")?;
135    }
136
137    Ok(())
138}