solar_interface/diagnostics/emitter/
human.rs

1use super::{Diag, Emitter, io_panic, rustc::FileWithAnnotatedLines};
2use crate::{
3    SourceMap,
4    diagnostics::{Level, MultiSpan, Style, SubDiagnostic},
5    source_map::SourceFile,
6};
7use annotate_snippets::{Annotation, Level as ASLevel, Message, Renderer, Snippet};
8use anstream::{AutoStream, ColorChoice};
9use std::{
10    any::Any,
11    io::{self, Write},
12    ops::Range,
13    sync::Arc,
14};
15
16// TODO: Tabs are not formatted correctly: https://github.com/rust-lang/annotate-snippets-rs/issues/25
17
18type Writer = dyn Write + Send + 'static;
19
20const DEFAULT_RENDERER: Renderer = Renderer::plain()
21    .error(Level::Error.style())
22    .warning(Level::Warning.style())
23    .info(Level::Note.style())
24    .note(Level::Note.style())
25    .help(Level::Help.style())
26    .line_no(Style::LineNumber.to_color_spec(Level::Note))
27    .emphasis(anstyle::Style::new().bold())
28    .none(anstyle::Style::new());
29
30/// Diagnostic emitter that emits to an arbitrary [`io::Write`] writer in human-readable format.
31pub struct HumanEmitter {
32    writer_type_id: std::any::TypeId,
33    real_writer: *mut Writer,
34    writer: AutoStream<Box<Writer>>,
35    source_map: Option<Arc<SourceMap>>,
36    renderer: Renderer,
37}
38
39// SAFETY: `real_writer` always points to the `Writer` in `writer`.
40unsafe impl Send for HumanEmitter {}
41
42impl Emitter for HumanEmitter {
43    fn emit_diagnostic(&mut self, diagnostic: &Diag) {
44        self.snippet(diagnostic, |this, snippet| {
45            writeln!(this.writer, "{}\n", this.renderer.render(snippet))?;
46            this.writer.flush()
47        })
48        .unwrap_or_else(|e| io_panic(e));
49    }
50
51    fn source_map(&self) -> Option<&Arc<SourceMap>> {
52        self.source_map.as_ref()
53    }
54
55    fn supports_color(&self) -> bool {
56        match self.writer.current_choice() {
57            ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
58            ColorChoice::Auto | ColorChoice::Never => false,
59        }
60    }
61}
62
63impl HumanEmitter {
64    /// Creates a new `HumanEmitter` that writes to given writer.
65    ///
66    /// Note that a color choice of `Auto` will be treated as `Never` because the writer opaque
67    /// at this point. Prefer calling [`AutoStream::choice`] on the writer if it is known
68    /// before-hand.
69    pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
70        // TODO: Clean this up on next anstream release
71        let writer_type_id = writer.type_id();
72        let mut real_writer = Box::new(writer) as Box<Writer>;
73        Self {
74            writer_type_id,
75            real_writer: &mut *real_writer,
76            writer: AutoStream::new(real_writer, color),
77            source_map: None,
78            renderer: DEFAULT_RENDERER,
79        }
80    }
81
82    /// Creates a new `HumanEmitter` that writes to stderr, for use in tests.
83    pub fn test() -> Self {
84        struct TestWriter(io::Stderr);
85
86        impl Write for TestWriter {
87            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
88                // The main difference between `stderr`: use the `eprint!` macro so that the output
89                // can get captured by the test harness.
90                eprint!("{}", String::from_utf8_lossy(buf));
91                Ok(buf.len())
92            }
93
94            fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
95                self.write(buf).map(drop)
96            }
97
98            fn flush(&mut self) -> io::Result<()> {
99                self.0.flush()
100            }
101        }
102
103        Self::new(TestWriter(io::stderr()), ColorChoice::Always)
104    }
105
106    /// Creates a new `HumanEmitter` that writes to stderr.
107    pub fn stderr(mut color_choice: ColorChoice) -> Self {
108        let stderr = io::stderr();
109        // Call `AutoStream::choice` on `io::Stderr` rather than later on `Box<dyn Write>`.
110        if color_choice == ColorChoice::Auto {
111            color_choice = AutoStream::choice(&stderr);
112        }
113        // `io::Stderr` is not buffered.
114        Self::new(io::BufWriter::new(stderr), color_choice)
115    }
116
117    /// Sets the source map.
118    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
119        self.set_source_map(source_map);
120        self
121    }
122
123    /// Sets the source map.
124    pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
125        self.source_map = source_map;
126    }
127
128    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
129    pub fn ui_testing(mut self, yes: bool) -> Self {
130        self.renderer = self.renderer.anonymized_line_numbers(yes);
131        self
132    }
133
134    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
135    pub fn set_ui_testing(&mut self, yes: bool) {
136        self.renderer =
137            std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
138    }
139
140    /// Downcasts the underlying writer to the specified type.
141    fn downcast_writer<T: Any>(&self) -> Option<&T> {
142        if self.writer_type_id == std::any::TypeId::of::<T>() {
143            Some(unsafe { &*(self.real_writer as *const T) })
144        } else {
145            None
146        }
147    }
148
149    /// Downcasts the underlying writer to the specified type.
150    fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
151        if self.writer_type_id == std::any::TypeId::of::<T>() {
152            Some(unsafe { &mut *(self.real_writer as *mut T) })
153        } else {
154            None
155        }
156    }
157
158    /// Formats the given `diagnostic` into a [`Message`] suitable for use with the renderer.
159    fn snippet<R>(&mut self, diagnostic: &Diag, f: impl FnOnce(&mut Self, Message<'_>) -> R) -> R {
160        // Current format (annotate-snippets 0.10.0) (comments in <...>):
161        /*
162        title.level[title.id]: title.label
163           --> snippets[0].origin
164            |
165         LL | snippets[0].source[ann[0].range] <ann = snippets[0].annotations>
166            | ^^^^^^^^^^^^^^^^ ann[0].level: ann[0].label <type is skipped for error, warning>
167         LL | snippets[0].source[ann[1].range]
168            | ---------------- ann[1].level: ann[1].label
169            |
170           ::: snippets[1].origin
171            |
172        etc...
173            |
174            = footer[0].level: footer[0].label <I believe the .id here is always ignored>
175            = footer[1].level: footer[1].label
176            = ...
177        */
178
179        let title = OwnedMessage::from_diagnostic(diagnostic);
180
181        let owned_snippets = self
182            .source_map
183            .as_deref()
184            .map(|sm| OwnedSnippet::collect(sm, diagnostic))
185            .unwrap_or_default();
186
187        // Dummy subdiagnostics go in the footer, while non-dummy ones go in the slices.
188        let owned_footers: Vec<_> = diagnostic
189            .children
190            .iter()
191            .filter(|sub| sub.span.is_dummy())
192            .map(|sub| OwnedMessage::from_subdiagnostic(sub, self.supports_color()))
193            .collect();
194
195        let snippet = title
196            .as_ref()
197            .snippets(owned_snippets.iter().map(OwnedSnippet::as_ref))
198            .footers(owned_footers.iter().map(OwnedMessage::as_ref));
199        f(self, snippet)
200    }
201}
202
203/// Diagnostic emitter that emits diagnostics in human-readable format to a local buffer.
204pub struct HumanBufferEmitter {
205    inner: HumanEmitter,
206}
207
208impl Emitter for HumanBufferEmitter {
209    #[inline]
210    fn emit_diagnostic(&mut self, diagnostic: &Diag) {
211        self.inner.emit_diagnostic(diagnostic);
212    }
213
214    #[inline]
215    fn source_map(&self) -> Option<&Arc<SourceMap>> {
216        Emitter::source_map(&self.inner)
217    }
218
219    #[inline]
220    fn supports_color(&self) -> bool {
221        self.inner.supports_color()
222    }
223}
224
225impl HumanBufferEmitter {
226    /// Creates a new `BufferEmitter` that writes to a local buffer.
227    pub fn new(mut color: ColorChoice) -> Self {
228        if color == ColorChoice::Auto {
229            color = anstream::AutoStream::choice(&std::io::stderr());
230        }
231        Self { inner: HumanEmitter::new(Vec::<u8>::new(), color) }
232    }
233
234    /// Sets the source map.
235    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
236        self.inner = self.inner.source_map(source_map);
237        self
238    }
239
240    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
241    pub fn ui_testing(mut self, yes: bool) -> Self {
242        self.inner = self.inner.ui_testing(yes);
243        self
244    }
245
246    /// Returns a reference to the underlying human emitter.
247    pub fn inner(&self) -> &HumanEmitter {
248        &self.inner
249    }
250
251    /// Returns a mutable reference to the underlying human emitter.
252    pub fn inner_mut(&mut self) -> &mut HumanEmitter {
253        &mut self.inner
254    }
255
256    /// Returns a reference to the buffer.
257    pub fn buffer(&self) -> &str {
258        let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
259        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
260        // SAFETY: The buffer is guaranteed to be valid UTF-8.
261        unsafe { std::str::from_utf8_unchecked(buffer) }
262    }
263
264    /// Returns a mutable reference to the buffer.
265    pub fn buffer_mut(&mut self) -> &mut String {
266        let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
267        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
268        // SAFETY: The buffer is guaranteed to be valid UTF-8.
269        unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
270    }
271}
272
273#[derive(Debug)]
274struct OwnedMessage {
275    id: Option<String>,
276    label: String,
277    level: ASLevel,
278}
279
280impl OwnedMessage {
281    fn from_diagnostic(diag: &Diag) -> Self {
282        Self { id: diag.id(), label: diag.label().into_owned(), level: to_as_level(diag.level) }
283    }
284
285    fn from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Self {
286        Self {
287            id: None,
288            label: sub.label_with_style(supports_color).into_owned(),
289            level: to_as_level(sub.level),
290        }
291    }
292
293    fn as_ref(&self) -> Message<'_> {
294        let mut msg = self.level.title(&self.label);
295        if let Some(id) = &self.id {
296            msg = msg.id(id);
297        }
298        msg
299    }
300}
301
302#[derive(Debug)]
303struct OwnedAnnotation {
304    range: Range<usize>,
305    label: String,
306    level: ASLevel,
307}
308
309impl OwnedAnnotation {
310    fn as_ref(&self) -> Annotation<'_> {
311        self.level.span(self.range.clone()).label(&self.label)
312    }
313}
314
315#[derive(Debug)]
316struct OwnedSnippet {
317    origin: String,
318    source: String,
319    line_start: usize,
320    fold: bool,
321    annotations: Vec<OwnedAnnotation>,
322}
323
324impl OwnedSnippet {
325    fn collect(sm: &SourceMap, diagnostic: &Diag) -> Vec<Self> {
326        // Collect main diagnostic.
327        let mut files = Self::collect_files(sm, &diagnostic.span);
328        files.iter_mut().for_each(|file| file.set_level(diagnostic.level));
329
330        // Collect subdiagnostics.
331        for sub in &diagnostic.children {
332            let label = sub.label();
333            for mut sub_file in Self::collect_files(sm, &sub.span) {
334                for line in &mut sub_file.lines {
335                    for ann in &mut line.annotations {
336                        ann.level = Some(sub.level);
337                        if ann.is_primary && ann.label.is_none() {
338                            ann.label = Some(label.to_string());
339                        }
340                    }
341                }
342
343                if let Some(main_file) =
344                    files.iter_mut().find(|main_file| Arc::ptr_eq(&main_file.file, &sub_file.file))
345                {
346                    main_file.add_lines(sub_file.lines);
347                } else {
348                    files.push(sub_file);
349                }
350            }
351        }
352
353        files
354            .iter()
355            .map(|file| file_to_snippet(sm, &file.file, &file.lines, diagnostic.level))
356            .collect()
357    }
358
359    fn collect_files(sm: &SourceMap, msp: &MultiSpan) -> Vec<FileWithAnnotatedLines> {
360        let mut annotated_files = FileWithAnnotatedLines::collect_annotations(sm, msp);
361        if let Some(primary_span) = msp.primary_span()
362            && !primary_span.is_dummy()
363            && annotated_files.len() > 1
364        {
365            let primary_lo = sm.lookup_char_pos(primary_span.lo());
366            if let Ok(pos) =
367                annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name))
368            {
369                annotated_files.swap(0, pos);
370            }
371        }
372        annotated_files
373    }
374
375    fn as_ref(&self) -> Snippet<'_> {
376        Snippet::source(&self.source)
377            .line_start(self.line_start)
378            .origin(&self.origin)
379            .fold(self.fold)
380            .annotations(self.annotations.iter().map(OwnedAnnotation::as_ref))
381    }
382}
383
384type MultiLine<'a> = (Option<&'a String>, usize);
385
386fn multi_line_at<'a, 'b>(mls: &'a mut Vec<MultiLine<'b>>, depth: usize) -> &'a mut MultiLine<'b> {
387    assert!(depth > 0);
388    if mls.len() < depth {
389        mls.resize_with(depth, || (None, 0));
390    }
391    &mut mls[depth - 1]
392}
393
394/// Merges back multi-line annotations that were split across multiple lines into a single
395/// annotation that's suitable for `annotate-snippets`.
396///
397/// Expects that lines are sorted.
398fn file_to_snippet(
399    sm: &SourceMap,
400    file: &SourceFile,
401    lines: &[super::rustc::Line],
402    default_level: Level,
403) -> OwnedSnippet {
404    debug_assert!(!lines.is_empty());
405
406    let first_line = lines.first().unwrap().line_index;
407    debug_assert!(first_line > 0, "line index is 1-based");
408    let last_line = lines.last().unwrap().line_index;
409    debug_assert!(last_line >= first_line);
410    debug_assert!(lines.is_sorted());
411    let snippet_base = file.line_position(first_line - 1).unwrap();
412
413    let mut snippet = OwnedSnippet {
414        origin: sm.filename_for_diagnostics(&file.name).to_string(),
415        source: file.get_lines(first_line - 1..=last_line - 1).unwrap_or_default().into(),
416        line_start: first_line,
417        fold: true,
418        annotations: Vec::new(),
419    };
420    let mut mls = Vec::new();
421    for line in lines {
422        let line_abs_pos = file.line_position(line.line_index - 1).unwrap();
423        let line_rel_pos = line_abs_pos - snippet_base;
424        // Returns the position of the given column in the local snippet.
425        // We have to convert the column char position to byte position.
426        let rel_pos = |c: &super::rustc::AnnotationColumn| {
427            line_rel_pos + char_to_byte_pos(&snippet.source[line_rel_pos..], c.file)
428        };
429
430        for ann in &line.annotations {
431            match ann.annotation_type {
432                super::rustc::AnnotationType::Singleline => {
433                    snippet.annotations.push(OwnedAnnotation {
434                        range: rel_pos(&ann.start_col)..rel_pos(&ann.end_col),
435                        label: ann.label.clone().unwrap_or_default(),
436                        level: to_as_level(ann.level.unwrap_or(default_level)),
437                    });
438                }
439                super::rustc::AnnotationType::MultilineStart(depth) => {
440                    *multi_line_at(&mut mls, depth) = (ann.label.as_ref(), rel_pos(&ann.start_col));
441                }
442                super::rustc::AnnotationType::MultilineLine(_) => {}
443                super::rustc::AnnotationType::MultilineEnd(depth) => {
444                    let (label, multiline_start_idx) = *multi_line_at(&mut mls, depth);
445                    let end_idx = rel_pos(&ann.end_col);
446                    debug_assert!(end_idx >= multiline_start_idx);
447                    snippet.annotations.push(OwnedAnnotation {
448                        range: multiline_start_idx..end_idx,
449                        label: label.or(ann.label.as_ref()).cloned().unwrap_or_default(),
450                        level: to_as_level(ann.level.unwrap_or(default_level)),
451                    });
452                }
453            }
454        }
455    }
456    snippet
457}
458
459fn to_as_level(level: Level) -> ASLevel {
460    match level {
461        Level::Bug | Level::Fatal | Level::Error => ASLevel::Error,
462        Level::Warning => ASLevel::Warning,
463        Level::Note | Level::OnceNote | Level::FailureNote => ASLevel::Note,
464        Level::Help | Level::OnceHelp => ASLevel::Help,
465        Level::Allow => ASLevel::Info,
466    }
467}
468
469fn char_to_byte_pos(s: &str, char_pos: usize) -> usize {
470    s.chars().take(char_pos).map(char::len_utf8).sum()
471}