solar_interface/diagnostics/emitter/
human.rs

1use super::{Diag, Emitter, io_panic, rustc::FileWithAnnotatedLines};
2use crate::{
3    SourceMap,
4    diagnostics::{Level, MultiSpan, Style, SubDiagnostic, SuggestionStyle},
5    source_map::SourceFile,
6};
7use annotate_snippets::{
8    Annotation, AnnotationKind, Group, Level as ASLevel, Message, Patch, Renderer, Report, Snippet,
9    Title, renderer::DecorStyle,
10};
11use anstream::{AutoStream, ColorChoice};
12use solar_config::HumanEmitterKind;
13use std::{
14    any::Any,
15    borrow::Cow,
16    collections::BTreeMap,
17    io::{self, Write},
18    sync::{Arc, OnceLock},
19};
20
21// TODO: Tabs are not formatted correctly: https://github.com/rust-lang/annotate-snippets-rs/issues/25
22
23type Writer = dyn Write + Send + 'static;
24
25const DEFAULT_RENDERER: Renderer = Renderer::styled()
26    .error(Level::Error.style())
27    .warning(Level::Warning.style())
28    .note(Level::Note.style())
29    .help(Level::Help.style())
30    .line_num(Style::LineNumber.to_color_spec(Level::Note))
31    .addition(Style::Addition.to_color_spec(Level::Note))
32    .removal(Style::Removal.to_color_spec(Level::Note))
33    .context(Style::LabelSecondary.to_color_spec(Level::Note));
34
35/// Diagnostic emitter that emits to an arbitrary [`io::Write`] writer in human-readable format.
36pub struct HumanEmitter {
37    writer_type_id: std::any::TypeId,
38    real_writer: *mut Writer,
39    writer: AutoStream<Box<Writer>>,
40    source_map: Option<Arc<SourceMap>>,
41    renderer: Renderer,
42}
43
44// SAFETY: `real_writer` always points to the `Writer` in `writer`.
45unsafe impl Send for HumanEmitter {}
46
47impl Emitter for HumanEmitter {
48    fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
49        self.snippet(diagnostic, |this, snippet| {
50            writeln!(this.writer, "{}\n", this.renderer.render(snippet))?;
51            this.writer.flush()
52        })
53        .unwrap_or_else(|e| io_panic(e));
54    }
55
56    fn source_map(&self) -> Option<&Arc<SourceMap>> {
57        self.source_map.as_ref()
58    }
59
60    fn supports_color(&self) -> bool {
61        match self.writer.current_choice() {
62            ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
63            ColorChoice::Auto | ColorChoice::Never => false,
64        }
65    }
66}
67
68impl HumanEmitter {
69    /// Creates a new `HumanEmitter` that writes to given writer.
70    ///
71    /// Note that a color choice of `Auto` will be treated as `Never` because the writer opaque
72    /// at this point. Prefer calling [`AutoStream::choice`] on the writer if it is known
73    /// before-hand.
74    pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
75        let writer_type_id = writer.type_id();
76        let mut real_writer = Box::new(writer) as Box<Writer>;
77        Self {
78            writer_type_id,
79            real_writer: &mut *real_writer,
80            writer: AutoStream::new(real_writer, color),
81            source_map: None,
82            renderer: DEFAULT_RENDERER,
83        }
84    }
85
86    /// Creates a new `HumanEmitter` that writes to stderr, for use in tests.
87    pub fn test() -> Self {
88        struct TestWriter;
89
90        impl Write for TestWriter {
91            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
92                // The main difference between `stderr`: use the `eprint!` macro so that the output
93                // can get captured by the test harness.
94                eprint!("{}", String::from_utf8_lossy(buf));
95                Ok(buf.len())
96            }
97
98            fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
99                self.write(buf).map(drop)
100            }
101
102            fn flush(&mut self) -> io::Result<()> {
103                io::stderr().flush()
104            }
105        }
106
107        Self::new(TestWriter, ColorChoice::Always)
108    }
109
110    /// Creates a new `HumanEmitter` that writes to stderr.
111    pub fn stderr(color_choice: ColorChoice) -> Self {
112        // `io::Stderr` is not buffered.
113        Self::new(io::BufWriter::new(io::stderr()), stderr_choice(color_choice))
114    }
115
116    /// Sets the source map.
117    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
118        self.set_source_map(source_map);
119        self
120    }
121
122    /// Sets the source map.
123    pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
124        self.source_map = source_map;
125    }
126
127    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
128    pub fn ui_testing(mut self, yes: bool) -> Self {
129        self.renderer = self.renderer.anonymized_line_numbers(yes);
130        self
131    }
132
133    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
134    pub fn set_ui_testing(&mut self, yes: bool) {
135        self.renderer =
136            std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
137    }
138
139    /// Sets the human emitter kind (unicode vs short).
140    pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
141        match kind {
142            HumanEmitterKind::Ascii => {
143                self.renderer = self.renderer.decor_style(DecorStyle::Ascii);
144            }
145            HumanEmitterKind::Unicode => {
146                self.renderer = self.renderer.decor_style(DecorStyle::Unicode);
147            }
148            HumanEmitterKind::Short => {
149                self.renderer = self.renderer.short_message(true);
150            }
151            _ => unimplemented!("{kind:?}"),
152        }
153        self
154    }
155
156    /// Sets the terminal width for formatting.
157    pub fn terminal_width(mut self, width: Option<usize>) -> Self {
158        if let Some(w) = width {
159            self.renderer = self.renderer.term_width(w);
160        }
161        self
162    }
163
164    /// Downcasts the underlying writer to the specified type.
165    fn downcast_writer<T: Any>(&self) -> Option<&T> {
166        if self.writer_type_id == std::any::TypeId::of::<T>() {
167            Some(unsafe { &*(self.real_writer as *const T) })
168        } else {
169            None
170        }
171    }
172
173    /// Downcasts the underlying writer to the specified type.
174    fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
175        if self.writer_type_id == std::any::TypeId::of::<T>() {
176            Some(unsafe { &mut *(self.real_writer as *mut T) })
177        } else {
178            None
179        }
180    }
181
182    /// Formats the given `diagnostic` into a [`Message`] suitable for use with the renderer.
183    fn snippet<R>(
184        &mut self,
185        diagnostic: &mut Diag,
186        f: impl FnOnce(&mut Self, Report<'_>) -> R,
187    ) -> R {
188        // Current format (annotate-snippets 0.12.0) (comments in <...>):
189        /*
190        title.level[title.id]: title.label
191           --> snippets[0].path:ll:cc
192            |
193         LL | snippets[0].source[ann[0].range] <ann = snippets[0].annotations; these are diag.span_label()s>
194            | ^^^^^^^^^^^^^^^^ ann[0].label <primary>
195         LL | snippets[0].source[ann[1].range]
196            | ---------------- ann[1].label <secondary>
197            |
198           ::: snippets[1].path:ll:cc
199            |
200        etc...
201            |
202            = footer[0].level: footer[0].label
203            = footer[1].level: footer[1].label
204            = ...
205        <other groups for subdiagnostics, same as above without footers>
206        */
207
208        // Process suggestions. Inline primary span if necessary.
209        let mut primary_span = Cow::Borrowed(&diagnostic.span);
210        self.primary_span_formatted(&mut primary_span, &mut diagnostic.suggestions);
211
212        // Render suggestions unless style is `HideCodeAlways`.
213        // Note that if the span was previously inlined, suggestions will be empty.
214        let children = diagnostic
215            .suggestions
216            .iter()
217            .filter(|sugg| sugg.style != SuggestionStyle::HideCodeAlways)
218            .collect::<Vec<_>>();
219
220        let sm = self.source_map.as_deref();
221        let title = title_from_diagnostic(diagnostic);
222        let snippets = sm.map(|sm| iter_snippets(sm, &primary_span)).into_iter().flatten();
223
224        // Dummy subdiagnostics go in the main group's footer, non-dummy ones go as separate groups.
225        let subs = |d| diagnostic.children.iter().filter(move |sub| sub.span.is_dummy() == d);
226        let sub_groups = subs(false).map(|sub| {
227            let mut g = Group::with_title(title_from_subdiagnostic(sub, self.supports_color()));
228            if let Some(sm) = sm {
229                g = g.elements(iter_snippets(sm, &sub.span));
230            }
231            g
232        });
233
234        let mut footers =
235            subs(true).map(|sub| message_from_subdiagnostic(sub, self.supports_color())).peekable();
236        let footer_group =
237            footers.peek().is_some().then(|| Group::with_level(ASLevel::NOTE).elements(footers));
238
239        // Create suggestion groups for non-inline suggestions
240        let suggestion_groups = children.iter().flat_map(|suggestion| {
241            let sm = self.source_map.as_deref()?;
242
243            // For each substitution, create a separate group
244            // Currently we typically only have one substitution per suggestion
245            for substitution in &suggestion.substitutions {
246                // Group parts by file
247                let mut parts_by_file: BTreeMap<_, Vec<_>> = BTreeMap::new();
248                for part in &substitution.parts {
249                    let file = sm.lookup_source_file(part.span.lo());
250                    parts_by_file.entry(file.name.clone()).or_default().push(part);
251                }
252
253                if parts_by_file.is_empty() {
254                    continue;
255                }
256
257                let mut snippets = vec![];
258                for (filename, parts) in parts_by_file {
259                    let file = sm.get_file_ref(&filename)?;
260                    let mut snippet = Snippet::source(file.src.to_string())
261                        .path(sm.filename_for_diagnostics(&file.name).to_string())
262                        .fold(true);
263
264                    for part in parts {
265                        if let Ok(range) = sm.span_to_range(part.span) {
266                            snippet = snippet.patch(Patch::new(range, part.snippet.as_str()));
267                        }
268                    }
269                    snippets.push(snippet);
270                }
271
272                if !snippets.is_empty() {
273                    let title = ASLevel::HELP.secondary_title(suggestion.msg.as_str());
274                    return Some(Group::with_title(title).elements(snippets));
275                }
276            }
277
278            None
279        });
280
281        let main_group = Group::with_title(title).elements(snippets);
282        let report = std::iter::once(main_group)
283            .chain(suggestion_groups)
284            .chain(footer_group)
285            .chain(sub_groups)
286            .collect::<Vec<_>>();
287        f(self, &report)
288    }
289}
290
291/// Diagnostic emitter that emits diagnostics in human-readable format to a local buffer.
292pub struct HumanBufferEmitter {
293    inner: HumanEmitter,
294}
295
296impl Emitter for HumanBufferEmitter {
297    #[inline]
298    fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
299        self.inner.emit_diagnostic(diagnostic);
300    }
301
302    #[inline]
303    fn source_map(&self) -> Option<&Arc<SourceMap>> {
304        Emitter::source_map(&self.inner)
305    }
306
307    #[inline]
308    fn supports_color(&self) -> bool {
309        self.inner.supports_color()
310    }
311}
312
313impl HumanBufferEmitter {
314    /// Creates a new `BufferEmitter` that writes to a local buffer.
315    pub fn new(color_choice: ColorChoice) -> Self {
316        Self { inner: HumanEmitter::new(Vec::<u8>::new(), stderr_choice(color_choice)) }
317    }
318
319    /// Sets the source map.
320    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
321        self.inner = self.inner.source_map(source_map);
322        self
323    }
324
325    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
326    pub fn ui_testing(mut self, yes: bool) -> Self {
327        self.inner = self.inner.ui_testing(yes);
328        self
329    }
330
331    /// Sets the human emitter kind (unicode vs short).
332    pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
333        self.inner = self.inner.human_kind(kind);
334        self
335    }
336
337    /// Sets the terminal width for formatting.
338    pub fn terminal_width(mut self, width: Option<usize>) -> Self {
339        self.inner = self.inner.terminal_width(width);
340        self
341    }
342
343    /// Returns a reference to the underlying human emitter.
344    pub fn inner(&self) -> &HumanEmitter {
345        &self.inner
346    }
347
348    /// Returns a mutable reference to the underlying human emitter.
349    pub fn inner_mut(&mut self) -> &mut HumanEmitter {
350        &mut self.inner
351    }
352
353    /// Returns a reference to the buffer.
354    pub fn buffer(&self) -> &str {
355        let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
356        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
357        // SAFETY: The buffer is guaranteed to be valid UTF-8.
358        unsafe { std::str::from_utf8_unchecked(buffer) }
359    }
360
361    /// Returns a mutable reference to the buffer.
362    pub fn buffer_mut(&mut self) -> &mut String {
363        let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
364        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
365        // SAFETY: The buffer is guaranteed to be valid UTF-8.
366        unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
367    }
368}
369
370fn title_from_diagnostic(diag: &Diag) -> Title<'_> {
371    let mut title = to_as_level(diag.level).primary_title(diag.label());
372    if let Some(id) = diag.id() {
373        title = title.id(id);
374    }
375    title
376}
377
378fn title_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Title<'_> {
379    to_as_level(sub.level).secondary_title(sub.label_with_style(supports_color))
380}
381
382fn message_from_subdiagnostic(sub: &SubDiagnostic, supports_color: bool) -> Message<'_> {
383    to_as_level(sub.level).message(sub.label_with_style(supports_color))
384}
385
386fn iter_snippets<'a>(
387    sm: &SourceMap,
388    msp: &MultiSpan,
389) -> impl Iterator<Item = Snippet<'a, Annotation<'a>>> {
390    collect_files(sm, msp).into_iter().map(|file| file_to_snippet(sm, &file.file, &file.lines))
391}
392
393fn collect_files(sm: &SourceMap, msp: &MultiSpan) -> Vec<FileWithAnnotatedLines> {
394    let mut annotated_files = FileWithAnnotatedLines::collect_annotations(sm, msp);
395    // Make sure our primary file comes first
396    if let Some(primary_span) = msp.primary_span()
397        && !primary_span.is_dummy()
398        && annotated_files.len() > 1
399    {
400        let primary_lo = sm.lookup_char_pos(primary_span.lo());
401        if let Ok(pos) =
402            annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name))
403        {
404            annotated_files.swap(0, pos);
405        }
406    }
407    annotated_files
408}
409
410/// Merges back multi-line annotations that were split across multiple lines into a single
411/// annotation that's suitable for `annotate-snippets`.
412///
413/// Expects that lines are sorted.
414fn file_to_snippet<'a>(
415    sm: &SourceMap,
416    file: &SourceFile,
417    lines: &[super::rustc::Line],
418) -> Snippet<'a, Annotation<'a>> {
419    /// `label, start_idx`
420    type MultiLine<'a> = (Option<&'a String>, usize);
421    fn multi_line_at<'a, 'b>(
422        mls: &'a mut Vec<MultiLine<'b>>,
423        depth: usize,
424    ) -> &'a mut MultiLine<'b> {
425        assert!(depth > 0);
426        if mls.len() < depth {
427            mls.resize_with(depth, || (None, 0));
428        }
429        &mut mls[depth - 1]
430    }
431
432    debug_assert!(!lines.is_empty());
433
434    let first_line = lines.first().unwrap().line_index;
435    debug_assert!(first_line > 0, "line index is 1-based");
436    let last_line = lines.last().unwrap().line_index;
437    debug_assert!(last_line >= first_line);
438    debug_assert!(lines.is_sorted());
439    let snippet_base = file.line_position(first_line - 1).unwrap();
440
441    let source = file.get_lines(first_line - 1..=last_line - 1).unwrap_or_default();
442    let mut annotations = Vec::new();
443    let mut push_annotation = |kind: AnnotationKind, span, label| {
444        annotations.push(kind.span(span).label(label));
445    };
446    let annotation_kind = |is_primary: bool| {
447        if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context }
448    };
449
450    let mut mls = Vec::new();
451    for line in lines {
452        let line_abs_pos = file.line_position(line.line_index - 1).unwrap();
453        let line_rel_pos = line_abs_pos - snippet_base;
454        // Returns the position of the given column in the local snippet.
455        // We have to convert the column char position to byte position.
456        let rel_pos = |c: &super::rustc::AnnotationColumn| {
457            line_rel_pos + char_to_byte_pos(&source[line_rel_pos..], c.file)
458        };
459
460        for ann in &line.annotations {
461            match ann.annotation_type {
462                super::rustc::AnnotationType::Singleline => {
463                    push_annotation(
464                        annotation_kind(ann.is_primary),
465                        rel_pos(&ann.start_col)..rel_pos(&ann.end_col),
466                        ann.label.clone().unwrap_or_default(),
467                    );
468                }
469                super::rustc::AnnotationType::MultilineStart(depth) => {
470                    *multi_line_at(&mut mls, depth) = (ann.label.as_ref(), rel_pos(&ann.start_col));
471                }
472                super::rustc::AnnotationType::MultilineLine(_depth) => {
473                    // TODO: unvalidated
474                    push_annotation(
475                        AnnotationKind::Visible,
476                        line_rel_pos..line_rel_pos,
477                        String::new(),
478                    );
479                }
480                super::rustc::AnnotationType::MultilineEnd(depth) => {
481                    let (label, multiline_start_idx) = *multi_line_at(&mut mls, depth);
482                    let end_idx = rel_pos(&ann.end_col);
483                    debug_assert!(end_idx >= multiline_start_idx);
484                    push_annotation(
485                        annotation_kind(ann.is_primary),
486                        multiline_start_idx..end_idx,
487                        label.or(ann.label.as_ref()).cloned().unwrap_or_default(),
488                    );
489                }
490            }
491        }
492    }
493    Snippet::source(source.to_string())
494        .path(sm.filename_for_diagnostics(&file.name).to_string())
495        .line_start(first_line)
496        .fold(true)
497        .annotations(annotations)
498}
499
500fn to_as_level<'a>(level: Level) -> ASLevel<'a> {
501    match level {
502        Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR,
503        Level::Warning => ASLevel::WARNING,
504        Level::Note | Level::OnceNote => ASLevel::NOTE,
505        Level::Help | Level::OnceHelp => ASLevel::HELP,
506        Level::Allow => ASLevel::INFO,
507    }
508    .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) })
509}
510
511fn char_to_byte_pos(s: &str, char_pos: usize) -> usize {
512    s.chars().take(char_pos).map(char::len_utf8).sum()
513}
514
515fn stderr_choice(color_choice: ColorChoice) -> ColorChoice {
516    static AUTO: OnceLock<ColorChoice> = OnceLock::new();
517    if color_choice == ColorChoice::Auto {
518        *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr()))
519    } else {
520        color_choice
521    }
522}