rustc_errors/
annotate_snippet_emitter_writer.rs

1//! Emit diagnostics using the `annotate-snippets` library
2//!
3//! This is the equivalent of `./emitter.rs` but making use of the
4//! [`annotate-snippets`][annotate_snippets] library instead of building the output ourselves.
5//!
6//! [annotate_snippets]: https://docs.rs/crate/annotate-snippets/
7
8use std::borrow::Cow;
9use std::error::Report;
10use std::fmt::Debug;
11use std::io;
12use std::io::Write;
13use std::sync::Arc;
14
15use annotate_snippets::renderer::DEFAULT_TERM_WIDTH;
16use annotate_snippets::{AnnotationKind, Group, Origin, Padding, Patch, Renderer, Snippet};
17use anstream::ColorChoice;
18use derive_setters::Setters;
19use rustc_data_structures::sync::IntoDynSyncSend;
20use rustc_error_messages::{FluentArgs, SpanLabel};
21use rustc_lint_defs::pluralize;
22use rustc_span::source_map::SourceMap;
23use rustc_span::{BytePos, FileName, Pos, SourceFile, Span};
24use tracing::debug;
25
26use crate::emitter::{
27    ConfusionType, Destination, MAX_SUGGESTIONS, OutputTheme, detect_confusion_type, is_different,
28    normalize_whitespace, should_show_source_code,
29};
30use crate::registry::Registry;
31use crate::translation::{Translator, to_fluent_args};
32use crate::{
33    CodeSuggestion, DiagInner, DiagMessage, Emitter, ErrCode, Level, MultiSpan, Style, Subdiag,
34    SuggestionStyle, TerminalUrl,
35};
36
37/// Generates diagnostics using annotate-snippet
38#[derive(Setters)]
39pub struct AnnotateSnippetEmitter {
40    #[setters(skip)]
41    dst: IntoDynSyncSend<Destination>,
42    sm: Option<Arc<SourceMap>>,
43    #[setters(skip)]
44    translator: Translator,
45    short_message: bool,
46    ui_testing: bool,
47    ignored_directories_in_source_blocks: Vec<String>,
48    diagnostic_width: Option<usize>,
49
50    macro_backtrace: bool,
51    track_diagnostics: bool,
52    terminal_url: TerminalUrl,
53    theme: OutputTheme,
54}
55
56impl Debug for AnnotateSnippetEmitter {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("AnnotateSnippetEmitter")
59            .field("short_message", &self.short_message)
60            .field("ui_testing", &self.ui_testing)
61            .field(
62                "ignored_directories_in_source_blocks",
63                &self.ignored_directories_in_source_blocks,
64            )
65            .field("diagnostic_width", &self.diagnostic_width)
66            .field("macro_backtrace", &self.macro_backtrace)
67            .field("track_diagnostics", &self.track_diagnostics)
68            .field("terminal_url", &self.terminal_url)
69            .field("theme", &self.theme)
70            .finish()
71    }
72}
73
74impl Emitter for AnnotateSnippetEmitter {
75    /// The entry point for the diagnostics generation
76    fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) {
77        let fluent_args = to_fluent_args(diag.args.iter());
78
79        if self.track_diagnostics && diag.span.has_primary_spans() && !diag.span.is_dummy() {
80            diag.children.insert(0, diag.emitted_at_sub_diag());
81        }
82
83        let mut suggestions = diag.suggestions.unwrap_tag();
84        self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args);
85
86        self.fix_multispans_in_extern_macros_and_render_macro_backtrace(
87            &mut diag.span,
88            &mut diag.children,
89            &diag.level,
90            self.macro_backtrace,
91        );
92
93        self.emit_messages_default(
94            &diag.level,
95            &diag.messages,
96            &fluent_args,
97            &diag.code,
98            &diag.span,
99            &diag.children,
100            suggestions,
101        );
102    }
103
104    fn source_map(&self) -> Option<&SourceMap> {
105        self.sm.as_deref()
106    }
107
108    fn should_show_explain(&self) -> bool {
109        !self.short_message
110    }
111
112    fn translator(&self) -> &Translator {
113        &self.translator
114    }
115
116    fn supports_color(&self) -> bool {
117        false
118    }
119}
120
121fn annotation_level_for_level(level: Level) -> annotate_snippets::level::Level<'static> {
122    match level {
123        Level::Bug | Level::DelayedBug => {
124            annotate_snippets::Level::ERROR.with_name("error: internal compiler error")
125        }
126        Level::Fatal | Level::Error => annotate_snippets::level::ERROR,
127        Level::ForceWarning | Level::Warning => annotate_snippets::Level::WARNING,
128        Level::Note | Level::OnceNote => annotate_snippets::Level::NOTE,
129        Level::Help | Level::OnceHelp => annotate_snippets::Level::HELP,
130        Level::FailureNote => annotate_snippets::Level::NOTE.no_name(),
131        Level::Allow => panic!("Should not call with Allow"),
132        Level::Expect => panic!("Should not call with Expect"),
133    }
134}
135
136impl AnnotateSnippetEmitter {
137    pub fn new(dst: Destination, translator: Translator) -> Self {
138        Self {
139            dst: IntoDynSyncSend(dst),
140            sm: None,
141            translator,
142            short_message: false,
143            ui_testing: false,
144            ignored_directories_in_source_blocks: Vec::new(),
145            diagnostic_width: None,
146            macro_backtrace: false,
147            track_diagnostics: false,
148            terminal_url: TerminalUrl::No,
149            theme: OutputTheme::Ascii,
150        }
151    }
152
153    fn emit_messages_default(
154        &mut self,
155        level: &Level,
156        msgs: &[(DiagMessage, Style)],
157        args: &FluentArgs<'_>,
158        code: &Option<ErrCode>,
159        msp: &MultiSpan,
160        children: &[Subdiag],
161        suggestions: Vec<CodeSuggestion>,
162    ) {
163        let renderer = self.renderer();
164        let annotation_level = annotation_level_for_level(*level);
165
166        // If at least one portion of the message is styled, we need to
167        // "pre-style" the message
168        let mut title = if msgs.iter().any(|(_, style)| style != &crate::Style::NoStyle) {
169            annotation_level
170                .clone()
171                .secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level, args)))
172        } else {
173            annotation_level.clone().primary_title(self.translator.translate_messages(msgs, args))
174        };
175
176        if let Some(c) = code {
177            title = title.id(c.to_string());
178            if let TerminalUrl::Yes = self.terminal_url {
179                title = title.id_url(format!("https://doc.rust-lang.org/error_codes/{c}.html"));
180            }
181        }
182
183        let mut report = vec![];
184        let mut group = Group::with_title(title);
185
186        // If we don't have span information, emit and exit
187        let Some(sm) = self.sm.as_ref() else {
188            group = group.elements(children.iter().map(|c| {
189                let msg = self.translator.translate_messages(&c.messages, args).to_string();
190                let level = annotation_level_for_level(c.level);
191                level.message(msg)
192            }));
193
194            report.push(group);
195            if let Err(e) = emit_to_destination(
196                renderer.render(&report),
197                level,
198                &mut self.dst,
199                self.short_message,
200            ) {
201                panic!("failed to emit error: {e}");
202            }
203            return;
204        };
205
206        let mut file_ann = collect_annotations(args, msp, sm, &self.translator);
207
208        // Make sure our primary file comes first
209        let primary_span = msp.primary_span().unwrap_or_default();
210        if !primary_span.is_dummy() {
211            let primary_lo = sm.lookup_char_pos(primary_span.lo());
212            if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) {
213                file_ann.swap(0, pos);
214            }
215
216            let file_ann_len = file_ann.len();
217            for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() {
218                if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) {
219                    if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
220                        group = group.element(snippet);
221                    }
222                // we can't annotate anything if the source is unavailable.
223                } else if !self.short_message {
224                    // We'll just print unannotated messages
225                    group = self.unannotated_messages(
226                        annotations,
227                        &file.name,
228                        sm,
229                        file_idx,
230                        &mut report,
231                        group,
232                        &annotation_level,
233                    );
234                    // If this is the last annotation for a file, and
235                    // this is the last file, and the first child is a
236                    // "secondary" message, we need to add padding
237                    // ╭▸ /rustc/FAKE_PREFIX/library/core/src/clone.rs:236:13
238                    // │
239                    // ├ note: the late bound lifetime parameter
240                    // │ (<- It adds *this*)
241                    // ╰ warning: this was previously accepted
242                    if let Some(c) = children.first()
243                        && (!c.span.has_primary_spans() && !c.span.has_span_labels())
244                        && file_idx == file_ann_len - 1
245                    {
246                        group = group.element(Padding);
247                    }
248                }
249            }
250        }
251
252        for c in children {
253            let level = annotation_level_for_level(c.level);
254
255            // If at least one portion of the message is styled, we need to
256            // "pre-style" the message
257            let msg = if c.messages.iter().any(|(_, style)| style != &crate::Style::NoStyle) {
258                Cow::Owned(self.pre_style_msgs(&c.messages, c.level, args))
259            } else {
260                self.translator.translate_messages(&c.messages, args)
261            };
262
263            // This is a secondary message with no span info
264            if !c.span.has_primary_spans() && !c.span.has_span_labels() {
265                group = group.element(level.clone().message(msg));
266                continue;
267            }
268
269            report.push(std::mem::replace(
270                &mut group,
271                Group::with_title(level.clone().secondary_title(msg)),
272            ));
273
274            let mut file_ann = collect_annotations(args, &c.span, sm, &self.translator);
275            let primary_span = c.span.primary_span().unwrap_or_default();
276            if !primary_span.is_dummy() {
277                let primary_lo = sm.lookup_char_pos(primary_span.lo());
278                if let Ok(pos) =
279                    file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name))
280                {
281                    file_ann.swap(0, pos);
282                }
283            }
284
285            for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() {
286                if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) {
287                    if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) {
288                        group = group.element(snippet);
289                    }
290                // we can't annotate anything if the source is unavailable.
291                } else if !self.short_message {
292                    // We'll just print unannotated messages
293                    group = self.unannotated_messages(
294                        annotations,
295                        &file.name,
296                        sm,
297                        file_idx,
298                        &mut report,
299                        group,
300                        &level,
301                    );
302                }
303            }
304        }
305
306        let suggestions_expected = suggestions
307            .iter()
308            .filter(|s| {
309                matches!(
310                    s.style,
311                    SuggestionStyle::HideCodeInline
312                        | SuggestionStyle::ShowCode
313                        | SuggestionStyle::ShowAlways
314                )
315            })
316            .count();
317        for suggestion in suggestions {
318            match suggestion.style {
319                SuggestionStyle::CompletelyHidden => {
320                    // do not display this suggestion, it is meant only for tools
321                }
322                SuggestionStyle::HideCodeAlways => {
323                    let msg = self
324                        .translator
325                        .translate_messages(&[(suggestion.msg.to_owned(), Style::HeaderMsg)], args);
326                    group = group.element(annotate_snippets::Level::HELP.message(msg));
327                }
328                SuggestionStyle::HideCodeInline
329                | SuggestionStyle::ShowCode
330                | SuggestionStyle::ShowAlways => {
331                    let substitutions = suggestion
332                        .substitutions
333                        .into_iter()
334                        .filter(|subst| {
335                            // Suggestions coming from macros can have malformed spans. This is a heavy
336                            // handed approach to avoid ICEs by ignoring the suggestion outright.
337                            let invalid =
338                                subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err());
339                            if invalid {
340                                debug!("suggestion contains an invalid span: {:?}", subst);
341                            }
342                            !invalid
343                        })
344                        .filter_map(|mut subst| {
345                            // Assumption: all spans are in the same file, and all spans
346                            // are disjoint. Sort in ascending order.
347                            subst.parts.sort_by_key(|part| part.span.lo());
348                            // Verify the assumption that all spans are disjoint
349                            debug_assert_eq!(
350                                subst.parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)),
351                                None,
352                                "all spans must be disjoint",
353                            );
354
355                            let lo = subst.parts.iter().map(|part| part.span.lo()).min()?;
356                            let lo_file = sm.lookup_source_file(lo);
357                            let hi = subst.parts.iter().map(|part| part.span.hi()).max()?;
358                            let hi_file = sm.lookup_source_file(hi);
359
360                            // The different spans might belong to different contexts, if so ignore suggestion.
361                            if lo_file.stable_id != hi_file.stable_id {
362                                return None;
363                            }
364
365                            // We can't splice anything if the source is unavailable.
366                            if !sm.ensure_source_file_source_present(&lo_file) {
367                                return None;
368                            }
369
370                            // Account for cases where we are suggesting the same code that's already
371                            // there. This shouldn't happen often, but in some cases for multipart
372                            // suggestions it's much easier to handle it here than in the origin.
373                            subst.parts.retain(|p| is_different(sm, &p.snippet, p.span));
374
375                            if subst.parts.is_empty() { None } else { Some(subst) }
376                        })
377                        .collect::<Vec<_>>();
378
379                    if substitutions.is_empty() {
380                        continue;
381                    }
382                    let mut msg = self
383                        .translator
384                        .translate_message(&suggestion.msg, args)
385                        .map_err(Report::new)
386                        .unwrap()
387                        .to_string();
388
389                    let lo = substitutions
390                        .iter()
391                        .find_map(|sub| sub.parts.first().map(|p| p.span.lo()))
392                        .unwrap();
393                    let file = sm.lookup_source_file(lo);
394
395                    let filename =
396                        sm.filename_for_diagnostics(&file.name).to_string_lossy().to_string();
397
398                    let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS);
399
400                    let subs = substitutions
401                        .into_iter()
402                        .take(MAX_SUGGESTIONS)
403                        .filter_map(|sub| {
404                            let mut confusion_type = ConfusionType::None;
405                            for part in &sub.parts {
406                                let part_confusion =
407                                    detect_confusion_type(sm, &part.snippet, part.span);
408                                confusion_type = confusion_type.combine(part_confusion);
409                            }
410
411                            if !matches!(confusion_type, ConfusionType::None) {
412                                msg.push_str(confusion_type.label_text());
413                            }
414
415                            let mut parts = sub
416                                .parts
417                                .into_iter()
418                                .filter_map(|p| {
419                                    if is_different(sm, &p.snippet, p.span) {
420                                        Some((p.span, p.snippet))
421                                    } else {
422                                        None
423                                    }
424                                })
425                                .collect::<Vec<_>>();
426
427                            if parts.is_empty() {
428                                None
429                            } else {
430                                let spans = parts.iter().map(|(span, _)| *span).collect::<Vec<_>>();
431                                // The suggestion adds an entire line of code, ending on a newline, so we'll also
432                                // print the *following* line, to provide context of what we're advising people to
433                                // do. Otherwise you would only see contextless code that can be confused for
434                                // already existing code, despite the colors and UI elements.
435                                // We special case `#[derive(_)]\n` and other attribute suggestions, because those
436                                // are the ones where context is most useful.
437                                let fold = if let [(p, snippet)] = &mut parts[..]
438                                    && snippet.trim().starts_with("#[")
439                                    // This allows for spaces to come between the attribute and the newline
440                                    && snippet.trim().ends_with("]")
441                                    && snippet.ends_with('\n')
442                                    && p.hi() == p.lo()
443                                    && let Ok(b) = sm.span_to_prev_source(*p)
444                                    && let b = b.rsplit_once('\n').unwrap_or_else(|| ("", &b)).1
445                                    && b.trim().is_empty()
446                                {
447                                    // FIXME: This is a hack:
448                                    // The span for attribute suggestions often times points to the
449                                    // beginning of an item, disregarding leading whitespace. This
450                                    // causes the attribute to be properly indented, but leaves original
451                                    // item without indentation when rendered.
452                                    // This fixes that problem by adjusting the span to point to the start
453                                    // of the whitespace, and adds the whitespace to the replacement.
454                                    //
455                                    // Source: "    extern "custom" fn negate(a: i64) -> i64 {\n"
456                                    // Span: 4..4
457                                    // Replacement: "#[unsafe(naked)]\n"
458                                    //
459                                    // Before:
460                                    // help: convert this to an `#[unsafe(naked)]` function
461                                    //    |
462                                    // LL +     #[unsafe(naked)]
463                                    // LL | extern "custom" fn negate(a: i64) -> i64 {
464                                    //    |
465                                    //
466                                    // After
467                                    // help: convert this to an `#[unsafe(naked)]` function
468                                    //    |
469                                    // LL +     #[unsafe(naked)]
470                                    // LL |     extern "custom" fn negate(a: i64) -> i64 {
471                                    //    |
472                                    if !b.is_empty() && !snippet.ends_with(b) {
473                                        snippet.insert_str(0, b);
474                                        let offset = BytePos(b.len() as u32);
475                                        *p = p.with_lo(p.lo() - offset).shrink_to_lo();
476                                    }
477                                    false
478                                } else {
479                                    true
480                                };
481
482                                if let Some((bounding_span, source, line_offset)) =
483                                    shrink_file(spans.as_slice(), &file.name, sm)
484                                {
485                                    let adj_lo = bounding_span.lo().to_usize();
486                                    Some(
487                                        Snippet::source(source)
488                                            .line_start(line_offset)
489                                            .path(filename.clone())
490                                            .fold(fold)
491                                            .patches(parts.into_iter().map(
492                                                |(span, replacement)| {
493                                                    let lo =
494                                                        span.lo().to_usize().saturating_sub(adj_lo);
495                                                    let hi =
496                                                        span.hi().to_usize().saturating_sub(adj_lo);
497
498                                                    Patch::new(lo..hi, replacement)
499                                                },
500                                            )),
501                                    )
502                                } else {
503                                    None
504                                }
505                            }
506                        })
507                        .collect::<Vec<_>>();
508                    if !subs.is_empty() {
509                        report.push(std::mem::replace(
510                            &mut group,
511                            Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)),
512                        ));
513
514                        group = group.elements(subs);
515                        if other_suggestions > 0 {
516                            group = group.element(
517                                annotate_snippets::Level::NOTE.no_name().message(format!(
518                                    "and {} other candidate{}",
519                                    other_suggestions,
520                                    pluralize!(other_suggestions)
521                                )),
522                            );
523                        }
524                    }
525                }
526            }
527        }
528
529        // FIXME: This hack should be removed once annotate_snippets is the
530        // default emitter.
531        if suggestions_expected > 0 && report.is_empty() {
532            group = group.element(Padding);
533        }
534
535        if !group.is_empty() {
536            report.push(group);
537        }
538        if let Err(e) =
539            emit_to_destination(renderer.render(&report), level, &mut self.dst, self.short_message)
540        {
541            panic!("failed to emit error: {e}");
542        }
543    }
544
545    fn renderer(&self) -> Renderer {
546        let width = if let Some(width) = self.diagnostic_width {
547            width
548        } else if self.ui_testing || cfg!(miri) {
549            DEFAULT_TERM_WIDTH
550        } else {
551            termize::dimensions().map(|(w, _)| w).unwrap_or(DEFAULT_TERM_WIDTH)
552        };
553        let decor_style = match self.theme {
554            OutputTheme::Ascii => annotate_snippets::renderer::DecorStyle::Ascii,
555            OutputTheme::Unicode => annotate_snippets::renderer::DecorStyle::Unicode,
556        };
557
558        match self.dst.current_choice() {
559            ColorChoice::AlwaysAnsi | ColorChoice::Always | ColorChoice::Auto => Renderer::styled(),
560            ColorChoice::Never => Renderer::plain(),
561        }
562        .term_width(width)
563        .anonymized_line_numbers(self.ui_testing)
564        .decor_style(decor_style)
565        .short_message(self.short_message)
566    }
567
568    fn pre_style_msgs(
569        &self,
570        msgs: &[(DiagMessage, Style)],
571        level: Level,
572        args: &FluentArgs<'_>,
573    ) -> String {
574        msgs.iter()
575            .filter_map(|(m, style)| {
576                let text = self.translator.translate_message(m, args).map_err(Report::new).unwrap();
577                let style = style.anstyle(level);
578                if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) }
579            })
580            .collect()
581    }
582
583    fn annotated_snippet<'a>(
584        &self,
585        annotations: Vec<Annotation>,
586        file_name: &FileName,
587        sm: &Arc<SourceMap>,
588    ) -> Option<Snippet<'a, annotate_snippets::Annotation<'a>>> {
589        let spans = annotations.iter().map(|a| a.span).collect::<Vec<_>>();
590        if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) {
591            let adj_lo = bounding_span.lo().to_usize();
592            let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string();
593            Some(Snippet::source(source).line_start(offset_line).path(filename).annotations(
594                annotations.into_iter().map(move |a| {
595                    let lo = a.span.lo().to_usize().saturating_sub(adj_lo);
596                    let hi = a.span.hi().to_usize().saturating_sub(adj_lo);
597                    let ann = a.kind.span(lo..hi);
598                    if let Some(label) = a.label { ann.label(label) } else { ann }
599                }),
600            ))
601        } else {
602            None
603        }
604    }
605
606    fn unannotated_messages<'a>(
607        &self,
608        annotations: Vec<Annotation>,
609        file_name: &FileName,
610        sm: &Arc<SourceMap>,
611        file_idx: usize,
612        report: &mut Vec<Group<'a>>,
613        mut group: Group<'a>,
614        level: &annotate_snippets::level::Level<'static>,
615    ) -> Group<'a> {
616        let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string();
617        let mut line_tracker = vec![];
618        for (i, a) in annotations.into_iter().enumerate() {
619            let lo = sm.lookup_char_pos(a.span.lo());
620            let hi = sm.lookup_char_pos(a.span.hi());
621            if i == 0 || (a.label.is_some()) {
622                // Render each new file after the first in its own Group
623                //    ╭▸ $DIR/deriving-meta-unknown-trait.rs:1:10
624                //    │
625                // LL │ #[derive(Eqr)]
626                //    │          ━━━
627                //    ╰╴ (<- It makes it so *this* will get printed)
628                //    ╭▸ $SRC_DIR/core/src/option.rs:594:0
629                //    ⸬  $SRC_DIR/core/src/option.rs:602:4
630                //    │
631                //    ╰ note: not covered
632                if i == 0 && file_idx != 0 {
633                    report.push(std::mem::replace(&mut group, Group::with_level(level.clone())));
634                }
635
636                if !line_tracker.contains(&lo.line) && (i == 0 || hi.line <= lo.line) {
637                    line_tracker.push(lo.line);
638                    // ╭▸ $SRC_DIR/core/src/option.rs:594:0 (<- It adds *this*)
639                    // ⸬  $SRC_DIR/core/src/option.rs:602:4
640                    // │
641                    // ╰ note: not covered
642                    group = group.element(
643                        Origin::path(filename.clone())
644                            .line(sm.doctest_offset_line(file_name, lo.line))
645                            .char_column(lo.col_display),
646                    );
647                }
648
649                if hi.line > lo.line
650                    && a.label.as_ref().is_some_and(|l| !l.is_empty())
651                    && !line_tracker.contains(&hi.line)
652                {
653                    line_tracker.push(hi.line);
654                    // ╭▸ $SRC_DIR/core/src/option.rs:594:0
655                    // ⸬  $SRC_DIR/core/src/option.rs:602:4 (<- It adds *this*)
656                    // │
657                    // ╰ note: not covered
658                    group = group.element(
659                        Origin::path(filename.clone())
660                            .line(sm.doctest_offset_line(file_name, hi.line))
661                            .char_column(hi.col_display),
662                    );
663                }
664
665                if let Some(label) = a.label
666                    && !label.is_empty()
667                {
668                    // ╭▸ $SRC_DIR/core/src/option.rs:594:0
669                    // ⸬  $SRC_DIR/core/src/option.rs:602:4
670                    // │ (<- It adds *this*)
671                    // ╰ note: not covered (<- and *this*)
672                    group = group
673                        .element(Padding)
674                        .element(annotate_snippets::Level::NOTE.message(label));
675                }
676            }
677        }
678        group
679    }
680}
681
682fn emit_to_destination(
683    rendered: String,
684    lvl: &Level,
685    dst: &mut Destination,
686    short_message: bool,
687) -> io::Result<()> {
688    use crate::lock;
689    let _buffer_lock = lock::acquire_global_lock("rustc_errors");
690    writeln!(dst, "{rendered}")?;
691    if !short_message && !lvl.is_failure_note() {
692        writeln!(dst)?;
693    }
694    dst.flush()?;
695    Ok(())
696}
697
698#[derive(Debug)]
699struct Annotation {
700    kind: AnnotationKind,
701    span: Span,
702    label: Option<String>,
703}
704
705fn collect_annotations(
706    args: &FluentArgs<'_>,
707    msp: &MultiSpan,
708    sm: &Arc<SourceMap>,
709    translator: &Translator,
710) -> Vec<(Arc<SourceFile>, Vec<Annotation>)> {
711    let mut output: Vec<(Arc<SourceFile>, Vec<Annotation>)> = vec![];
712
713    for SpanLabel { span, is_primary, label } in msp.span_labels() {
714        // If we don't have a useful span, pick the primary span if that exists.
715        // Worst case we'll just print an error at the top of the main file.
716        let span = match (span.is_dummy(), msp.primary_span()) {
717            (_, None) | (false, _) => span,
718            (true, Some(span)) => span,
719        };
720        let file = sm.lookup_source_file(span.lo());
721
722        let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context };
723
724        let label = label.as_ref().map(|m| {
725            normalize_whitespace(
726                &translator.translate_message(m, args).map_err(Report::new).unwrap(),
727            )
728        });
729
730        let ann = Annotation { kind, span, label };
731        if sm.is_valid_span(ann.span).is_ok() {
732            // Look through each of our files for the one we're adding to. We
733            // use each files `stable_id` to avoid issues with file name
734            // collisions when multiple versions of the same crate are present
735            // in the dependency graph
736            if let Some((_, annotations)) =
737                output.iter_mut().find(|(f, _)| f.stable_id == file.stable_id)
738            {
739                annotations.push(ann);
740            } else {
741                output.push((file, vec![ann]));
742            }
743        }
744    }
745
746    // Sort annotations within each file by line number
747    for (_, ann) in output.iter_mut() {
748        ann.sort_by_key(|a| {
749            let lo = sm.lookup_char_pos(a.span.lo());
750            lo.line
751        });
752    }
753    output
754}
755
756fn shrink_file(
757    spans: &[Span],
758    file_name: &FileName,
759    sm: &Arc<SourceMap>,
760) -> Option<(Span, String, usize)> {
761    let lo_byte = spans.iter().map(|s| s.lo()).min()?;
762    let lo_loc = sm.lookup_char_pos(lo_byte);
763
764    let hi_byte = spans.iter().map(|s| s.hi()).max()?;
765    let hi_loc = sm.lookup_char_pos(hi_byte);
766
767    if lo_loc.file.stable_id != hi_loc.file.stable_id {
768        // this may happen when spans cross file boundaries due to macro expansion.
769        return None;
770    }
771
772    let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start;
773    let hi = hi_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end;
774
775    let bounding_span = Span::with_root_ctxt(lo, hi);
776    let source = sm.span_to_snippet(bounding_span).ok()?;
777    let offset_line = sm.doctest_offset_line(file_name, lo_loc.line);
778
779    Some((bounding_span, source, offset_line))
780}