From 354b9779bf9b021c56f3ea14ed338e34377aa575 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Sat, 29 Mar 2025 09:48:39 -0600 Subject: [PATCH] chore: Update to the latest annotate-snippets --- Cargo.lock | 34 +- compiler/rustc_errors/Cargo.toml | 2 +- .../src/annotate_snippet_emitter_writer.rs | 774 +++++++++++++++--- compiler/rustc_session/src/session.rs | 19 +- tests/ui/annotate-snippet/missing-type.stderr | 15 + .../ui/annotate-snippet/multiple-files.stderr | 7 +- tests/ui/annotate-snippet/multispan.stderr | 51 ++ 7 files changed, 780 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c290b392d591..2320e33bc4bf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", - "unicode-width 0.2.1", + "unicode-width 0.2.2", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47224528f74de27d1d06aad6a5dda4f865b6ebe2e56c538943d746a7270cb67e" +dependencies = [ + "anstyle", + "unicode-width 0.2.2", ] [[package]] @@ -136,7 +146,7 @@ dependencies = [ "anstyle-lossy", "anstyle-parse", "html-escape", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -677,7 +687,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -808,7 +818,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] @@ -1485,7 +1495,7 @@ version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -1887,7 +1897,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "web-time", ] @@ -3756,7 +3766,7 @@ dependencies = [ name = "rustc_errors" version = "0.0.0" dependencies = [ - "annotate-snippets 0.11.5", + "annotate-snippets 0.12.7", "anstream", "anstyle", "derive_setters", @@ -4331,7 +4341,7 @@ dependencies = [ "thin-vec", "tracing", "unicode-normalization", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -4590,7 +4600,7 @@ dependencies = [ "sha1", "sha2", "tracing", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -5936,9 +5946,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -6223,7 +6233,7 @@ dependencies = [ "bumpalo", "leb128fmt", "memchr", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "wasm-encoder 0.240.0", ] diff --git a/compiler/rustc_errors/Cargo.toml b/compiler/rustc_errors/Cargo.toml index b7b5cbd35741f..6ade87ea3b255 100644 --- a/compiler/rustc_errors/Cargo.toml +++ b/compiler/rustc_errors/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] # tidy-alphabetical-start -annotate-snippets = "0.11" +annotate-snippets = "0.12.7" anstream = "0.6.20" anstyle = "1.0.13" derive_setters = "0.1.6" diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs index 2eb3c23259ffa..854e3ddf15e4a 100644 --- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -5,32 +5,70 @@ //! //! [annotate_snippets]: https://docs.rs/crate/annotate-snippets/ +use std::borrow::Cow; +use std::error::Report; +use std::fmt::Debug; +use std::io; +use std::io::Write; use std::sync::Arc; -use annotate_snippets::{Renderer, Snippet}; -use rustc_error_messages::FluentArgs; -use rustc_span::SourceFile; +use annotate_snippets::renderer::DEFAULT_TERM_WIDTH; +use annotate_snippets::{AnnotationKind, Group, Origin, Padding, Patch, Renderer, Snippet}; +use anstream::ColorChoice; +use derive_setters::Setters; +use rustc_data_structures::sync::IntoDynSyncSend; +use rustc_error_messages::{FluentArgs, SpanLabel}; +use rustc_lint_defs::pluralize; use rustc_span::source_map::SourceMap; +use rustc_span::{BytePos, FileName, Pos, SourceFile, Span}; +use tracing::debug; -use crate::emitter::FileWithAnnotatedLines; +use crate::emitter::{ + ConfusionType, Destination, MAX_SUGGESTIONS, OutputTheme, detect_confusion_type, is_different, + normalize_whitespace, should_show_source_code, +}; use crate::registry::Registry; -use crate::snippet::Line; use crate::translation::{Translator, to_fluent_args}; use crate::{ CodeSuggestion, DiagInner, DiagMessage, Emitter, ErrCode, Level, MultiSpan, Style, Subdiag, + SuggestionStyle, TerminalUrl, }; /// Generates diagnostics using annotate-snippet +#[derive(Setters)] pub struct AnnotateSnippetEmitter { - source_map: Option>, + #[setters(skip)] + dst: IntoDynSyncSend, + sm: Option>, + #[setters(skip)] translator: Translator, - - /// If true, hides the longer explanation text short_message: bool, - /// If true, will normalize line numbers with `LL` to prevent noise in UI test diffs. ui_testing: bool, + ignored_directories_in_source_blocks: Vec, + diagnostic_width: Option, macro_backtrace: bool, + track_diagnostics: bool, + terminal_url: TerminalUrl, + theme: OutputTheme, +} + +impl Debug for AnnotateSnippetEmitter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnnotateSnippetEmitter") + .field("short_message", &self.short_message) + .field("ui_testing", &self.ui_testing) + .field( + "ignored_directories_in_source_blocks", + &self.ignored_directories_in_source_blocks, + ) + .field("diagnostic_width", &self.diagnostic_width) + .field("macro_backtrace", &self.macro_backtrace) + .field("track_diagnostics", &self.track_diagnostics) + .field("terminal_url", &self.terminal_url) + .field("theme", &self.theme) + .finish() + } } impl Emitter for AnnotateSnippetEmitter { @@ -38,6 +76,10 @@ impl Emitter for AnnotateSnippetEmitter { fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) { let fluent_args = to_fluent_args(diag.args.iter()); + if self.track_diagnostics && diag.span.has_primary_spans() && !diag.span.is_dummy() { + diag.children.insert(0, diag.emitted_at_sub_diag()); + } + let mut suggestions = diag.suggestions.unwrap_tag(); self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args); @@ -55,12 +97,12 @@ impl Emitter for AnnotateSnippetEmitter { &diag.code, &diag.span, &diag.children, - &suggestions, + suggestions, ); } fn source_map(&self) -> Option<&SourceMap> { - self.source_map.as_deref() + self.sm.as_deref() } fn should_show_explain(&self) -> bool { @@ -70,128 +112,648 @@ impl Emitter for AnnotateSnippetEmitter { fn translator(&self) -> &Translator { &self.translator } -} -/// Provides the source string for the given `line` of `file` -fn source_string(file: Arc, line: &Line) -> String { - file.get_line(line.line_index - 1).map(|a| a.to_string()).unwrap_or_default() + fn supports_color(&self) -> bool { + false + } } -/// Maps [`crate::Level`] to [`annotate_snippets::Level`] -fn annotation_level_for_level(level: Level) -> annotate_snippets::Level { +fn annotation_level_for_level(level: Level) -> annotate_snippets::level::Level<'static> { match level { - Level::Bug | Level::Fatal | Level::Error | Level::DelayedBug => { - annotate_snippets::Level::Error + Level::Bug | Level::DelayedBug => { + annotate_snippets::Level::ERROR.with_name("error: internal compiler error") } - Level::ForceWarning | Level::Warning => annotate_snippets::Level::Warning, - Level::Note | Level::OnceNote => annotate_snippets::Level::Note, - Level::Help | Level::OnceHelp => annotate_snippets::Level::Help, - // FIXME(#59346): Not sure how to map this level - Level::FailureNote => annotate_snippets::Level::Error, + Level::Fatal | Level::Error => annotate_snippets::level::ERROR, + Level::ForceWarning | Level::Warning => annotate_snippets::Level::WARNING, + Level::Note | Level::OnceNote => annotate_snippets::Level::NOTE, + Level::Help | Level::OnceHelp => annotate_snippets::Level::HELP, + Level::FailureNote => annotate_snippets::Level::NOTE.no_name(), Level::Allow => panic!("Should not call with Allow"), Level::Expect => panic!("Should not call with Expect"), } } impl AnnotateSnippetEmitter { - pub fn new( - source_map: Option>, - translator: Translator, - short_message: bool, - macro_backtrace: bool, - ) -> Self { - Self { source_map, translator, short_message, ui_testing: false, macro_backtrace } - } - - /// Allows to modify `Self` to enable or disable the `ui_testing` flag. - /// - /// If this is set to true, line numbers will be normalized as `LL` in the output. - pub fn ui_testing(mut self, ui_testing: bool) -> Self { - self.ui_testing = ui_testing; - self + pub fn new(dst: Destination, translator: Translator) -> Self { + Self { + dst: IntoDynSyncSend(dst), + sm: None, + translator, + short_message: false, + ui_testing: false, + ignored_directories_in_source_blocks: Vec::new(), + diagnostic_width: None, + macro_backtrace: false, + track_diagnostics: false, + terminal_url: TerminalUrl::No, + theme: OutputTheme::Ascii, + } } fn emit_messages_default( &mut self, level: &Level, - messages: &[(DiagMessage, Style)], + msgs: &[(DiagMessage, Style)], args: &FluentArgs<'_>, code: &Option, msp: &MultiSpan, - _children: &[Subdiag], - _suggestions: &[CodeSuggestion], + children: &[Subdiag], + suggestions: Vec, ) { - let message = self.translator.translate_messages(messages, args); - if let Some(source_map) = &self.source_map { - // Make sure our primary file comes first - let primary_lo = if let Some(primary_span) = msp.primary_span().as_ref() { - if primary_span.is_dummy() { - // FIXME(#59346): Not sure when this is the case and what - // should be done if it happens - return; - } else { - source_map.lookup_char_pos(primary_span.lo()) + let renderer = self.renderer(); + let annotation_level = annotation_level_for_level(*level); + + // If at least one portion of the message is styled, we need to + // "pre-style" the message + let mut title = if msgs.iter().any(|(_, style)| style != &crate::Style::NoStyle) { + annotation_level + .clone() + .secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level, args))) + } else { + annotation_level.clone().primary_title(self.translator.translate_messages(msgs, args)) + }; + + if let Some(c) = code { + title = title.id(c.to_string()); + if let TerminalUrl::Yes = self.terminal_url { + title = title.id_url(format!("https://doc.rust-lang.org/error_codes/{c}.html")); + } + } + + let mut report = vec![]; + let mut group = Group::with_title(title); + + // If we don't have span information, emit and exit + let Some(sm) = self.sm.as_ref() else { + group = group.elements(children.iter().map(|c| { + let msg = self.translator.translate_messages(&c.messages, args).to_string(); + let level = annotation_level_for_level(c.level); + level.message(msg) + })); + + report.push(group); + if let Err(e) = emit_to_destination( + renderer.render(&report), + level, + &mut self.dst, + self.short_message, + ) { + panic!("failed to emit error: {e}"); + } + return; + }; + + let mut file_ann = collect_annotations(args, msp, sm, &self.translator); + + // Make sure our primary file comes first + let primary_span = msp.primary_span().unwrap_or_default(); + if !primary_span.is_dummy() { + let primary_lo = sm.lookup_char_pos(primary_span.lo()); + if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) { + file_ann.swap(0, pos); + } + + for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() { + if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + group = group.element(snippet); + } + // we can't annotate anything if the source is unavailable. + } else if !self.short_message { + // We'll just print unannotated messages + group = self.unannotated_messages( + annotations, + &file.name, + sm, + file_idx, + &mut report, + group, + &annotation_level, + ); + // If this is the last annotation for a file, and + // this is the last file, and the first child is a + // "secondary" message, we need to add padding + // ╭▸ /rustc/FAKE_PREFIX/library/core/src/clone.rs:236:13 + // │ + // ├ note: the late bound lifetime parameter + // │ (<- It adds *this*) + // ╰ warning: this was previously accepted + if let Some(c) = children.first() + && (!c.span.has_primary_spans() && !c.span.has_span_labels()) + { + group = group.element(Padding); + } } + } + } + + for c in children { + let level = annotation_level_for_level(c.level); + + // If at least one portion of the message is styled, we need to + // "pre-style" the message + let msg = if c.messages.iter().any(|(_, style)| style != &crate::Style::NoStyle) { + Cow::Owned(self.pre_style_msgs(&c.messages, c.level, args)) } else { - // FIXME(#59346): Not sure when this is the case and what - // should be done if it happens - return; + self.translator.translate_messages(&c.messages, args) }; - let mut annotated_files = FileWithAnnotatedLines::collect_annotations(self, args, msp); - if let Ok(pos) = - annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name)) - { - annotated_files.swap(0, pos); + + // This is a secondary message with no span info + if !c.span.has_primary_spans() && !c.span.has_span_labels() { + group = group.element(level.clone().message(msg)); + continue; + } + + report.push(std::mem::replace( + &mut group, + Group::with_title(level.clone().secondary_title(msg)), + )); + + let mut file_ann = collect_annotations(args, &c.span, sm, &self.translator); + let primary_span = c.span.primary_span().unwrap_or_default(); + if !primary_span.is_dummy() { + let primary_lo = sm.lookup_char_pos(primary_span.lo()); + if let Ok(pos) = + file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) + { + file_ann.swap(0, pos); + } } - // owned: file name, line source, line index, annotations - type Owned = (String, String, usize, Vec); - let annotated_files: Vec = annotated_files - .into_iter() - .flat_map(|annotated_file| { - let file = annotated_file.file; - annotated_file - .lines + + for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() { + if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + group = group.element(snippet); + } + // we can't annotate anything if the source is unavailable. + } else if !self.short_message { + // We'll just print unannotated messages + group = self.unannotated_messages( + annotations, + &file.name, + sm, + file_idx, + &mut report, + group, + &level, + ); + } + } + } + + let suggestions_expected = suggestions + .iter() + .filter(|s| { + matches!( + s.style, + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways + ) + }) + .count(); + for suggestion in suggestions { + match suggestion.style { + SuggestionStyle::CompletelyHidden => { + // do not display this suggestion, it is meant only for tools + } + SuggestionStyle::HideCodeAlways => { + let msg = self + .translator + .translate_messages(&[(suggestion.msg.to_owned(), Style::HeaderMsg)], args); + group = group.element(annotate_snippets::Level::HELP.message(msg)); + } + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways => { + let substitutions = suggestion + .substitutions .into_iter() - .map(|line| { - // Ensure the source file is present before we try - // to load a string from it. - // FIXME(#115869): support -Z ignore-directory-in-diagnostics-source-blocks - source_map.ensure_source_file_source_present(&file); - ( - format!("{}", source_map.filename_for_diagnostics(&file.name)), - source_string(Arc::clone(&file), &line), - line.line_index, - line.annotations, - ) + .filter_map(|mut subst| { + // Suggestions coming from macros can have malformed spans. This is a heavy + // handed approach to avoid ICEs by ignoring the suggestion outright. + let invalid = + subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err()); + if invalid { + debug!("suggestion contains an invalid span: {:?}", subst); + } + + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + subst.parts.sort_by_key(|part| part.span.lo()); + // Verify the assumption that all spans are disjoint + assert_eq!( + subst.parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)), + None, + "all spans must be disjoint", + ); + + // Account for cases where we are suggesting the same code that's already + // there. This shouldn't happen often, but in some cases for multipart + // suggestions it's much easier to handle it here than in the origin. + subst.parts.retain(|p| is_different(sm, &p.snippet, p.span)); + + let item_span = subst.parts.first()?; + let file = sm.lookup_source_file(item_span.span.lo()); + if !invalid + && should_show_source_code( + &self.ignored_directories_in_source_blocks, + sm, + &file, + ) + { + Some(subst) + } else { + None + } + }) + .collect::>(); + + if substitutions.is_empty() { + continue; + } + let mut msg = self + .translator + .translate_message(&suggestion.msg, args) + .map_err(Report::new) + .unwrap() + .to_string(); + + let lo = substitutions + .iter() + .find_map(|sub| sub.parts.first().map(|p| p.span.lo())) + .unwrap(); + let file = sm.lookup_source_file(lo); + + let filename = + sm.filename_for_diagnostics(&file.name).to_string_lossy().to_string(); + + let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS); + + let subs = substitutions + .into_iter() + .take(MAX_SUGGESTIONS) + .filter_map(|sub| { + let mut confusion_type = ConfusionType::None; + for part in &sub.parts { + let part_confusion = + detect_confusion_type(sm, &part.snippet, part.span); + confusion_type = confusion_type.combine(part_confusion); + } + + if !matches!(confusion_type, ConfusionType::None) { + msg.push_str(confusion_type.label_text()); + } + + let mut parts = sub + .parts + .into_iter() + .filter_map(|p| { + if is_different(sm, &p.snippet, p.span) { + Some((p.span, p.snippet)) + } else { + None + } + }) + .collect::>(); + + if parts.is_empty() { + None + } else { + let spans = parts.iter().map(|(span, _)| *span).collect::>(); + // The suggestion adds an entire line of code, ending on a newline, so we'll also + // print the *following* line, to provide context of what we're advising people to + // do. Otherwise you would only see contextless code that can be confused for + // already existing code, despite the colors and UI elements. + // We special case `#[derive(_)]\n` and other attribute suggestions, because those + // are the ones where context is most useful. + let fold = if let [(p, snippet)] = &mut parts[..] + && snippet.trim().starts_with("#[") + // This allows for spaces to come between the attribute and the newline + && snippet.trim().ends_with("]") + && snippet.ends_with('\n') + && p.hi() == p.lo() + && let Ok(b) = sm.span_to_prev_source(*p) + && let b = b.rsplit_once('\n').unwrap_or_else(|| ("", &b)).1 + && b.trim().is_empty() + { + // FIXME: This is a hack: + // The span for attribute suggestions often times points to the + // beginning of an item, disregarding leading whitespace. This + // causes the attribute to be properly indented, but leaves original + // item without indentation when rendered. + // This fixes that problem by adjusting the span to point to the start + // of the whitespace, and adds the whitespace to the replacement. + // + // Source: " extern "custom" fn negate(a: i64) -> i64 {\n" + // Span: 4..4 + // Replacement: "#[unsafe(naked)]\n" + // + // Before: + // help: convert this to an `#[unsafe(naked)]` function + // | + // LL + #[unsafe(naked)] + // LL | extern "custom" fn negate(a: i64) -> i64 { + // | + // + // After + // help: convert this to an `#[unsafe(naked)]` function + // | + // LL + #[unsafe(naked)] + // LL | extern "custom" fn negate(a: i64) -> i64 { + // | + if !b.is_empty() && !snippet.ends_with(b) { + snippet.insert_str(0, b); + let offset = BytePos(b.len() as u32); + *p = p.with_lo(p.lo() - offset).shrink_to_lo(); + } + false + } else { + true + }; + + if let Some((bounding_span, source, line_offset)) = + shrink_file(spans.as_slice(), &file.name, sm) + { + let adj_lo = bounding_span.lo().to_usize(); + Some( + Snippet::source(source) + .line_start(line_offset) + .path(filename.clone()) + .fold(fold) + .patches(parts.into_iter().map( + |(span, replacement)| { + let lo = + span.lo().to_usize().saturating_sub(adj_lo); + let hi = + span.hi().to_usize().saturating_sub(adj_lo); + + Patch::new(lo..hi, replacement) + }, + )), + ) + } else { + None + } + } }) - .collect::>() - }) - .collect(); - let code = code.map(|code| code.to_string()); - - let snippets = - annotated_files.iter().map(|(file_name, source, line_index, annotations)| { - Snippet::source(source) - .line_start(*line_index) - .origin(file_name) - // FIXME(#59346): Not really sure when `fold` should be true or false - .fold(false) - .annotations(annotations.iter().map(|annotation| { - annotation_level_for_level(*level) - .span(annotation.start_col.display..annotation.end_col.display) - .label(annotation.label.as_deref().unwrap_or_default()) - })) - }); - let mut message = annotation_level_for_level(*level).title(&message).snippets(snippets); - if let Some(code) = code.as_deref() { - message = message.id(code) + .collect::>(); + if !subs.is_empty() { + report.push(std::mem::replace( + &mut group, + Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)), + )); + + group = group.elements(subs); + if other_suggestions > 0 { + group = group.element( + annotate_snippets::Level::NOTE.no_name().message(format!( + "and {} other candidate{}", + other_suggestions, + pluralize!(other_suggestions) + )), + ); + } + } + } } - // FIXME(#59346): Figure out if we can _always_ print to stderr or not. - // `emitter.rs` has the `Destination` enum that lists various possible output - // destinations. - let renderer = Renderer::plain().anonymized_line_numbers(self.ui_testing); - eprintln!("{}", renderer.render(message)) } - // FIXME(#59346): Is it ok to return None if there's no source_map? + + // FIXME: This hack should be removed once annotate_snippets is the + // default emitter. + if suggestions_expected > 0 && report.is_empty() { + group = group.element(Padding); + } + + if !group.is_empty() { + report.push(group); + } + if let Err(e) = + emit_to_destination(renderer.render(&report), level, &mut self.dst, self.short_message) + { + panic!("failed to emit error: {e}"); + } + } + + fn renderer(&self) -> Renderer { + let width = if let Some(width) = self.diagnostic_width { + width + } else if self.ui_testing || cfg!(miri) { + DEFAULT_TERM_WIDTH + } else { + termize::dimensions().map(|(w, _)| w).unwrap_or(DEFAULT_TERM_WIDTH) + }; + let decor_style = match self.theme { + OutputTheme::Ascii => annotate_snippets::renderer::DecorStyle::Ascii, + OutputTheme::Unicode => annotate_snippets::renderer::DecorStyle::Unicode, + }; + + match self.dst.current_choice() { + ColorChoice::AlwaysAnsi | ColorChoice::Always | ColorChoice::Auto => Renderer::styled(), + ColorChoice::Never => Renderer::plain(), + } + .term_width(width) + .anonymized_line_numbers(self.ui_testing) + .decor_style(decor_style) + .short_message(self.short_message) } + + fn pre_style_msgs( + &self, + msgs: &[(DiagMessage, Style)], + level: Level, + args: &FluentArgs<'_>, + ) -> String { + msgs.iter() + .filter_map(|(m, style)| { + let text = self.translator.translate_message(m, args).map_err(Report::new).unwrap(); + let style = style.anstyle(level); + if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) } + }) + .collect() + } + + fn annotated_snippet<'a>( + &self, + annotations: Vec, + file_name: &FileName, + sm: &Arc, + ) -> Option>> { + let spans = annotations.iter().map(|a| a.span).collect::>(); + if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) { + let adj_lo = bounding_span.lo().to_usize(); + let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string(); + Some(Snippet::source(source).line_start(offset_line).path(filename).annotations( + annotations.into_iter().map(move |a| { + let lo = a.span.lo().to_usize().saturating_sub(adj_lo); + let hi = a.span.hi().to_usize().saturating_sub(adj_lo); + let ann = a.kind.span(lo..hi); + if let Some(label) = a.label { ann.label(label) } else { ann } + }), + )) + } else { + None + } + } + + fn unannotated_messages<'a>( + &self, + annotations: Vec, + file_name: &FileName, + sm: &Arc, + file_idx: usize, + report: &mut Vec>, + mut group: Group<'a>, + level: &annotate_snippets::level::Level<'static>, + ) -> Group<'a> { + let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string(); + let mut line_tracker = vec![]; + for (i, a) in annotations.into_iter().enumerate() { + let lo = sm.lookup_char_pos(a.span.lo()); + let hi = sm.lookup_char_pos(a.span.hi()); + if i == 0 || (a.label.is_some()) { + // Render each new file after the first in its own Group + // ╭▸ $DIR/deriving-meta-unknown-trait.rs:1:10 + // │ + // LL │ #[derive(Eqr)] + // │ ━━━ + // ╰╴ (<- It makes it so *this* will get printed) + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ + // ╰ note: not covered + if i == 0 && file_idx != 0 { + report.push(std::mem::replace(&mut group, Group::with_level(level.clone()))); + } + + if !line_tracker.contains(&lo.line) { + line_tracker.push(lo.line); + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 (<- It adds *this*) + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ + // ╰ note: not covered + group = group.element( + Origin::path(filename.clone()) + .line(sm.doctest_offset_line(file_name, lo.line)) + .char_column(lo.col_display), + ); + } + + if hi.line > lo.line + && a.label.as_ref().is_some_and(|l| !l.is_empty()) + && !line_tracker.contains(&hi.line) + { + line_tracker.push(hi.line); + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 (<- It adds *this*) + // │ + // ╰ note: not covered + group = group.element( + Origin::path(filename.clone()) + .line(sm.doctest_offset_line(file_name, hi.line)) + .char_column(hi.col_display), + ); + } + + if let Some(label) = a.label + && !label.is_empty() + { + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ (<- It adds *this*) + // ╰ note: not covered (<- and *this*) + group = group + .element(Padding) + .element(annotate_snippets::Level::NOTE.message(label)); + } + } + } + group + } +} + +fn emit_to_destination( + rendered: String, + lvl: &Level, + dst: &mut Destination, + short_message: bool, +) -> io::Result<()> { + use crate::lock; + let _buffer_lock = lock::acquire_global_lock("rustc_errors"); + writeln!(dst, "{rendered}")?; + if !short_message && !lvl.is_failure_note() { + writeln!(dst)?; + } + dst.flush()?; + Ok(()) +} + +#[derive(Debug)] +struct Annotation { + kind: AnnotationKind, + span: Span, + label: Option, +} + +fn collect_annotations( + args: &FluentArgs<'_>, + msp: &MultiSpan, + sm: &Arc, + translator: &Translator, +) -> Vec<(Arc, Vec)> { + let mut output: Vec<(Arc, Vec)> = vec![]; + + for SpanLabel { span, is_primary, label } in msp.span_labels() { + // If we don't have a useful span, pick the primary span if that exists. + // Worst case we'll just print an error at the top of the main file. + let span = match (span.is_dummy(), msp.primary_span()) { + (_, None) | (false, _) => span, + (true, Some(span)) => span, + }; + let file = sm.lookup_source_file(span.lo()); + + let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context }; + + let label = label.as_ref().map(|m| { + normalize_whitespace( + &translator.translate_message(m, args).map_err(Report::new).unwrap(), + ) + }); + + let ann = Annotation { kind, span, label }; + if sm.is_valid_span(ann.span).is_ok() { + // Look through each of our files for the one we're adding to. We + // use each files `stable_id` to avoid issues with file name + // collisions when multiple versions of the same crate are present + // in the dependency graph + if let Some((_, annotations)) = + output.iter_mut().find(|(f, _)| f.stable_id == file.stable_id) + { + annotations.push(ann); + } else { + output.push((file, vec![ann])); + } + } + } + output +} + +fn shrink_file( + spans: &[Span], + file_name: &FileName, + sm: &Arc, +) -> Option<(Span, String, usize)> { + let lo_byte = spans.iter().map(|s| s.lo()).min()?; + let lo_loc = sm.lookup_char_pos(lo_byte); + let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start; + + let hi_byte = spans.iter().map(|s| s.hi()).max()?; + let hi_loc = sm.lookup_char_pos(hi_byte); + let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end; + + let bounding_span = Span::with_root_ctxt(lo, hi); + let source = sm.span_to_snippet(bounding_span).unwrap_or_default(); + let offset_line = sm.doctest_offset_line(file_name, lo_loc.line); + + Some((bounding_span, source, offset_line)) } diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index f243c8be20882..522faf1db9ff1 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -955,7 +955,24 @@ fn default_emitter( if let HumanReadableErrorType::AnnotateSnippet = kind { let emitter = - AnnotateSnippetEmitter::new(source_map, translator, short, macro_backtrace); + AnnotateSnippetEmitter::new(stderr_destination(color_config), translator) + .sm(source_map) + .short_message(short) + .diagnostic_width(sopts.diagnostic_width) + .macro_backtrace(macro_backtrace) + .track_diagnostics(track_diagnostics) + .terminal_url(terminal_url) + .theme(if let HumanReadableErrorType::Unicode = kind { + OutputTheme::Unicode + } else { + OutputTheme::Ascii + }) + .ignored_directories_in_source_blocks( + sopts + .unstable_opts + .ignore_directory_in_diagnostics_source_blocks + .clone(), + ); Box::new(emitter.ui_testing(sopts.unstable_opts.ui_testing)) } else { let emitter = HumanEmitter::new(stderr_destination(color_config), translator) diff --git a/tests/ui/annotate-snippet/missing-type.stderr b/tests/ui/annotate-snippet/missing-type.stderr index c16f022a77fa3..5cc9cc9529f40 100644 --- a/tests/ui/annotate-snippet/missing-type.stderr +++ b/tests/ui/annotate-snippet/missing-type.stderr @@ -4,3 +4,18 @@ error[E0412]: cannot find type `Iter` in this scope LL | let x: Iter; | ^^^^ not found in this scope | +help: consider importing one of these structs + | +LL + use std::collections::binary_heap::Iter; + | +LL + use std::collections::btree_map::Iter; + | +LL + use std::collections::btree_set::Iter; + | +LL + use std::collections::hash_map::Iter; + | + = and 9 other candidates + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0412`. diff --git a/tests/ui/annotate-snippet/multiple-files.stderr b/tests/ui/annotate-snippet/multiple-files.stderr index 4236ec811d04b..ffdc9482bbf24 100644 --- a/tests/ui/annotate-snippet/multiple-files.stderr +++ b/tests/ui/annotate-snippet/multiple-files.stderr @@ -7,5 +7,8 @@ LL | other_file::WithPrivateMethod.private_method(); ::: $DIR/auxiliary/other_file.rs:5:5 | LL | fn private_method(&self) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ private method defined here - | + | ------------------------ private method defined here + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0624`. diff --git a/tests/ui/annotate-snippet/multispan.stderr b/tests/ui/annotate-snippet/multispan.stderr index baed54c59a4e9..8acb60e27a19b 100644 --- a/tests/ui/annotate-snippet/multispan.stderr +++ b/tests/ui/annotate-snippet/multispan.stderr @@ -4,39 +4,90 @@ error: hello to you, too! LL | hello!(hi); | ^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:15:12 + | +LL | hello!(hi); + | ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:18:5 | LL | hello!(hi hi); | ^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:18:12 + | +LL | hello!(hi hi); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:21:5 | LL | hello!(hi hi hi); | ^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:21:12 + | +LL | hello!(hi hi hi); + | ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:24:5 | LL | hello!(hi hey hi yo hi beep beep hi hi); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:24:12 + | +LL | hello!(hi hey hi yo hi beep beep hi hi); + | ^^ ^^ ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:25:5 | LL | hello!(hi there, hi how are you? hi... hi.); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:25:12 + | +LL | hello!(hi there, hi how are you? hi... hi.); + | ^^ ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:26:5 | LL | hello!(whoah. hi di hi di ho); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:26:19 + | +LL | hello!(whoah. hi di hi di ho); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:27:5 | LL | hello!(hi good hi and good bye); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:27:12 + | +LL | hello!(hi good hi and good bye); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 7 previous errors +