1use 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#[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 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 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 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 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 } else if !self.short_message {
224 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 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 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 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 } else if !self.short_message {
292 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 }
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 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 subst.parts.sort_by_key(|part| part.span.lo());
348 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 if lo_file.stable_id != hi_file.stable_id {
362 return None;
363 }
364
365 if !sm.ensure_source_file_source_present(&lo_file) {
367 return None;
368 }
369
370 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 let fold = if let [(p, snippet)] = &mut parts[..]
438 && snippet.trim().starts_with("#[")
439 && 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 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 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 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 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 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 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 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 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 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 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}