Skip to main content

solar_interface/diagnostics/emitter/
human.rs

1use super::{Diag, Emitter};
2use crate::{
3    SourceMap, Span,
4    diagnostics::{
5        ConfusionType, DiagId, DiagMsg, Level, MultiSpan, SpanLabel, Style, SubDiagnostic,
6        SuggestionStyle, Suggestions, detect_confusion_type, emitter::normalize_whitespace,
7        is_different,
8    },
9    source_map::{FileName, SourceFile},
10};
11use annotate_snippets::{
12    Annotation as ASAnnotation, AnnotationKind, Group, Level as ASLevel, Padding, Patch, Renderer,
13    Snippet, renderer::DecorStyle,
14};
15use anstream::{AutoStream, ColorChoice};
16use solar_config::HumanEmitterKind;
17use solar_data_structures::pluralize;
18use std::{
19    any::Any,
20    borrow::Cow,
21    io::{self, Write},
22    sync::{Arc, OnceLock},
23};
24
25type Writer = dyn Write + Send + 'static;
26
27/// Maximum number of suggestions to be shown
28///
29/// Arbitrary, but taken from trait import suggestion limit
30pub(super) const MAX_SUGGESTIONS: usize = 4;
31
32const DEFAULT_RENDERER: Renderer = Renderer::styled()
33    .error(Level::Error.style())
34    .warning(Level::Warning.style())
35    .note(Level::Note.style())
36    .help(Level::Help.style())
37    .line_num(Style::LineNumber.to_color_spec(Level::Note))
38    .addition(Style::Addition.to_color_spec(Level::Note))
39    .removal(Style::Removal.to_color_spec(Level::Note))
40    .context(Style::LabelSecondary.to_color_spec(Level::Note));
41
42/// Diagnostic emitter that emits to an arbitrary [`io::Write`] writer in human-readable format.
43pub struct HumanEmitter {
44    writer_type_id: std::any::TypeId,
45    real_writer: *mut Writer,
46    writer: AutoStream<Box<Writer>>,
47    source_map: Option<Arc<SourceMap>>,
48    renderer: Renderer,
49}
50
51// SAFETY: `real_writer` always points to the `Writer` in `writer`.
52unsafe impl Send for HumanEmitter {}
53
54impl Emitter for HumanEmitter {
55    fn emit_diagnostic(&mut self, diag: &mut Diag) {
56        let mut primary_span = Cow::Owned(std::mem::take(&mut diag.span));
57        let mut suggestions = Cow::Owned(std::mem::take(&mut diag.suggestions));
58        self.primary_span_formatted(&mut primary_span, &mut suggestions);
59
60        self.emit_messages_default(
61            &diag.level,
62            &diag.messages,
63            &diag.code,
64            &primary_span,
65            &diag.children,
66            &suggestions,
67        );
68
69        diag.span = primary_span.into_owned();
70        diag.suggestions = suggestions.into_owned();
71    }
72
73    fn emit_diagnostic_ref(&mut self, diag: &Diag) {
74        let mut primary_span = Cow::Borrowed(&diag.span);
75        let mut suggestions = Cow::Borrowed(&diag.suggestions);
76        self.primary_span_formatted(&mut primary_span, &mut suggestions);
77
78        self.emit_messages_default(
79            &diag.level,
80            &diag.messages,
81            &diag.code,
82            &primary_span,
83            &diag.children,
84            &suggestions,
85        );
86    }
87
88    fn source_map(&self) -> Option<&Arc<SourceMap>> {
89        self.source_map.as_ref()
90    }
91
92    fn supports_color(&self) -> bool {
93        match self.writer.current_choice() {
94            ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
95            ColorChoice::Auto | ColorChoice::Never => false,
96        }
97    }
98}
99
100impl HumanEmitter {
101    /// Creates a new `HumanEmitter` that writes to given writer.
102    ///
103    /// Note that a color choice of `Auto` will be treated as `Never` because the writer opaque
104    /// at this point. Prefer calling [`AutoStream::choice`] on the writer if it is known
105    /// before-hand.
106    pub fn new<W: Write + Send + 'static>(writer: W, color: ColorChoice) -> Self {
107        let writer_type_id = writer.type_id();
108        let mut real_writer = Box::new(writer) as Box<Writer>;
109        Self {
110            writer_type_id,
111            real_writer: &mut *real_writer,
112            writer: AutoStream::new(real_writer, color),
113            source_map: None,
114            renderer: DEFAULT_RENDERER,
115        }
116        .human_kind(Default::default())
117    }
118
119    /// Creates a new `HumanEmitter` that writes to stderr, for use in tests.
120    pub fn test() -> Self {
121        struct TestWriter;
122
123        impl Write for TestWriter {
124            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
125                // The main difference between `stderr`: use the `eprint!` macro so that the output
126                // can get captured by the test harness.
127                eprint!("{}", String::from_utf8_lossy(buf));
128                Ok(buf.len())
129            }
130
131            fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
132                self.write(buf).map(drop)
133            }
134
135            fn flush(&mut self) -> io::Result<()> {
136                io::stderr().flush()
137            }
138        }
139
140        Self::new(TestWriter, ColorChoice::Always)
141    }
142
143    /// Creates a new `HumanEmitter` that writes to stderr.
144    pub fn stderr(color_choice: ColorChoice) -> Self {
145        // `io::Stderr` is not buffered.
146        Self::new(io::BufWriter::new(io::stderr()), stderr_choice(color_choice))
147    }
148
149    /// Sets the source map.
150    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
151        self.set_source_map(source_map);
152        self
153    }
154
155    /// Sets the source map.
156    pub fn set_source_map(&mut self, source_map: Option<Arc<SourceMap>>) {
157        self.source_map = source_map;
158    }
159
160    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
161    pub fn ui_testing(mut self, yes: bool) -> Self {
162        self.renderer = self.renderer.anonymized_line_numbers(yes);
163        self
164    }
165
166    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
167    pub fn set_ui_testing(&mut self, yes: bool) {
168        self.renderer =
169            std::mem::replace(&mut self.renderer, DEFAULT_RENDERER).anonymized_line_numbers(yes);
170    }
171
172    /// Sets the human emitter kind (unicode vs short).
173    pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
174        match kind {
175            HumanEmitterKind::Ascii => {
176                self.renderer = self.renderer.decor_style(DecorStyle::Ascii);
177            }
178            HumanEmitterKind::Unicode => {
179                self.renderer = self.renderer.decor_style(DecorStyle::Unicode);
180            }
181            HumanEmitterKind::Short => {
182                self.renderer = self.renderer.short_message(true);
183            }
184            _ => unimplemented!("{kind:?}"),
185        }
186        self
187    }
188
189    /// Sets the terminal width for formatting.
190    pub fn terminal_width(mut self, width: Option<usize>) -> Self {
191        if let Some(w) = width {
192            self.renderer = self.renderer.term_width(w);
193        }
194        self
195    }
196
197    /// Downcasts the underlying writer to the specified type.
198    fn downcast_writer<T: Any>(&self) -> Option<&T> {
199        if self.writer_type_id == std::any::TypeId::of::<T>() {
200            Some(unsafe { &*(self.real_writer as *const T) })
201        } else {
202            None
203        }
204    }
205
206    /// Downcasts the underlying writer to the specified type.
207    fn downcast_writer_mut<T: Any>(&mut self) -> Option<&mut T> {
208        if self.writer_type_id == std::any::TypeId::of::<T>() {
209            Some(unsafe { &mut *(self.real_writer as *mut T) })
210        } else {
211            None
212        }
213    }
214
215    fn emit_messages_default(
216        &mut self,
217        level: &Level,
218        msgs: &[(DiagMsg, Style)],
219        code: &Option<DiagId>,
220        msp: &MultiSpan,
221        children: &[SubDiagnostic],
222        suggestions: &Suggestions,
223    ) {
224        let renderer = &self.renderer;
225        let annotation_level = annotation_level_for_level(*level);
226
227        // If at least one portion of the message is styled, we need to
228        // "pre-style" the message
229        let mut title = if msgs.iter().any(|(_, style)| style != &Style::NoStyle) {
230            annotation_level.clone().secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level)))
231        } else {
232            annotation_level.clone().primary_title(self.no_style_msgs(msgs))
233        };
234
235        if let Some(c) = code {
236            title = title.id(c.as_str());
237            // Unlike rustc, there is no URL associated with DiagId yet.
238            // TODO: Add URL mapping
239            // if let TerminalUrl::Yes = self.terminal_url {
240            //     title = title.id_url(format!("<TODO_URL>/error_codes/{c}.html"));
241            // }
242        }
243
244        let mut report = vec![];
245        let mut group = Group::with_title(title);
246
247        // If we don't have span information, emit and exit
248        let Some(sm) = self.source_map.as_ref() else {
249            group = group.elements(children.iter().map(|c| {
250                let msg = self.no_style_msgs(&c.messages);
251                let level = annotation_level_for_level(c.level);
252                level.message(msg)
253            }));
254
255            report.push(group);
256            if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
257                panic!("failed to emit error: {e}");
258            }
259            return;
260        };
261
262        let mut file_ann = collect_annotations(msp, sm);
263
264        // Make sure our primary file comes first
265        let primary_span = msp.primary_span().unwrap_or_default();
266        if !primary_span.is_dummy() {
267            let primary_lo = sm.lookup_char_pos(primary_span.lo());
268            if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) {
269                file_ann.swap(0, pos);
270            }
271
272            for (file, annotations) in file_ann.into_iter() {
273                if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
274                    group = group.element(snippet);
275                }
276            }
277        }
278
279        for c in children {
280            let level = annotation_level_for_level(c.level);
281
282            // If at least one portion of the message is styled, we need to
283            // "pre-style" the message
284            let msg = if c.messages.iter().any(|(_, style)| style != &Style::NoStyle) {
285                Cow::Owned(self.pre_style_msgs(&c.messages, c.level))
286            } else {
287                Cow::Owned(self.no_style_msgs(&c.messages))
288            };
289
290            // This is a secondary message with no span info
291            if !c.span.has_primary_spans() && !c.span.has_span_labels() {
292                group = group.element(level.clone().message(msg));
293                continue;
294            }
295
296            report.push(std::mem::replace(
297                &mut group,
298                Group::with_title(level.clone().secondary_title(msg)),
299            ));
300
301            let mut file_ann = collect_annotations(&c.span, sm);
302            let primary_span = c.span.primary_span().unwrap_or_default();
303            if !primary_span.is_dummy() {
304                let primary_lo = sm.lookup_char_pos(primary_span.lo());
305                if let Ok(pos) =
306                    file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name))
307                {
308                    file_ann.swap(0, pos);
309                }
310            }
311
312            for (file, annotations) in file_ann.into_iter() {
313                if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
314                    group = group.element(snippet);
315                }
316            }
317        }
318
319        let suggestions_expected = suggestions
320            .iter()
321            .filter(|s| {
322                matches!(
323                    s.style,
324                    SuggestionStyle::HideCodeInline
325                        | SuggestionStyle::ShowCode
326                        | SuggestionStyle::ShowAlways
327                )
328            })
329            .count();
330        for suggestion in suggestions.unwrap_tag() {
331            match suggestion.style {
332                SuggestionStyle::CompletelyHidden => {
333                    // do not display this suggestion, it is meant only for tools
334                }
335                SuggestionStyle::HideCodeAlways => {
336                    let msg = self.no_style_msgs(&[(suggestion.msg.to_owned(), Style::HeaderMsg)]);
337                    group = group.element(annotate_snippets::Level::HELP.message(msg));
338                }
339                SuggestionStyle::HideCodeInline
340                | SuggestionStyle::ShowCode
341                | SuggestionStyle::ShowAlways => {
342                    let substitutions = suggestion
343                        .substitutions
344                        .iter()
345                        .cloned() // clone is required to sort and filter duplicated spans
346                        .filter_map(|mut subst| {
347                            // Suggestions coming from macros can have malformed spans. This is a
348                            // heavy handed approach to avoid ICEs by
349                            // ignoring the suggestion outright.
350                            let invalid =
351                                subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err());
352                            if invalid {
353                                debug!("suggestion contains an invalid span: {:?}", subst);
354                            }
355
356                            // Assumption: all spans are in the same file, and all spans
357                            // are disjoint. Sort in ascending order.
358                            subst.parts.sort_by_key(|part| part.span.lo());
359                            // Verify the assumption that all spans are disjoint
360                            assert_eq!(
361                                subst.parts.windows(2).find(|s| s[0].span.overlaps(s[1].span)),
362                                None,
363                                "all spans must be disjoint",
364                            );
365
366                            // Account for cases where we are suggesting the same code that's
367                            // already there. This shouldn't happen
368                            // often, but in some cases for multipart
369                            // suggestions it's much easier to handle it here than in the origin.
370                            subst.parts.retain(|p| is_different(sm, &p.snippet, p.span));
371
372                            if !invalid { Some(subst) } else { None }
373                        })
374                        .collect::<Vec<_>>();
375
376                    if substitutions.is_empty() {
377                        continue;
378                    }
379                    let mut msg = suggestion.msg.to_string();
380
381                    let lo = substitutions
382                        .iter()
383                        .find_map(|sub| sub.parts.first().map(|p| p.span.lo()))
384                        .unwrap();
385                    let file = sm.lookup_source_file(lo);
386
387                    let filename = sm.filename_for_diagnostics(&file.name).to_string();
388
389                    let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS);
390
391                    let subs = substitutions
392                        .into_iter()
393                        .take(MAX_SUGGESTIONS)
394                        .filter_map(|sub| {
395                            let mut confusion_type = ConfusionType::None;
396                            for part in &sub.parts {
397                                let part_confusion =
398                                    detect_confusion_type(sm, &part.snippet, part.span);
399                                confusion_type = confusion_type.combine(part_confusion);
400                            }
401
402                            if !matches!(confusion_type, ConfusionType::None) {
403                                msg.push_str(confusion_type.label_text());
404                            }
405
406                            let parts = sub
407                                .parts
408                                .into_iter()
409                                .filter_map(|p| {
410                                    if is_different(sm, &p.snippet, p.span) {
411                                        Some((p.span, p.snippet))
412                                    } else {
413                                        None
414                                    }
415                                })
416                                .collect::<Vec<_>>();
417
418                            if parts.is_empty() {
419                                None
420                            } else {
421                                let spans = parts.iter().map(|(span, _)| *span).collect::<Vec<_>>();
422
423                                // Unlike rustc, there is no attribute suggestion in Solidity (yet).
424                                // When similar feature to attribute arrives, refer to rustc's
425                                // implementation https://github.com/rust-lang/rust/blob/4146079cee94242771864147e32fb5d9adbd34f8/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs#L424
426                                let fold = true;
427
428                                if let Some((bounding_span, source, line_offset)) =
429                                    shrink_file(spans.as_slice(), &file.name, sm)
430                                {
431                                    let adj_lo = bounding_span.lo().to_usize();
432                                    Some(
433                                        Snippet::source(source)
434                                            .line_start(line_offset)
435                                            .path(filename.clone())
436                                            .fold(fold)
437                                            .patches(parts.into_iter().map(
438                                                |(span, replacement)| {
439                                                    let lo =
440                                                        span.lo().to_usize().saturating_sub(adj_lo);
441                                                    let hi =
442                                                        span.hi().to_usize().saturating_sub(adj_lo);
443
444                                                    Patch::new(lo..hi, replacement.into_inner())
445                                                },
446                                            )),
447                                    )
448                                } else {
449                                    None
450                                }
451                            }
452                        })
453                        .collect::<Vec<_>>();
454                    if !subs.is_empty() {
455                        report.push(std::mem::replace(
456                            &mut group,
457                            Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)),
458                        ));
459
460                        group = group.elements(subs);
461                        if other_suggestions > 0 {
462                            group = group.element(
463                                annotate_snippets::Level::NOTE.no_name().message(format!(
464                                    "and {} other candidate{}",
465                                    other_suggestions,
466                                    pluralize!(other_suggestions)
467                                )),
468                            );
469                        }
470                    }
471                }
472            }
473        }
474
475        // FIXME: This hack should be removed once annotate_snippets is the
476        // default emitter.
477        if suggestions_expected > 0 && report.is_empty() {
478            group = group.element(Padding);
479        }
480
481        if !group.is_empty() {
482            report.push(group);
483        }
484
485        if let Err(e) = emit_to_destination(renderer.render(&report), level, &mut self.writer) {
486            panic!("failed to emit error: {e}");
487        }
488    }
489
490    fn pre_style_msgs(&self, msgs: &[(DiagMsg, Style)], level: Level) -> String {
491        msgs.iter()
492            .filter_map(|(m, style)| {
493                let text = m.as_str();
494                let style = style.to_color_spec(level);
495                if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) }
496            })
497            .collect()
498    }
499
500    // Unlike rustc, there is no translation.
501    // Since the behavior of `translator.translate_messages` does not contains styling,
502    // this function to concatenate messages instead can be used.
503    fn no_style_msgs(&self, msgs: &[(DiagMsg, Style)]) -> String {
504        msgs.iter().map(|(m, _)| m.as_str()).collect()
505    }
506
507    fn annotated_snippet<'a>(
508        &self,
509        annotations: Vec<Annotation>,
510        file_name: &FileName,
511        sm: &Arc<SourceMap>,
512    ) -> Option<Snippet<'a, ASAnnotation<'a>>> {
513        let spans = annotations.iter().map(|a| a.span).collect::<Vec<_>>();
514        if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) {
515            let adj_lo = bounding_span.lo().to_usize();
516
517            let filename = sm.filename_for_diagnostics(file_name).to_string();
518
519            Some(Snippet::source(source).line_start(offset_line).path(filename).annotations(
520                annotations.into_iter().map(move |a| {
521                    let lo = a.span.lo().to_usize().saturating_sub(adj_lo);
522                    let hi = a.span.hi().to_usize().saturating_sub(adj_lo);
523                    let ann = a.kind.span(lo..hi);
524                    if let Some(label) = a.label { ann.label(label) } else { ann }
525                }),
526            ))
527        } else {
528            None
529        }
530    }
531}
532
533/// Diagnostic emitter that emits diagnostics in human-readable format to a local buffer.
534pub struct HumanBufferEmitter {
535    inner: HumanEmitter,
536}
537
538impl Emitter for HumanBufferEmitter {
539    #[inline]
540    fn emit_diagnostic(&mut self, diagnostic: &mut Diag) {
541        self.inner.emit_diagnostic(diagnostic);
542    }
543
544    #[inline]
545    fn emit_diagnostic_ref(&mut self, diagnostic: &Diag) {
546        self.inner.emit_diagnostic_ref(diagnostic);
547    }
548
549    #[inline]
550    fn source_map(&self) -> Option<&Arc<SourceMap>> {
551        Emitter::source_map(&self.inner)
552    }
553
554    #[inline]
555    fn supports_color(&self) -> bool {
556        self.inner.supports_color()
557    }
558}
559
560impl HumanBufferEmitter {
561    /// Creates a new `BufferEmitter` that writes to a local buffer.
562    pub fn new(color_choice: ColorChoice) -> Self {
563        Self { inner: HumanEmitter::new(Vec::<u8>::new(), stderr_choice(color_choice)) }
564    }
565
566    /// Sets the source map.
567    pub fn source_map(mut self, source_map: Option<Arc<SourceMap>>) -> Self {
568        self.inner = self.inner.source_map(source_map);
569        self
570    }
571
572    /// Sets whether to emit diagnostics in a way that is suitable for UI testing.
573    pub fn ui_testing(mut self, yes: bool) -> Self {
574        self.inner = self.inner.ui_testing(yes);
575        self
576    }
577
578    /// Sets the human emitter kind (unicode vs short).
579    pub fn human_kind(mut self, kind: HumanEmitterKind) -> Self {
580        self.inner = self.inner.human_kind(kind);
581        self
582    }
583
584    /// Sets the terminal width for formatting.
585    pub fn terminal_width(mut self, width: Option<usize>) -> Self {
586        self.inner = self.inner.terminal_width(width);
587        self
588    }
589
590    /// Returns a reference to the underlying human emitter.
591    pub fn inner(&self) -> &HumanEmitter {
592        &self.inner
593    }
594
595    /// Returns a mutable reference to the underlying human emitter.
596    pub fn inner_mut(&mut self) -> &mut HumanEmitter {
597        &mut self.inner
598    }
599
600    /// Returns a reference to the buffer.
601    pub fn buffer(&self) -> &str {
602        let buffer = self.inner.downcast_writer::<Vec<u8>>().unwrap();
603        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
604        // SAFETY: The buffer is guaranteed to be valid UTF-8.
605        unsafe { std::str::from_utf8_unchecked(buffer) }
606    }
607
608    /// Returns a mutable reference to the buffer.
609    pub fn buffer_mut(&mut self) -> &mut String {
610        let buffer = self.inner.downcast_writer_mut::<Vec<u8>>().unwrap();
611        debug_assert!(std::str::from_utf8(buffer).is_ok(), "HumanEmitter wrote invalid UTF-8");
612        // SAFETY: The buffer is guaranteed to be valid UTF-8.
613        unsafe { &mut *(buffer as *mut Vec<u8> as *mut String) }
614    }
615}
616
617fn annotation_level_for_level<'a>(level: Level) -> ASLevel<'a> {
618    match level {
619        Level::Bug | Level::Fatal | Level::Error | Level::FailureNote => ASLevel::ERROR,
620        Level::Warning => ASLevel::WARNING,
621        Level::Note | Level::OnceNote => ASLevel::NOTE,
622        Level::Help | Level::OnceHelp => ASLevel::HELP,
623        Level::Allow => ASLevel::INFO,
624    }
625    .with_name(if level == Level::FailureNote { None } else { Some(level.to_str()) })
626}
627
628fn stderr_choice(color_choice: ColorChoice) -> ColorChoice {
629    static AUTO: OnceLock<ColorChoice> = OnceLock::new();
630    if color_choice == ColorChoice::Auto {
631        *AUTO.get_or_init(|| anstream::AutoStream::choice(&std::io::stderr()))
632    } else {
633        color_choice
634    }
635}
636
637fn emit_to_destination(rendered: String, lvl: &Level, dst: &mut Writer) -> io::Result<()> {
638    // Unlike rustc, there is no lock
639    // use crate::lock;
640    // let _buffer_lock = lock::acquire_global_lock("rustc_errors");
641
642    writeln!(dst, "{rendered}")?;
643    if !lvl.is_failure_note() {
644        writeln!(dst)?;
645    }
646    dst.flush()?;
647    Ok(())
648}
649
650#[derive(Debug)]
651struct Annotation {
652    kind: AnnotationKind,
653    span: Span,
654    label: Option<String>,
655}
656
657fn collect_annotations(
658    msp: &MultiSpan,
659    sm: &Arc<SourceMap>,
660) -> Vec<(Arc<SourceFile>, Vec<Annotation>)> {
661    let mut output: Vec<(Arc<SourceFile>, Vec<Annotation>)> = vec![];
662
663    for SpanLabel { span, is_primary, label } in msp.span_labels() {
664        // If we don't have a useful span, pick the primary span if that exists.
665        // Worst case we'll just print an error at the top of the main file.
666        let span = match (span.is_dummy(), msp.primary_span()) {
667            (_, None) | (false, _) => span,
668            (true, Some(span)) => span,
669        };
670        let file = sm.lookup_source_file(span.lo());
671
672        let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context };
673
674        let label = label.as_ref().map(|m| normalize_whitespace(m));
675
676        let ann = Annotation { kind, span, label };
677        if sm.is_valid_span(ann.span).is_ok() {
678            // Look through each of our files for the one we're adding to.
679            if let Some((_, annotations)) =
680                output.iter_mut().find(|(f, _)| f.start_pos == file.start_pos)
681            {
682                annotations.push(ann);
683            } else {
684                output.push((file, vec![ann]));
685            }
686        }
687    }
688
689    // Sort annotations within each file by line number.
690    for (_, ann) in output.iter_mut() {
691        ann.sort_by_key(|a| {
692            let lo = sm.lookup_char_pos(a.span.lo());
693            lo.line
694        });
695    }
696
697    output
698}
699
700fn shrink_file(
701    spans: &[Span],
702    _file_name: &FileName,
703    sm: &Arc<SourceMap>,
704) -> Option<(Span, String, usize)> {
705    let lo_byte = spans.iter().map(|s| s.lo()).min()?;
706    let lo_loc = sm.lookup_char_pos(lo_byte);
707
708    let hi_byte = spans.iter().map(|s| s.hi()).max()?;
709    let hi_loc = sm.lookup_char_pos(hi_byte);
710
711    if lo_loc.file.start_pos != hi_loc.file.start_pos {
712        // This may happen when spans cross file boundaries due to macro expansion.
713        return None;
714    }
715
716    let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start;
717    let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end;
718
719    let bounding_span = Span::new(lo, hi);
720    let source = sm.span_to_snippet(bounding_span).ok()?;
721    let offset_line = lo_loc.line;
722
723    Some((bounding_span, source, offset_line))
724}