Skip to main content

plc_diagnostics/reporter/
codespan.rs

1use codespan_reporting::{
2    diagnostic::{Diagnostic, Label},
3    files::SimpleFiles,
4    term::termcolor::{Buffer, ColorChoice, StandardStream, WriteColor},
5};
6use plc_source::source_location::CodeSpan;
7
8use crate::diagnostics::Severity;
9
10use super::{DiagnosticReporter, ResolvedDiagnostics};
11
12enum Writer {
13    /// Indicates that the writer will store its output into a buffer
14    Buffer(Buffer),
15
16    /// Indicates that the writer will redirect its output to the terminal
17    Stream(StandardStream),
18}
19
20impl std::io::Write for Writer {
21    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
22        match self {
23            Writer::Buffer(writer) => writer.write(buf),
24            Writer::Stream(writer) => writer.write(buf),
25        }
26    }
27
28    fn flush(&mut self) -> std::io::Result<()> {
29        match self {
30            Writer::Buffer(writer) => writer.flush(),
31            Writer::Stream(writer) => writer.flush(),
32        }
33    }
34}
35
36impl WriteColor for Writer {
37    fn supports_color(&self) -> bool {
38        match self {
39            Writer::Buffer(writer) => writer.supports_color(),
40            Writer::Stream(writer) => writer.supports_color(),
41        }
42    }
43
44    fn set_color(&mut self, spec: &codespan_reporting::term::termcolor::ColorSpec) -> std::io::Result<()> {
45        match self {
46            Writer::Buffer(writer) => writer.set_color(spec),
47            Writer::Stream(writer) => writer.set_color(spec),
48        }
49    }
50
51    fn reset(&mut self) -> std::io::Result<()> {
52        match self {
53            Writer::Buffer(writer) => writer.reset(),
54            Writer::Stream(writer) => writer.reset(),
55        }
56    }
57}
58
59/// A reporter that reports diagnostics using [`codespan_reporting`].
60pub struct CodeSpanDiagnosticReporter {
61    files: SimpleFiles<String, String>,
62    config: codespan_reporting::term::Config,
63    writer: Writer,
64}
65
66impl CodeSpanDiagnosticReporter {
67    /// Creates a new reporter which redirects its output to the terminal
68    pub(crate) fn terminal(config: codespan_reporting::term::Config, writer: StandardStream) -> Self {
69        CodeSpanDiagnosticReporter { files: SimpleFiles::new(), config, writer: Writer::Stream(writer) }
70    }
71
72    /// Creates a new reporter which stores its output in a buffer
73    pub(crate) fn buffered() -> CodeSpanDiagnosticReporter {
74        CodeSpanDiagnosticReporter { writer: Writer::Buffer(Buffer::no_color()), ..Default::default() }
75    }
76
77    fn emit(&mut self, diag: Diagnostic<usize>) -> Result<(), codespan_reporting::files::Error> {
78        codespan_reporting::term::emit(&mut self.writer, &self.config, &self.files, &diag)
79    }
80
81    fn file_name(&self, handle: usize) -> String {
82        use codespan_reporting::files::Files;
83        self.files.name(handle).map(|name| name.to_string()).unwrap_or_default()
84    }
85}
86
87impl Default for CodeSpanDiagnosticReporter {
88    /// creates the default CodeSpanDiagnosticReporter reporting to StdErr, with colors
89    fn default() -> Self {
90        Self::terminal(
91            codespan_reporting::term::Config {
92                display_style: codespan_reporting::term::DisplayStyle::Rich,
93                tab_width: 2,
94                styles: codespan_reporting::term::Styles::default(),
95                chars: codespan_reporting::term::Chars::default(),
96                start_context_lines: 5,
97                end_context_lines: 3,
98                before_label_lines: 0,
99                after_label_lines: 0,
100            },
101            StandardStream::stderr(ColorChoice::Always),
102        )
103    }
104}
105
106impl DiagnosticReporter for CodeSpanDiagnosticReporter {
107    fn report(&mut self, diagnostics: &[ResolvedDiagnostics]) {
108        for d in diagnostics {
109            let diagnostic_factory = match d.severity {
110                Severity::Error => codespan_reporting::diagnostic::Diagnostic::error(),
111                Severity::Warning => codespan_reporting::diagnostic::Diagnostic::warning(),
112                Severity::Info => codespan_reporting::diagnostic::Diagnostic::note(),
113                Severity::Ignore => {
114                    //Do nothing
115                    continue;
116                }
117            };
118
119            let mut labels = vec![];
120            let mut notes = vec![];
121
122            // A text range renders a source snippet; a diagram (block) location
123            // has no text, so it is reported as a note pointing at the block.
124            match &d.main_location.span {
125                CodeSpan::None => {}
126                span => match span.to_range() {
127                    Some(range) => labels.push(
128                        Label::primary(d.main_location.file_handle, range).with_message(d.message.as_str()),
129                    ),
130                    None => notes.push(format!("{}: {}", self.file_name(d.main_location.file_handle), span)),
131                },
132            }
133
134            if let Some(additional_locations) = &d.additional_locations {
135                labels.extend(additional_locations.iter().filter_map(|it| {
136                    it.span
137                        .to_range()
138                        .map(|range| Label::secondary(it.file_handle, range).with_message("see also"))
139                }));
140            }
141
142            let diag = diagnostic_factory
143                .with_labels(labels)
144                .with_notes(notes)
145                .with_message(d.message.as_str())
146                .with_code(&d.code);
147
148            let result = self.emit(diag);
149            if result.is_err() && d.main_location.is_internal() {
150                eprintln!("<internal>: {}", d.message);
151            }
152        }
153    }
154
155    fn register(&mut self, path: String, src: String) -> usize {
156        self.files.add(path, src)
157    }
158
159    fn buffer(&self) -> Option<String> {
160        match &self.writer {
161            Writer::Buffer(buffer) => Some(String::from_utf8_lossy(buffer.as_slice()).to_string()),
162            Writer::Stream(_) => None,
163        }
164    }
165}