//! The current rustc diagnostics emitter. //! //! An `Emitter` takes care of generating the output from a `DiagnosticBuilder` struct. //! //! There are various `Emitter` implementations that generate different output formats such as //! JSON and human readable output. //! //! The output types are defined in `rustc_session::config::ErrorOutputType`. use Destination::*; use rustc_span::source_map::SourceMap; use rustc_span::{FileLines, SourceFile, Span}; use crate::snippet::{ Annotation, AnnotationColumn, AnnotationType, Line, MultilineAnnotation, Style, StyledString, }; use crate::styled_buffer::StyledBuffer; use crate::translation::{to_fluent_args, Translate}; use crate::{ diagnostic::DiagnosticLocation, CodeSuggestion, Diagnostic, DiagnosticId, DiagnosticMessage, FluentBundle, Handler, LazyFallbackBundle, Level, MultiSpan, SubDiagnostic, SubstitutionHighlight, SuggestionStyle, TerminalUrl, }; use rustc_lint_defs::pluralize; use derive_setters::Setters; use rustc_data_structures::fx::{FxHashMap, FxIndexMap}; use rustc_data_structures::sync::Lrc; use rustc_error_messages::{FluentArgs, SpanLabel}; use rustc_span::hygiene::{ExpnKind, MacroKind}; use std::borrow::Cow; use std::cmp::{max, min, Reverse}; use std::error::Report; use std::io::prelude::*; use std::io::{self, IsTerminal}; use std::iter; use std::path::Path; use termcolor::{Ansi, BufferWriter, ColorChoice, ColorSpec, StandardStream}; use termcolor::{Buffer, Color, WriteColor}; /// Default column width, used in tests and when terminal dimensions cannot be determined. const DEFAULT_COLUMN_WIDTH: usize = 140; /// Describes the way the content of the `rendered` field of the json output is generated #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum HumanReadableErrorType { Default(ColorConfig), AnnotateSnippet(ColorConfig), Short(ColorConfig), } impl HumanReadableErrorType { /// Returns a (`short`, `color`) tuple pub fn unzip(self) -> (bool, ColorConfig) { match self { HumanReadableErrorType::Default(cc) => (false, cc), HumanReadableErrorType::Short(cc) => (true, cc), HumanReadableErrorType::AnnotateSnippet(cc) => (false, cc), } } pub fn new_emitter( self, mut dst: Box, fallback_bundle: LazyFallbackBundle, ) -> EmitterWriter { let (short, color_config) = self.unzip(); let color = color_config.suggests_using_colors(); if !dst.supports_color() && color { dst = Box::new(Ansi::new(dst)); } EmitterWriter::new(dst, fallback_bundle).short_message(short) } } #[derive(Clone, Copy, Debug)] struct Margin { /// The available whitespace in the left that can be consumed when centering. pub whitespace_left: usize, /// The column of the beginning of left-most span. pub span_left: usize, /// The column of the end of right-most span. pub span_right: usize, /// The beginning of the line to be displayed. pub computed_left: usize, /// The end of the line to be displayed. pub computed_right: usize, /// The current width of the terminal. Uses value of `DEFAULT_COLUMN_WIDTH` constant by default /// and in tests. pub column_width: usize, /// The end column of a span label, including the span. Doesn't account for labels not in the /// same line as the span. pub label_right: usize, } impl Margin { fn new( whitespace_left: usize, span_left: usize, span_right: usize, label_right: usize, column_width: usize, max_line_len: usize, ) -> Self { // The 6 is padding to give a bit of room for `...` when displaying: // ``` // error: message // --> file.rs:16:58 // | // 16 | ... fn foo(self) -> Self::Bar { // | ^^^^^^^^^ // ``` let mut m = Margin { whitespace_left: whitespace_left.saturating_sub(6), span_left: span_left.saturating_sub(6), span_right: span_right + 6, computed_left: 0, computed_right: 0, column_width, label_right: label_right + 6, }; m.compute(max_line_len); m } fn was_cut_left(&self) -> bool { self.computed_left > 0 } fn was_cut_right(&self, line_len: usize) -> bool { let right = if self.computed_right == self.span_right || self.computed_right == self.label_right { // Account for the "..." padding given above. Otherwise we end up with code lines that // do fit but end in "..." as if they were trimmed. self.computed_right - 6 } else { self.computed_right }; right < line_len && self.computed_left + self.column_width < line_len } fn compute(&mut self, max_line_len: usize) { // When there's a lot of whitespace (>20), we want to trim it as it is useless. self.computed_left = if self.whitespace_left > 20 { self.whitespace_left - 16 // We want some padding. } else { 0 }; // We want to show as much as possible, max_line_len is the right-most boundary for the // relevant code. self.computed_right = max(max_line_len, self.computed_left); if self.computed_right - self.computed_left > self.column_width { // Trimming only whitespace isn't enough, let's get craftier. if self.label_right - self.whitespace_left <= self.column_width { // Attempt to fit the code window only trimming whitespace. self.computed_left = self.whitespace_left; self.computed_right = self.computed_left + self.column_width; } else if self.label_right - self.span_left <= self.column_width { // Attempt to fit the code window considering only the spans and labels. let padding_left = (self.column_width - (self.label_right - self.span_left)) / 2; self.computed_left = self.span_left.saturating_sub(padding_left); self.computed_right = self.computed_left + self.column_width; } else if self.span_right - self.span_left <= self.column_width { // Attempt to fit the code window considering the spans and labels plus padding. let padding_left = (self.column_width - (self.span_right - self.span_left)) / 5 * 2; self.computed_left = self.span_left.saturating_sub(padding_left); self.computed_right = self.computed_left + self.column_width; } else { // Mostly give up but still don't show the full line. self.computed_left = self.span_left; self.computed_right = self.span_right; } } } fn left(&self, line_len: usize) -> usize { min(self.computed_left, line_len) } fn right(&self, line_len: usize) -> usize { if line_len.saturating_sub(self.computed_left) <= self.column_width { line_len } else { min(line_len, self.computed_right) } } } const ANONYMIZED_LINE_NUM: &str = "LL"; /// Emitter trait for emitting errors. pub trait Emitter: Translate { /// Emit a structured diagnostic. fn emit_diagnostic(&mut self, diag: &Diagnostic); /// Emit a notification that an artifact has been output. /// This is currently only supported for the JSON format, /// other formats can, and will, simply ignore it. fn emit_artifact_notification(&mut self, _path: &Path, _artifact_type: &str) {} fn emit_future_breakage_report(&mut self, _diags: Vec) {} /// Emit list of unused externs fn emit_unused_externs( &mut self, _lint_level: rustc_lint_defs::Level, _unused_externs: &[&str], ) { } /// Checks if should show explanations about "rustc --explain" fn should_show_explain(&self) -> bool { true } /// Checks if we can use colors in the current output stream. fn supports_color(&self) -> bool { false } fn source_map(&self) -> Option<&Lrc>; /// Formats the substitutions of the primary_span /// /// There are a lot of conditions to this method, but in short: /// /// * If the current `Diagnostic` has only one visible `CodeSuggestion`, /// we format the `help` suggestion depending on the content of the /// substitutions. In that case, we return the modified span only. /// /// * If the current `Diagnostic` has multiple suggestions, /// we return the original `primary_span` and the original suggestions. fn primary_span_formatted<'a>( &mut self, diag: &'a Diagnostic, fluent_args: &FluentArgs<'_>, ) -> (MultiSpan, &'a [CodeSuggestion]) { let mut primary_span = diag.span.clone(); let suggestions = diag.suggestions.as_deref().unwrap_or(&[]); if let Some((sugg, rest)) = suggestions.split_first() { let msg = self.translate_message(&sugg.msg, fluent_args).map_err(Report::new).unwrap(); if rest.is_empty() && // ^ if there is only one suggestion // don't display multi-suggestions as labels sugg.substitutions.len() == 1 && // don't display multipart suggestions as labels sugg.substitutions[0].parts.len() == 1 && // don't display long messages as labels msg.split_whitespace().count() < 10 && // don't display multiline suggestions as labels !sugg.substitutions[0].parts[0].snippet.contains('\n') && ![ // when this style is set we want the suggestion to be a message, not inline SuggestionStyle::HideCodeAlways, // trivial suggestion for tooling's sake, never shown SuggestionStyle::CompletelyHidden, // subtle suggestion, never shown inline SuggestionStyle::ShowAlways, ].contains(&sugg.style) { let substitution = &sugg.substitutions[0].parts[0].snippet.trim(); let msg = if substitution.is_empty() || sugg.style.hide_inline() { // This substitution is only removal OR we explicitly don't want to show the // code inline (`hide_inline`). Therefore, we don't show the substitution. format!("help: {msg}") } else { // Show the default suggestion text with the substitution format!( "help: {}{}: `{}`", msg, if self.source_map().is_some_and(|sm| is_case_difference( sm, substitution, sugg.substitutions[0].parts[0].span, )) { " (notice the capitalization)" } else { "" }, substitution, ) }; primary_span.push_span_label(sugg.substitutions[0].parts[0].span, msg); // We return only the modified primary_span (primary_span, &[]) } else { // if there are multiple suggestions, print them all in full // to be consistent. We could try to figure out if we can // make one (or the first one) inline, but that would give // undue importance to a semi-random suggestion (primary_span, suggestions) } } else { (primary_span, suggestions) } } fn fix_multispans_in_extern_macros_and_render_macro_backtrace( &self, span: &mut MultiSpan, children: &mut Vec, level: &Level, backtrace: bool, ) { // Check for spans in macros, before `fix_multispans_in_extern_macros` // has a chance to replace them. let has_macro_spans: Vec<_> = iter::once(&*span) .chain(children.iter().map(|child| &child.span)) .flat_map(|span| span.primary_spans()) .flat_map(|sp| sp.macro_backtrace()) .filter_map(|expn_data| { match expn_data.kind { ExpnKind::Root => None, // Skip past non-macro entries, just in case there // are some which do actually involve macros. ExpnKind::Desugaring(..) | ExpnKind::AstPass(..) => None, ExpnKind::Macro(macro_kind, name) => Some((macro_kind, name)), } }) .collect(); if !backtrace { self.fix_multispans_in_extern_macros(span, children); } self.render_multispans_macro_backtrace(span, children, backtrace); if !backtrace { if let Some((macro_kind, name)) = has_macro_spans.first() { // Mark the actual macro this originates from let and_then = if let Some((macro_kind, last_name)) = has_macro_spans.last() && last_name != name { let descr = macro_kind.descr(); format!( " which comes from the expansion of the {descr} `{last_name}`", ) } else { "".to_string() }; let descr = macro_kind.descr(); let msg = format!( "this {level} originates in the {descr} `{name}`{and_then} \ (in Nightly builds, run with -Z macro-backtrace for more info)", ); children.push(SubDiagnostic { level: Level::Note, message: vec![(DiagnosticMessage::from(msg), Style::NoStyle)], span: MultiSpan::new(), render_span: None, }); } } } fn render_multispans_macro_backtrace( &self, span: &mut MultiSpan, children: &mut Vec, backtrace: bool, ) { for span in iter::once(span).chain(children.iter_mut().map(|child| &mut child.span)) { self.render_multispan_macro_backtrace(span, backtrace); } } fn render_multispan_macro_backtrace(&self, span: &mut MultiSpan, always_backtrace: bool) { let mut new_labels: Vec<(Span, String)> = vec![]; for &sp in span.primary_spans() { if sp.is_dummy() { continue; } // FIXME(eddyb) use `retain` on `macro_backtrace` to remove all the // entries we don't want to print, to make sure the indices being // printed are contiguous (or omitted if there's only one entry). let macro_backtrace: Vec<_> = sp.macro_backtrace().collect(); for (i, trace) in macro_backtrace.iter().rev().enumerate() { if trace.def_site.is_dummy() { continue; } if always_backtrace { new_labels.push(( trace.def_site, format!( "in this expansion of `{}`{}", trace.kind.descr(), if macro_backtrace.len() > 1 { // if macro_backtrace.len() == 1 it'll be // pointed at by "in this macro invocation" format!(" (#{})", i + 1) } else { String::new() }, ), )); } // Don't add a label on the call site if the diagnostic itself // already points to (a part of) that call site, as the label // is meant for showing the relevant invocation when the actual // diagnostic is pointing to some part of macro definition. // // This also handles the case where an external span got replaced // with the call site span by `fix_multispans_in_extern_macros`. // // NB: `-Zmacro-backtrace` overrides this, for uniformity, as the // "in this expansion of" label above is always added in that mode, // and it needs an "in this macro invocation" label to match that. let redundant_span = trace.call_site.contains(sp); if !redundant_span || always_backtrace { let msg: Cow<'static, _> = match trace.kind { ExpnKind::Macro(MacroKind::Attr, _) => { "this procedural macro expansion".into() } ExpnKind::Macro(MacroKind::Derive, _) => { "this derive macro expansion".into() } ExpnKind::Macro(MacroKind::Bang, _) => "this macro invocation".into(), ExpnKind::Root => "the crate root".into(), ExpnKind::AstPass(kind) => kind.descr().into(), ExpnKind::Desugaring(kind) => { format!("this {} desugaring", kind.descr()).into() } }; new_labels.push(( trace.call_site, format!( "in {}{}", msg, if macro_backtrace.len() > 1 && always_backtrace { // only specify order when the macro // backtrace is multiple levels deep format!(" (#{})", i + 1) } else { String::new() }, ), )); } if !always_backtrace { break; } } } for (label_span, label_text) in new_labels { span.push_span_label(label_span, label_text); } } // This does a small "fix" for multispans by looking to see if it can find any that // point directly at external macros. Since these are often difficult to read, // this will change the span to point at the use site. fn fix_multispans_in_extern_macros( &self, span: &mut MultiSpan, children: &mut Vec, ) { debug!("fix_multispans_in_extern_macros: before: span={:?} children={:?}", span, children); self.fix_multispan_in_extern_macros(span); for child in children.iter_mut() { self.fix_multispan_in_extern_macros(&mut child.span); } debug!("fix_multispans_in_extern_macros: after: span={:?} children={:?}", span, children); } // This "fixes" MultiSpans that contain `Span`s pointing to locations inside of external macros. // Since these locations are often difficult to read, // we move these spans from the external macros to their corresponding use site. fn fix_multispan_in_extern_macros(&self, span: &mut MultiSpan) { let Some(source_map) = self.source_map() else { return }; // First, find all the spans in external macros and point instead at their use site. let replacements: Vec<(Span, Span)> = span .primary_spans() .iter() .copied() .chain(span.span_labels().iter().map(|sp_label| sp_label.span)) .filter_map(|sp| { if !sp.is_dummy() && source_map.is_imported(sp) { let maybe_callsite = sp.source_callsite(); if sp != maybe_callsite { return Some((sp, maybe_callsite)); } } None }) .collect(); // After we have them, make sure we replace these 'bad' def sites with their use sites. for (from, to) in replacements { span.replace(from, to); } } } impl Translate for EmitterWriter { fn fluent_bundle(&self) -> Option<&Lrc> { self.fluent_bundle.as_ref() } fn fallback_fluent_bundle(&self) -> &FluentBundle { &self.fallback_bundle } } impl Emitter for EmitterWriter { fn source_map(&self) -> Option<&Lrc> { self.sm.as_ref() } fn emit_diagnostic(&mut self, diag: &Diagnostic) { let fluent_args = to_fluent_args(diag.args()); let mut children = diag.children.clone(); let (mut primary_span, suggestions) = self.primary_span_formatted(diag, &fluent_args); debug!("emit_diagnostic: suggestions={:?}", suggestions); self.fix_multispans_in_extern_macros_and_render_macro_backtrace( &mut primary_span, &mut children, &diag.level, self.macro_backtrace, ); self.emit_messages_default( &diag.level, &diag.message, &fluent_args, &diag.code, &primary_span, &children, suggestions, self.track_diagnostics.then_some(&diag.emitted_at), ); } fn should_show_explain(&self) -> bool { !self.short_message } fn supports_color(&self) -> bool { self.dst.supports_color() } } /// An emitter that does nothing when emitting a non-fatal diagnostic. /// Fatal diagnostics are forwarded to `fatal_handler` to avoid silent /// failures of rustc, as witnessed e.g. in issue #89358. pub struct SilentEmitter { pub fatal_handler: Handler, pub fatal_note: Option, } impl Translate for SilentEmitter { fn fluent_bundle(&self) -> Option<&Lrc> { None } fn fallback_fluent_bundle(&self) -> &FluentBundle { panic!("silent emitter attempted to translate message") } } impl Emitter for SilentEmitter { fn source_map(&self) -> Option<&Lrc> { None } fn emit_diagnostic(&mut self, d: &Diagnostic) { if d.level == Level::Fatal { let mut d = d.clone(); if let Some(ref note) = self.fatal_note { d.note(note.clone()); } self.fatal_handler.emit_diagnostic(&mut d); } } } /// Maximum number of suggestions to be shown /// /// Arbitrary, but taken from trait import suggestion limit pub const MAX_SUGGESTIONS: usize = 4; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ColorConfig { Auto, Always, Never, } impl ColorConfig { pub fn to_color_choice(self) -> ColorChoice { match self { ColorConfig::Always => { if io::stderr().is_terminal() { ColorChoice::Always } else { ColorChoice::AlwaysAnsi } } ColorConfig::Never => ColorChoice::Never, ColorConfig::Auto if io::stderr().is_terminal() => ColorChoice::Auto, ColorConfig::Auto => ColorChoice::Never, } } fn suggests_using_colors(self) -> bool { match self { ColorConfig::Always | ColorConfig::Auto => true, ColorConfig::Never => false, } } } /// Handles the writing of `HumanReadableErrorType::Default` and `HumanReadableErrorType::Short` #[derive(Setters)] pub struct EmitterWriter { #[setters(skip)] dst: Destination, sm: Option>, fluent_bundle: Option>, #[setters(skip)] fallback_bundle: LazyFallbackBundle, short_message: bool, teach: bool, ui_testing: bool, diagnostic_width: Option, macro_backtrace: bool, track_diagnostics: bool, terminal_url: TerminalUrl, } #[derive(Debug)] pub struct FileWithAnnotatedLines { pub file: Lrc, pub lines: Vec, multiline_depth: usize, } impl EmitterWriter { pub fn stderr(color_config: ColorConfig, fallback_bundle: LazyFallbackBundle) -> EmitterWriter { let dst = Destination::from_stderr(color_config); Self::create(dst, fallback_bundle) } fn create(dst: Destination, fallback_bundle: LazyFallbackBundle) -> EmitterWriter { EmitterWriter { dst, sm: None, fluent_bundle: None, fallback_bundle, short_message: false, teach: false, ui_testing: false, diagnostic_width: None, macro_backtrace: false, track_diagnostics: false, terminal_url: TerminalUrl::No, } } pub fn new( dst: Box, fallback_bundle: LazyFallbackBundle, ) -> EmitterWriter { Self::create(Raw(dst), fallback_bundle) } fn maybe_anonymized(&self, line_num: usize) -> Cow<'static, str> { if self.ui_testing { Cow::Borrowed(ANONYMIZED_LINE_NUM) } else { Cow::Owned(line_num.to_string()) } } fn draw_line( &self, buffer: &mut StyledBuffer, source_string: &str, line_index: usize, line_offset: usize, width_offset: usize, code_offset: usize, margin: Margin, ) { // Tabs are assumed to have been replaced by spaces in calling code. debug_assert!(!source_string.contains('\t')); let line_len = source_string.len(); // Create the source line we will highlight. let left = margin.left(line_len); let right = margin.right(line_len); // On long lines, we strip the source line, accounting for unicode. let mut taken = 0; let code: String = source_string .chars() .skip(left) .take_while(|ch| { // Make sure that the trimming on the right will fall within the terminal width. // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. // For now, just accept that sometimes the code line will be longer than desired. let next = unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); if taken + next > right - left { return false; } taken += next; true }) .collect(); buffer.puts(line_offset, code_offset, &code, Style::Quotation); if margin.was_cut_left() { // We have stripped some code/whitespace from the beginning, make it clear. buffer.puts(line_offset, code_offset, "...", Style::LineNumber); } if margin.was_cut_right(line_len) { // We have stripped some code after the right-most span end, make it clear we did so. buffer.puts(line_offset, code_offset + taken - 3, "...", Style::LineNumber); } buffer.puts(line_offset, 0, &self.maybe_anonymized(line_index), Style::LineNumber); draw_col_separator_no_space(buffer, line_offset, width_offset - 2); } #[instrument(level = "trace", skip(self), ret)] fn render_source_line( &self, buffer: &mut StyledBuffer, file: Lrc, line: &Line, width_offset: usize, code_offset: usize, margin: Margin, ) -> Vec<(usize, Style)> { // Draw: // // LL | ... code ... // | ^^-^ span label // | | // | secondary span label // // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it // | | | | // | | | actual code found in your source code and the spans we use to mark it // | | when there's too much wasted space to the left, trim it // | vertical divider between the column number and the code // column number if line.line_index == 0 { return Vec::new(); } let source_string = match file.get_line(line.line_index - 1) { Some(s) => normalize_whitespace(&s), None => return Vec::new(), }; trace!(?source_string); let line_offset = buffer.num_lines(); // Left trim let left = margin.left(source_string.len()); // Account for unicode characters of width !=0 that were removed. let left = source_string .chars() .take(left) .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) .sum(); self.draw_line( buffer, &source_string, line.line_index, line_offset, width_offset, code_offset, margin, ); // Special case when there's only one annotation involved, it is the start of a multiline // span and there's no text at the beginning of the code line. Instead of doing the whole // graph: // // 2 | fn foo() { // | _^ // 3 | | // 4 | | } // | |_^ test // // we simplify the output to: // // 2 | / fn foo() { // 3 | | // 4 | | } // | |_^ test let mut buffer_ops = vec![]; let mut annotations = vec![]; let mut short_start = true; for ann in &line.annotations { if let AnnotationType::MultilineStart(depth) = ann.annotation_type { if source_string.chars().take(ann.start_col.display).all(|c| c.is_whitespace()) { let style = if ann.is_primary { Style::UnderlinePrimary } else { Style::UnderlineSecondary }; annotations.push((depth, style)); buffer_ops.push((line_offset, width_offset + depth - 1, '/', style)); } else { short_start = false; break; } } else if let AnnotationType::MultilineLine(_) = ann.annotation_type { } else { short_start = false; break; } } if short_start { for (y, x, c, s) in buffer_ops { buffer.putc(y, x, c, s); } return annotations; } // We want to display like this: // // vec.push(vec.pop().unwrap()); // --- ^^^ - previous borrow ends here // | | // | error occurs here // previous borrow of `vec` occurs here // // But there are some weird edge cases to be aware of: // // vec.push(vec.pop().unwrap()); // -------- - previous borrow ends here // || // |this makes no sense // previous borrow of `vec` occurs here // // For this reason, we group the lines into "highlight lines" // and "annotations lines", where the highlight lines have the `^`. // Sort the annotations by (start, end col) // The labels are reversed, sort and then reversed again. // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where // the letter signifies the span. Here we are only sorting by the // span and hence, the order of the elements with the same span will // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get // (C1, C2, B1, B2, A1, A2). All the elements with the same span are // still ordered first to last, but all the elements with different // spans are ordered by their spans in last to first order. Last to // first order is important, because the jiggly lines and | are on // the left, so the rightmost span needs to be rendered first, // otherwise the lines would end up needing to go over a message. let mut annotations = line.annotations.clone(); annotations.sort_by_key(|a| Reverse(a.start_col)); // First, figure out where each label will be positioned. // // In the case where you have the following annotations: // // vec.push(vec.pop().unwrap()); // -------- - previous borrow ends here [C] // || // |this makes no sense [B] // previous borrow of `vec` occurs here [A] // // `annotations_position` will hold [(2, A), (1, B), (0, C)]. // // We try, when possible, to stick the rightmost annotation at the end // of the highlight line: // // vec.push(vec.pop().unwrap()); // --- --- - previous borrow ends here // // But sometimes that's not possible because one of the other // annotations overlaps it. For example, from the test // `span_overlap_label`, we have the following annotations // (written on distinct lines for clarity): // // fn foo(x: u32) { // -------------- // - // // In this case, we can't stick the rightmost-most label on // the highlight line, or we would get: // // fn foo(x: u32) { // -------- x_span // | // fn_span // // which is totally weird. Instead we want: // // fn foo(x: u32) { // -------------- // | | // | x_span // fn_span // // which is...less weird, at least. In fact, in general, if // the rightmost span overlaps with any other span, we should // use the "hang below" version, so we can at least make it // clear where the span *starts*. There's an exception for this // logic, when the labels do not have a message: // // fn foo(x: u32) { // -------------- // | // x_span // // instead of: // // fn foo(x: u32) { // -------------- // | | // | x_span // // let mut annotations_position = vec![]; let mut line_len = 0; let mut p = 0; for (i, annotation) in annotations.iter().enumerate() { for (j, next) in annotations.iter().enumerate() { if overlaps(next, annotation, 0) // This label overlaps with another one and both && annotation.has_label() // take space (they have text and are not && j > i // multiline lines). && p == 0 // We're currently on the first line, move the label one line down { // If we're overlapping with an un-labelled annotation with the same span // we can just merge them in the output if next.start_col == annotation.start_col && next.end_col == annotation.end_col && !next.has_label() { continue; } // This annotation needs a new line in the output. p += 1; break; } } annotations_position.push((p, annotation)); for (j, next) in annotations.iter().enumerate() { if j > i { let l = next.label.as_ref().map_or(0, |label| label.len() + 2); if (overlaps(next, annotation, l) // Do not allow two labels to be in the same // line if they overlap including padding, to // avoid situations like: // // fn foo(x: u32) { // -------^------ // | | // fn_spanx_span // && annotation.has_label() // Both labels must have some text, otherwise && next.has_label()) // they are not overlapping. // Do not add a new line if this annotation // or the next are vertical line placeholders. || (annotation.takes_space() // If either this or the next annotation is && next.has_label()) // multiline start/end, move it to a new line || (annotation.has_label() // so as not to overlap the horizontal lines. && next.takes_space()) || (annotation.takes_space() && next.takes_space()) || (overlaps(next, annotation, l) && next.end_col <= annotation.end_col && next.has_label() && p == 0) // Avoid #42595. { // This annotation needs a new line in the output. p += 1; break; } } } line_len = max(line_len, p); } if line_len != 0 { line_len += 1; } // If there are no annotations or the only annotations on this line are // MultilineLine, then there's only code being shown, stop processing. if line.annotations.iter().all(|a| a.is_line()) { return vec![]; } // Write the column separator. // // After this we will have: // // 2 | fn foo() { // | // | // | // 3 | // 4 | } // | for pos in 0..=line_len { draw_col_separator(buffer, line_offset + pos + 1, width_offset - 2); } // Write the horizontal lines for multiline annotations // (only the first and last lines need this). // // After this we will have: // // 2 | fn foo() { // | __________ // | // | // 3 | // 4 | } // | _ for &(pos, annotation) in &annotations_position { let style = if annotation.is_primary { Style::UnderlinePrimary } else { Style::UnderlineSecondary }; let pos = pos + 1; match annotation.annotation_type { AnnotationType::MultilineStart(depth) | AnnotationType::MultilineEnd(depth) => { draw_range( buffer, '_', line_offset + pos, width_offset + depth, (code_offset + annotation.start_col.display).saturating_sub(left), style, ); } _ if self.teach => { buffer.set_style_range( line_offset, (code_offset + annotation.start_col.display).saturating_sub(left), (code_offset + annotation.end_col.display).saturating_sub(left), style, annotation.is_primary, ); } _ => {} } } // Write the vertical lines for labels that are on a different line as the underline. // // After this we will have: // // 2 | fn foo() { // | __________ // | | | // | | // 3 | | // 4 | | } // | |_ for &(pos, annotation) in &annotations_position { let style = if annotation.is_primary { Style::UnderlinePrimary } else { Style::UnderlineSecondary }; let pos = pos + 1; if pos > 1 && (annotation.has_label() || annotation.takes_space()) { for p in line_offset + 1..=line_offset + pos { buffer.putc( p, (code_offset + annotation.start_col.display).saturating_sub(left), '|', style, ); } } match annotation.annotation_type { AnnotationType::MultilineStart(depth) => { for p in line_offset + pos + 1..line_offset + line_len + 2 { buffer.putc(p, width_offset + depth - 1, '|', style); } } AnnotationType::MultilineEnd(depth) => { for p in line_offset..=line_offset + pos { buffer.putc(p, width_offset + depth - 1, '|', style); } } _ => (), } } // Write the labels on the annotations that actually have a label. // // After this we will have: // // 2 | fn foo() { // | __________ // | | // | something about `foo` // 3 | // 4 | } // | _ test for &(pos, annotation) in &annotations_position { let style = if annotation.is_primary { Style::LabelPrimary } else { Style::LabelSecondary }; let (pos, col) = if pos == 0 { (pos + 1, (annotation.end_col.display + 1).saturating_sub(left)) } else { (pos + 2, annotation.start_col.display.saturating_sub(left)) }; if let Some(ref label) = annotation.label { buffer.puts(line_offset + pos, code_offset + col, label, style); } } // Sort from biggest span to smallest span so that smaller spans are // represented in the output: // // x | fn foo() // | ^^^---^^ // | | | // | | something about `foo` // | something about `fn foo()` annotations_position.sort_by_key(|(_, ann)| { // Decreasing order. When annotations share the same length, prefer `Primary`. (Reverse(ann.len()), ann.is_primary) }); // Write the underlines. // // After this we will have: // // 2 | fn foo() { // | ____-_____^ // | | // | something about `foo` // 3 | // 4 | } // | _^ test for &(_, annotation) in &annotations_position { let (underline, style) = if annotation.is_primary { ('^', Style::UnderlinePrimary) } else { ('-', Style::UnderlineSecondary) }; for p in annotation.start_col.display..annotation.end_col.display { buffer.putc( line_offset + 1, (code_offset + p).saturating_sub(left), underline, style, ); } } annotations_position .iter() .filter_map(|&(_, annotation)| match annotation.annotation_type { AnnotationType::MultilineStart(p) | AnnotationType::MultilineEnd(p) => { let style = if annotation.is_primary { Style::LabelPrimary } else { Style::LabelSecondary }; Some((p, style)) } _ => None, }) .collect::>() } fn get_multispan_max_line_num(&mut self, msp: &MultiSpan) -> usize { let Some(ref sm) = self.sm else { return 0; }; let will_be_emitted = |span: Span| { !span.is_dummy() && { let file = sm.lookup_source_file(span.hi()); sm.ensure_source_file_source_present(file) } }; let mut max = 0; for primary_span in msp.primary_spans() { if will_be_emitted(*primary_span) { let hi = sm.lookup_char_pos(primary_span.hi()); max = (hi.line).max(max); } } if !self.short_message { for span_label in msp.span_labels() { if will_be_emitted(span_label.span) { let hi = sm.lookup_char_pos(span_label.span.hi()); max = (hi.line).max(max); } } } max } fn get_max_line_num(&mut self, span: &MultiSpan, children: &[SubDiagnostic]) -> usize { let primary = self.get_multispan_max_line_num(span); children .iter() .map(|sub| self.get_multispan_max_line_num(&sub.span)) .max() .unwrap_or(0) .max(primary) } /// Adds a left margin to every line but the first, given a padding length and the label being /// displayed, keeping the provided highlighting. fn msg_to_buffer( &self, buffer: &mut StyledBuffer, msg: &[(DiagnosticMessage, Style)], args: &FluentArgs<'_>, padding: usize, label: &str, override_style: Option