diff options
Diffstat (limited to 'compiler/rustc_errors/src')
22 files changed, 10674 insertions, 0 deletions
diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs new file mode 100644 index 00000000000..f0636b600b7 --- /dev/null +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -0,0 +1,213 @@ +//! Emit diagnostics using the `annotate-snippets` library +//! +//! This is the equivalent of `./emitter.rs` but making use of the +//! [`annotate-snippets`][annotate_snippets] library instead of building the output ourselves. +//! +//! [annotate_snippets]: https://docs.rs/crate/annotate-snippets/ + +use std::sync::Arc; + +use annotate_snippets::{Renderer, Snippet}; +use rustc_error_messages::FluentArgs; +use rustc_span::SourceFile; +use rustc_span::source_map::SourceMap; + +use crate::emitter::FileWithAnnotatedLines; +use crate::registry::Registry; +use crate::snippet::Line; +use crate::translation::{Translate, to_fluent_args}; +use crate::{ + CodeSuggestion, DiagInner, DiagMessage, Emitter, ErrCode, FluentBundle, LazyFallbackBundle, + Level, MultiSpan, Style, Subdiag, +}; + +/// Generates diagnostics using annotate-snippet +pub struct AnnotateSnippetEmitter { + source_map: Option<Arc<SourceMap>>, + fluent_bundle: Option<Arc<FluentBundle>>, + fallback_bundle: LazyFallbackBundle, + + /// 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, + + macro_backtrace: bool, +} + +impl Translate for AnnotateSnippetEmitter { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + self.fluent_bundle.as_deref() + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &self.fallback_bundle + } +} + +impl Emitter for AnnotateSnippetEmitter { + /// The entry point for the diagnostics generation + fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) { + let fluent_args = to_fluent_args(diag.args.iter()); + + let mut suggestions = diag.suggestions.unwrap_tag(); + self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args); + + self.fix_multispans_in_extern_macros_and_render_macro_backtrace( + &mut diag.span, + &mut diag.children, + &diag.level, + self.macro_backtrace, + ); + + self.emit_messages_default( + &diag.level, + &diag.messages, + &fluent_args, + &diag.code, + &diag.span, + &diag.children, + &suggestions, + ); + } + + fn source_map(&self) -> Option<&SourceMap> { + self.source_map.as_deref() + } + + fn should_show_explain(&self) -> bool { + !self.short_message + } +} + +/// Provides the source string for the given `line` of `file` +fn source_string(file: Arc<SourceFile>, line: &Line) -> String { + file.get_line(line.line_index - 1).map(|a| a.to_string()).unwrap_or_default() +} + +/// Maps [`crate::Level`] to [`annotate_snippets::Level`] +fn annotation_level_for_level(level: Level) -> annotate_snippets::Level { + match level { + Level::Bug | Level::Fatal | Level::Error | Level::DelayedBug => { + 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, + // FIXME(#59346): Not sure how to map this level + Level::FailureNote => annotate_snippets::Level::Error, + Level::Allow => panic!("Should not call with Allow"), + Level::Expect(_) => panic!("Should not call with Expect"), + } +} + +impl AnnotateSnippetEmitter { + pub fn new( + source_map: Option<Arc<SourceMap>>, + fluent_bundle: Option<Arc<FluentBundle>>, + fallback_bundle: LazyFallbackBundle, + short_message: bool, + macro_backtrace: bool, + ) -> Self { + Self { + source_map, + fluent_bundle, + fallback_bundle, + 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 + } + + fn emit_messages_default( + &mut self, + level: &Level, + messages: &[(DiagMessage, Style)], + args: &FluentArgs<'_>, + code: &Option<ErrCode>, + msp: &MultiSpan, + _children: &[Subdiag], + _suggestions: &[CodeSuggestion], + ) { + let message = self.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()) + } + } else { + // FIXME(#59346): Not sure when this is the case and what + // should be done if it happens + return; + }; + 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); + } + // owned: file name, line source, line index, annotations + type Owned = (String, String, usize, Vec<crate::snippet::Annotation>); + let annotated_files: Vec<Owned> = annotated_files + .into_iter() + .flat_map(|annotated_file| { + let file = annotated_file.file; + annotated_file + .lines + .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, + ) + }) + .collect::<Vec<Owned>>() + }) + .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) + } + // 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? + } +} diff --git a/compiler/rustc_errors/src/codes.rs b/compiler/rustc_errors/src/codes.rs new file mode 100644 index 00000000000..947cf27ca79 --- /dev/null +++ b/compiler/rustc_errors/src/codes.rs @@ -0,0 +1,39 @@ +//! This module defines the following. +//! - The `ErrCode` type. +//! - A constant for every error code, with a name like `E0123`. +//! - A static table `DIAGNOSTICS` pairing every error code constant with its +//! long description text. + +use std::fmt; + +rustc_index::newtype_index! { + #[max = 9999] // Because all error codes have four digits. + #[orderable] + #[encodable] + #[debug_format = "ErrCode({})"] + pub struct ErrCode {} +} + +impl fmt::Display for ErrCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "E{:04}", self.as_u32()) + } +} + +macro_rules! define_error_code_constants_and_diagnostics_table { + ($($name:ident: $num:literal,)*) => ( + $( + pub const $name: $crate::ErrCode = $crate::ErrCode::from_u32($num); + )* + pub static DIAGNOSTICS: &[($crate::ErrCode, &str)] = &[ + $( ( + $name, + include_str!( + concat!("../../rustc_error_codes/src/error_codes/", stringify!($name), ".md") + ) + ), )* + ]; + ) +} + +rustc_error_codes::error_codes!(define_error_code_constants_and_diagnostics_table); diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs new file mode 100644 index 00000000000..29a74ed3f4e --- /dev/null +++ b/compiler/rustc_errors/src/diagnostic.rs @@ -0,0 +1,1439 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug}; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::panic; +use std::path::PathBuf; +use std::thread::panicking; + +use rustc_data_structures::fx::FxIndexMap; +use rustc_error_messages::{FluentValue, fluent_value_from_str_list_sep_by_and}; +use rustc_lint_defs::Applicability; +use rustc_macros::{Decodable, Encodable}; +use rustc_span::source_map::Spanned; +use rustc_span::{DUMMY_SP, Span, Symbol}; +use tracing::debug; + +use crate::snippet::Style; +use crate::{ + CodeSuggestion, DiagCtxtHandle, DiagMessage, ErrCode, ErrorGuaranteed, ExplicitBug, Level, + MultiSpan, StashKey, SubdiagMessage, Substitution, SubstitutionPart, SuggestionStyle, + Suggestions, +}; + +/// Simplified version of `FluentArg` that can implement `Encodable` and `Decodable`. Collection of +/// `DiagArg` are converted to `FluentArgs` (consuming the collection) at the start of diagnostic +/// emission. +pub type DiagArg<'iter> = (&'iter DiagArgName, &'iter DiagArgValue); + +/// Name of a diagnostic argument. +pub type DiagArgName = Cow<'static, str>; + +/// Simplified version of `FluentValue` that can implement `Encodable` and `Decodable`. Converted +/// to a `FluentValue` by the emitter to be used in diagnostic translation. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)] +pub enum DiagArgValue { + Str(Cow<'static, str>), + // This gets converted to a `FluentNumber`, which is an `f64`. An `i32` + // safely fits in an `f64`. Any integers bigger than that will be converted + // to strings in `into_diag_arg` and stored using the `Str` variant. + Number(i32), + StrListSepByAnd(Vec<Cow<'static, str>>), +} + +pub type DiagArgMap = FxIndexMap<DiagArgName, DiagArgValue>; + +/// Trait for types that `Diag::emit` can return as a "guarantee" (or "proof") +/// token that the emission happened. +pub trait EmissionGuarantee: Sized { + /// This exists so that bugs and fatal errors can both result in `!` (an + /// abort) when emitted, but have different aborting behaviour. + type EmitResult = Self; + + /// Implementation of `Diag::emit`, fully controlled by each `impl` of + /// `EmissionGuarantee`, to make it impossible to create a value of + /// `Self::EmitResult` without actually performing the emission. + #[track_caller] + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult; +} + +impl EmissionGuarantee for ErrorGuaranteed { + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult { + diag.emit_producing_error_guaranteed() + } +} + +impl EmissionGuarantee for () { + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult { + diag.emit_producing_nothing(); + } +} + +/// Marker type which enables implementation of `create_bug` and `emit_bug` functions for +/// bug diagnostics. +#[derive(Copy, Clone)] +pub struct BugAbort; + +impl EmissionGuarantee for BugAbort { + type EmitResult = !; + + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult { + diag.emit_producing_nothing(); + panic::panic_any(ExplicitBug); + } +} + +/// Marker type which enables implementation of `create_fatal` and `emit_fatal` functions for +/// fatal diagnostics. +#[derive(Copy, Clone)] +pub struct FatalAbort; + +impl EmissionGuarantee for FatalAbort { + type EmitResult = !; + + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult { + diag.emit_producing_nothing(); + crate::FatalError.raise() + } +} + +impl EmissionGuarantee for rustc_span::fatal_error::FatalError { + fn emit_producing_guarantee(diag: Diag<'_, Self>) -> Self::EmitResult { + diag.emit_producing_nothing(); + rustc_span::fatal_error::FatalError + } +} + +/// Trait implemented by error types. This is rarely implemented manually. Instead, use +/// `#[derive(Diagnostic)]` -- see [rustc_macros::Diagnostic]. +/// +/// When implemented manually, it should be generic over the emission +/// guarantee, i.e.: +/// ```ignore (fragment) +/// impl<'a, G: EmissionGuarantee> Diagnostic<'a, G> for Foo { ... } +/// ``` +/// rather than being specific: +/// ```ignore (fragment) +/// impl<'a> Diagnostic<'a> for Bar { ... } // the default type param is `ErrorGuaranteed` +/// impl<'a> Diagnostic<'a, ()> for Baz { ... } +/// ``` +/// There are two reasons for this. +/// - A diagnostic like `Foo` *could* be emitted at any level -- `level` is +/// passed in to `into_diag` from outside. Even if in practice it is +/// always emitted at a single level, we let the diagnostic creation/emission +/// site determine the level (by using `create_err`, `emit_warn`, etc.) +/// rather than the `Diagnostic` impl. +/// - Derived impls are always generic, and it's good for the hand-written +/// impls to be consistent with them. +#[rustc_diagnostic_item = "Diagnostic"] +pub trait Diagnostic<'a, G: EmissionGuarantee = ErrorGuaranteed> { + /// Write out as a diagnostic out of `DiagCtxt`. + #[must_use] + fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, G>; +} + +impl<'a, T, G> Diagnostic<'a, G> for Spanned<T> +where + T: Diagnostic<'a, G>, + G: EmissionGuarantee, +{ + fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, G> { + self.node.into_diag(dcx, level).with_span(self.span) + } +} + +/// Converts a value of a type into a `DiagArg` (typically a field of an `Diag` struct). +/// Implemented as a custom trait rather than `From` so that it is implemented on the type being +/// converted rather than on `DiagArgValue`, which enables types from other `rustc_*` crates to +/// implement this. +pub trait IntoDiagArg { + fn into_diag_arg(self) -> DiagArgValue; +} + +impl IntoDiagArg for DiagArgValue { + fn into_diag_arg(self) -> DiagArgValue { + self + } +} + +impl From<DiagArgValue> for FluentValue<'static> { + fn from(val: DiagArgValue) -> Self { + match val { + DiagArgValue::Str(s) => From::from(s), + DiagArgValue::Number(n) => From::from(n), + DiagArgValue::StrListSepByAnd(l) => fluent_value_from_str_list_sep_by_and(l), + } + } +} + +/// Trait implemented by error types. This should not be implemented manually. Instead, use +/// `#[derive(Subdiagnostic)]` -- see [rustc_macros::Subdiagnostic]. +#[rustc_diagnostic_item = "Subdiagnostic"] +pub trait Subdiagnostic +where + Self: Sized, +{ + /// Add a subdiagnostic to an existing diagnostic. + fn add_to_diag<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) { + self.add_to_diag_with(diag, &|_, m| m); + } + + /// Add a subdiagnostic to an existing diagnostic where `f` is invoked on every message used + /// (to optionally perform eager translation). + fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>( + self, + diag: &mut Diag<'_, G>, + f: &F, + ); +} + +pub trait SubdiagMessageOp<G: EmissionGuarantee> = + Fn(&mut Diag<'_, G>, SubdiagMessage) -> SubdiagMessage; + +/// Trait implemented by lint types. This should not be implemented manually. Instead, use +/// `#[derive(LintDiagnostic)]` -- see [rustc_macros::LintDiagnostic]. +#[rustc_diagnostic_item = "LintDiagnostic"] +pub trait LintDiagnostic<'a, G: EmissionGuarantee> { + /// Decorate and emit a lint. + fn decorate_lint<'b>(self, diag: &'b mut Diag<'a, G>); +} + +#[derive(Clone, Debug, Encodable, Decodable)] +pub(crate) struct DiagLocation { + file: Cow<'static, str>, + line: u32, + col: u32, +} + +impl DiagLocation { + #[track_caller] + fn caller() -> Self { + let loc = panic::Location::caller(); + DiagLocation { file: loc.file().into(), line: loc.line(), col: loc.column() } + } +} + +impl fmt::Display for DiagLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}:{}", self.file, self.line, self.col) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)] +pub struct IsLint { + /// The lint name. + pub(crate) name: String, + /// Indicates whether this lint should show up in cargo's future breakage report. + has_future_breakage: bool, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DiagStyledString(pub Vec<StringPart>); + +impl DiagStyledString { + pub fn new() -> DiagStyledString { + DiagStyledString(vec![]) + } + pub fn push_normal<S: Into<String>>(&mut self, t: S) { + self.0.push(StringPart::normal(t)); + } + pub fn push_highlighted<S: Into<String>>(&mut self, t: S) { + self.0.push(StringPart::highlighted(t)); + } + pub fn push<S: Into<String>>(&mut self, t: S, highlight: bool) { + if highlight { + self.push_highlighted(t); + } else { + self.push_normal(t); + } + } + pub fn normal<S: Into<String>>(t: S) -> DiagStyledString { + DiagStyledString(vec![StringPart::normal(t)]) + } + + pub fn highlighted<S: Into<String>>(t: S) -> DiagStyledString { + DiagStyledString(vec![StringPart::highlighted(t)]) + } + + pub fn content(&self) -> String { + self.0.iter().map(|x| x.content.as_str()).collect::<String>() + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct StringPart { + content: String, + style: Style, +} + +impl StringPart { + pub fn normal<S: Into<String>>(content: S) -> StringPart { + StringPart { content: content.into(), style: Style::NoStyle } + } + + pub fn highlighted<S: Into<String>>(content: S) -> StringPart { + StringPart { content: content.into(), style: Style::Highlight } + } +} + +/// The main part of a diagnostic. Note that `Diag`, which wraps this type, is +/// used for most operations, and should be used instead whenever possible. +/// This type should only be used when `Diag`'s lifetime causes difficulties, +/// e.g. when storing diagnostics within `DiagCtxt`. +#[must_use] +#[derive(Clone, Debug, Encodable, Decodable)] +pub struct DiagInner { + // NOTE(eddyb) this is private to disallow arbitrary after-the-fact changes, + // outside of what methods in this crate themselves allow. + pub(crate) level: Level, + + pub messages: Vec<(DiagMessage, Style)>, + pub code: Option<ErrCode>, + pub span: MultiSpan, + pub children: Vec<Subdiag>, + pub suggestions: Suggestions, + pub args: DiagArgMap, + + /// This is not used for highlighting or rendering any error message. Rather, it can be used + /// as a sort key to sort a buffer of diagnostics. By default, it is the primary span of + /// `span` if there is one. Otherwise, it is `DUMMY_SP`. + pub sort_span: Span, + + pub is_lint: Option<IsLint>, + + pub long_ty_path: Option<PathBuf>, + /// With `-Ztrack_diagnostics` enabled, + /// we print where in rustc this error was emitted. + pub(crate) emitted_at: DiagLocation, +} + +impl DiagInner { + #[track_caller] + pub fn new<M: Into<DiagMessage>>(level: Level, message: M) -> Self { + DiagInner::new_with_messages(level, vec![(message.into(), Style::NoStyle)]) + } + + #[track_caller] + pub fn new_with_messages(level: Level, messages: Vec<(DiagMessage, Style)>) -> Self { + DiagInner { + level, + messages, + code: None, + span: MultiSpan::new(), + children: vec![], + suggestions: Suggestions::Enabled(vec![]), + args: Default::default(), + sort_span: DUMMY_SP, + is_lint: None, + long_ty_path: None, + emitted_at: DiagLocation::caller(), + } + } + + #[inline(always)] + pub fn level(&self) -> Level { + self.level + } + + pub fn is_error(&self) -> bool { + match self.level { + Level::Bug | Level::Fatal | Level::Error | Level::DelayedBug => true, + + Level::ForceWarning(_) + | Level::Warning + | Level::Note + | Level::OnceNote + | Level::Help + | Level::OnceHelp + | Level::FailureNote + | Level::Allow + | Level::Expect(_) => false, + } + } + + /// Indicates whether this diagnostic should show up in cargo's future breakage report. + pub(crate) fn has_future_breakage(&self) -> bool { + matches!(self.is_lint, Some(IsLint { has_future_breakage: true, .. })) + } + + pub(crate) fn is_force_warn(&self) -> bool { + match self.level { + Level::ForceWarning(_) => { + assert!(self.is_lint.is_some()); + true + } + _ => false, + } + } + + // See comment on `Diag::subdiagnostic_message_to_diagnostic_message`. + pub(crate) fn subdiagnostic_message_to_diagnostic_message( + &self, + attr: impl Into<SubdiagMessage>, + ) -> DiagMessage { + let msg = + self.messages.iter().map(|(msg, _)| msg).next().expect("diagnostic with no messages"); + msg.with_subdiagnostic_message(attr.into()) + } + + pub(crate) fn sub( + &mut self, + level: Level, + message: impl Into<SubdiagMessage>, + span: MultiSpan, + ) { + let sub = Subdiag { + level, + messages: vec![( + self.subdiagnostic_message_to_diagnostic_message(message), + Style::NoStyle, + )], + span, + }; + self.children.push(sub); + } + + pub(crate) fn arg(&mut self, name: impl Into<DiagArgName>, arg: impl IntoDiagArg) { + self.args.insert(name.into(), arg.into_diag_arg()); + } + + /// Fields used for Hash, and PartialEq trait. + fn keys( + &self, + ) -> ( + &Level, + &[(DiagMessage, Style)], + &Option<ErrCode>, + &MultiSpan, + &[Subdiag], + &Suggestions, + Vec<(&DiagArgName, &DiagArgValue)>, + &Option<IsLint>, + ) { + ( + &self.level, + &self.messages, + &self.code, + &self.span, + &self.children, + &self.suggestions, + self.args.iter().collect(), + // omit self.sort_span + &self.is_lint, + // omit self.emitted_at + ) + } +} + +impl Hash for DiagInner { + fn hash<H>(&self, state: &mut H) + where + H: Hasher, + { + self.keys().hash(state); + } +} + +impl PartialEq for DiagInner { + fn eq(&self, other: &Self) -> bool { + self.keys() == other.keys() + } +} + +/// A "sub"-diagnostic attached to a parent diagnostic. +/// For example, a note attached to an error. +#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)] +pub struct Subdiag { + pub level: Level, + pub messages: Vec<(DiagMessage, Style)>, + pub span: MultiSpan, +} + +/// Used for emitting structured error messages and other diagnostic information. +/// Wraps a `DiagInner`, adding some useful things. +/// - The `dcx` field, allowing it to (a) emit itself, and (b) do a drop check +/// that it has been emitted or cancelled. +/// - The `EmissionGuarantee`, which determines the type returned from `emit`. +/// +/// Each constructed `Diag` must be consumed by a function such as `emit`, +/// `cancel`, `delay_as_bug`, or `into_diag`. A panic occurs if a `Diag` +/// is dropped without being consumed by one of these functions. +/// +/// If there is some state in a downstream crate you would like to access in +/// the methods of `Diag` here, consider extending `DiagCtxtFlags`. +#[must_use] +pub struct Diag<'a, G: EmissionGuarantee = ErrorGuaranteed> { + pub dcx: DiagCtxtHandle<'a>, + + /// Why the `Option`? It is always `Some` until the `Diag` is consumed via + /// `emit`, `cancel`, etc. At that point it is consumed and replaced with + /// `None`. Then `drop` checks that it is `None`; if not, it panics because + /// a diagnostic was built but not used. + /// + /// Why the Box? `DiagInner` is a large type, and `Diag` is often used as a + /// return value, especially within the frequently-used `PResult` type. In + /// theory, return value optimization (RVO) should avoid unnecessary + /// copying. In practice, it does not (at the time of writing). + diag: Option<Box<DiagInner>>, + + _marker: PhantomData<G>, +} + +// Cloning a `Diag` is a recipe for a diagnostic being emitted twice, which +// would be bad. +impl<G> !Clone for Diag<'_, G> {} + +rustc_data_structures::static_assert_size!(Diag<'_, ()>, 3 * std::mem::size_of::<usize>()); + +impl<G: EmissionGuarantee> Deref for Diag<'_, G> { + type Target = DiagInner; + + fn deref(&self) -> &DiagInner { + self.diag.as_ref().unwrap() + } +} + +impl<G: EmissionGuarantee> DerefMut for Diag<'_, G> { + fn deref_mut(&mut self) -> &mut DiagInner { + self.diag.as_mut().unwrap() + } +} + +impl<G: EmissionGuarantee> Debug for Diag<'_, G> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.diag.fmt(f) + } +} + +/// `Diag` impls many `&mut self -> &mut Self` methods. Each one modifies an +/// existing diagnostic, either in a standalone fashion, e.g. +/// `err.code(code);`, or in a chained fashion to make multiple modifications, +/// e.g. `err.code(code).span(span);`. +/// +/// This macro creates an equivalent `self -> Self` method, with a `with_` +/// prefix. This can be used in a chained fashion when making a new diagnostic, +/// e.g. `let err = struct_err(msg).with_code(code);`, or emitting a new +/// diagnostic, e.g. `struct_err(msg).with_code(code).emit();`. +/// +/// Although the latter method can be used to modify an existing diagnostic, +/// e.g. `err = err.with_code(code);`, this should be avoided because the former +/// method gives shorter code, e.g. `err.code(code);`. +/// +/// Note: the `with_` methods are added only when needed. If you want to use +/// one and it's not defined, feel free to add it. +/// +/// Note: any doc comments must be within the `with_fn!` call. +macro_rules! with_fn { + { + $with_f:ident, + $(#[$attrs:meta])* + pub fn $f:ident(&mut $self:ident, $($name:ident: $ty:ty),* $(,)?) -> &mut Self { + $($body:tt)* + } + } => { + // The original function. + $(#[$attrs])* + #[doc = concat!("See [`Diag::", stringify!($f), "()`].")] + pub fn $f(&mut $self, $($name: $ty),*) -> &mut Self { + $($body)* + } + + // The `with_*` variant. + $(#[$attrs])* + #[doc = concat!("See [`Diag::", stringify!($f), "()`].")] + pub fn $with_f(mut $self, $($name: $ty),*) -> Self { + $self.$f($($name),*); + $self + } + }; +} + +impl<'a, G: EmissionGuarantee> Diag<'a, G> { + #[rustc_lint_diagnostics] + #[track_caller] + pub fn new(dcx: DiagCtxtHandle<'a>, level: Level, message: impl Into<DiagMessage>) -> Self { + Self::new_diagnostic(dcx, DiagInner::new(level, message)) + } + + /// Allow moving diagnostics between different error tainting contexts + pub fn with_dcx(mut self, dcx: DiagCtxtHandle<'_>) -> Diag<'_, G> { + Diag { dcx, diag: self.diag.take(), _marker: PhantomData } + } + + /// Creates a new `Diag` with an already constructed diagnostic. + #[track_caller] + pub(crate) fn new_diagnostic(dcx: DiagCtxtHandle<'a>, diag: DiagInner) -> Self { + debug!("Created new diagnostic"); + Self { dcx, diag: Some(Box::new(diag)), _marker: PhantomData } + } + + /// Delay emission of this diagnostic as a bug. + /// + /// This can be useful in contexts where an error indicates a bug but + /// typically this only happens when other compilation errors have already + /// happened. In those cases this can be used to defer emission of this + /// diagnostic as a bug in the compiler only if no other errors have been + /// emitted. + /// + /// In the meantime, though, callsites are required to deal with the "bug" + /// locally in whichever way makes the most sense. + #[rustc_lint_diagnostics] + #[track_caller] + pub fn downgrade_to_delayed_bug(&mut self) { + assert!( + matches!(self.level, Level::Error | Level::DelayedBug), + "downgrade_to_delayed_bug: cannot downgrade {:?} to DelayedBug: not an error", + self.level + ); + self.level = Level::DelayedBug; + } + + with_fn! { with_span_label, + /// Appends a labeled span to the diagnostic. + /// + /// Labels are used to convey additional context for the diagnostic's primary span. They will + /// be shown together with the original diagnostic's span, *not* with spans added by + /// `span_note`, `span_help`, etc. Therefore, if the primary span is not displayable (because + /// the span is `DUMMY_SP` or the source code isn't found), labels will not be displayed + /// either. + /// + /// Implementation-wise, the label span is pushed onto the [`MultiSpan`] that was created when + /// the diagnostic was constructed. However, the label span is *not* considered a + /// ["primary span"][`MultiSpan`]; only the `Span` supplied when creating the diagnostic is + /// primary. + #[rustc_lint_diagnostics] + pub fn span_label(&mut self, span: Span, label: impl Into<SubdiagMessage>) -> &mut Self { + let msg = self.subdiagnostic_message_to_diagnostic_message(label); + self.span.push_span_label(span, msg); + self + } } + + with_fn! { with_span_labels, + /// Labels all the given spans with the provided label. + /// See [`Self::span_label()`] for more information. + #[rustc_lint_diagnostics] + pub fn span_labels(&mut self, spans: impl IntoIterator<Item = Span>, label: &str) -> &mut Self { + for span in spans { + self.span_label(span, label.to_string()); + } + self + } } + + #[rustc_lint_diagnostics] + pub fn replace_span_with(&mut self, after: Span, keep_label: bool) -> &mut Self { + let before = self.span.clone(); + self.span(after); + for span_label in before.span_labels() { + if let Some(label) = span_label.label { + if span_label.is_primary && keep_label { + self.span.push_span_label(after, label); + } else { + self.span.push_span_label(span_label.span, label); + } + } + } + self + } + + #[rustc_lint_diagnostics] + pub fn note_expected_found( + &mut self, + expected_label: &dyn fmt::Display, + expected: DiagStyledString, + found_label: &dyn fmt::Display, + found: DiagStyledString, + ) -> &mut Self { + self.note_expected_found_extra( + expected_label, + expected, + found_label, + found, + DiagStyledString::normal(""), + DiagStyledString::normal(""), + ) + } + + #[rustc_lint_diagnostics] + pub fn note_expected_found_extra( + &mut self, + expected_label: &dyn fmt::Display, + expected: DiagStyledString, + found_label: &dyn fmt::Display, + found: DiagStyledString, + expected_extra: DiagStyledString, + found_extra: DiagStyledString, + ) -> &mut Self { + let expected_label = expected_label.to_string(); + let expected_label = if expected_label.is_empty() { + "expected".to_string() + } else { + format!("expected {expected_label}") + }; + let found_label = found_label.to_string(); + let found_label = if found_label.is_empty() { + "found".to_string() + } else { + format!("found {found_label}") + }; + let (found_padding, expected_padding) = if expected_label.len() > found_label.len() { + (expected_label.len() - found_label.len(), 0) + } else { + (0, found_label.len() - expected_label.len()) + }; + let mut msg = vec![StringPart::normal(format!( + "{}{} `", + " ".repeat(expected_padding), + expected_label + ))]; + msg.extend(expected.0); + msg.push(StringPart::normal(format!("`"))); + msg.extend(expected_extra.0); + msg.push(StringPart::normal(format!("\n"))); + msg.push(StringPart::normal(format!("{}{} `", " ".repeat(found_padding), found_label))); + msg.extend(found.0); + msg.push(StringPart::normal(format!("`"))); + msg.extend(found_extra.0); + + // For now, just attach these as notes. + self.highlighted_note(msg); + self + } + + #[rustc_lint_diagnostics] + pub fn note_trait_signature(&mut self, name: Symbol, signature: String) -> &mut Self { + self.highlighted_note(vec![ + StringPart::normal(format!("`{name}` from trait: `")), + StringPart::highlighted(signature), + StringPart::normal("`"), + ]); + self + } + + with_fn! { with_note, + /// Add a note attached to this diagnostic. + #[rustc_lint_diagnostics] + pub fn note(&mut self, msg: impl Into<SubdiagMessage>) -> &mut Self { + self.sub(Level::Note, msg, MultiSpan::new()); + self + } } + + #[rustc_lint_diagnostics] + pub fn highlighted_note(&mut self, msg: Vec<StringPart>) -> &mut Self { + self.sub_with_highlights(Level::Note, msg, MultiSpan::new()); + self + } + + #[rustc_lint_diagnostics] + pub fn highlighted_span_note( + &mut self, + span: impl Into<MultiSpan>, + msg: Vec<StringPart>, + ) -> &mut Self { + self.sub_with_highlights(Level::Note, msg, span.into()); + self + } + + /// This is like [`Diag::note()`], but it's only printed once. + #[rustc_lint_diagnostics] + pub fn note_once(&mut self, msg: impl Into<SubdiagMessage>) -> &mut Self { + self.sub(Level::OnceNote, msg, MultiSpan::new()); + self + } + + with_fn! { with_span_note, + /// Prints the span with a note above it. + /// This is like [`Diag::note()`], but it gets its own span. + #[rustc_lint_diagnostics] + pub fn span_note( + &mut self, + sp: impl Into<MultiSpan>, + msg: impl Into<SubdiagMessage>, + ) -> &mut Self { + self.sub(Level::Note, msg, sp.into()); + self + } } + + /// Prints the span with a note above it. + /// This is like [`Diag::note_once()`], but it gets its own span. + #[rustc_lint_diagnostics] + pub fn span_note_once<S: Into<MultiSpan>>( + &mut self, + sp: S, + msg: impl Into<SubdiagMessage>, + ) -> &mut Self { + self.sub(Level::OnceNote, msg, sp.into()); + self + } + + with_fn! { with_warn, + /// Add a warning attached to this diagnostic. + #[rustc_lint_diagnostics] + pub fn warn(&mut self, msg: impl Into<SubdiagMessage>) -> &mut Self { + self.sub(Level::Warning, msg, MultiSpan::new()); + self + } } + + /// Prints the span with a warning above it. + /// This is like [`Diag::warn()`], but it gets its own span. + #[rustc_lint_diagnostics] + pub fn span_warn<S: Into<MultiSpan>>( + &mut self, + sp: S, + msg: impl Into<SubdiagMessage>, + ) -> &mut Self { + self.sub(Level::Warning, msg, sp.into()); + self + } + + with_fn! { with_help, + /// Add a help message attached to this diagnostic. + #[rustc_lint_diagnostics] + pub fn help(&mut self, msg: impl Into<SubdiagMessage>) -> &mut Self { + self.sub(Level::Help, msg, MultiSpan::new()); + self + } } + + /// This is like [`Diag::help()`], but it's only printed once. + #[rustc_lint_diagnostics] + pub fn help_once(&mut self, msg: impl Into<SubdiagMessage>) -> &mut Self { + self.sub(Level::OnceHelp, msg, MultiSpan::new()); + self + } + + /// Add a help message attached to this diagnostic with a customizable highlighted message. + #[rustc_lint_diagnostics] + pub fn highlighted_help(&mut self, msg: Vec<StringPart>) -> &mut Self { + self.sub_with_highlights(Level::Help, msg, MultiSpan::new()); + self + } + + /// Add a help message attached to this diagnostic with a customizable highlighted message. + #[rustc_lint_diagnostics] + pub fn highlighted_span_help( + &mut self, + span: impl Into<MultiSpan>, + msg: Vec<StringPart>, + ) -> &mut Self { + self.sub_with_highlights(Level::Help, msg, span.into()); + self + } + + /// Prints the span with some help above it. + /// This is like [`Diag::help()`], but it gets its own span. + #[rustc_lint_diagnostics] + pub fn span_help<S: Into<MultiSpan>>( + &mut self, + sp: S, + msg: impl Into<SubdiagMessage>, + ) -> &mut Self { + self.sub(Level::Help, msg, sp.into()); + self + } + + /// Disallow attaching suggestions to this diagnostic. + /// Any suggestions attached e.g. with the `span_suggestion_*` methods + /// (before and after the call to `disable_suggestions`) will be ignored. + #[rustc_lint_diagnostics] + pub fn disable_suggestions(&mut self) -> &mut Self { + self.suggestions = Suggestions::Disabled; + self + } + + /// Prevent new suggestions from being added to this diagnostic. + /// + /// Suggestions added before the call to `.seal_suggestions()` will be preserved + /// and new suggestions will be ignored. + #[rustc_lint_diagnostics] + pub fn seal_suggestions(&mut self) -> &mut Self { + if let Suggestions::Enabled(suggestions) = &mut self.suggestions { + let suggestions_slice = std::mem::take(suggestions).into_boxed_slice(); + self.suggestions = Suggestions::Sealed(suggestions_slice); + } + self + } + + /// Helper for pushing to `self.suggestions`. + /// + /// A new suggestion is added if suggestions are enabled for this diagnostic. + /// Otherwise, they are ignored. + #[rustc_lint_diagnostics] + fn push_suggestion(&mut self, suggestion: CodeSuggestion) { + for subst in &suggestion.substitutions { + for part in &subst.parts { + let span = part.span; + let call_site = span.ctxt().outer_expn_data().call_site; + if span.in_derive_expansion() && span.overlaps_or_adjacent(call_site) { + // Ignore if spans is from derive macro. + return; + } + } + } + + if let Suggestions::Enabled(suggestions) = &mut self.suggestions { + suggestions.push(suggestion); + } + } + + with_fn! { with_multipart_suggestion, + /// Show a suggestion that has multiple parts to it. + /// In other words, multiple changes need to be applied as part of this suggestion. + #[rustc_lint_diagnostics] + pub fn multipart_suggestion( + &mut self, + msg: impl Into<SubdiagMessage>, + suggestion: Vec<(Span, String)>, + applicability: Applicability, + ) -> &mut Self { + self.multipart_suggestion_with_style( + msg, + suggestion, + applicability, + SuggestionStyle::ShowCode, + ) + } } + + /// Show a suggestion that has multiple parts to it, always as its own subdiagnostic. + /// In other words, multiple changes need to be applied as part of this suggestion. + #[rustc_lint_diagnostics] + pub fn multipart_suggestion_verbose( + &mut self, + msg: impl Into<SubdiagMessage>, + suggestion: Vec<(Span, String)>, + applicability: Applicability, + ) -> &mut Self { + self.multipart_suggestion_with_style( + msg, + suggestion, + applicability, + SuggestionStyle::ShowAlways, + ) + } + + /// [`Diag::multipart_suggestion()`] but you can set the [`SuggestionStyle`]. + #[rustc_lint_diagnostics] + pub fn multipart_suggestion_with_style( + &mut self, + msg: impl Into<SubdiagMessage>, + mut suggestion: Vec<(Span, String)>, + applicability: Applicability, + style: SuggestionStyle, + ) -> &mut Self { + let mut seen = crate::FxHashSet::default(); + suggestion.retain(|(span, msg)| seen.insert((span.lo(), span.hi(), msg.clone()))); + + let parts = suggestion + .into_iter() + .map(|(span, snippet)| SubstitutionPart { snippet, span }) + .collect::<Vec<_>>(); + + assert!(!parts.is_empty()); + debug_assert_eq!( + parts.iter().find(|part| part.span.is_empty() && part.snippet.is_empty()), + None, + "Span must not be empty and have no suggestion", + ); + debug_assert_eq!( + parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)), + None, + "suggestion must not have overlapping parts", + ); + + self.push_suggestion(CodeSuggestion { + substitutions: vec![Substitution { parts }], + msg: self.subdiagnostic_message_to_diagnostic_message(msg), + style, + applicability, + }); + self + } + + /// Prints out a message with for a multipart suggestion without showing the suggested code. + /// + /// This is intended to be used for suggestions that are obvious in what the changes need to + /// be from the message, showing the span label inline would be visually unpleasant + /// (marginally overlapping spans or multiline spans) and showing the snippet window wouldn't + /// improve understandability. + #[rustc_lint_diagnostics] + pub fn tool_only_multipart_suggestion( + &mut self, + msg: impl Into<SubdiagMessage>, + suggestion: Vec<(Span, String)>, + applicability: Applicability, + ) -> &mut Self { + self.multipart_suggestion_with_style( + msg, + suggestion, + applicability, + SuggestionStyle::CompletelyHidden, + ) + } + + with_fn! { with_span_suggestion, + /// Prints out a message with a suggested edit of the code. + /// + /// In case of short messages and a simple suggestion, rustc displays it as a label: + /// + /// ```text + /// try adding parentheses: `(tup.0).1` + /// ``` + /// + /// The message + /// + /// * should not end in any punctuation (a `:` is added automatically) + /// * should not be a question (avoid language like "did you mean") + /// * should not contain any phrases like "the following", "as shown", etc. + /// * may look like "to do xyz, use" or "to do xyz, use abc" + /// * may contain a name of a function, variable, or type, but not whole expressions + /// + /// See `CodeSuggestion` for more information. + #[rustc_lint_diagnostics] + pub fn span_suggestion( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestion_with_style( + sp, + msg, + suggestion, + applicability, + SuggestionStyle::ShowCode, + ); + self + } } + + /// [`Diag::span_suggestion()`] but you can set the [`SuggestionStyle`]. + #[rustc_lint_diagnostics] + pub fn span_suggestion_with_style( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + style: SuggestionStyle, + ) -> &mut Self { + debug_assert!( + !(sp.is_empty() && suggestion.to_string().is_empty()), + "Span must not be empty and have no suggestion" + ); + self.push_suggestion(CodeSuggestion { + substitutions: vec![Substitution { + parts: vec![SubstitutionPart { snippet: suggestion.to_string(), span: sp }], + }], + msg: self.subdiagnostic_message_to_diagnostic_message(msg), + style, + applicability, + }); + self + } + + with_fn! { with_span_suggestion_verbose, + /// Always show the suggested change. + #[rustc_lint_diagnostics] + pub fn span_suggestion_verbose( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestion_with_style( + sp, + msg, + suggestion, + applicability, + SuggestionStyle::ShowAlways, + ); + self + } } + + with_fn! { with_span_suggestions, + /// Prints out a message with multiple suggested edits of the code. + /// See also [`Diag::span_suggestion()`]. + #[rustc_lint_diagnostics] + pub fn span_suggestions( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestions: impl IntoIterator<Item = String>, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestions_with_style( + sp, + msg, + suggestions, + applicability, + SuggestionStyle::ShowCode, + ) + } } + + #[rustc_lint_diagnostics] + pub fn span_suggestions_with_style( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestions: impl IntoIterator<Item = String>, + applicability: Applicability, + style: SuggestionStyle, + ) -> &mut Self { + let substitutions = suggestions + .into_iter() + .map(|snippet| { + debug_assert!( + !(sp.is_empty() && snippet.is_empty()), + "Span must not be empty and have no suggestion" + ); + Substitution { parts: vec![SubstitutionPart { snippet, span: sp }] } + }) + .collect(); + self.push_suggestion(CodeSuggestion { + substitutions, + msg: self.subdiagnostic_message_to_diagnostic_message(msg), + style, + applicability, + }); + self + } + + /// Prints out a message with multiple suggested edits of the code, where each edit consists of + /// multiple parts. + /// See also [`Diag::multipart_suggestion()`]. + #[rustc_lint_diagnostics] + pub fn multipart_suggestions( + &mut self, + msg: impl Into<SubdiagMessage>, + suggestions: impl IntoIterator<Item = Vec<(Span, String)>>, + applicability: Applicability, + ) -> &mut Self { + let substitutions = suggestions + .into_iter() + .map(|sugg| { + let mut parts = sugg + .into_iter() + .map(|(span, snippet)| SubstitutionPart { snippet, span }) + .collect::<Vec<_>>(); + + parts.sort_unstable_by_key(|part| part.span); + + assert!(!parts.is_empty()); + debug_assert_eq!( + parts.iter().find(|part| part.span.is_empty() && part.snippet.is_empty()), + None, + "Span must not be empty and have no suggestion", + ); + debug_assert_eq!( + parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)), + None, + "suggestion must not have overlapping parts", + ); + + Substitution { parts } + }) + .collect(); + + self.push_suggestion(CodeSuggestion { + substitutions, + msg: self.subdiagnostic_message_to_diagnostic_message(msg), + style: SuggestionStyle::ShowCode, + applicability, + }); + self + } + + with_fn! { with_span_suggestion_short, + /// Prints out a message with a suggested edit of the code. If the suggestion is presented + /// inline, it will only show the message and not the suggestion. + /// + /// See `CodeSuggestion` for more information. + #[rustc_lint_diagnostics] + pub fn span_suggestion_short( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestion_with_style( + sp, + msg, + suggestion, + applicability, + SuggestionStyle::HideCodeInline, + ); + self + } } + + /// Prints out a message for a suggestion without showing the suggested code. + /// + /// This is intended to be used for suggestions that are obvious in what the changes need to + /// be from the message, showing the span label inline would be visually unpleasant + /// (marginally overlapping spans or multiline spans) and showing the snippet window wouldn't + /// improve understandability. + #[rustc_lint_diagnostics] + pub fn span_suggestion_hidden( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestion_with_style( + sp, + msg, + suggestion, + applicability, + SuggestionStyle::HideCodeAlways, + ); + self + } + + with_fn! { with_tool_only_span_suggestion, + /// Adds a suggestion to the JSON output that will not be shown in the CLI. + /// + /// This is intended to be used for suggestions that are *very* obvious in what the changes + /// need to be from the message, but we still want other tools to be able to apply them. + #[rustc_lint_diagnostics] + pub fn tool_only_span_suggestion( + &mut self, + sp: Span, + msg: impl Into<SubdiagMessage>, + suggestion: impl ToString, + applicability: Applicability, + ) -> &mut Self { + self.span_suggestion_with_style( + sp, + msg, + suggestion, + applicability, + SuggestionStyle::CompletelyHidden, + ); + self + } } + + /// Add a subdiagnostic from a type that implements `Subdiagnostic` (see + /// [rustc_macros::Subdiagnostic]). Performs eager translation of any translatable messages + /// used in the subdiagnostic, so suitable for use with repeated messages (i.e. re-use of + /// interpolated variables). + #[rustc_lint_diagnostics] + pub fn subdiagnostic(&mut self, subdiagnostic: impl Subdiagnostic) -> &mut Self { + let dcx = self.dcx; + subdiagnostic.add_to_diag_with(self, &|diag, msg| { + let args = diag.args.iter(); + let msg = diag.subdiagnostic_message_to_diagnostic_message(msg); + dcx.eagerly_translate(msg, args) + }); + self + } + + with_fn! { with_span, + /// Add a span. + #[rustc_lint_diagnostics] + pub fn span(&mut self, sp: impl Into<MultiSpan>) -> &mut Self { + self.span = sp.into(); + if let Some(span) = self.span.primary_span() { + self.sort_span = span; + } + self + } } + + #[rustc_lint_diagnostics] + pub fn is_lint(&mut self, name: String, has_future_breakage: bool) -> &mut Self { + self.is_lint = Some(IsLint { name, has_future_breakage }); + self + } + + with_fn! { with_code, + /// Add an error code. + #[rustc_lint_diagnostics] + pub fn code(&mut self, code: ErrCode) -> &mut Self { + self.code = Some(code); + self + } } + + with_fn! { with_primary_message, + /// Add a primary message. + #[rustc_lint_diagnostics] + pub fn primary_message(&mut self, msg: impl Into<DiagMessage>) -> &mut Self { + self.messages[0] = (msg.into(), Style::NoStyle); + self + } } + + with_fn! { with_arg, + /// Add an argument. + #[rustc_lint_diagnostics] + pub fn arg( + &mut self, + name: impl Into<DiagArgName>, + arg: impl IntoDiagArg, + ) -> &mut Self { + self.deref_mut().arg(name, arg); + self + } } + + /// Helper function that takes a `SubdiagMessage` and returns a `DiagMessage` by + /// combining it with the primary message of the diagnostic (if translatable, otherwise it just + /// passes the user's string along). + pub(crate) fn subdiagnostic_message_to_diagnostic_message( + &self, + attr: impl Into<SubdiagMessage>, + ) -> DiagMessage { + self.deref().subdiagnostic_message_to_diagnostic_message(attr) + } + + /// Convenience function for internal use, clients should use one of the + /// public methods above. + /// + /// Used by `proc_macro_server` for implementing `server::Diagnostic`. + pub fn sub(&mut self, level: Level, message: impl Into<SubdiagMessage>, span: MultiSpan) { + self.deref_mut().sub(level, message, span); + } + + /// Convenience function for internal use, clients should use one of the + /// public methods above. + fn sub_with_highlights(&mut self, level: Level, messages: Vec<StringPart>, span: MultiSpan) { + let messages = messages + .into_iter() + .map(|m| (self.subdiagnostic_message_to_diagnostic_message(m.content), m.style)) + .collect(); + let sub = Subdiag { level, messages, span }; + self.children.push(sub); + } + + /// Takes the diagnostic. For use by methods that consume the Diag: `emit`, + /// `cancel`, etc. Afterwards, `drop` is the only code that will be run on + /// `self`. + fn take_diag(&mut self) -> DiagInner { + if let Some(path) = &self.long_ty_path { + self.note(format!( + "the full name for the type has been written to '{}'", + path.display() + )); + self.note("consider using `--verbose` to print the full type name to the console"); + } + Box::into_inner(self.diag.take().unwrap()) + } + + /// This method allows us to access the path of the file where "long types" are written to. + /// + /// When calling `Diag::emit`, as part of that we will check if a `long_ty_path` has been set, + /// and if it has been then we add a note mentioning the file where the "long types" were + /// written to. + /// + /// When calling `tcx.short_string()` after a `Diag` is constructed, the preferred way of doing + /// so is `tcx.short_string(ty, diag.long_ty_path())`. The diagnostic itself is the one that + /// keeps the existence of a "long type" anywhere in the diagnostic, so the note telling the + /// user where we wrote the file to is only printed once at most, *and* it makes it much harder + /// to forget to set it. + /// + /// If the diagnostic hasn't been created before a "short ty string" is created, then you should + /// ensure that this method is called to set it `*diag.long_ty_path() = path`. + /// + /// As a rule of thumb, if you see or add at least one `tcx.short_string()` call anywhere, in a + /// scope, `diag.long_ty_path()` should be called once somewhere close by. + pub fn long_ty_path(&mut self) -> &mut Option<PathBuf> { + &mut self.long_ty_path + } + + /// Most `emit_producing_guarantee` functions use this as a starting point. + fn emit_producing_nothing(mut self) { + let diag = self.take_diag(); + self.dcx.emit_diagnostic(diag); + } + + /// `ErrorGuaranteed::emit_producing_guarantee` uses this. + fn emit_producing_error_guaranteed(mut self) -> ErrorGuaranteed { + let diag = self.take_diag(); + + // The only error levels that produce `ErrorGuaranteed` are + // `Error` and `DelayedBug`. But `DelayedBug` should never occur here + // because delayed bugs have their level changed to `Bug` when they are + // actually printed, so they produce an ICE. + // + // (Also, even though `level` isn't `pub`, the whole `DiagInner` could + // be overwritten with a new one thanks to `DerefMut`. So this assert + // protects against that, too.) + assert!( + matches!(diag.level, Level::Error | Level::DelayedBug), + "invalid diagnostic level ({:?})", + diag.level, + ); + + let guar = self.dcx.emit_diagnostic(diag); + guar.unwrap() + } + + /// Emit and consume the diagnostic. + #[track_caller] + pub fn emit(self) -> G::EmitResult { + G::emit_producing_guarantee(self) + } + + /// Emit the diagnostic unless `delay` is true, + /// in which case the emission will be delayed as a bug. + /// + /// See `emit` and `delay_as_bug` for details. + #[track_caller] + pub fn emit_unless(mut self, delay: bool) -> G::EmitResult { + if delay { + self.downgrade_to_delayed_bug(); + } + self.emit() + } + + /// Cancel and consume the diagnostic. (A diagnostic must either be emitted or + /// cancelled or it will panic when dropped). + pub fn cancel(mut self) { + self.diag = None; + drop(self); + } + + /// See `DiagCtxt::stash_diagnostic` for details. + pub fn stash(mut self, span: Span, key: StashKey) -> Option<ErrorGuaranteed> { + let diag = self.take_diag(); + self.dcx.stash_diagnostic(span, key, diag) + } + + /// Delay emission of this diagnostic as a bug. + /// + /// This can be useful in contexts where an error indicates a bug but + /// typically this only happens when other compilation errors have already + /// happened. In those cases this can be used to defer emission of this + /// diagnostic as a bug in the compiler only if no other errors have been + /// emitted. + /// + /// In the meantime, though, callsites are required to deal with the "bug" + /// locally in whichever way makes the most sense. + #[track_caller] + pub fn delay_as_bug(mut self) -> G::EmitResult { + self.downgrade_to_delayed_bug(); + self.emit() + } +} + +/// Destructor bomb: every `Diag` must be consumed (emitted, cancelled, etc.) +/// or we emit a bug. +impl<G: EmissionGuarantee> Drop for Diag<'_, G> { + fn drop(&mut self) { + match self.diag.take() { + Some(diag) if !panicking() => { + self.dcx.emit_diagnostic(DiagInner::new( + Level::Bug, + DiagMessage::from("the following error was constructed but not emitted"), + )); + self.dcx.emit_diagnostic(*diag); + panic!("error was constructed but not emitted"); + } + _ => {} + } + } +} + +#[macro_export] +macro_rules! struct_span_code_err { + ($dcx:expr, $span:expr, $code:expr, $($message:tt)*) => ({ + $dcx.struct_span_err($span, format!($($message)*)).with_code($code) + }) +} diff --git a/compiler/rustc_errors/src/diagnostic_impls.rs b/compiler/rustc_errors/src/diagnostic_impls.rs new file mode 100644 index 00000000000..d179396398f --- /dev/null +++ b/compiler/rustc_errors/src/diagnostic_impls.rs @@ -0,0 +1,411 @@ +use std::backtrace::Backtrace; +use std::borrow::Cow; +use std::fmt; +use std::num::ParseIntError; +use std::path::{Path, PathBuf}; +use std::process::ExitStatus; + +use rustc_abi::TargetDataLayoutErrors; +use rustc_ast::util::parser::ExprPrecedence; +use rustc_ast_pretty::pprust; +use rustc_macros::Subdiagnostic; +use rustc_span::edition::Edition; +use rustc_span::{Ident, MacroRulesNormalizedIdent, Span, Symbol}; +use rustc_target::spec::{PanicStrategy, SplitDebuginfo, StackProtector, TargetTuple}; +use rustc_type_ir::{ClosureKind, FloatTy}; +use {rustc_ast as ast, rustc_hir as hir}; + +use crate::diagnostic::DiagLocation; +use crate::{ + Diag, DiagArgValue, DiagCtxtHandle, Diagnostic, EmissionGuarantee, ErrCode, IntoDiagArg, Level, + SubdiagMessageOp, Subdiagnostic, fluent_generated as fluent, +}; + +pub struct DiagArgFromDisplay<'a>(pub &'a dyn fmt::Display); + +impl IntoDiagArg for DiagArgFromDisplay<'_> { + fn into_diag_arg(self) -> DiagArgValue { + self.0.to_string().into_diag_arg() + } +} + +impl<'a> From<&'a dyn fmt::Display> for DiagArgFromDisplay<'a> { + fn from(t: &'a dyn fmt::Display) -> Self { + DiagArgFromDisplay(t) + } +} + +impl<'a, T: fmt::Display> From<&'a T> for DiagArgFromDisplay<'a> { + fn from(t: &'a T) -> Self { + DiagArgFromDisplay(t) + } +} + +impl<'a, T: Clone + IntoDiagArg> IntoDiagArg for &'a T { + fn into_diag_arg(self) -> DiagArgValue { + self.clone().into_diag_arg() + } +} + +#[macro_export] +macro_rules! into_diag_arg_using_display { + ($( $ty:ty ),+ $(,)?) => { + $( + impl IntoDiagArg for $ty { + fn into_diag_arg(self) -> DiagArgValue { + self.to_string().into_diag_arg() + } + } + )+ + } +} + +macro_rules! into_diag_arg_for_number { + ($( $ty:ty ),+ $(,)?) => { + $( + impl IntoDiagArg for $ty { + fn into_diag_arg(self) -> DiagArgValue { + // Convert to a string if it won't fit into `Number`. + #[allow(irrefutable_let_patterns)] + if let Ok(n) = TryInto::<i32>::try_into(self) { + DiagArgValue::Number(n) + } else { + self.to_string().into_diag_arg() + } + } + } + )+ + } +} + +into_diag_arg_using_display!( + ast::ParamKindOrd, + std::io::Error, + Box<dyn std::error::Error>, + std::num::NonZero<u32>, + hir::Target, + Edition, + Ident, + MacroRulesNormalizedIdent, + ParseIntError, + StackProtector, + &TargetTuple, + SplitDebuginfo, + ExitStatus, + ErrCode, +); + +impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::TraitRef<I> { + fn into_diag_arg(self) -> DiagArgValue { + self.to_string().into_diag_arg() + } +} + +impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::ExistentialTraitRef<I> { + fn into_diag_arg(self) -> DiagArgValue { + self.to_string().into_diag_arg() + } +} + +impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::UnevaluatedConst<I> { + fn into_diag_arg(self) -> rustc_errors::DiagArgValue { + format!("{self:?}").into_diag_arg() + } +} + +impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::FnSig<I> { + fn into_diag_arg(self) -> rustc_errors::DiagArgValue { + format!("{self:?}").into_diag_arg() + } +} + +impl<I: rustc_type_ir::Interner, T> IntoDiagArg for rustc_type_ir::Binder<I, T> +where + T: IntoDiagArg, +{ + fn into_diag_arg(self) -> DiagArgValue { + self.skip_binder().into_diag_arg() + } +} + +into_diag_arg_for_number!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize); + +impl IntoDiagArg for bool { + fn into_diag_arg(self) -> DiagArgValue { + if self { + DiagArgValue::Str(Cow::Borrowed("true")) + } else { + DiagArgValue::Str(Cow::Borrowed("false")) + } + } +} + +impl IntoDiagArg for char { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(format!("{self:?}"))) + } +} + +impl IntoDiagArg for Vec<char> { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::StrListSepByAnd( + self.into_iter().map(|c| Cow::Owned(format!("{c:?}"))).collect(), + ) + } +} + +impl IntoDiagArg for Symbol { + fn into_diag_arg(self) -> DiagArgValue { + self.to_ident_string().into_diag_arg() + } +} + +impl<'a> IntoDiagArg for &'a str { + fn into_diag_arg(self) -> DiagArgValue { + self.to_string().into_diag_arg() + } +} + +impl IntoDiagArg for String { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self)) + } +} + +impl<'a> IntoDiagArg for Cow<'a, str> { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.into_owned())) + } +} + +impl<'a> IntoDiagArg for &'a Path { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.display().to_string())) + } +} + +impl IntoDiagArg for PathBuf { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.display().to_string())) + } +} + +impl IntoDiagArg for PanicStrategy { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.desc().to_string())) + } +} + +impl IntoDiagArg for hir::ConstContext { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Borrowed(match self { + hir::ConstContext::ConstFn => "const_fn", + hir::ConstContext::Static(_) => "static", + hir::ConstContext::Const { .. } => "const", + })) + } +} + +impl IntoDiagArg for ast::Expr { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(pprust::expr_to_string(&self))) + } +} + +impl IntoDiagArg for ast::Path { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(pprust::path_to_string(&self))) + } +} + +impl IntoDiagArg for ast::token::Token { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(pprust::token_to_string(&self)) + } +} + +impl IntoDiagArg for ast::token::TokenKind { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(pprust::token_kind_to_string(&self)) + } +} + +impl IntoDiagArg for FloatTy { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Borrowed(self.name_str())) + } +} + +impl IntoDiagArg for std::ffi::CString { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned())) + } +} + +impl IntoDiagArg for rustc_data_structures::small_c_str::SmallCStr { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned())) + } +} + +impl IntoDiagArg for ast::Visibility { + fn into_diag_arg(self) -> DiagArgValue { + let s = pprust::vis_to_string(&self); + let s = s.trim_end().to_string(); + DiagArgValue::Str(Cow::Owned(s)) + } +} + +impl IntoDiagArg for rustc_lint_defs::Level { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Borrowed(self.to_cmd_flag())) + } +} + +impl<Id> IntoDiagArg for hir::def::Res<Id> { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Borrowed(self.descr())) + } +} + +impl IntoDiagArg for DiagLocation { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::from(self.to_string())) + } +} + +impl IntoDiagArg for Backtrace { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::from(self.to_string())) + } +} + +impl IntoDiagArg for Level { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::from(self.to_string())) + } +} + +impl IntoDiagArg for ClosureKind { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(self.as_str().into()) + } +} + +impl IntoDiagArg for hir::def::Namespace { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Str(Cow::Borrowed(self.descr())) + } +} + +impl IntoDiagArg for ExprPrecedence { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::Number(self as i32) + } +} + +#[derive(Clone)] +pub struct DiagSymbolList<S = Symbol>(Vec<S>); + +impl<S> From<Vec<S>> for DiagSymbolList<S> { + fn from(v: Vec<S>) -> Self { + DiagSymbolList(v) + } +} + +impl<S> FromIterator<S> for DiagSymbolList<S> { + fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self { + iter.into_iter().collect::<Vec<_>>().into() + } +} + +impl<S: std::fmt::Display> IntoDiagArg for DiagSymbolList<S> { + fn into_diag_arg(self) -> DiagArgValue { + DiagArgValue::StrListSepByAnd( + self.0.into_iter().map(|sym| Cow::Owned(format!("`{sym}`"))).collect(), + ) + } +} + +impl<G: EmissionGuarantee> Diagnostic<'_, G> for TargetDataLayoutErrors<'_> { + fn into_diag(self, dcx: DiagCtxtHandle<'_>, level: Level) -> Diag<'_, G> { + match self { + TargetDataLayoutErrors::InvalidAddressSpace { addr_space, err, cause } => { + Diag::new(dcx, level, fluent::errors_target_invalid_address_space) + .with_arg("addr_space", addr_space) + .with_arg("cause", cause) + .with_arg("err", err) + } + TargetDataLayoutErrors::InvalidBits { kind, bit, cause, err } => { + Diag::new(dcx, level, fluent::errors_target_invalid_bits) + .with_arg("kind", kind) + .with_arg("bit", bit) + .with_arg("cause", cause) + .with_arg("err", err) + } + TargetDataLayoutErrors::MissingAlignment { cause } => { + Diag::new(dcx, level, fluent::errors_target_missing_alignment) + .with_arg("cause", cause) + } + TargetDataLayoutErrors::InvalidAlignment { cause, err } => { + Diag::new(dcx, level, fluent::errors_target_invalid_alignment) + .with_arg("cause", cause) + .with_arg("err_kind", err.diag_ident()) + .with_arg("align", err.align()) + } + TargetDataLayoutErrors::InconsistentTargetArchitecture { dl, target } => { + Diag::new(dcx, level, fluent::errors_target_inconsistent_architecture) + .with_arg("dl", dl) + .with_arg("target", target) + } + TargetDataLayoutErrors::InconsistentTargetPointerWidth { pointer_size, target } => { + Diag::new(dcx, level, fluent::errors_target_inconsistent_pointer_width) + .with_arg("pointer_size", pointer_size) + .with_arg("target", target) + } + TargetDataLayoutErrors::InvalidBitsSize { err } => { + Diag::new(dcx, level, fluent::errors_target_invalid_bits_size).with_arg("err", err) + } + } + } +} + +/// Utility struct used to apply a single label while highlighting multiple spans +pub struct SingleLabelManySpans { + pub spans: Vec<Span>, + pub label: &'static str, +} +impl Subdiagnostic for SingleLabelManySpans { + fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>( + self, + diag: &mut Diag<'_, G>, + _: &F, + ) { + diag.span_labels(self.spans, self.label); + } +} + +#[derive(Subdiagnostic)] +#[label(errors_expected_lifetime_parameter)] +pub struct ExpectedLifetimeParameter { + #[primary_span] + pub span: Span, + pub count: usize, +} + +#[derive(Subdiagnostic)] +#[suggestion(errors_indicate_anonymous_lifetime, code = "{suggestion}", style = "verbose")] +pub struct IndicateAnonymousLifetime { + #[primary_span] + pub span: Span, + pub count: usize, + pub suggestion: String, +} + +#[derive(Subdiagnostic)] +pub struct ElidedLifetimeInPathSubdiag { + #[subdiagnostic] + pub expected: ExpectedLifetimeParameter, + #[subdiagnostic] + pub indicate: Option<IndicateAnonymousLifetime>, +} diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs new file mode 100644 index 00000000000..15cf285e7ff --- /dev/null +++ b/compiler/rustc_errors/src/emitter.rs @@ -0,0 +1,3472 @@ +//! The current rustc diagnostics emitter. +//! +//! An `Emitter` takes care of generating the output from a `Diag` 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 std::borrow::Cow; +use std::cmp::{Reverse, max, min}; +use std::error::Report; +use std::io::prelude::*; +use std::io::{self, IsTerminal}; +use std::iter; +use std::path::Path; +use std::sync::Arc; + +use derive_setters::Setters; +use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet}; +use rustc_data_structures::sync::{DynSend, IntoDynSyncSend}; +use rustc_error_messages::{FluentArgs, SpanLabel}; +use rustc_lexer; +use rustc_lint_defs::pluralize; +use rustc_span::hygiene::{ExpnKind, MacroKind}; +use rustc_span::source_map::SourceMap; +use rustc_span::{FileLines, FileName, SourceFile, Span, char_width, str_width}; +use termcolor::{Buffer, BufferWriter, Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use tracing::{debug, instrument, trace, warn}; + +use crate::diagnostic::DiagLocation; +use crate::registry::Registry; +use crate::snippet::{ + Annotation, AnnotationColumn, AnnotationType, Line, MultilineAnnotation, Style, StyledString, +}; +use crate::styled_buffer::StyledBuffer; +use crate::translation::{Translate, to_fluent_args}; +use crate::{ + CodeSuggestion, DiagInner, DiagMessage, ErrCode, FluentBundle, LazyFallbackBundle, Level, + MultiSpan, Subdiag, SubstitutionHighlight, SuggestionStyle, TerminalUrl, +}; + +/// 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, + Unicode, + AnnotateSnippet, + Short, +} + +impl HumanReadableErrorType { + pub fn short(&self) -> bool { + *self == HumanReadableErrorType::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 leftmost span. + pub span_left: usize, + /// The column of the end of rightmost 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 { + // FIXME: This comment refers to the only callsite of this method. + // Rephrase it or refactor it, so it can stand on its own. + // Account for the "..." padding given above. Otherwise we end up with code lines + // that do fit but end in "..." as if they were trimmed. + // FIXME: Don't hard-code this offset. Is this meant to represent + // `2 * str_width(self.margin())`? + 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 rightmost 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"; + +pub type DynEmitter = dyn Emitter + DynSend; + +/// Emitter trait for emitting errors. +pub trait Emitter: Translate { + /// Emit a structured diagnostic. + fn emit_diagnostic(&mut self, diag: DiagInner, registry: &Registry); + + /// Emit a notification that an artifact has been output. + /// Currently only supported for the JSON format. + fn emit_artifact_notification(&mut self, _path: &Path, _artifact_type: &str) {} + + /// Emit a report about future breakage. + /// Currently only supported for the JSON format. + fn emit_future_breakage_report(&mut self, _diags: Vec<DiagInner>, _registry: &Registry) {} + + /// Emit list of unused externs. + /// Currently only supported for the JSON format. + 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<&SourceMap>; + + /// Formats the substitutions of the primary_span + /// + /// There are a lot of conditions to this method, but in short: + /// + /// * If the current `DiagInner` has only one visible `CodeSuggestion`, + /// we format the `help` suggestion depending on the content of the + /// substitutions. In that case, we modify the span and clear the + /// suggestions. + /// + /// * If the current `DiagInner` has multiple suggestions, + /// we leave `primary_span` and the suggestions untouched. + fn primary_span_formatted( + &mut self, + primary_span: &mut MultiSpan, + suggestions: &mut Vec<CodeSuggestion>, + fluent_args: &FluentArgs<'_>, + ) { + 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 + && let [substitution] = sugg.substitutions.as_slice() + // don't display multipart suggestions as labels + && let [part] = substitution.parts.as_slice() + // don't display long messages as labels + && msg.split_whitespace().count() < 10 + // don't display multiline suggestions as labels + && !part.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 snippet = part.snippet.trim(); + let msg = if snippet.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, snippet, part.span,)) + { + " (notice the capitalization)" + } else { + "" + }, + snippet, + ) + }; + primary_span.push_span_label(part.span, msg); + + // We return only the modified primary_span + suggestions.clear(); + } 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 + } + } else { + // do nothing + } + } + + fn fix_multispans_in_extern_macros_and_render_macro_backtrace( + &self, + span: &mut MultiSpan, + children: &mut Vec<Subdiag>, + 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(Subdiag { + level: Level::Note, + messages: vec![(DiagMessage::from(msg), Style::NoStyle)], + span: MultiSpan::new(), + }); + } + } + } + + fn render_multispans_macro_backtrace( + &self, + span: &mut MultiSpan, + children: &mut Vec<Subdiag>, + 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 = FxIndexSet::default(); + + 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.insert(( + 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.insert(( + 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<Subdiag>) { + 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 HumanEmitter { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + self.fluent_bundle.as_deref() + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &self.fallback_bundle + } +} + +impl Emitter for HumanEmitter { + fn source_map(&self) -> Option<&SourceMap> { + self.sm.as_deref() + } + + fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) { + let fluent_args = to_fluent_args(diag.args.iter()); + + let mut suggestions = diag.suggestions.unwrap_tag(); + self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args); + + self.fix_multispans_in_extern_macros_and_render_macro_backtrace( + &mut diag.span, + &mut diag.children, + &diag.level, + self.macro_backtrace, + ); + + self.emit_messages_default( + &diag.level, + &diag.messages, + &fluent_args, + &diag.code, + &diag.span, + &diag.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_emitter` to avoid silent +/// failures of rustc, as witnessed e.g. in issue #89358. +pub struct SilentEmitter { + pub fatal_emitter: Box<dyn Emitter + DynSend>, + pub fatal_note: Option<String>, + pub emit_fatal_diagnostic: bool, +} + +impl Translate for SilentEmitter { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + None + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + self.fatal_emitter.fallback_fluent_bundle() + } +} + +impl Emitter for SilentEmitter { + fn source_map(&self) -> Option<&SourceMap> { + None + } + + fn emit_diagnostic(&mut self, mut diag: DiagInner, registry: &Registry) { + if self.emit_fatal_diagnostic && diag.level == Level::Fatal { + if let Some(fatal_note) = &self.fatal_note { + diag.sub(Level::Note, fatal_note.clone(), MultiSpan::new()); + } + self.fatal_emitter.emit_diagnostic(diag, registry); + } + } +} + +/// 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, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputTheme { + Ascii, + Unicode, +} + +/// Handles the writing of `HumanReadableErrorType::Default` and `HumanReadableErrorType::Short` +#[derive(Setters)] +pub struct HumanEmitter { + #[setters(skip)] + dst: IntoDynSyncSend<Destination>, + sm: Option<Arc<SourceMap>>, + fluent_bundle: Option<Arc<FluentBundle>>, + #[setters(skip)] + fallback_bundle: LazyFallbackBundle, + short_message: bool, + teach: bool, + ui_testing: bool, + ignored_directories_in_source_blocks: Vec<String>, + diagnostic_width: Option<usize>, + + macro_backtrace: bool, + track_diagnostics: bool, + terminal_url: TerminalUrl, + theme: OutputTheme, +} + +#[derive(Debug)] +pub(crate) struct FileWithAnnotatedLines { + pub(crate) file: Arc<SourceFile>, + pub(crate) lines: Vec<Line>, + multiline_depth: usize, +} + +impl HumanEmitter { + pub fn new(dst: Destination, fallback_bundle: LazyFallbackBundle) -> HumanEmitter { + HumanEmitter { + dst: IntoDynSyncSend(dst), + sm: None, + fluent_bundle: None, + fallback_bundle, + short_message: false, + teach: 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 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); + // FIXME: The following code looks fishy. See #132860. + // 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. + let next = char_width(*ch); + if taken + next > right - left { + return false; + } + taken += next; + true + }) + .collect(); + buffer.puts(line_offset, code_offset, &code, Style::Quotation); + let placeholder = self.margin(); + if margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + buffer.puts(line_offset, code_offset, placeholder, Style::LineNumber); + } + if margin.was_cut_right(line_len) { + let padding = str_width(placeholder); + // We have stripped some code after the rightmost span end, make it clear we did so. + buffer.puts(line_offset, code_offset + taken - padding, placeholder, Style::LineNumber); + } + buffer.puts(line_offset, 0, &self.maybe_anonymized(line_index), Style::LineNumber); + + self.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: Arc<SourceFile>, + line: &Line, + width_offset: usize, + code_offset: usize, + margin: Margin, + close_window: bool, + ) -> 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()); + + // FIXME: This looks fishy. See #132860. + // Account for unicode characters of width !=0 that were removed. + let left = source_string.chars().take(left).map(|ch| char_width(ch)).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 uline = self.underline(ann.is_primary); + let chr = uline.multiline_whole_line; + annotations.push((depth, uline.style)); + buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.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 + // <EMPTY LINE> + // + let mut annotations_position = vec![]; + let mut line_len: usize = 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![]; + } + + if annotations_position + .iter() + .all(|(_, ann)| matches!(ann.annotation_type, AnnotationType::MultilineStart(_))) + && let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() + { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multiline spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_position { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + + // Write the column separator. + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + for pos in 0..=line_len { + self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2); + } + if close_window { + self.draw_col_separator_end(buffer, line_offset + line_len + 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 underline = self.underline(annotation.is_primary); + let pos = pos + 1; + match annotation.annotation_type { + AnnotationType::MultilineStart(depth) | AnnotationType::MultilineEnd(depth) => { + self.draw_range( + buffer, + underline.multiline_horizontal, + line_offset + pos, + width_offset + depth, + (code_offset + annotation.start_col.display).saturating_sub(left), + underline.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), + underline.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 underline = self.underline(annotation.is_primary); + 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), + match annotation.annotation_type { + AnnotationType::MultilineLine(_) => underline.multiline_vertical, + _ => underline.vertical_text_line, + }, + underline.style, + ); + } + if let AnnotationType::MultilineStart(_) = annotation.annotation_type { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start_col.display).saturating_sub(left), + underline.bottom_right, + underline.style, + ); + } + if let AnnotationType::MultilineEnd(_) = annotation.annotation_type + && annotation.has_label() + { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start_col.display).saturating_sub(left), + underline.multiline_bottom_right_with_text, + underline.style, + ); + } + } + match annotation.annotation_type { + AnnotationType::MultilineStart(depth) => { + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.top_left, + underline.style, + ); + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + } + AnnotationType::MultilineEnd(depth) => { + for p in line_offset..line_offset + pos { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.bottom_left, + underline.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 { + if annotation.end_col.display == 0 { + (pos + 1, (annotation.end_col.display + 2).saturating_sub(left)) + } else { + (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 &(pos, annotation) in &annotations_position { + let uline = self.underline(annotation.is_primary); + for p in annotation.start_col.display..annotation.end_col.display { + // The default span label underline. + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + uline.underline, + uline.style, + ); + } + + if pos == 0 + && matches!( + annotation.annotation_type, + AnnotationType::MultilineStart(_) | AnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on the same line. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start_col.display).saturating_sub(left), + match annotation.annotation_type { + AnnotationType::MultilineStart(_) => uline.top_right_flat, + AnnotationType::MultilineEnd(_) => uline.multiline_end_same_line, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 + && matches!( + annotation.annotation_type, + AnnotationType::MultilineStart(_) | AnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on another line, + // so we start going down first. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start_col.display).saturating_sub(left), + match annotation.annotation_type { + AnnotationType::MultilineStart(_) => uline.multiline_start_down, + AnnotationType::MultilineEnd(_) => uline.multiline_end_up, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 && annotation.has_label() { + // The beginning of a span label with an actual label, we'll point down. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start_col.display).saturating_sub(left), + uline.label_start, + uline.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::<Vec<_>>() + } + + 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()); + should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &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: &[Subdiag]) -> 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 msgs_to_buffer( + &self, + buffer: &mut StyledBuffer, + msgs: &[(DiagMessage, Style)], + args: &FluentArgs<'_>, + padding: usize, + label: &str, + override_style: Option<Style>, + ) -> usize { + // The extra 5 ` ` is padding that's always needed to align to the `note: `: + // + // error: message + // --> file.rs:13:20 + // | + // 13 | <CODE> + // | ^^^^ + // | + // = note: multiline + // message + // ++^^^----xx + // | | | | + // | | | magic `2` + // | | length of label + // | magic `3` + // `max_line_num_len` + let padding = " ".repeat(padding + label.len() + 5); + + /// Returns `override` if it is present and `style` is `NoStyle` or `style` otherwise + fn style_or_override(style: Style, override_: Option<Style>) -> Style { + match (style, override_) { + (Style::NoStyle, Some(override_)) => override_, + _ => style, + } + } + + let mut line_number = 0; + + // Provided the following diagnostic message: + // + // let msgs = vec![ + // (" + // ("highlighted multiline\nstring to\nsee how it ", Style::NoStyle), + // ("looks", Style::Highlight), + // ("with\nvery ", Style::NoStyle), + // ("weird", Style::Highlight), + // (" formats\n", Style::NoStyle), + // ("see?", Style::Highlight), + // ]; + // + // the expected output on a note is (* surround the highlighted text) + // + // = note: highlighted multiline + // string to + // see how it *looks* with + // very *weird* formats + // see? + for (text, style) in msgs.iter() { + let text = self.translate_message(text, args).map_err(Report::new).unwrap(); + let text = &normalize_whitespace(&text); + let lines = text.split('\n').collect::<Vec<_>>(); + if lines.len() > 1 { + for (i, line) in lines.iter().enumerate() { + if i != 0 { + line_number += 1; + buffer.append(line_number, &padding, Style::NoStyle); + } + buffer.append(line_number, line, style_or_override(*style, override_style)); + } + } else { + buffer.append(line_number, text, style_or_override(*style, override_style)); + } + } + line_number + } + + #[instrument(level = "trace", skip(self, args), ret)] + fn emit_messages_default_inner( + &mut self, + msp: &MultiSpan, + msgs: &[(DiagMessage, Style)], + args: &FluentArgs<'_>, + code: &Option<ErrCode>, + level: &Level, + max_line_num_len: usize, + is_secondary: bool, + emitted_at: Option<&DiagLocation>, + is_cont: bool, + ) -> io::Result<()> { + let mut buffer = StyledBuffer::new(); + + if !msp.has_primary_spans() && !msp.has_span_labels() && is_secondary && !self.short_message + { + // This is a secondary message with no span info + for _ in 0..max_line_num_len { + buffer.prepend(0, " ", Style::NoStyle); + } + self.draw_note_separator(&mut buffer, 0, max_line_num_len + 1, is_cont); + if *level != Level::FailureNote { + buffer.append(0, level.to_str(), Style::MainHeaderMsg); + buffer.append(0, ": ", Style::NoStyle); + } + let printed_lines = + self.msgs_to_buffer(&mut buffer, msgs, args, max_line_num_len, "note", None); + if is_cont && matches!(self.theme, OutputTheme::Unicode) { + // There's another note after this one, associated to the subwindow above. + // We write additional vertical lines to join them: + // ╭▸ test.rs:3:3 + // │ + // 3 │ code + // │ ━━━━ + // │ + // ├ note: foo + // │ bar + // ╰ note: foo + // bar + for i in 1..=printed_lines { + self.draw_col_separator_no_space(&mut buffer, i, max_line_num_len + 1); + } + } + } else { + let mut label_width = 0; + // The failure note level itself does not provide any useful diagnostic information + if *level != Level::FailureNote { + buffer.append(0, level.to_str(), Style::Level(*level)); + label_width += level.to_str().len(); + } + if let Some(code) = code { + buffer.append(0, "[", Style::Level(*level)); + let code = if let TerminalUrl::Yes = self.terminal_url { + let path = "https://doc.rust-lang.org/error_codes"; + format!("\x1b]8;;{path}/{code}.html\x07{code}\x1b]8;;\x07") + } else { + code.to_string() + }; + buffer.append(0, &code, Style::Level(*level)); + buffer.append(0, "]", Style::Level(*level)); + label_width += 2 + code.len(); + } + let header_style = if is_secondary { + Style::HeaderMsg + } else if self.short_message { + // For short messages avoid bolding the message, as it doesn't look great (#63835). + Style::NoStyle + } else { + Style::MainHeaderMsg + }; + if *level != Level::FailureNote { + buffer.append(0, ": ", header_style); + label_width += 2; + } + let mut line = 0; + for (text, style) in msgs.iter() { + let text = self.translate_message(text, args).map_err(Report::new).unwrap(); + // Account for newlines to align output to its label. + for text in normalize_whitespace(&text).lines() { + buffer.append( + line, + &format!( + "{}{}", + if line == 0 { String::new() } else { " ".repeat(label_width) }, + text + ), + match style { + Style::Highlight => *style, + _ => header_style, + }, + ); + line += 1; + } + // We add lines above, but if the last line has no explicit newline (which would + // yield an empty line), then we revert one line up to continue with the next + // styled text chunk on the same line as the last one from the prior one. Otherwise + // every `text` would appear on their own line (because even though they didn't end + // in '\n', they advanced `line` by one). + if line > 0 { + line -= 1; + } + } + if self.short_message { + let labels = msp + .span_labels() + .into_iter() + .filter_map(|label| match label.label { + Some(msg) if label.is_primary => { + let text = self.translate_message(&msg, args).ok()?; + if !text.trim().is_empty() { Some(text.to_string()) } else { None } + } + _ => None, + }) + .collect::<Vec<_>>() + .join(", "); + if !labels.is_empty() { + buffer.append(line, ": ", Style::NoStyle); + buffer.append(line, &labels, Style::NoStyle); + } + } + } + let mut annotated_files = FileWithAnnotatedLines::collect_annotations(self, args, msp); + trace!("{annotated_files:#?}"); + + // Make sure our primary file comes first + let primary_span = msp.primary_span().unwrap_or_default(); + let (Some(sm), false) = (self.sm.as_ref(), primary_span.is_dummy()) else { + // If we don't have span information, emit and exit + return emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message); + }; + let primary_lo = sm.lookup_char_pos(primary_span.lo()); + if let Ok(pos) = + annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name)) + { + annotated_files.swap(0, pos); + } + + // Print out the annotate source lines that correspond with the error + for annotated_file in annotated_files { + // we can't annotate anything if the source is unavailable. + if !should_show_source_code( + &self.ignored_directories_in_source_blocks, + sm, + &annotated_file.file, + ) { + if !self.short_message { + // We'll just print an unannotated message. + for (annotation_id, line) in annotated_file.lines.iter().enumerate() { + let mut annotations = line.annotations.clone(); + annotations.sort_by_key(|a| Reverse(a.start_col)); + let mut line_idx = buffer.num_lines(); + + let labels: Vec<_> = annotations + .iter() + .filter_map(|a| Some((a.label.as_ref()?, a.is_primary))) + .filter(|(l, _)| !l.is_empty()) + .collect(); + + if annotation_id == 0 || !labels.is_empty() { + buffer.append( + line_idx, + &format!( + "{}:{}:{}", + sm.filename_for_diagnostics(&annotated_file.file.name), + sm.doctest_offset_line( + &annotated_file.file.name, + line.line_index + ), + annotations[0].start_col.file + 1, + ), + Style::LineAndColumn, + ); + if annotation_id == 0 { + buffer.prepend(line_idx, self.file_start(), Style::LineNumber); + } else { + buffer.prepend( + line_idx, + self.secondary_file_start(), + Style::LineNumber, + ); + } + for _ in 0..max_line_num_len { + buffer.prepend(line_idx, " ", Style::NoStyle); + } + line_idx += 1; + } + for (label, is_primary) in labels.into_iter() { + let style = if is_primary { + Style::LabelPrimary + } else { + Style::LabelSecondary + }; + let pipe = self.col_separator(); + buffer.prepend(line_idx, &format!(" {pipe}"), Style::LineNumber); + for _ in 0..max_line_num_len { + buffer.prepend(line_idx, " ", Style::NoStyle); + } + line_idx += 1; + let chr = self.note_separator(); + buffer.append(line_idx, &format!(" {chr} note: "), style); + for _ in 0..max_line_num_len { + buffer.prepend(line_idx, " ", Style::NoStyle); + } + buffer.append(line_idx, label, style); + line_idx += 1; + } + } + } + continue; + } + + // print out the span location and spacer before we print the annotated source + // to do this, we need to know if this span will be primary + let is_primary = primary_lo.file.name == annotated_file.file.name; + if is_primary { + let loc = primary_lo.clone(); + if !self.short_message { + // remember where we are in the output buffer for easy reference + let buffer_msg_line_offset = buffer.num_lines(); + + buffer.prepend(buffer_msg_line_offset, self.file_start(), Style::LineNumber); + buffer.append( + buffer_msg_line_offset, + &format!( + "{}:{}:{}", + sm.filename_for_diagnostics(&loc.file.name), + sm.doctest_offset_line(&loc.file.name, loc.line), + loc.col.0 + 1, + ), + Style::LineAndColumn, + ); + for _ in 0..max_line_num_len { + buffer.prepend(buffer_msg_line_offset, " ", Style::NoStyle); + } + } else { + buffer.prepend( + 0, + &format!( + "{}:{}:{}: ", + sm.filename_for_diagnostics(&loc.file.name), + sm.doctest_offset_line(&loc.file.name, loc.line), + loc.col.0 + 1, + ), + Style::LineAndColumn, + ); + } + } else if !self.short_message { + // remember where we are in the output buffer for easy reference + let buffer_msg_line_offset = buffer.num_lines(); + + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + self.draw_col_separator_no_space( + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + + // Then, the secondary file indicator + buffer.prepend( + buffer_msg_line_offset + 1, + self.secondary_file_start(), + Style::LineNumber, + ); + let loc = if let Some(first_line) = annotated_file.lines.first() { + let col = if let Some(first_annotation) = first_line.annotations.first() { + format!(":{}", first_annotation.start_col.file + 1) + } else { + String::new() + }; + format!( + "{}:{}{}", + sm.filename_for_diagnostics(&annotated_file.file.name), + sm.doctest_offset_line(&annotated_file.file.name, first_line.line_index), + col + ) + } else { + format!("{}", sm.filename_for_diagnostics(&annotated_file.file.name)) + }; + buffer.append(buffer_msg_line_offset + 1, &loc, Style::LineAndColumn); + for _ in 0..max_line_num_len { + buffer.prepend(buffer_msg_line_offset + 1, " ", Style::NoStyle); + } + } + + if !self.short_message { + // Put in the spacer between the location and annotated source + let buffer_msg_line_offset = buffer.num_lines(); + self.draw_col_separator_no_space( + &mut buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + + // Contains the vertical lines' positions for active multiline annotations + let mut multilines = FxIndexMap::default(); + + // Get the left-side margin to remove it + let mut whitespace_margin = usize::MAX; + for line_idx in 0..annotated_file.lines.len() { + let file = Arc::clone(&annotated_file.file); + let line = &annotated_file.lines[line_idx]; + if let Some(source_string) = + line.line_index.checked_sub(1).and_then(|l| file.get_line(l)) + { + // Whitespace can only be removed (aka considered leading) + // if the lexer considers it whitespace. + // non-rustc_lexer::is_whitespace() chars are reported as an + // error (ex. no-break-spaces \u{a0}), and thus can't be considered + // for removal during error reporting. + let leading_whitespace = source_string + .chars() + .take_while(|c| rustc_lexer::is_whitespace(*c)) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if source_string.chars().any(|c| !rustc_lexer::is_whitespace(c)) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + } + } + if whitespace_margin == usize::MAX { + whitespace_margin = 0; + } + + // Left-most column any visible span points at. + let mut span_left_margin = usize::MAX; + for line in &annotated_file.lines { + for ann in &line.annotations { + span_left_margin = min(span_left_margin, ann.start_col.display); + span_left_margin = min(span_left_margin, ann.end_col.display); + } + } + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + // Right-most column any visible span points at. + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + for line in &annotated_file.lines { + max_line_len = max( + max_line_len, + line.line_index + .checked_sub(1) + .and_then(|l| annotated_file.file.get_line(l)) + .map_or(0, |s| s.len()), + ); + for ann in &line.annotations { + span_right_margin = max(span_right_margin, ann.start_col.display); + span_right_margin = max(span_right_margin, ann.end_col.display); + // FIXME: account for labels not in the same line + let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1); + label_right_margin = + max(label_right_margin, ann.end_col.display + label_right); + } + } + + let width_offset = 3 + max_line_num_len; + let code_offset = if annotated_file.multiline_depth == 0 { + width_offset + } else { + width_offset + annotated_file.multiline_depth + 1 + }; + + let column_width = if let Some(width) = self.diagnostic_width { + width.saturating_sub(code_offset) + } else if self.ui_testing || cfg!(miri) { + DEFAULT_COLUMN_WIDTH + } else { + termize::dimensions() + .map(|(w, _)| w.saturating_sub(code_offset)) + .unwrap_or(DEFAULT_COLUMN_WIDTH) + }; + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + column_width, + max_line_len, + ); + + // Next, output the annotate source for this file + for line_idx in 0..annotated_file.lines.len() { + let previous_buffer_line = buffer.num_lines(); + + let depths = self.render_source_line( + &mut buffer, + Arc::clone(&annotated_file.file), + &annotated_file.lines[line_idx], + width_offset, + code_offset, + margin, + !is_cont && line_idx + 1 == annotated_file.lines.len(), + ); + + let mut to_add = FxHashMap::default(); + + for (depth, style) in depths { + // FIXME(#120456) - is `swap_remove` correct? + if multilines.swap_remove(&depth).is_none() { + to_add.insert(depth, style); + } + } + + // Set the multiline annotation vertical lines to the left of + // the code in this line. + for (depth, style) in &multilines { + for line in previous_buffer_line..buffer.num_lines() { + self.draw_multiline_line( + &mut buffer, + line, + width_offset, + *depth, + *style, + ); + } + } + // check to see if we need to print out or elide lines that come between + // this annotated line and the next one. + if line_idx < (annotated_file.lines.len() - 1) { + let line_idx_delta = annotated_file.lines[line_idx + 1].line_index + - annotated_file.lines[line_idx].line_index; + if line_idx_delta > 2 { + let last_buffer_line_num = buffer.num_lines(); + self.draw_line_separator( + &mut buffer, + last_buffer_line_num, + width_offset, + ); + + // Set the multiline annotation vertical lines on `...` bridging line. + for (depth, style) in &multilines { + self.draw_multiline_line( + &mut buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_file.lines.get(line_idx) { + for ann in &line.annotations { + if let AnnotationType::MultilineStart(pos) = ann.annotation_type + { + // In the case where we have elided the entire start of the + // multispan because those lines were empty, we still need + // to draw the `|`s across the `...`. + self.draw_multiline_line( + &mut buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary { + Style::UnderlinePrimary + } else { + Style::UnderlineSecondary + }, + ); + } + } + } + } else if line_idx_delta == 2 { + let unannotated_line = annotated_file + .file + .get_line(annotated_file.lines[line_idx].line_index) + .unwrap_or_else(|| Cow::from("")); + + let last_buffer_line_num = buffer.num_lines(); + + self.draw_line( + &mut buffer, + &normalize_whitespace(&unannotated_line), + annotated_file.lines[line_idx + 1].line_index - 1, + last_buffer_line_num, + width_offset, + code_offset, + margin, + ); + + for (depth, style) in &multilines { + self.draw_multiline_line( + &mut buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_file.lines.get(line_idx) { + for ann in &line.annotations { + if let AnnotationType::MultilineStart(pos) = ann.annotation_type + { + self.draw_multiline_line( + &mut buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary { + Style::UnderlinePrimary + } else { + Style::UnderlineSecondary + }, + ); + } + } + } + } + } + + multilines.extend(&to_add); + } + } + trace!("buffer: {:#?}", buffer.render()); + } + + if let Some(tracked) = emitted_at { + let track = format!("-Ztrack-diagnostics: created at {tracked}"); + let len = buffer.num_lines(); + buffer.append(len, &track, Style::NoStyle); + } + + // final step: take our styled buffer, render it, then output it + emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message)?; + + Ok(()) + } + + fn emit_suggestion_default( + &mut self, + span: &MultiSpan, + suggestion: &CodeSuggestion, + args: &FluentArgs<'_>, + level: &Level, + max_line_num_len: usize, + ) -> io::Result<()> { + let Some(ref sm) = self.sm else { + return Ok(()); + }; + + // Render the replacements for each suggestion + let suggestions = suggestion.splice_lines(sm); + debug!(?suggestions); + + if suggestions.is_empty() { + // Here we check if there are suggestions that have actual code changes. We sometimes + // suggest the same code that is already there, instead of changing how we produce the + // suggestions and filtering there, we just don't emit the suggestion. + // Suggestions coming from macros can also have malformed spans. This is a heavy handed + // approach to avoid ICEs by ignoring the suggestion outright. + return Ok(()); + } + + let mut buffer = StyledBuffer::new(); + + // Render the suggestion message + buffer.append(0, level.to_str(), Style::Level(*level)); + buffer.append(0, ": ", Style::HeaderMsg); + + let mut msg = vec![(suggestion.msg.to_owned(), Style::NoStyle)]; + if suggestions + .iter() + .take(MAX_SUGGESTIONS) + .any(|(_, _, _, only_capitalization)| *only_capitalization) + { + msg.push((" (notice the capitalization difference)".into(), Style::NoStyle)); + } + self.msgs_to_buffer( + &mut buffer, + &msg, + args, + max_line_num_len, + "suggestion", + Some(Style::HeaderMsg), + ); + + let mut row_num = 2; + for (i, (complete, parts, highlights, _)) in + suggestions.iter().enumerate().take(MAX_SUGGESTIONS) + { + debug!(?complete, ?parts, ?highlights); + + let has_deletion = parts.iter().any(|p| p.is_deletion(sm)); + let is_multiline = complete.lines().count() > 1; + + if i == 0 { + self.draw_col_separator_start(&mut buffer, row_num - 1, max_line_num_len + 1); + } else { + buffer.puts( + row_num - 1, + max_line_num_len + 1, + self.multi_suggestion_separator(), + Style::LineNumber, + ); + } + if let Some(span) = span.primary_span() { + // Compare the primary span of the diagnostic with the span of the suggestion + // being emitted. If they belong to the same file, we don't *need* to show the + // file name, saving in verbosity, but if it *isn't* we do need it, otherwise we're + // telling users to make a change but not clarifying *where*. + let loc = sm.lookup_char_pos(parts[0].span.lo()); + if loc.file.name != sm.span_to_filename(span) && loc.file.name.is_real() { + // --> file.rs:line:col + // | + let arrow = self.file_start(); + buffer.puts(row_num - 1, 0, arrow, Style::LineNumber); + let filename = sm.filename_for_diagnostics(&loc.file.name); + let offset = sm.doctest_offset_line(&loc.file.name, loc.line); + let message = format!("{}:{}:{}", filename, offset, loc.col.0 + 1); + if row_num == 2 { + let col = usize::max(max_line_num_len + 1, arrow.len()); + buffer.puts(1, col, &message, Style::LineAndColumn); + } else { + buffer.append(row_num - 1, &message, Style::LineAndColumn); + } + for _ in 0..max_line_num_len { + buffer.prepend(row_num - 1, " ", Style::NoStyle); + } + self.draw_col_separator_no_space(&mut buffer, row_num, max_line_num_len + 1); + row_num += 1; + } + } + let show_code_change = if has_deletion && !is_multiline { + DisplaySuggestion::Diff + } else if let [part] = &parts[..] + && part.snippet.ends_with('\n') + && part.snippet.trim() == complete.trim() + { + // We are adding a line(s) of code before code that was already there. + DisplaySuggestion::Add + } else if (parts.len() != 1 || parts[0].snippet.trim() != complete.trim()) + && !is_multiline + { + DisplaySuggestion::Underline + } else { + DisplaySuggestion::None + }; + + if let DisplaySuggestion::Diff = show_code_change { + row_num += 1; + } + + let file_lines = sm + .span_to_lines(parts[0].span) + .expect("span_to_lines failed when emitting suggestion"); + + assert!(!file_lines.lines.is_empty() || parts[0].span.is_dummy()); + + let line_start = sm.lookup_char_pos(parts[0].span.lo()).line; + let mut lines = complete.lines(); + if lines.clone().next().is_none() { + // Account for a suggestion to completely remove a line(s) with whitespace (#94192). + let line_end = sm.lookup_char_pos(parts[0].span.hi()).line; + for line in line_start..=line_end { + buffer.puts( + row_num - 1 + line - line_start, + 0, + &self.maybe_anonymized(line), + Style::LineNumber, + ); + buffer.puts( + row_num - 1 + line - line_start, + max_line_num_len + 1, + "- ", + Style::Removal, + ); + buffer.puts( + row_num - 1 + line - line_start, + max_line_num_len + 3, + &normalize_whitespace(&file_lines.file.get_line(line - 1).unwrap()), + Style::Removal, + ); + } + row_num += line_end - line_start; + } + let mut unhighlighted_lines = Vec::new(); + let mut last_pos = 0; + let mut is_item_attribute = false; + for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() { + last_pos = line_pos; + debug!(%line_pos, %line, ?highlight_parts); + + // Remember lines that are not highlighted to hide them if needed + if highlight_parts.is_empty() { + unhighlighted_lines.push((line_pos, line)); + continue; + } + if highlight_parts.len() == 1 + && line.trim().starts_with("#[") + && line.trim().ends_with(']') + { + is_item_attribute = true; + } + + match unhighlighted_lines.len() { + 0 => (), + // Since we show first line, "..." line and last line, + // There is no reason to hide if there are 3 or less lines + // (because then we just replace a line with ... which is + // not helpful) + n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| { + self.draw_code_line( + &mut buffer, + &mut row_num, + &[], + p + line_start, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ) + }), + // Print first unhighlighted line, "..." and last unhighlighted line, like so: + // + // LL | this line was highlighted + // LL | this line is just for context + // ... + // LL | this line is just for context + // LL | this line was highlighted + _ => { + let last_line = unhighlighted_lines.pop(); + let first_line = unhighlighted_lines.drain(..).next(); + + if let Some((p, l)) = first_line { + self.draw_code_line( + &mut buffer, + &mut row_num, + &[], + p + line_start, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ) + } + + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + Style::LineNumber, + ); + row_num += 1; + + if let Some((p, l)) = last_line { + self.draw_code_line( + &mut buffer, + &mut row_num, + &[], + p + line_start, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ) + } + } + } + + self.draw_code_line( + &mut buffer, + &mut row_num, + highlight_parts, + line_pos + line_start, + line, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ) + } + if let DisplaySuggestion::Add = show_code_change + && is_item_attribute + { + // 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 file_lines = sm + .span_to_lines(parts[0].span.shrink_to_hi()) + .expect("span_to_lines failed when emitting suggestion"); + let line_num = sm.lookup_char_pos(parts[0].span.lo()).line; + if let Some(line) = file_lines.file.get_line(line_num - 1) { + let line = normalize_whitespace(&line); + self.draw_code_line( + &mut buffer, + &mut row_num, + &[], + line_num + last_pos + 1, + &line, + DisplaySuggestion::None, + max_line_num_len, + &file_lines, + is_multiline, + ) + } + } + + // This offset and the ones below need to be signed to account for replacement code + // that is shorter than the original code. + let mut offsets: Vec<(usize, isize)> = Vec::new(); + // Only show an underline in the suggestions if the suggestion is not the + // entirety of the code being shown and the displayed code is not multiline. + if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add = + show_code_change + { + for part in parts { + let snippet = if let Ok(snippet) = sm.span_to_snippet(part.span) { + snippet + } else { + String::new() + }; + let span_start_pos = sm.lookup_char_pos(part.span.lo()).col_display; + let span_end_pos = sm.lookup_char_pos(part.span.hi()).col_display; + + // If this addition is _only_ whitespace, then don't trim it, + // or else we're just not rendering anything. + let is_whitespace_addition = part.snippet.trim().is_empty(); + + // Do not underline the leading... + let start = if is_whitespace_addition { + 0 + } else { + part.snippet.len().saturating_sub(part.snippet.trim_start().len()) + }; + // ...or trailing spaces. Account for substitutions containing unicode + // characters. + let sub_len: usize = str_width(if is_whitespace_addition { + &part.snippet + } else { + part.snippet.trim() + }); + + let offset: isize = offsets + .iter() + .filter_map( + |(start, v)| if span_start_pos < *start { None } else { Some(v) }, + ) + .sum(); + let underline_start = (span_start_pos + start) as isize + offset; + let underline_end = (span_start_pos + start + sub_len) as isize + offset; + assert!(underline_start >= 0 && underline_end >= 0); + let padding: usize = max_line_num_len + 3; + for p in underline_start..underline_end { + if let DisplaySuggestion::Underline = show_code_change + && is_different(sm, &part.snippet, part.span) + { + // If this is a replacement, underline with `~`, if this is an addition + // underline with `+`. + buffer.putc( + row_num, + (padding as isize + p) as usize, + if part.is_addition(sm) { '+' } else { self.diff() }, + Style::Addition, + ); + } + } + if let DisplaySuggestion::Diff = show_code_change { + // Colorize removal with red in diff format. + + // Below, there's some tricky buffer indexing going on. `row_num` at this + // point corresponds to: + // + // | + // LL | CODE + // | ++++ <- `row_num` + // + // in the buffer. When we have a diff format output, we end up with + // + // | + // LL - OLDER <- row_num - 2 + // LL + NEWER + // | <- row_num + // + // The `row_num - 2` is to select the buffer line that has the "old version + // of the diff" at that point. When the removal is a single line, `i` is + // `0`, `newlines` is `1` so `(newlines - i - 1)` ends up being `0`, so row + // points at `LL - OLDER`. When the removal corresponds to multiple lines, + // we end up with `newlines > 1` and `i` being `0..newlines - 1`. + // + // | + // LL - OLDER <- row_num - 2 - (newlines - last_i - 1) + // LL - CODE + // LL - BEING + // LL - REMOVED <- row_num - 2 - (newlines - first_i - 1) + // LL + NEWER + // | <- row_num + + let newlines = snippet.lines().count(); + if newlines > 0 && row_num > newlines { + // Account for removals where the part being removed spans multiple + // lines. + // FIXME: We check the number of rows because in some cases, like in + // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered + // suggestion will only show the first line of code being replaced. The + // proper way of doing this would be to change the suggestion rendering + // logic to show the whole prior snippet, but the current output is not + // too bad to begin with, so we side-step that issue here. + for (i, line) in snippet.lines().enumerate() { + let line = normalize_whitespace(line); + let row = row_num - 2 - (newlines - i - 1); + // On the first line, we highlight between the start of the part + // span, and the end of that line. + // On the last line, we highlight between the start of the line, and + // the column of the part span end. + // On all others, we highlight the whole line. + let start = if i == 0 { + (padding as isize + span_start_pos as isize) as usize + } else { + padding + }; + let end = if i == 0 { + (padding as isize + + span_start_pos as isize + + line.len() as isize) + as usize + } else if i == newlines - 1 { + (padding as isize + span_end_pos as isize) as usize + } else { + (padding as isize + line.len() as isize) as usize + }; + buffer.set_style_range(row, start, end, Style::Removal, true); + } + } else { + // The removed code fits all in one line. + buffer.set_style_range( + row_num - 2, + (padding as isize + span_start_pos as isize) as usize, + (padding as isize + span_end_pos as isize) as usize, + Style::Removal, + true, + ); + } + } + + // length of the code after substitution + let full_sub_len = str_width(&part.snippet) as isize; + + // length of the code to be substituted + let snippet_len = span_end_pos as isize - span_start_pos as isize; + // For multiple substitutions, use the position *after* the previous + // substitutions have happened, only when further substitutions are + // located strictly after. + offsets.push((span_end_pos, full_sub_len - snippet_len)); + } + row_num += 1; + } + + // if we elided some lines, add an ellipsis + if lines.next().is_some() { + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + Style::LineNumber, + ); + } else { + let row = match show_code_change { + DisplaySuggestion::Diff + | DisplaySuggestion::Add + | DisplaySuggestion::Underline => row_num - 1, + DisplaySuggestion::None => row_num, + }; + self.draw_col_separator_end(&mut buffer, row, max_line_num_len + 1); + row_num = row + 1; + } + } + if suggestions.len() > MAX_SUGGESTIONS { + let others = suggestions.len() - MAX_SUGGESTIONS; + let msg = format!("and {} other candidate{}", others, pluralize!(others)); + buffer.puts(row_num, max_line_num_len + 3, &msg, Style::NoStyle); + } + + emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message)?; + Ok(()) + } + + #[instrument(level = "trace", skip(self, args, code, children, suggestions))] + fn emit_messages_default( + &mut self, + level: &Level, + messages: &[(DiagMessage, Style)], + args: &FluentArgs<'_>, + code: &Option<ErrCode>, + span: &MultiSpan, + children: &[Subdiag], + suggestions: &[CodeSuggestion], + emitted_at: Option<&DiagLocation>, + ) { + let max_line_num_len = if self.ui_testing { + ANONYMIZED_LINE_NUM.len() + } else { + let n = self.get_max_line_num(span, children); + num_decimal_digits(n) + }; + + match self.emit_messages_default_inner( + span, + messages, + args, + code, + level, + max_line_num_len, + false, + emitted_at, + !children.is_empty() + || suggestions.iter().any(|s| s.style != SuggestionStyle::CompletelyHidden), + ) { + Ok(()) => { + if !children.is_empty() + || suggestions.iter().any(|s| s.style != SuggestionStyle::CompletelyHidden) + { + let mut buffer = StyledBuffer::new(); + if !self.short_message { + if let Some(child) = children.iter().next() + && child.span.primary_spans().is_empty() + { + // We'll continue the vertical bar to point into the next note. + self.draw_col_separator_no_space(&mut buffer, 0, max_line_num_len + 1); + } else { + // We'll close the vertical bar to visually end the code window. + self.draw_col_separator_end(&mut buffer, 0, max_line_num_len + 1); + } + } + if let Err(e) = emit_to_destination( + &buffer.render(), + level, + &mut self.dst, + self.short_message, + ) { + panic!("failed to emit error: {e}") + } + } + if !self.short_message { + for (i, child) in children.iter().enumerate() { + assert!(child.level.can_be_subdiag()); + let span = &child.span; + // FIXME: audit that this behaves correctly with suggestions. + let should_close = match children.get(i + 1) { + Some(c) => !c.span.primary_spans().is_empty(), + None => i + 1 == children.len(), + }; + if let Err(err) = self.emit_messages_default_inner( + span, + &child.messages, + args, + &None, + &child.level, + max_line_num_len, + true, + None, + !should_close, + ) { + panic!("failed to emit error: {err}"); + } + } + for (i, sugg) in suggestions.iter().enumerate() { + match sugg.style { + SuggestionStyle::CompletelyHidden => { + // do not display this suggestion, it is meant only for tools + } + SuggestionStyle::HideCodeAlways => { + if let Err(e) = self.emit_messages_default_inner( + &MultiSpan::new(), + &[(sugg.msg.to_owned(), Style::HeaderMsg)], + args, + &None, + &Level::Help, + max_line_num_len, + true, + None, + // FIXME: this needs to account for the suggestion type, + // some don't take any space. + i + 1 != suggestions.len(), + ) { + panic!("failed to emit error: {e}"); + } + } + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways => { + if let Err(e) = self.emit_suggestion_default( + span, + sugg, + args, + &Level::Help, + max_line_num_len, + ) { + panic!("failed to emit error: {e}"); + } + } + } + } + } + } + Err(e) => panic!("failed to emit error: {e}"), + } + + match writeln!(self.dst) { + Err(e) => panic!("failed to emit error: {e}"), + _ => { + if let Err(e) = self.dst.flush() { + panic!("failed to emit error: {e}") + } + } + } + } + + fn draw_code_line( + &self, + buffer: &mut StyledBuffer, + row_num: &mut usize, + highlight_parts: &[SubstitutionHighlight], + line_num: usize, + line_to_add: &str, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + file_lines: &FileLines, + is_multiline: bool, + ) { + if let DisplaySuggestion::Diff = show_code_change { + // We need to print more than one line if the span we need to remove is multiline. + // For more info: https://github.com/rust-lang/rust/issues/92741 + let lines_to_remove = file_lines.lines.iter().take(file_lines.lines.len() - 1); + for (index, line_to_remove) in lines_to_remove.enumerate() { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + index), + Style::LineNumber, + ); + buffer.puts(*row_num - 1, max_line_num_len + 1, "- ", Style::Removal); + let line = normalize_whitespace( + &file_lines.file.get_line(line_to_remove.line_index).unwrap(), + ); + buffer.puts(*row_num - 1, max_line_num_len + 3, &line, Style::NoStyle); + *row_num += 1; + } + // If the last line is exactly equal to the line we need to add, we can skip both of + // them. This allows us to avoid output like the following: + // 2 - & + // 2 + if true { true } else { false } + // 3 - if true { true } else { false } + // If those lines aren't equal, we print their diff + let last_line_index = file_lines.lines[file_lines.lines.len() - 1].line_index; + let last_line = &file_lines.file.get_line(last_line_index).unwrap(); + if last_line != line_to_add { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + file_lines.lines.len() - 1), + Style::LineNumber, + ); + buffer.puts(*row_num - 1, max_line_num_len + 1, "- ", Style::Removal); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &normalize_whitespace(last_line), + Style::NoStyle, + ); + if !line_to_add.trim().is_empty() { + // Check if after the removal, the line is left with only whitespace. If so, we + // will not show an "addition" line, as removing the whole line is what the user + // would really want. + // For example, for the following: + // | + // 2 - .await + // 2 + (note the left over whitespace) + // | + // We really want + // | + // 2 - .await + // | + // *row_num -= 1; + buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition); + buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle); + } else { + *row_num -= 1; + } + } else { + *row_num -= 2; + } + } else if is_multiline { + buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber); + match &highlight_parts { + [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => { + buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition); + } + [] => { + // FIXME: needed? Doesn't get exercised in any test. + self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1); + } + _ => { + let diff = self.diff(); + buffer.puts( + *row_num, + max_line_num_len + 1, + &format!("{diff} "), + Style::Addition, + ); + } + } + // LL | line_to_add + // ++^^^ + // | | + // | magic `3` + // `max_line_num_len` + buffer.puts( + *row_num, + max_line_num_len + 3, + &normalize_whitespace(line_to_add), + Style::NoStyle, + ); + } else if let DisplaySuggestion::Add = show_code_change { + buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition); + buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle); + } else { + buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber); + self.draw_col_separator(buffer, *row_num, max_line_num_len + 1); + buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle); + } + + // Colorize addition/replacements with green. + for &SubstitutionHighlight { start, end } in highlight_parts { + // This is a no-op for empty ranges + if start != end { + // Account for tabs when highlighting (#87972). + let tabs: usize = line_to_add + .chars() + .take(start) + .map(|ch| match ch { + '\t' => 3, + _ => 0, + }) + .sum(); + buffer.set_style_range( + *row_num, + max_line_num_len + 3 + start + tabs, + max_line_num_len + 3 + end + tabs, + Style::Addition, + true, + ); + } + } + *row_num += 1; + } + + fn underline(&self, is_primary: bool) -> UnderlineParts { + // X0 Y0 + // label_start > ┯━━━━ < underline + // │ < vertical_text_line + // text + + // multiline_start_down ⤷ X0 Y0 + // top_left > ┌───╿──┘ < top_right_flat + // top_left > ┏│━━━┙ < top_right + // multiline_vertical > ┃│ + // ┃│ X1 Y1 + // ┃│ X2 Y2 + // ┃└────╿──┘ < multiline_end_same_line + // bottom_left > ┗━━━━━┥ < bottom_right_with_text + // multiline_horizontal ^ `X` is a good letter + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┗━━━━┛ < multiline_end_same_line + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┃ ╿ < multiline_end_up + // ┗━━┛ < bottom_right + + match (self.theme, is_primary) { + (OutputTheme::Ascii, true) => UnderlineParts { + style: Style::UnderlinePrimary, + underline: '^', + label_start: '^', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '^', + bottom_right: '|', + top_left: ' ', + top_right_flat: '^', + bottom_left: '|', + multiline_end_up: '^', + multiline_end_same_line: '^', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Ascii, false) => UnderlineParts { + style: Style::UnderlineSecondary, + underline: '-', + label_start: '-', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '-', + bottom_right: '|', + top_left: ' ', + top_right_flat: '-', + bottom_left: '|', + multiline_end_up: '-', + multiline_end_same_line: '-', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Unicode, true) => UnderlineParts { + style: Style::UnderlinePrimary, + underline: '━', + label_start: '┯', + vertical_text_line: '│', + multiline_vertical: '┃', + multiline_horizontal: '━', + multiline_whole_line: '┏', + multiline_start_down: '╿', + bottom_right: '┙', + top_left: '┏', + top_right_flat: '┛', + bottom_left: '┗', + multiline_end_up: '╿', + multiline_end_same_line: '┛', + multiline_bottom_right_with_text: '┥', + }, + (OutputTheme::Unicode, false) => UnderlineParts { + style: Style::UnderlineSecondary, + underline: '─', + label_start: '┬', + vertical_text_line: '│', + multiline_vertical: '│', + multiline_horizontal: '─', + multiline_whole_line: '┌', + multiline_start_down: '│', + bottom_right: '┘', + top_left: '┌', + top_right_flat: '┘', + bottom_left: '└', + multiline_end_up: '│', + multiline_end_same_line: '┘', + multiline_bottom_right_with_text: '┤', + }, + } + } + + fn col_separator(&self) -> char { + match self.theme { + OutputTheme::Ascii => '|', + OutputTheme::Unicode => '│', + } + } + + fn note_separator(&self) -> char { + match self.theme { + OutputTheme::Ascii => '=', + OutputTheme::Unicode => '╰', + } + } + + fn multi_suggestion_separator(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "|", + OutputTheme::Unicode => "├╴", + } + } + + fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + buffer.puts(line, col, &format!("{chr} "), Style::LineNumber); + } + + fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + self.draw_col_separator_no_space_with_style(buffer, chr, line, col, Style::LineNumber); + } + + fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + Style::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╭', + line, + col, + Style::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + Style::LineNumber, + ); + } + } + } + + fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + Style::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╰', + line, + col, + Style::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + Style::LineNumber, + ); + } + } + } + + fn draw_col_separator_no_space_with_style( + &self, + buffer: &mut StyledBuffer, + chr: char, + line: usize, + col: usize, + style: Style, + ) { + buffer.putc(line, col, chr, style); + } + + fn draw_range( + &self, + buffer: &mut StyledBuffer, + symbol: char, + line: usize, + col_from: usize, + col_to: usize, + style: Style, + ) { + for col in col_from..col_to { + buffer.putc(line, col, symbol, style); + } + } + + fn draw_note_separator( + &self, + buffer: &mut StyledBuffer, + line: usize, + col: usize, + is_cont: bool, + ) { + let chr = match self.theme { + OutputTheme::Ascii => "= ", + OutputTheme::Unicode if is_cont => "├ ", + OutputTheme::Unicode => "╰ ", + }; + buffer.puts(line, col, chr, Style::LineNumber); + } + + fn draw_multiline_line( + &self, + buffer: &mut StyledBuffer, + line: usize, + offset: usize, + depth: usize, + style: Style, + ) { + let chr = match (style, self.theme) { + (Style::UnderlinePrimary | Style::LabelPrimary, OutputTheme::Ascii) => '|', + (_, OutputTheme::Ascii) => '|', + (Style::UnderlinePrimary | Style::LabelPrimary, OutputTheme::Unicode) => '┃', + (_, OutputTheme::Unicode) => '│', + }; + buffer.putc(line, offset + depth - 1, chr, style); + } + + fn file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "--> ", + OutputTheme::Unicode => " ╭▸ ", + } + } + + fn secondary_file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "::: ", + OutputTheme::Unicode => " ⸬ ", + } + } + + fn diff(&self) -> char { + match self.theme { + OutputTheme::Ascii => '~', + OutputTheme::Unicode => '±', + } + } + + fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let (column, dots) = match self.theme { + OutputTheme::Ascii => (0, "..."), + OutputTheme::Unicode => (col - 2, "‡"), + }; + buffer.puts(line, column, dots, Style::LineNumber); + } + + fn margin(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "...", + OutputTheme::Unicode => "…", + } + } +} + +#[derive(Debug, Clone, Copy)] +struct UnderlineParts { + style: Style, + underline: char, + label_start: char, + vertical_text_line: char, + multiline_vertical: char, + multiline_horizontal: char, + multiline_whole_line: char, + multiline_start_down: char, + bottom_right: char, + top_left: char, + top_right_flat: char, + bottom_left: char, + multiline_end_up: char, + multiline_end_same_line: char, + multiline_bottom_right_with_text: char, +} + +#[derive(Clone, Copy, Debug)] +enum DisplaySuggestion { + Underline, + Diff, + None, + Add, +} + +impl FileWithAnnotatedLines { + /// Preprocess all the annotations so that they are grouped by file and by line number + /// This helps us quickly iterate over the whole message (including secondary file spans) + pub(crate) fn collect_annotations( + emitter: &dyn Emitter, + args: &FluentArgs<'_>, + msp: &MultiSpan, + ) -> Vec<FileWithAnnotatedLines> { + fn add_annotation_to_file( + file_vec: &mut Vec<FileWithAnnotatedLines>, + file: Arc<SourceFile>, + line_index: usize, + ann: Annotation, + ) { + for slot in file_vec.iter_mut() { + // Look through each of our files for the one we're adding to + if slot.file.name == file.name { + // See if we already have a line for it + for line_slot in &mut slot.lines { + if line_slot.line_index == line_index { + line_slot.annotations.push(ann); + return; + } + } + // We don't have a line yet, create one + slot.lines.push(Line { line_index, annotations: vec![ann] }); + slot.lines.sort(); + return; + } + } + // This is the first time we're seeing the file + file_vec.push(FileWithAnnotatedLines { + file, + lines: vec![Line { line_index, annotations: vec![ann] }], + multiline_depth: 0, + }); + } + + let mut output = vec![]; + let mut multiline_annotations = vec![]; + + if let Some(sm) = emitter.source_map() { + 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 lo = sm.lookup_char_pos(span.lo()); + let mut hi = sm.lookup_char_pos(span.hi()); + + // Watch out for "empty spans". If we get a span like 6..6, we + // want to just display a `^` at 6, so convert that to + // 6..7. This is degenerate input, but it's best to degrade + // gracefully -- and the parser likes to supply a span like + // that for EOF, in particular. + + if lo.col_display == hi.col_display && lo.line == hi.line { + hi.col_display += 1; + } + + let label = label.as_ref().map(|m| { + normalize_whitespace( + &emitter.translate_message(m, args).map_err(Report::new).unwrap(), + ) + }); + + if lo.line != hi.line { + let ml = MultilineAnnotation { + depth: 1, + line_start: lo.line, + line_end: hi.line, + start_col: AnnotationColumn::from_loc(&lo), + end_col: AnnotationColumn::from_loc(&hi), + is_primary, + label, + overlaps_exactly: false, + }; + multiline_annotations.push((lo.file, ml)); + } else { + let ann = Annotation { + start_col: AnnotationColumn::from_loc(&lo), + end_col: AnnotationColumn::from_loc(&hi), + is_primary, + label, + annotation_type: AnnotationType::Singleline, + }; + add_annotation_to_file(&mut output, lo.file, lo.line, ann); + }; + } + } + + // Find overlapping multiline annotations, put them at different depths + multiline_annotations.sort_by_key(|(_, ml)| (ml.line_start, usize::MAX - ml.line_end)); + for (_, ann) in multiline_annotations.clone() { + for (_, a) in multiline_annotations.iter_mut() { + // Move all other multiline annotations overlapping with this one + // one level to the right. + if !(ann.same_span(a)) + && num_overlap(ann.line_start, ann.line_end, a.line_start, a.line_end, true) + { + a.increase_depth(); + } else if ann.same_span(a) && &ann != a { + a.overlaps_exactly = true; + } else { + break; + } + } + } + + let mut max_depth = 0; // max overlapping multiline spans + for (_, ann) in &multiline_annotations { + max_depth = max(max_depth, ann.depth); + } + // Change order of multispan depth to minimize the number of overlaps in the ASCII art. + for (_, a) in multiline_annotations.iter_mut() { + a.depth = max_depth - a.depth + 1; + } + for (file, ann) in multiline_annotations { + let mut end_ann = ann.as_end(); + if !ann.overlaps_exactly { + // avoid output like + // + // | foo( + // | _____^ + // | |_____| + // | || bar, + // | || ); + // | || ^ + // | ||______| + // | |______foo + // | baz + // + // and instead get + // + // | foo( + // | _____^ + // | | bar, + // | | ); + // | | ^ + // | | | + // | |______foo + // | baz + add_annotation_to_file( + &mut output, + Arc::clone(&file), + ann.line_start, + ann.as_start(), + ); + // 4 is the minimum vertical length of a multiline span when presented: two lines + // of code and two lines of underline. This is not true for the special case where + // the beginning doesn't have an underline, but the current logic seems to be + // working correctly. + let middle = min(ann.line_start + 4, ann.line_end); + // We'll show up to 4 lines past the beginning of the multispan start. + // We will *not* include the tail of lines that are only whitespace, a comment or + // a bare delimiter. + let filter = |s: &str| { + let s = s.trim(); + // Consider comments as empty, but don't consider docstrings to be empty. + !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) + // Consider lines with nothing but whitespace, a single delimiter as empty. + && !["", "{", "}", "(", ")", "[", "]"].contains(&s) + }; + let until = (ann.line_start..middle) + .rev() + .filter_map(|line| file.get_line(line - 1).map(|s| (line + 1, s))) + .find(|(_, s)| filter(s)) + .map(|(line, _)| line) + .unwrap_or(ann.line_start); + for line in ann.line_start + 1..until { + // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). + add_annotation_to_file(&mut output, Arc::clone(&file), line, ann.as_line()); + } + let line_end = ann.line_end - 1; + let end_is_empty = file.get_line(line_end - 1).is_some_and(|s| !filter(&s)); + if middle < line_end && !end_is_empty { + add_annotation_to_file(&mut output, Arc::clone(&file), line_end, ann.as_line()); + } + } else { + end_ann.annotation_type = AnnotationType::Singleline; + } + add_annotation_to_file(&mut output, file, ann.line_end, end_ann); + } + for file_vec in output.iter_mut() { + file_vec.multiline_depth = max_depth; + } + output + } +} + +// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until +// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which +// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. +// This is also why we need the max number of decimal digits within a `usize`. +fn num_decimal_digits(num: usize) -> usize { + #[cfg(target_pointer_width = "64")] + const MAX_DIGITS: usize = 20; + + #[cfg(target_pointer_width = "32")] + const MAX_DIGITS: usize = 10; + + #[cfg(target_pointer_width = "16")] + const MAX_DIGITS: usize = 5; + + let mut lim = 10; + for num_digits in 1..MAX_DIGITS { + if num < lim { + return num_digits; + } + lim = lim.wrapping_mul(10); + } + MAX_DIGITS +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +// Keep the following list in sync with `rustc_span::char_width`. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + // In terminals without Unicode support the following will be garbled, but in *all* terminals + // the underlying codepoint will be as well. We could gate this replacement behind a "unicode + // support" gate. + ('\0', "␀"), + ('\u{0001}', "␁"), + ('\u{0002}', "␂"), + ('\u{0003}', "␃"), + ('\u{0004}', "␄"), + ('\u{0005}', "␅"), + ('\u{0006}', "␆"), + ('\u{0007}', "␇"), + ('\u{0008}', "␈"), + ('\t', " "), // We do our own tab replacement + ('\u{000b}', "␋"), + ('\u{000c}', "␌"), + ('\u{000d}', "␍"), + ('\u{000e}', "␎"), + ('\u{000f}', "␏"), + ('\u{0010}', "␐"), + ('\u{0011}', "␑"), + ('\u{0012}', "␒"), + ('\u{0013}', "␓"), + ('\u{0014}', "␔"), + ('\u{0015}', "␕"), + ('\u{0016}', "␖"), + ('\u{0017}', "␗"), + ('\u{0018}', "␘"), + ('\u{0019}', "␙"), + ('\u{001a}', "␚"), + ('\u{001b}', "␛"), + ('\u{001c}', "␜"), + ('\u{001d}', "␝"), + ('\u{001e}', "␞"), + ('\u{001f}', "␟"), + ('\u{007f}', "␡"), + ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. + ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently + ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. + ('\u{202d}', "�"), + ('\u{202e}', "�"), + ('\u{2066}', "�"), + ('\u{2067}', "�"), + ('\u{2068}', "�"), + ('\u{2069}', "�"), +]; + +fn normalize_whitespace(s: &str) -> String { + const { + let mut i = 1; + while i < OUTPUT_REPLACEMENTS.len() { + assert!( + OUTPUT_REPLACEMENTS[i - 1].0 < OUTPUT_REPLACEMENTS[i].0, + "The OUTPUT_REPLACEMENTS array must be sorted (for binary search to work) \ + and must contain no duplicate entries" + ); + i += 1; + } + } + // Scan the input string for a character in the ordered table above. + // If it's present, replace it with its alternative string (it can be more than 1 char!). + // Otherwise, retain the input char. + s.chars().fold(String::with_capacity(s.len()), |mut s, c| { + match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { + Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), + _ => s.push(c), + } + s + }) +} + +fn num_overlap( + a_start: usize, + a_end: usize, + b_start: usize, + b_end: usize, + inclusive: bool, +) -> bool { + let extra = if inclusive { 1 } else { 0 }; + (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) +} + +fn overlaps(a1: &Annotation, a2: &Annotation, padding: usize) -> bool { + num_overlap( + a1.start_col.display, + a1.end_col.display + padding, + a2.start_col.display, + a2.end_col.display, + false, + ) +} + +fn emit_to_destination( + rendered_buffer: &[Vec<StyledString>], + lvl: &Level, + dst: &mut Destination, + short_message: bool, +) -> io::Result<()> { + use crate::lock; + + // In order to prevent error message interleaving, where multiple error lines get intermixed + // when multiple compiler processes error simultaneously, we emit errors with additional + // steps. + // + // On Unix systems, we write into a buffered terminal rather than directly to a terminal. When + // the .flush() is called we take the buffer created from the buffered writes and write it at + // one shot. Because the Unix systems use ANSI for the colors, which is a text-based styling + // scheme, this buffered approach works and maintains the styling. + // + // On Windows, styling happens through calls to a terminal API. This prevents us from using the + // same buffering approach. Instead, we use a global Windows mutex, which we acquire long + // enough to output the full error message, then we release. + let _buffer_lock = lock::acquire_global_lock("rustc_errors"); + for (pos, line) in rendered_buffer.iter().enumerate() { + for part in line { + let style = part.style.color_spec(*lvl); + dst.set_color(&style)?; + write!(dst, "{}", part.text)?; + dst.reset()?; + } + if !short_message && (!lvl.is_failure_note() || pos != rendered_buffer.len() - 1) { + writeln!(dst)?; + } + } + dst.flush()?; + Ok(()) +} + +pub type Destination = Box<dyn WriteColor + Send>; + +struct Buffy { + buffer_writer: BufferWriter, + buffer: Buffer, +} + +impl Write for Buffy { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.buffer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.buffer_writer.print(&self.buffer)?; + self.buffer.clear(); + Ok(()) + } +} + +impl Drop for Buffy { + fn drop(&mut self) { + if !self.buffer.is_empty() { + self.flush().unwrap(); + panic!("buffers need to be flushed in order to print their contents"); + } + } +} + +impl WriteColor for Buffy { + fn supports_color(&self) -> bool { + self.buffer.supports_color() + } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.buffer.set_color(spec) + } + + fn reset(&mut self) -> io::Result<()> { + self.buffer.reset() + } +} + +pub fn stderr_destination(color: ColorConfig) -> Destination { + let choice = color.to_color_choice(); + // On Windows we'll be performing global synchronization on the entire + // system for emitting rustc errors, so there's no need to buffer + // anything. + // + // On non-Windows we rely on the atomicity of `write` to ensure errors + // don't get all jumbled up. + if cfg!(windows) { + Box::new(StandardStream::stderr(choice)) + } else { + let buffer_writer = BufferWriter::stderr(choice); + let buffer = buffer_writer.buffer(); + Box::new(Buffy { buffer_writer, buffer }) + } +} + +/// On Windows, BRIGHT_BLUE is hard to read on black. Use cyan instead. +/// +/// See #36178. +const BRIGHT_BLUE: Color = if cfg!(windows) { Color::Cyan } else { Color::Blue }; + +impl Style { + fn color_spec(&self, lvl: Level) -> ColorSpec { + let mut spec = ColorSpec::new(); + match self { + Style::Addition => { + spec.set_fg(Some(Color::Green)).set_intense(true); + } + Style::Removal => { + spec.set_fg(Some(Color::Red)).set_intense(true); + } + Style::LineAndColumn => {} + Style::LineNumber => { + spec.set_bold(true); + spec.set_intense(true); + spec.set_fg(Some(BRIGHT_BLUE)); + } + Style::Quotation => {} + Style::MainHeaderMsg => { + spec.set_bold(true); + if cfg!(windows) { + spec.set_intense(true).set_fg(Some(Color::White)); + } + } + Style::UnderlinePrimary | Style::LabelPrimary => { + spec = lvl.color(); + spec.set_bold(true); + } + Style::UnderlineSecondary | Style::LabelSecondary => { + spec.set_bold(true).set_intense(true); + spec.set_fg(Some(BRIGHT_BLUE)); + } + Style::HeaderMsg | Style::NoStyle => {} + Style::Level(lvl) => { + spec = lvl.color(); + spec.set_bold(true); + } + Style::Highlight => { + spec.set_bold(true).set_fg(Some(Color::Magenta)); + } + } + spec + } +} + +/// Whether the original and suggested code are the same. +pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool { + let found = match sm.span_to_snippet(sp) { + Ok(snippet) => snippet, + Err(e) => { + warn!(error = ?e, "Invalid span {:?}", sp); + return true; + } + }; + found != suggested +} + +/// Whether the original and suggested code are visually similar enough to warrant extra wording. +pub fn is_case_difference(sm: &SourceMap, suggested: &str, sp: Span) -> bool { + // FIXME: this should probably be extended to also account for `FO0` → `FOO` and unicode. + let found = match sm.span_to_snippet(sp) { + Ok(snippet) => snippet, + Err(e) => { + warn!(error = ?e, "Invalid span {:?}", sp); + return false; + } + }; + let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z']; + // All the chars that differ in capitalization are confusable (above): + let confusable = iter::zip(found.chars(), suggested.chars()) + .filter(|(f, s)| f != s) + .all(|(f, s)| (ascii_confusables.contains(&f) || ascii_confusables.contains(&s))); + confusable && found.to_lowercase() == suggested.to_lowercase() + // FIXME: We sometimes suggest the same thing we already have, which is a + // bug, but be defensive against that here. + && found != suggested +} + +pub(crate) fn should_show_source_code( + ignored_directories: &[String], + sm: &SourceMap, + file: &SourceFile, +) -> bool { + if !sm.ensure_source_file_source_present(file) { + return false; + } + + let FileName::Real(name) = &file.name else { return true }; + name.local_path() + .map(|path| ignored_directories.iter().all(|dir| !path.starts_with(dir))) + .unwrap_or(true) +} diff --git a/compiler/rustc_errors/src/error.rs b/compiler/rustc_errors/src/error.rs new file mode 100644 index 00000000000..462467d9fa0 --- /dev/null +++ b/compiler/rustc_errors/src/error.rs @@ -0,0 +1,139 @@ +use std::borrow::Cow; +use std::error::Error; +use std::fmt; + +use rustc_error_messages::fluent_bundle::resolver::errors::{ReferenceKind, ResolverError}; +use rustc_error_messages::{FluentArgs, FluentError}; + +#[derive(Debug)] +pub enum TranslateError<'args> { + One { + id: &'args Cow<'args, str>, + args: &'args FluentArgs<'args>, + kind: TranslateErrorKind<'args>, + }, + Two { + primary: Box<TranslateError<'args>>, + fallback: Box<TranslateError<'args>>, + }, +} + +impl<'args> TranslateError<'args> { + pub fn message(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self { + Self::One { id, args, kind: TranslateErrorKind::MessageMissing } + } + + pub fn primary(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self { + Self::One { id, args, kind: TranslateErrorKind::PrimaryBundleMissing } + } + + pub fn attribute( + id: &'args Cow<'args, str>, + args: &'args FluentArgs<'args>, + attr: &'args str, + ) -> Self { + Self::One { id, args, kind: TranslateErrorKind::AttributeMissing { attr } } + } + + pub fn value(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self { + Self::One { id, args, kind: TranslateErrorKind::ValueMissing } + } + + pub fn fluent( + id: &'args Cow<'args, str>, + args: &'args FluentArgs<'args>, + errs: Vec<FluentError>, + ) -> Self { + Self::One { id, args, kind: TranslateErrorKind::Fluent { errs } } + } + + pub fn and(self, fallback: TranslateError<'args>) -> TranslateError<'args> { + Self::Two { primary: Box::new(self), fallback: Box::new(fallback) } + } +} + +#[derive(Debug)] +pub enum TranslateErrorKind<'args> { + MessageMissing, + PrimaryBundleMissing, + AttributeMissing { attr: &'args str }, + ValueMissing, + Fluent { errs: Vec<FluentError> }, +} + +impl fmt::Display for TranslateError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use TranslateErrorKind::*; + + match self { + Self::One { id, args, kind } => { + writeln!(f, "failed while formatting fluent string `{id}`: ")?; + match kind { + MessageMissing => writeln!(f, "message was missing")?, + PrimaryBundleMissing => writeln!(f, "the primary bundle was missing")?, + AttributeMissing { attr } => { + writeln!(f, "the attribute `{attr}` was missing")?; + writeln!(f, "help: add `.{attr} = <message>`")?; + } + ValueMissing => writeln!(f, "the value was missing")?, + Fluent { errs } => { + for err in errs { + match err { + FluentError::ResolverError(ResolverError::Reference( + ReferenceKind::Message { id, .. } + | ReferenceKind::Variable { id, .. }, + )) => { + if args.iter().any(|(arg_id, _)| arg_id == id) { + writeln!( + f, + "argument `{id}` exists but was not referenced correctly" + )?; + writeln!(f, "help: try using `{{${id}}}` instead")?; + } else { + writeln!( + f, + "the fluent string has an argument `{id}` that was not found." + )?; + let vars: Vec<&str> = + args.iter().map(|(a, _v)| a).collect(); + match &*vars { + [] => writeln!(f, "help: no arguments are available")?, + [one] => writeln!( + f, + "help: the argument `{one}` is available" + )?, + [first, middle @ .., last] => { + write!(f, "help: the arguments `{first}`")?; + for a in middle { + write!(f, ", `{a}`")?; + } + writeln!(f, " and `{last}` are available")?; + } + } + } + } + _ => writeln!(f, "{err}")?, + } + } + } + } + } + // If someone cares about primary bundles, they'll probably notice it's missing + // regardless or will be using `debug_assertions` + // so we skip the arm below this one to avoid confusing the regular user. + Self::Two { primary: box Self::One { kind: PrimaryBundleMissing, .. }, fallback } => { + fmt::Display::fmt(fallback, f)?; + } + Self::Two { primary, fallback } => { + writeln!( + f, + "first, fluent formatting using the primary bundle failed:\n {primary}\n \ + while attempting to recover by using the fallback bundle instead, another error occurred:\n{fallback}" + )?; + } + } + Ok(()) + } +} + +impl Error for TranslateError<'_> {} diff --git a/compiler/rustc_errors/src/json.rs b/compiler/rustc_errors/src/json.rs new file mode 100644 index 00000000000..7d7f364fec2 --- /dev/null +++ b/compiler/rustc_errors/src/json.rs @@ -0,0 +1,602 @@ +//! A JSON emitter for errors. +//! +//! This works by converting errors to a simplified structural format (see the +//! structs at the start of the file) and then serializing them. These should +//! contain as much information about the error as possible. +//! +//! The format of the JSON output should be considered *unstable*. For now the +//! structs at the end of this file (Diagnostic*) specify the error format. + +// FIXME: spec the JSON output properly. + +use std::error::Report; +use std::io::{self, Write}; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::vec; + +use derive_setters::Setters; +use rustc_data_structures::sync::IntoDynSyncSend; +use rustc_error_messages::FluentArgs; +use rustc_lint_defs::Applicability; +use rustc_span::Span; +use rustc_span::hygiene::ExpnData; +use rustc_span::source_map::{FilePathMapping, SourceMap}; +use serde::Serialize; +use termcolor::{ColorSpec, WriteColor}; + +use crate::diagnostic::IsLint; +use crate::emitter::{ + ColorConfig, Destination, Emitter, HumanEmitter, HumanReadableErrorType, OutputTheme, + should_show_source_code, +}; +use crate::registry::Registry; +use crate::translation::{Translate, to_fluent_args}; +use crate::{ + CodeSuggestion, FluentBundle, LazyFallbackBundle, MultiSpan, SpanLabel, Subdiag, Suggestions, + TerminalUrl, +}; + +#[cfg(test)] +mod tests; + +#[derive(Setters)] +pub struct JsonEmitter { + #[setters(skip)] + dst: IntoDynSyncSend<Box<dyn Write + Send>>, + #[setters(skip)] + sm: Option<Arc<SourceMap>>, + fluent_bundle: Option<Arc<FluentBundle>>, + #[setters(skip)] + fallback_bundle: LazyFallbackBundle, + #[setters(skip)] + pretty: bool, + ui_testing: bool, + ignored_directories_in_source_blocks: Vec<String>, + #[setters(skip)] + json_rendered: HumanReadableErrorType, + color_config: ColorConfig, + diagnostic_width: Option<usize>, + macro_backtrace: bool, + track_diagnostics: bool, + terminal_url: TerminalUrl, +} + +impl JsonEmitter { + pub fn new( + dst: Box<dyn Write + Send>, + sm: Option<Arc<SourceMap>>, + fallback_bundle: LazyFallbackBundle, + pretty: bool, + json_rendered: HumanReadableErrorType, + color_config: ColorConfig, + ) -> JsonEmitter { + JsonEmitter { + dst: IntoDynSyncSend(dst), + sm, + fluent_bundle: None, + fallback_bundle, + pretty, + ui_testing: false, + ignored_directories_in_source_blocks: Vec::new(), + json_rendered, + color_config, + diagnostic_width: None, + macro_backtrace: false, + track_diagnostics: false, + terminal_url: TerminalUrl::No, + } + } + + fn emit(&mut self, val: EmitTyped<'_>) -> io::Result<()> { + if self.pretty { + serde_json::to_writer_pretty(&mut *self.dst, &val)? + } else { + serde_json::to_writer(&mut *self.dst, &val)? + }; + self.dst.write_all(b"\n")?; + self.dst.flush() + } +} + +#[derive(Serialize)] +#[serde(tag = "$message_type", rename_all = "snake_case")] +enum EmitTyped<'a> { + Diagnostic(Diagnostic), + Artifact(ArtifactNotification<'a>), + FutureIncompat(FutureIncompatReport<'a>), + UnusedExtern(UnusedExterns<'a>), +} + +impl Translate for JsonEmitter { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + self.fluent_bundle.as_deref() + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &self.fallback_bundle + } +} + +impl Emitter for JsonEmitter { + fn emit_diagnostic(&mut self, diag: crate::DiagInner, registry: &Registry) { + let data = Diagnostic::from_errors_diagnostic(diag, self, registry); + let result = self.emit(EmitTyped::Diagnostic(data)); + if let Err(e) = result { + panic!("failed to print diagnostics: {e:?}"); + } + } + + fn emit_artifact_notification(&mut self, path: &Path, artifact_type: &str) { + let data = ArtifactNotification { artifact: path, emit: artifact_type }; + let result = self.emit(EmitTyped::Artifact(data)); + if let Err(e) = result { + panic!("failed to print notification: {e:?}"); + } + } + + fn emit_future_breakage_report(&mut self, diags: Vec<crate::DiagInner>, registry: &Registry) { + let data: Vec<FutureBreakageItem<'_>> = diags + .into_iter() + .map(|mut diag| { + // Allowed or expected lints don't normally (by definition) emit a lint + // but future incompat lints are special and are emitted anyway. + // + // So to avoid ICEs and confused users we "upgrade" the lint level for + // those `FutureBreakageItem` to warn. + if matches!(diag.level, crate::Level::Allow | crate::Level::Expect(..)) { + diag.level = crate::Level::Warning; + } + FutureBreakageItem { + diagnostic: EmitTyped::Diagnostic(Diagnostic::from_errors_diagnostic( + diag, self, registry, + )), + } + }) + .collect(); + let report = FutureIncompatReport { future_incompat_report: data }; + let result = self.emit(EmitTyped::FutureIncompat(report)); + if let Err(e) = result { + panic!("failed to print future breakage report: {e:?}"); + } + } + + fn emit_unused_externs(&mut self, lint_level: rustc_lint_defs::Level, unused_externs: &[&str]) { + let lint_level = lint_level.as_str(); + let data = UnusedExterns { lint_level, unused_extern_names: unused_externs }; + let result = self.emit(EmitTyped::UnusedExtern(data)); + if let Err(e) = result { + panic!("failed to print unused externs: {e:?}"); + } + } + + fn source_map(&self) -> Option<&SourceMap> { + self.sm.as_deref() + } + + fn should_show_explain(&self) -> bool { + !self.json_rendered.short() + } +} + +// The following data types are provided just for serialisation. + +#[derive(Serialize)] +struct Diagnostic { + /// The primary error message. + message: String, + code: Option<DiagnosticCode>, + /// "error: internal compiler error", "error", "warning", "note", "help". + level: &'static str, + spans: Vec<DiagnosticSpan>, + /// Associated diagnostic messages. + children: Vec<Diagnostic>, + /// The message as rustc would render it. + rendered: Option<String>, +} + +#[derive(Serialize)] +struct DiagnosticSpan { + file_name: String, + byte_start: u32, + byte_end: u32, + /// 1-based. + line_start: usize, + line_end: usize, + /// 1-based, character offset. + column_start: usize, + column_end: usize, + /// Is this a "primary" span -- meaning the point, or one of the points, + /// where the error occurred? + is_primary: bool, + /// Source text from the start of line_start to the end of line_end. + text: Vec<DiagnosticSpanLine>, + /// Label that should be placed at this location (if any) + label: Option<String>, + /// If we are suggesting a replacement, this will contain text + /// that should be sliced in atop this span. + suggested_replacement: Option<String>, + /// If the suggestion is approximate + suggestion_applicability: Option<Applicability>, + /// Macro invocations that created the code at this span, if any. + expansion: Option<Box<DiagnosticSpanMacroExpansion>>, +} + +#[derive(Serialize)] +struct DiagnosticSpanLine { + text: String, + + /// 1-based, character offset in self.text. + highlight_start: usize, + + highlight_end: usize, +} + +#[derive(Serialize)] +struct DiagnosticSpanMacroExpansion { + /// span where macro was applied to generate this code; note that + /// this may itself derive from a macro (if + /// `span.expansion.is_some()`) + span: DiagnosticSpan, + + /// name of macro that was applied (e.g., "foo!" or "#[derive(Eq)]") + macro_decl_name: String, + + /// span where macro was defined (if known) + def_site_span: DiagnosticSpan, +} + +#[derive(Serialize)] +struct DiagnosticCode { + /// The error code (e.g. "E1234"), if the diagnostic has one. Or the lint + /// name, if it's a lint without an error code. + code: String, + /// An explanation for the code. + explanation: Option<&'static str>, +} + +#[derive(Serialize)] +struct ArtifactNotification<'a> { + /// The path of the artifact. + artifact: &'a Path, + /// What kind of artifact we're emitting. + emit: &'a str, +} + +#[derive(Serialize)] +struct FutureBreakageItem<'a> { + // Always EmitTyped::Diagnostic, but we want to make sure it gets serialized + // with "$message_type". + diagnostic: EmitTyped<'a>, +} + +#[derive(Serialize)] +struct FutureIncompatReport<'a> { + future_incompat_report: Vec<FutureBreakageItem<'a>>, +} + +// NOTE: Keep this in sync with the equivalent structs in rustdoc's +// doctest component (as well as cargo). +// We could unify this struct the one in rustdoc but they have different +// ownership semantics, so doing so would create wasteful allocations. +#[derive(Serialize)] +struct UnusedExterns<'a> { + /// The severity level of the unused dependencies lint + lint_level: &'a str, + /// List of unused externs by their names. + unused_extern_names: &'a [&'a str], +} + +impl Diagnostic { + /// Converts from `rustc_errors::DiagInner` to `Diagnostic`. + fn from_errors_diagnostic( + diag: crate::DiagInner, + je: &JsonEmitter, + registry: &Registry, + ) -> Diagnostic { + let args = to_fluent_args(diag.args.iter()); + let sugg_to_diag = |sugg: &CodeSuggestion| { + let translated_message = + je.translate_message(&sugg.msg, &args).map_err(Report::new).unwrap(); + Diagnostic { + message: translated_message.to_string(), + code: None, + level: "help", + spans: DiagnosticSpan::from_suggestion(sugg, &args, je), + children: vec![], + rendered: None, + } + }; + let sugg = match &diag.suggestions { + Suggestions::Enabled(suggestions) => suggestions.iter().map(sugg_to_diag), + Suggestions::Sealed(suggestions) => suggestions.iter().map(sugg_to_diag), + Suggestions::Disabled => [].iter().map(sugg_to_diag), + }; + + // generate regular command line output and store it in the json + + // A threadsafe buffer for writing. + #[derive(Default, Clone)] + struct BufWriter(Arc<Mutex<Vec<u8>>>); + + impl Write for BufWriter { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.0.lock().unwrap().write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.0.lock().unwrap().flush() + } + } + impl WriteColor for BufWriter { + fn supports_color(&self) -> bool { + false + } + + fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { + Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let translated_message = je.translate_messages(&diag.messages, &args); + + let code = if let Some(code) = diag.code { + Some(DiagnosticCode { + code: code.to_string(), + explanation: registry.try_find_description(code).ok(), + }) + } else if let Some(IsLint { name, .. }) = &diag.is_lint { + Some(DiagnosticCode { code: name.to_string(), explanation: None }) + } else { + None + }; + let level = diag.level.to_str(); + let spans = DiagnosticSpan::from_multispan(&diag.span, &args, je); + let children = diag + .children + .iter() + .map(|c| Diagnostic::from_sub_diagnostic(c, &args, je)) + .chain(sugg) + .collect(); + + let buf = BufWriter::default(); + let mut dst: Destination = Box::new(buf.clone()); + let short = je.json_rendered.short(); + match je.color_config { + ColorConfig::Always | ColorConfig::Auto => dst = Box::new(termcolor::Ansi::new(dst)), + ColorConfig::Never => {} + } + HumanEmitter::new(dst, Arc::clone(&je.fallback_bundle)) + .short_message(short) + .sm(je.sm.clone()) + .fluent_bundle(je.fluent_bundle.clone()) + .diagnostic_width(je.diagnostic_width) + .macro_backtrace(je.macro_backtrace) + .track_diagnostics(je.track_diagnostics) + .terminal_url(je.terminal_url) + .ui_testing(je.ui_testing) + .ignored_directories_in_source_blocks(je.ignored_directories_in_source_blocks.clone()) + .theme(if let HumanReadableErrorType::Unicode = je.json_rendered { + OutputTheme::Unicode + } else { + OutputTheme::Ascii + }) + .emit_diagnostic(diag, registry); + let buf = Arc::try_unwrap(buf.0).unwrap().into_inner().unwrap(); + let buf = String::from_utf8(buf).unwrap(); + + Diagnostic { + message: translated_message.to_string(), + code, + level, + spans, + children, + rendered: Some(buf), + } + } + + fn from_sub_diagnostic( + subdiag: &Subdiag, + args: &FluentArgs<'_>, + je: &JsonEmitter, + ) -> Diagnostic { + let translated_message = je.translate_messages(&subdiag.messages, args); + Diagnostic { + message: translated_message.to_string(), + code: None, + level: subdiag.level.to_str(), + spans: DiagnosticSpan::from_multispan(&subdiag.span, args, je), + children: vec![], + rendered: None, + } + } +} + +impl DiagnosticSpan { + fn from_span_label( + span: SpanLabel, + suggestion: Option<(&String, Applicability)>, + args: &FluentArgs<'_>, + je: &JsonEmitter, + ) -> DiagnosticSpan { + Self::from_span_etc( + span.span, + span.is_primary, + span.label + .as_ref() + .map(|m| je.translate_message(m, args).unwrap()) + .map(|m| m.to_string()), + suggestion, + je, + ) + } + + fn from_span_etc( + span: Span, + is_primary: bool, + label: Option<String>, + suggestion: Option<(&String, Applicability)>, + je: &JsonEmitter, + ) -> DiagnosticSpan { + // obtain the full backtrace from the `macro_backtrace` + // helper; in some ways, it'd be better to expand the + // backtrace ourselves, but the `macro_backtrace` helper makes + // some decision, such as dropping some frames, and I don't + // want to duplicate that logic here. + let backtrace = span.macro_backtrace(); + DiagnosticSpan::from_span_full(span, is_primary, label, suggestion, backtrace, je) + } + + fn from_span_full( + mut span: Span, + is_primary: bool, + label: Option<String>, + suggestion: Option<(&String, Applicability)>, + mut backtrace: impl Iterator<Item = ExpnData>, + je: &JsonEmitter, + ) -> DiagnosticSpan { + let empty_source_map; + let sm = match &je.sm { + Some(s) => s, + None => { + span = rustc_span::DUMMY_SP; + empty_source_map = Arc::new(SourceMap::new(FilePathMapping::empty())); + empty_source_map + .new_source_file(std::path::PathBuf::from("empty.rs").into(), String::new()); + &empty_source_map + } + }; + let start = sm.lookup_char_pos(span.lo()); + // If this goes from the start of a line to the end and the replacement + // is an empty string, increase the length to include the newline so we don't + // leave an empty line + if start.col.0 == 0 + && let Some((suggestion, _)) = suggestion + && suggestion.is_empty() + && let Ok(after) = sm.span_to_next_source(span) + && after.starts_with('\n') + { + span = span.with_hi(span.hi() + rustc_span::BytePos(1)); + } + let end = sm.lookup_char_pos(span.hi()); + let backtrace_step = backtrace.next().map(|bt| { + let call_site = Self::from_span_full(bt.call_site, false, None, None, backtrace, je); + let def_site_span = Self::from_span_full( + sm.guess_head_span(bt.def_site), + false, + None, + None, + [].into_iter(), + je, + ); + Box::new(DiagnosticSpanMacroExpansion { + span: call_site, + macro_decl_name: bt.kind.descr(), + def_site_span, + }) + }); + + DiagnosticSpan { + file_name: sm.filename_for_diagnostics(&start.file.name).to_string(), + byte_start: start.file.original_relative_byte_pos(span.lo()).0, + byte_end: start.file.original_relative_byte_pos(span.hi()).0, + line_start: start.line, + line_end: end.line, + column_start: start.col.0 + 1, + column_end: end.col.0 + 1, + is_primary, + text: DiagnosticSpanLine::from_span(span, je), + suggested_replacement: suggestion.map(|x| x.0.clone()), + suggestion_applicability: suggestion.map(|x| x.1), + expansion: backtrace_step, + label, + } + } + + fn from_multispan( + msp: &MultiSpan, + args: &FluentArgs<'_>, + je: &JsonEmitter, + ) -> Vec<DiagnosticSpan> { + msp.span_labels() + .into_iter() + .map(|span_str| Self::from_span_label(span_str, None, args, je)) + .collect() + } + + fn from_suggestion( + suggestion: &CodeSuggestion, + args: &FluentArgs<'_>, + je: &JsonEmitter, + ) -> Vec<DiagnosticSpan> { + suggestion + .substitutions + .iter() + .flat_map(|substitution| { + substitution.parts.iter().map(move |suggestion_inner| { + let span_label = + SpanLabel { span: suggestion_inner.span, is_primary: true, label: None }; + DiagnosticSpan::from_span_label( + span_label, + Some((&suggestion_inner.snippet, suggestion.applicability)), + args, + je, + ) + }) + }) + .collect() + } +} + +impl DiagnosticSpanLine { + fn line_from_source_file( + sf: &rustc_span::SourceFile, + index: usize, + h_start: usize, + h_end: usize, + ) -> DiagnosticSpanLine { + DiagnosticSpanLine { + text: sf.get_line(index).map_or_else(String::new, |l| l.into_owned()), + highlight_start: h_start, + highlight_end: h_end, + } + } + + /// Creates a list of DiagnosticSpanLines from span - each line with any part + /// of `span` gets a DiagnosticSpanLine, with the highlight indicating the + /// `span` within the line. + fn from_span(span: Span, je: &JsonEmitter) -> Vec<DiagnosticSpanLine> { + je.sm + .as_ref() + .and_then(|sm| { + let lines = sm.span_to_lines(span).ok()?; + // We can't get any lines if the source is unavailable. + if !should_show_source_code( + &je.ignored_directories_in_source_blocks, + &sm, + &lines.file, + ) { + return None; + } + + let sf = &*lines.file; + let span_lines = lines + .lines + .iter() + .map(|line| { + DiagnosticSpanLine::line_from_source_file( + sf, + line.line_index, + line.start_col.0 + 1, + line.end_col.0 + 1, + ) + }) + .collect(); + Some(span_lines) + }) + .unwrap_or_default() + } +} diff --git a/compiler/rustc_errors/src/json/tests.rs b/compiler/rustc_errors/src/json/tests.rs new file mode 100644 index 00000000000..f7892db2961 --- /dev/null +++ b/compiler/rustc_errors/src/json/tests.rs @@ -0,0 +1,164 @@ +use std::str; + +use rustc_span::BytePos; +use rustc_span::source_map::FilePathMapping; +use serde::Deserialize; + +use super::*; +use crate::DiagCtxt; + +#[derive(Deserialize, Debug, PartialEq, Eq)] +struct TestData { + spans: Vec<SpanTestData>, +} + +#[derive(Deserialize, Debug, PartialEq, Eq)] +struct SpanTestData { + pub byte_start: u32, + pub byte_end: u32, + pub line_start: u32, + pub column_start: u32, + pub line_end: u32, + pub column_end: u32, +} + +struct Shared<T> { + data: Arc<Mutex<T>>, +} + +impl<T: Write> Write for Shared<T> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.data.lock().unwrap().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.data.lock().unwrap().flush() + } +} + +/// Test the span yields correct positions in JSON. +fn test_positions(code: &str, span: (u32, u32), expected_output: SpanTestData) { + rustc_span::create_default_session_globals_then(|| { + let sm = Arc::new(SourceMap::new(FilePathMapping::empty())); + sm.new_source_file(Path::new("test.rs").to_owned().into(), code.to_owned()); + let fallback_bundle = + crate::fallback_fluent_bundle(vec![crate::DEFAULT_LOCALE_RESOURCE], false); + + let output = Arc::new(Mutex::new(Vec::new())); + let je = JsonEmitter::new( + Box::new(Shared { data: output.clone() }), + Some(sm), + fallback_bundle, + true, // pretty + HumanReadableErrorType::Short, + ColorConfig::Never, + ); + + let span = Span::with_root_ctxt(BytePos(span.0), BytePos(span.1)); + DiagCtxt::new(Box::new(je)).handle().span_err(span, "foo"); + + let bytes = output.lock().unwrap(); + let actual_output = str::from_utf8(&bytes).unwrap(); + let actual_output: TestData = serde_json::from_str(actual_output).unwrap(); + let spans = actual_output.spans; + assert_eq!(spans.len(), 1); + + assert_eq!(expected_output, spans[0]) + }) +} + +#[test] +fn empty() { + test_positions(" ", (0, 1), SpanTestData { + byte_start: 0, + byte_end: 1, + line_start: 1, + column_start: 1, + line_end: 1, + column_end: 2, + }) +} + +#[test] +fn bom() { + test_positions("\u{feff} ", (0, 1), SpanTestData { + byte_start: 3, + byte_end: 4, + line_start: 1, + column_start: 1, + line_end: 1, + column_end: 2, + }) +} + +#[test] +fn lf_newlines() { + test_positions("\nmod foo;\nmod bar;\n", (5, 12), SpanTestData { + byte_start: 5, + byte_end: 12, + line_start: 2, + column_start: 5, + line_end: 3, + column_end: 3, + }) +} + +#[test] +fn crlf_newlines() { + test_positions("\r\nmod foo;\r\nmod bar;\r\n", (5, 12), SpanTestData { + byte_start: 6, + byte_end: 14, + line_start: 2, + column_start: 5, + line_end: 3, + column_end: 3, + }) +} + +#[test] +fn crlf_newlines_with_bom() { + test_positions("\u{feff}\r\nmod foo;\r\nmod bar;\r\n", (5, 12), SpanTestData { + byte_start: 9, + byte_end: 17, + line_start: 2, + column_start: 5, + line_end: 3, + column_end: 3, + }) +} + +#[test] +fn span_before_crlf() { + test_positions("foo\r\nbar", (2, 3), SpanTestData { + byte_start: 2, + byte_end: 3, + line_start: 1, + column_start: 3, + line_end: 1, + column_end: 4, + }) +} + +#[test] +fn span_on_crlf() { + test_positions("foo\r\nbar", (3, 4), SpanTestData { + byte_start: 3, + byte_end: 5, + line_start: 1, + column_start: 4, + line_end: 2, + column_end: 1, + }) +} + +#[test] +fn span_after_crlf() { + test_positions("foo\r\nbar", (4, 5), SpanTestData { + byte_start: 5, + byte_end: 6, + line_start: 2, + column_start: 1, + line_end: 2, + column_end: 2, + }) +} diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs new file mode 100644 index 00000000000..9af17db9a6e --- /dev/null +++ b/compiler/rustc_errors/src/lib.rs @@ -0,0 +1,1994 @@ +//! Diagnostics creation and emission for `rustc`. +//! +//! This module contains the code for creating and emitting diagnostics. + +// tidy-alphabetical-start +#![allow(incomplete_features)] +#![allow(internal_features)] +#![allow(rustc::diagnostic_outside_of_impl)] +#![allow(rustc::untranslatable_diagnostic)] +#![doc(html_root_url = "https://doc.rust-lang.org/nightly/nightly-rustc/")] +#![doc(rust_logo)] +#![feature(array_windows)] +#![feature(assert_matches)] +#![feature(associated_type_defaults)] +#![feature(box_into_inner)] +#![feature(box_patterns)] +#![feature(error_reporter)] +#![feature(extract_if)] +#![feature(if_let_guard)] +#![feature(let_chains)] +#![feature(negative_impls)] +#![feature(never_type)] +#![feature(rustc_attrs)] +#![feature(rustdoc_internals)] +#![feature(trait_alias)] +#![feature(try_blocks)] +#![feature(yeet_expr)] +#![warn(unreachable_pub)] +// tidy-alphabetical-end + +extern crate self as rustc_errors; + +use std::assert_matches::assert_matches; +use std::backtrace::{Backtrace, BacktraceStatus}; +use std::borrow::Cow; +use std::cell::Cell; +use std::error::Report; +use std::ffi::OsStr; +use std::hash::Hash; +use std::io::Write; +use std::num::NonZero; +use std::ops::DerefMut; +use std::path::{Path, PathBuf}; +use std::{fmt, panic}; + +use Level::*; +pub use codes::*; +pub use diagnostic::{ + BugAbort, Diag, DiagArg, DiagArgMap, DiagArgName, DiagArgValue, DiagInner, DiagStyledString, + Diagnostic, EmissionGuarantee, FatalAbort, IntoDiagArg, LintDiagnostic, StringPart, Subdiag, + SubdiagMessageOp, Subdiagnostic, +}; +pub use diagnostic_impls::{ + DiagArgFromDisplay, DiagSymbolList, ElidedLifetimeInPathSubdiag, ExpectedLifetimeParameter, + IndicateAnonymousLifetime, SingleLabelManySpans, +}; +pub use emitter::ColorConfig; +use emitter::{DynEmitter, Emitter, is_case_difference, is_different}; +use rustc_data_structures::AtomicRef; +use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; +use rustc_data_structures::stable_hasher::{Hash128, StableHasher}; +use rustc_data_structures::sync::{DynSend, Lock}; +pub use rustc_error_messages::{ + DiagMessage, FluentBundle, LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel, + SubdiagMessage, fallback_fluent_bundle, fluent_bundle, +}; +use rustc_lint_defs::LintExpectationId; +pub use rustc_lint_defs::{Applicability, listify, pluralize}; +use rustc_macros::{Decodable, Encodable}; +pub use rustc_span::ErrorGuaranteed; +pub use rustc_span::fatal_error::{FatalError, FatalErrorMarker}; +use rustc_span::source_map::SourceMap; +use rustc_span::{DUMMY_SP, Loc, Span}; +pub use snippet::Style; +// Used by external projects such as `rust-gpu`. +// See https://github.com/rust-lang/rust/pull/115393. +pub use termcolor::{Color, ColorSpec, WriteColor}; +use tracing::debug; + +use crate::registry::Registry; + +pub mod annotate_snippet_emitter_writer; +pub mod codes; +mod diagnostic; +mod diagnostic_impls; +pub mod emitter; +pub mod error; +pub mod json; +mod lock; +pub mod markdown; +pub mod registry; +mod snippet; +mod styled_buffer; +#[cfg(test)] +mod tests; +pub mod translation; + +pub type PResult<'a, T> = Result<T, Diag<'a>>; + +rustc_fluent_macro::fluent_messages! { "../messages.ftl" } + +// `PResult` is used a lot. Make sure it doesn't unintentionally get bigger. +#[cfg(target_pointer_width = "64")] +rustc_data_structures::static_assert_size!(PResult<'_, ()>, 24); +#[cfg(target_pointer_width = "64")] +rustc_data_structures::static_assert_size!(PResult<'_, bool>, 24); + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Encodable, Decodable)] +pub enum SuggestionStyle { + /// Hide the suggested code when displaying this suggestion inline. + HideCodeInline, + /// Always hide the suggested code but display the message. + HideCodeAlways, + /// Do not display this suggestion in the cli output, it is only meant for tools. + CompletelyHidden, + /// Always show the suggested code. + /// This will *not* show the code if the suggestion is inline *and* the suggested code is + /// empty. + ShowCode, + /// Always show the suggested code independently. + ShowAlways, +} + +impl SuggestionStyle { + fn hide_inline(&self) -> bool { + !matches!(*self, SuggestionStyle::ShowCode) + } +} + +/// Represents the help messages seen on a diagnostic. +#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)] +pub enum Suggestions { + /// Indicates that new suggestions can be added or removed from this diagnostic. + /// + /// `DiagInner`'s new_* methods initialize the `suggestions` field with + /// this variant. Also, this is the default variant for `Suggestions`. + Enabled(Vec<CodeSuggestion>), + /// Indicates that suggestions cannot be added or removed from this diagnostic. + /// + /// Gets toggled when `.seal_suggestions()` is called on the `DiagInner`. + Sealed(Box<[CodeSuggestion]>), + /// Indicates that no suggestion is available for this diagnostic. + /// + /// Gets toggled when `.disable_suggestions()` is called on the `DiagInner`. + Disabled, +} + +impl Suggestions { + /// Returns the underlying list of suggestions. + pub fn unwrap_tag(self) -> Vec<CodeSuggestion> { + match self { + Suggestions::Enabled(suggestions) => suggestions, + Suggestions::Sealed(suggestions) => suggestions.into_vec(), + Suggestions::Disabled => Vec::new(), + } + } +} + +impl Default for Suggestions { + fn default() -> Self { + Self::Enabled(vec![]) + } +} + +#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)] +pub struct CodeSuggestion { + /// Each substitute can have multiple variants due to multiple + /// applicable suggestions + /// + /// `foo.bar` might be replaced with `a.b` or `x.y` by replacing + /// `foo` and `bar` on their own: + /// + /// ```ignore (illustrative) + /// vec![ + /// Substitution { parts: vec![(0..3, "a"), (4..7, "b")] }, + /// Substitution { parts: vec![(0..3, "x"), (4..7, "y")] }, + /// ] + /// ``` + /// + /// or by replacing the entire span: + /// + /// ```ignore (illustrative) + /// vec![ + /// Substitution { parts: vec![(0..7, "a.b")] }, + /// Substitution { parts: vec![(0..7, "x.y")] }, + /// ] + /// ``` + pub substitutions: Vec<Substitution>, + pub msg: DiagMessage, + /// Visual representation of this suggestion. + pub style: SuggestionStyle, + /// Whether or not the suggestion is approximate + /// + /// Sometimes we may show suggestions with placeholders, + /// which are useful for users but not useful for + /// tools like rustfix + pub applicability: Applicability, +} + +#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)] +/// See the docs on `CodeSuggestion::substitutions` +pub struct Substitution { + pub parts: Vec<SubstitutionPart>, +} + +#[derive(Clone, Debug, PartialEq, Hash, Encodable, Decodable)] +pub struct SubstitutionPart { + pub span: Span, + pub snippet: String, +} + +/// Used to translate between `Span`s and byte positions within a single output line in highlighted +/// code of structured suggestions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SubstitutionHighlight { + start: usize, + end: usize, +} + +impl SubstitutionPart { + pub fn is_addition(&self, sm: &SourceMap) -> bool { + !self.snippet.is_empty() && !self.replaces_meaningful_content(sm) + } + + pub fn is_deletion(&self, sm: &SourceMap) -> bool { + self.snippet.trim().is_empty() && self.replaces_meaningful_content(sm) + } + + pub fn is_replacement(&self, sm: &SourceMap) -> bool { + !self.snippet.is_empty() && self.replaces_meaningful_content(sm) + } + + fn replaces_meaningful_content(&self, sm: &SourceMap) -> bool { + sm.span_to_snippet(self.span) + .map_or(!self.span.is_empty(), |snippet| !snippet.trim().is_empty()) + } +} + +impl CodeSuggestion { + /// Returns the assembled code suggestions, whether they should be shown with an underline + /// and whether the substitution only differs in capitalization. + pub(crate) fn splice_lines( + &self, + sm: &SourceMap, + ) -> Vec<(String, Vec<SubstitutionPart>, Vec<Vec<SubstitutionHighlight>>, bool)> { + // For the `Vec<Vec<SubstitutionHighlight>>` value, the first level of the vector + // corresponds to the output snippet's lines, while the second level corresponds to the + // substrings within that line that should be highlighted. + + use rustc_span::{CharPos, Pos}; + + /// Extracts a substring from the provided `line_opt` based on the specified low and high + /// indices, appends it to the given buffer `buf`, and returns the count of newline + /// characters in the substring for accurate highlighting. If `line_opt` is `None`, a + /// newline character is appended to the buffer, and 0 is returned. + /// + /// ## Returns + /// + /// The count of newline characters in the extracted substring. + fn push_trailing( + buf: &mut String, + line_opt: Option<&Cow<'_, str>>, + lo: &Loc, + hi_opt: Option<&Loc>, + ) -> usize { + let mut line_count = 0; + // Convert CharPos to Usize, as CharPose is character offset + // Extract low index and high index + let (lo, hi_opt) = (lo.col.to_usize(), hi_opt.map(|hi| hi.col.to_usize())); + if let Some(line) = line_opt { + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { + // Get high index while account for rare unicode and emoji with char_indices + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); + match hi_opt { + // If high index exist, take string from low to high index + Some(hi) if hi > lo => { + // count how many '\n' exist + line_count = line[lo..hi].matches('\n').count(); + buf.push_str(&line[lo..hi]) + } + Some(_) => (), + // If high index absence, take string from low index till end string.len + None => { + // count how many '\n' exist + line_count = line[lo..].matches('\n').count(); + buf.push_str(&line[lo..]) + } + } + } + // If high index is None + if hi_opt.is_none() { + buf.push('\n'); + } + } + line_count + } + + assert!(!self.substitutions.is_empty()); + + self.substitutions + .iter() + .filter(|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!("splice_lines: suggestion contains an invalid span: {:?}", subst); + } + !invalid + }) + .cloned() + .filter_map(|mut substitution| { + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + substitution.parts.sort_by_key(|part| part.span.lo()); + + // Find the bounding span. + let lo = substitution.parts.iter().map(|part| part.span.lo()).min()?; + let hi = substitution.parts.iter().map(|part| part.span.hi()).max()?; + let bounding_span = Span::with_root_ctxt(lo, hi); + // The different spans might belong to different contexts, if so ignore suggestion. + let lines = sm.span_to_lines(bounding_span).ok()?; + assert!(!lines.lines.is_empty() || bounding_span.is_dummy()); + + // We can't splice anything if the source is unavailable. + if !sm.ensure_source_file_source_present(&lines.file) { + return None; + } + + let mut highlights = vec![]; + // To build up the result, we do this for each span: + // - push the line segment trailing the previous span + // (at the beginning a "phantom" span pointing at the start of the line) + // - push lines between the previous and current span (if any) + // - if the previous and current span are not on the same line + // push the line segment leading up to the current span + // - splice in the span substitution + // + // Finally push the trailing line segment of the last span + let sf = &lines.file; + let mut prev_hi = sm.lookup_char_pos(bounding_span.lo()); + prev_hi.col = CharPos::from_usize(0); + let mut prev_line = + lines.lines.get(0).and_then(|line0| sf.get_line(line0.line_index)); + let mut buf = String::new(); + + let mut line_highlight = vec![]; + // We need to keep track of the difference between the existing code and the added + // or deleted code in order to point at the correct column *after* substitution. + let mut acc = 0; + let mut only_capitalization = false; + for part in &substitution.parts { + only_capitalization |= is_case_difference(sm, &part.snippet, part.span); + let cur_lo = sm.lookup_char_pos(part.span.lo()); + if prev_hi.line == cur_lo.line { + let mut count = + push_trailing(&mut buf, prev_line.as_ref(), &prev_hi, Some(&cur_lo)); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + acc = 0; + count -= 1; + } + } else { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let mut count = push_trailing(&mut buf, prev_line.as_ref(), &prev_hi, None); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + count -= 1; + } + // push lines between the previous and current span (if any) + for idx in prev_hi.line..(cur_lo.line - 1) { + if let Some(line) = sf.get_line(idx) { + buf.push_str(line.as_ref()); + buf.push('\n'); + highlights.push(std::mem::take(&mut line_highlight)); + } + } + if let Some(cur_line) = sf.get_line(cur_lo.line - 1) { + let end = match cur_line.char_indices().nth(cur_lo.col.to_usize()) { + Some((i, _)) => i, + None => cur_line.len(), + }; + buf.push_str(&cur_line[..end]); + } + } + // Add a whole line highlight per line in the snippet. + let len: isize = part + .snippet + .split('\n') + .next() + .unwrap_or(&part.snippet) + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + if !is_different(sm, &part.snippet, part.span) { + // 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. + } else { + line_highlight.push(SubstitutionHighlight { + start: (cur_lo.col.0 as isize + acc) as usize, + end: (cur_lo.col.0 as isize + acc + len) as usize, + }); + } + buf.push_str(&part.snippet); + let cur_hi = sm.lookup_char_pos(part.span.hi()); + // Account for the difference between the width of the current code and the + // snippet being suggested, so that the *later* suggestions are correctly + // aligned on the screen. Note that cur_hi and cur_lo can be on different + // lines, so cur_hi.col can be smaller than cur_lo.col + acc += len - (cur_hi.col.0 as isize - cur_lo.col.0 as isize); + prev_hi = cur_hi; + prev_line = sf.get_line(prev_hi.line - 1); + for line in part.snippet.split('\n').skip(1) { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let end: usize = line + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { start: 0, end }); + } + } + highlights.push(std::mem::take(&mut line_highlight)); + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line.as_ref(), &prev_hi, None); + } + // remove trailing newlines + while buf.ends_with('\n') { + buf.pop(); + } + if highlights.iter().all(|parts| parts.is_empty()) { + None + } else { + Some((buf, substitution.parts, highlights, only_capitalization)) + } + }) + .collect() + } +} + +/// Signifies that the compiler died with an explicit call to `.bug` +/// or `.span_bug` rather than a failed assertion, etc. +pub struct ExplicitBug; + +/// Signifies that the compiler died due to a delayed bug rather than a failed +/// assertion, etc. +pub struct DelayedBugPanic; + +/// A `DiagCtxt` deals with errors and other compiler output. +/// Certain errors (fatal, bug, unimpl) may cause immediate exit, +/// others log errors for later reporting. +pub struct DiagCtxt { + inner: Lock<DiagCtxtInner>, +} + +#[derive(Copy, Clone)] +pub struct DiagCtxtHandle<'a> { + dcx: &'a DiagCtxt, + /// Some contexts create `DiagCtxtHandle` with this field set, and thus all + /// errors emitted with it will automatically taint when emitting errors. + tainted_with_errors: Option<&'a Cell<Option<ErrorGuaranteed>>>, +} + +impl<'a> std::ops::Deref for DiagCtxtHandle<'a> { + type Target = &'a DiagCtxt; + + fn deref(&self) -> &Self::Target { + &self.dcx + } +} + +/// This inner struct exists to keep it all behind a single lock; +/// this is done to prevent possible deadlocks in a multi-threaded compiler, +/// as well as inconsistent state observation. +struct DiagCtxtInner { + flags: DiagCtxtFlags, + + registry: Registry, + + /// The error guarantees from all emitted errors. The length gives the error count. + err_guars: Vec<ErrorGuaranteed>, + /// The error guarantee from all emitted lint errors. The length gives the + /// lint error count. + lint_err_guars: Vec<ErrorGuaranteed>, + /// The delayed bugs and their error guarantees. + delayed_bugs: Vec<(DelayedDiagInner, ErrorGuaranteed)>, + + /// The error count shown to the user at the end. + deduplicated_err_count: usize, + /// The warning count shown to the user at the end. + deduplicated_warn_count: usize, + + emitter: Box<DynEmitter>, + + /// Must we produce a diagnostic to justify the use of the expensive + /// `trimmed_def_paths` function? Backtrace is the location of the call. + must_produce_diag: Option<Backtrace>, + + /// Has this diagnostic context printed any diagnostics? (I.e. has + /// `self.emitter.emit_diagnostic()` been called? + has_printed: bool, + + /// This flag indicates that an expected diagnostic was emitted and suppressed. + /// This is used for the `must_produce_diag` check. + suppressed_expected_diag: bool, + + /// This set contains the code of all emitted diagnostics to avoid + /// emitting the same diagnostic with extended help (`--teach`) twice, which + /// would be unnecessary repetition. + taught_diagnostics: FxHashSet<ErrCode>, + + /// Used to suggest rustc --explain `<error code>` + emitted_diagnostic_codes: FxIndexSet<ErrCode>, + + /// This set contains a hash of every diagnostic that has been emitted by + /// this `DiagCtxt`. These hashes is used to avoid emitting the same error + /// twice. + emitted_diagnostics: FxHashSet<Hash128>, + + /// Stashed diagnostics emitted in one stage of the compiler that may be + /// stolen and emitted/cancelled by other stages (e.g. to improve them and + /// add more information). All stashed diagnostics must be emitted with + /// `emit_stashed_diagnostics` by the time the `DiagCtxtInner` is dropped, + /// otherwise an assertion failure will occur. + stashed_diagnostics: FxIndexMap<(Span, StashKey), (DiagInner, Option<ErrorGuaranteed>)>, + + future_breakage_diagnostics: Vec<DiagInner>, + + /// expected diagnostic will have the level `Expect` which additionally + /// carries the [`LintExpectationId`] of the expectation that can be + /// marked as fulfilled. This is a collection of all [`LintExpectationId`]s + /// that have been marked as fulfilled this way. + /// + /// Emitting expectations after having stolen this field can happen. In particular, an + /// `#[expect(warnings)]` can easily make the `UNFULFILLED_LINT_EXPECTATIONS` lint expect + /// itself. To avoid needless complexity in this corner case, we tolerate failing to track + /// those expectations. + /// + /// [RFC-2383]: https://rust-lang.github.io/rfcs/2383-lint-reasons.html + fulfilled_expectations: FxIndexSet<LintExpectationId>, + + /// The file where the ICE information is stored. This allows delayed_span_bug backtraces to be + /// stored along side the main panic backtrace. + ice_file: Option<PathBuf>, +} + +/// A key denoting where from a diagnostic was stashed. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub enum StashKey { + ItemNoType, + UnderscoreForArrayLengths, + EarlySyntaxWarning, + CallIntoMethod, + /// When an invalid lifetime e.g. `'2` should be reinterpreted + /// as a char literal in the parser + LifetimeIsChar, + /// Maybe there was a typo where a comma was forgotten before + /// FRU syntax + MaybeFruTypo, + CallAssocMethod, + AssociatedTypeSuggestion, + MaybeForgetReturn, + /// Query cycle detected, stashing in favor of a better error. + Cycle, + UndeterminedMacroResolution, + /// Used by `Parser::maybe_recover_trailing_expr` + ExprInPat, + /// If in the parser we detect a field expr with turbofish generic params it's possible that + /// it's a method call without parens. If later on in `hir_typeck` we find out that this is + /// the case we suppress this message and we give a better suggestion. + GenericInFieldExpr, +} + +fn default_track_diagnostic<R>(diag: DiagInner, f: &mut dyn FnMut(DiagInner) -> R) -> R { + (*f)(diag) +} + +/// Diagnostics emitted by `DiagCtxtInner::emit_diagnostic` are passed through this function. Used +/// for tracking by incremental, to replay diagnostics as necessary. +pub static TRACK_DIAGNOSTIC: AtomicRef< + fn(DiagInner, &mut dyn FnMut(DiagInner) -> Option<ErrorGuaranteed>) -> Option<ErrorGuaranteed>, +> = AtomicRef::new(&(default_track_diagnostic as _)); + +#[derive(Copy, Clone, Default)] +pub struct DiagCtxtFlags { + /// If false, warning-level lints are suppressed. + /// (rustc: see `--allow warnings` and `--cap-lints`) + pub can_emit_warnings: bool, + /// If Some, the Nth error-level diagnostic is upgraded to bug-level. + /// (rustc: see `-Z treat-err-as-bug`) + pub treat_err_as_bug: Option<NonZero<usize>>, + /// Eagerly emit delayed bugs as errors, so that the compiler debugger may + /// see all of the errors being emitted at once. + pub eagerly_emit_delayed_bugs: bool, + /// Show macro backtraces. + /// (rustc: see `-Z macro-backtrace`) + pub macro_backtrace: bool, + /// If true, identical diagnostics are reported only once. + pub deduplicate_diagnostics: bool, + /// Track where errors are created. Enabled with `-Ztrack-diagnostics`. + pub track_diagnostics: bool, +} + +impl Drop for DiagCtxtInner { + fn drop(&mut self) { + // For tools using `interface::run_compiler` (e.g. rustc, rustdoc) + // stashed diagnostics will have already been emitted. But for others + // that don't use `interface::run_compiler` (e.g. rustfmt, some clippy + // lints) this fallback is necessary. + // + // Important: it is sound to produce an `ErrorGuaranteed` when stashing + // errors because they are guaranteed to be emitted here or earlier. + self.emit_stashed_diagnostics(); + + // Important: it is sound to produce an `ErrorGuaranteed` when emitting + // delayed bugs because they are guaranteed to be emitted here if + // necessary. + self.flush_delayed(); + + // Sanity check: did we use some of the expensive `trimmed_def_paths` functions + // unexpectedly, that is, without producing diagnostics? If so, for debugging purposes, we + // suggest where this happened and how to avoid it. + if !self.has_printed && !self.suppressed_expected_diag && !std::thread::panicking() { + if let Some(backtrace) = &self.must_produce_diag { + let suggestion = match backtrace.status() { + BacktraceStatus::Disabled => String::from( + "Backtraces are currently disabled: set `RUST_BACKTRACE=1` and re-run \ + to see where it happened.", + ), + BacktraceStatus::Captured => format!( + "This happened in the following `must_produce_diag` call's backtrace:\n\ + {backtrace}", + ), + _ => String::from("(impossible to capture backtrace where this happened)"), + }; + panic!( + "`trimmed_def_paths` called, diagnostics were expected but none were emitted. \ + Use `with_no_trimmed_paths` for debugging. {suggestion}" + ); + } + } + } +} + +impl DiagCtxt { + pub fn disable_warnings(mut self) -> Self { + self.inner.get_mut().flags.can_emit_warnings = false; + self + } + + pub fn with_flags(mut self, flags: DiagCtxtFlags) -> Self { + self.inner.get_mut().flags = flags; + self + } + + pub fn with_ice_file(mut self, ice_file: PathBuf) -> Self { + self.inner.get_mut().ice_file = Some(ice_file); + self + } + + pub fn with_registry(mut self, registry: Registry) -> Self { + self.inner.get_mut().registry = registry; + self + } + + pub fn new(emitter: Box<DynEmitter>) -> Self { + Self { inner: Lock::new(DiagCtxtInner::new(emitter)) } + } + + pub fn make_silent(&self, fatal_note: Option<String>, emit_fatal_diagnostic: bool) { + // An empty type that implements `Emitter` to temporarily swap in place of the real one, + // which will be used in constructing its replacement. + struct FalseEmitter; + + impl Emitter for FalseEmitter { + fn emit_diagnostic(&mut self, _: DiagInner, _: &Registry) { + unimplemented!("false emitter must only used during `make_silent`") + } + + fn source_map(&self) -> Option<&SourceMap> { + unimplemented!("false emitter must only used during `make_silent`") + } + } + + impl translation::Translate for FalseEmitter { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + unimplemented!("false emitter must only used during `make_silent`") + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + unimplemented!("false emitter must only used during `make_silent`") + } + } + + let mut inner = self.inner.borrow_mut(); + let mut prev_emitter = Box::new(FalseEmitter) as Box<dyn Emitter + DynSend>; + std::mem::swap(&mut inner.emitter, &mut prev_emitter); + let new_emitter = Box::new(emitter::SilentEmitter { + fatal_emitter: prev_emitter, + fatal_note, + emit_fatal_diagnostic, + }); + inner.emitter = new_emitter; + } + + pub fn set_emitter(&self, emitter: Box<dyn Emitter + DynSend>) { + self.inner.borrow_mut().emitter = emitter; + } + + /// Translate `message` eagerly with `args` to `SubdiagMessage::Eager`. + pub fn eagerly_translate<'a>( + &self, + message: DiagMessage, + args: impl Iterator<Item = DiagArg<'a>>, + ) -> SubdiagMessage { + let inner = self.inner.borrow(); + inner.eagerly_translate(message, args) + } + + /// Translate `message` eagerly with `args` to `String`. + pub fn eagerly_translate_to_string<'a>( + &self, + message: DiagMessage, + args: impl Iterator<Item = DiagArg<'a>>, + ) -> String { + let inner = self.inner.borrow(); + inner.eagerly_translate_to_string(message, args) + } + + // This is here to not allow mutation of flags; + // as of this writing it's used in Session::consider_optimizing and + // in tests in rustc_interface. + pub fn can_emit_warnings(&self) -> bool { + self.inner.borrow_mut().flags.can_emit_warnings + } + + /// Resets the diagnostic error count as well as the cached emitted diagnostics. + /// + /// NOTE: *do not* call this function from rustc. It is only meant to be called from external + /// tools that want to reuse a `Parser` cleaning the previously emitted diagnostics as well as + /// the overall count of emitted error diagnostics. + pub fn reset_err_count(&self) { + // Use destructuring so that if a field gets added to `DiagCtxtInner`, it's impossible to + // fail to update this method as well. + let mut inner = self.inner.borrow_mut(); + let DiagCtxtInner { + flags: _, + registry: _, + err_guars, + lint_err_guars, + delayed_bugs, + deduplicated_err_count, + deduplicated_warn_count, + emitter: _, + must_produce_diag, + has_printed, + suppressed_expected_diag, + taught_diagnostics, + emitted_diagnostic_codes, + emitted_diagnostics, + stashed_diagnostics, + future_breakage_diagnostics, + fulfilled_expectations, + ice_file: _, + } = inner.deref_mut(); + + // For the `Vec`s and `HashMap`s, we overwrite with an empty container to free the + // underlying memory (which `clear` would not do). + *err_guars = Default::default(); + *lint_err_guars = Default::default(); + *delayed_bugs = Default::default(); + *deduplicated_err_count = 0; + *deduplicated_warn_count = 0; + *must_produce_diag = None; + *has_printed = false; + *suppressed_expected_diag = false; + *taught_diagnostics = Default::default(); + *emitted_diagnostic_codes = Default::default(); + *emitted_diagnostics = Default::default(); + *stashed_diagnostics = Default::default(); + *future_breakage_diagnostics = Default::default(); + *fulfilled_expectations = Default::default(); + } + + pub fn handle<'a>(&'a self) -> DiagCtxtHandle<'a> { + DiagCtxtHandle { dcx: self, tainted_with_errors: None } + } + + /// Link this to a taintable context so that emitting errors will automatically set + /// the `Option<ErrorGuaranteed>` instead of having to do that manually at every error + /// emission site. + pub fn taintable_handle<'a>( + &'a self, + tainted_with_errors: &'a Cell<Option<ErrorGuaranteed>>, + ) -> DiagCtxtHandle<'a> { + DiagCtxtHandle { dcx: self, tainted_with_errors: Some(tainted_with_errors) } + } +} + +impl<'a> DiagCtxtHandle<'a> { + /// Stashes a diagnostic for possible later improvement in a different, + /// later stage of the compiler. Possible actions depend on the diagnostic + /// level: + /// - Level::Bug, Level:Fatal: not allowed, will trigger a panic. + /// - Level::Error: immediately counted as an error that has occurred, because it + /// is guaranteed to be emitted eventually. Can be later accessed with the + /// provided `span` and `key` through + /// [`DiagCtxtHandle::try_steal_modify_and_emit_err`] or + /// [`DiagCtxtHandle::try_steal_replace_and_emit_err`]. These do not allow + /// cancellation or downgrading of the error. Returns + /// `Some(ErrorGuaranteed)`. + /// - Level::DelayedBug: this does happen occasionally with errors that are + /// downgraded to delayed bugs. It is not stashed, but immediately + /// emitted as a delayed bug. This is because stashing it would cause it + /// to be counted by `err_count` which we don't want. It doesn't matter + /// that we cannot steal and improve it later, because it's not a + /// user-facing error. Returns `Some(ErrorGuaranteed)` as is normal for + /// delayed bugs. + /// - Level::Warning and lower (i.e. !is_error()): can be accessed with the + /// provided `span` and `key` through [`DiagCtxtHandle::steal_non_err()`]. This + /// allows cancelling and downgrading of the diagnostic. Returns `None`. + pub fn stash_diagnostic( + &self, + span: Span, + key: StashKey, + diag: DiagInner, + ) -> Option<ErrorGuaranteed> { + let guar = match diag.level { + Bug | Fatal => { + self.span_bug( + span, + format!("invalid level in `stash_diagnostic`: {:?}", diag.level), + ); + } + // We delay a bug here so that `-Ztreat-err-as-bug -Zeagerly-emit-delayed-bugs` + // can be used to create a backtrace at the stashing site instead of whenever the + // diagnostic context is dropped and thus delayed bugs are emitted. + Error => Some(self.span_delayed_bug(span, format!("stashing {key:?}"))), + DelayedBug => { + return self.inner.borrow_mut().emit_diagnostic(diag, self.tainted_with_errors); + } + ForceWarning(_) | Warning | Note | OnceNote | Help | OnceHelp | FailureNote | Allow + | Expect(_) => None, + }; + + // FIXME(Centril, #69537): Consider reintroducing panic on overwriting a stashed diagnostic + // if/when we have a more robust macro-friendly replacement for `(span, key)` as a key. + // See the PR for a discussion. + let key = (span.with_parent(None), key); + self.inner.borrow_mut().stashed_diagnostics.insert(key, (diag, guar)); + + guar + } + + /// Steal a previously stashed non-error diagnostic with the given `Span` + /// and [`StashKey`] as the key. Panics if the found diagnostic is an + /// error. + pub fn steal_non_err(self, span: Span, key: StashKey) -> Option<Diag<'a, ()>> { + let key = (span.with_parent(None), key); + // FIXME(#120456) - is `swap_remove` correct? + let (diag, guar) = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key)?; + assert!(!diag.is_error()); + assert!(guar.is_none()); + Some(Diag::new_diagnostic(self, diag)) + } + + /// Steals a previously stashed error with the given `Span` and + /// [`StashKey`] as the key, modifies it, and emits it. Returns `None` if + /// no matching diagnostic is found. Panics if the found diagnostic's level + /// isn't `Level::Error`. + pub fn try_steal_modify_and_emit_err<F>( + self, + span: Span, + key: StashKey, + mut modify_err: F, + ) -> Option<ErrorGuaranteed> + where + F: FnMut(&mut Diag<'_>), + { + let key = (span.with_parent(None), key); + // FIXME(#120456) - is `swap_remove` correct? + let err = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key); + err.map(|(err, guar)| { + // The use of `::<ErrorGuaranteed>` is safe because level is `Level::Error`. + assert_eq!(err.level, Error); + assert!(guar.is_some()); + let mut err = Diag::<ErrorGuaranteed>::new_diagnostic(self, err); + modify_err(&mut err); + assert_eq!(err.level, Error); + err.emit() + }) + } + + /// Steals a previously stashed error with the given `Span` and + /// [`StashKey`] as the key, cancels it if found, and emits `new_err`. + /// Panics if the found diagnostic's level isn't `Level::Error`. + pub fn try_steal_replace_and_emit_err( + self, + span: Span, + key: StashKey, + new_err: Diag<'_>, + ) -> ErrorGuaranteed { + let key = (span.with_parent(None), key); + // FIXME(#120456) - is `swap_remove` correct? + let old_err = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key); + match old_err { + Some((old_err, guar)) => { + assert_eq!(old_err.level, Error); + assert!(guar.is_some()); + // Because `old_err` has already been counted, it can only be + // safely cancelled because the `new_err` supplants it. + Diag::<ErrorGuaranteed>::new_diagnostic(self, old_err).cancel(); + } + None => {} + }; + new_err.emit() + } + + pub fn has_stashed_diagnostic(&self, span: Span, key: StashKey) -> bool { + self.inner.borrow().stashed_diagnostics.get(&(span.with_parent(None), key)).is_some() + } + + /// Emit all stashed diagnostics. + pub fn emit_stashed_diagnostics(&self) -> Option<ErrorGuaranteed> { + self.inner.borrow_mut().emit_stashed_diagnostics() + } + + /// This excludes delayed bugs. + #[inline] + pub fn err_count(&self) -> usize { + let inner = self.inner.borrow(); + inner.err_guars.len() + + inner.lint_err_guars.len() + + inner.stashed_diagnostics.values().filter(|(_diag, guar)| guar.is_some()).count() + } + + /// This excludes lint errors and delayed bugs. Unless absolutely + /// necessary, prefer `has_errors` to this method. + pub fn has_errors_excluding_lint_errors(&self) -> Option<ErrorGuaranteed> { + self.inner.borrow().has_errors_excluding_lint_errors() + } + + /// This excludes delayed bugs. + pub fn has_errors(&self) -> Option<ErrorGuaranteed> { + self.inner.borrow().has_errors() + } + + /// This excludes nothing. Unless absolutely necessary, prefer `has_errors` + /// to this method. + pub fn has_errors_or_delayed_bugs(&self) -> Option<ErrorGuaranteed> { + self.inner.borrow().has_errors_or_delayed_bugs() + } + + pub fn print_error_count(&self) { + let mut inner = self.inner.borrow_mut(); + + // Any stashed diagnostics should have been handled by + // `emit_stashed_diagnostics` by now. + assert!(inner.stashed_diagnostics.is_empty()); + + if inner.treat_err_as_bug() { + return; + } + + let warnings = match inner.deduplicated_warn_count { + 0 => Cow::from(""), + 1 => Cow::from("1 warning emitted"), + count => Cow::from(format!("{count} warnings emitted")), + }; + let errors = match inner.deduplicated_err_count { + 0 => Cow::from(""), + 1 => Cow::from("aborting due to 1 previous error"), + count => Cow::from(format!("aborting due to {count} previous errors")), + }; + + match (errors.len(), warnings.len()) { + (0, 0) => return, + (0, _) => { + // Use `ForceWarning` rather than `Warning` to guarantee emission, e.g. with a + // configuration like `--cap-lints allow --force-warn bare_trait_objects`. + inner.emit_diagnostic( + DiagInner::new(ForceWarning(None), DiagMessage::Str(warnings)), + None, + ); + } + (_, 0) => { + inner.emit_diagnostic(DiagInner::new(Error, errors), self.tainted_with_errors); + } + (_, _) => { + inner.emit_diagnostic( + DiagInner::new(Error, format!("{errors}; {warnings}")), + self.tainted_with_errors, + ); + } + } + + let can_show_explain = inner.emitter.should_show_explain(); + let are_there_diagnostics = !inner.emitted_diagnostic_codes.is_empty(); + if can_show_explain && are_there_diagnostics { + let mut error_codes = inner + .emitted_diagnostic_codes + .iter() + .filter_map(|&code| { + if inner.registry.try_find_description(code).is_ok() { + Some(code.to_string()) + } else { + None + } + }) + .collect::<Vec<_>>(); + if !error_codes.is_empty() { + error_codes.sort(); + if error_codes.len() > 1 { + let limit = if error_codes.len() > 9 { 9 } else { error_codes.len() }; + let msg1 = format!( + "Some errors have detailed explanations: {}{}", + error_codes[..limit].join(", "), + if error_codes.len() > 9 { "..." } else { "." } + ); + let msg2 = format!( + "For more information about an error, try `rustc --explain {}`.", + &error_codes[0] + ); + inner.emit_diagnostic(DiagInner::new(FailureNote, msg1), None); + inner.emit_diagnostic(DiagInner::new(FailureNote, msg2), None); + } else { + let msg = format!( + "For more information about this error, try `rustc --explain {}`.", + &error_codes[0] + ); + inner.emit_diagnostic(DiagInner::new(FailureNote, msg), None); + } + } + } + } + + /// This excludes delayed bugs. Used for early aborts after errors occurred + /// -- e.g. because continuing in the face of errors is likely to lead to + /// bad results, such as spurious/uninteresting additional errors -- when + /// returning an error `Result` is difficult. + pub fn abort_if_errors(&self) { + if let Some(guar) = self.has_errors() { + guar.raise_fatal(); + } + } + + /// `true` if we haven't taught a diagnostic with this code already. + /// The caller must then teach the user about such a diagnostic. + /// + /// Used to suppress emitting the same error multiple times with extended explanation when + /// calling `-Zteach`. + pub fn must_teach(&self, code: ErrCode) -> bool { + self.inner.borrow_mut().taught_diagnostics.insert(code) + } + + pub fn emit_diagnostic(&self, diagnostic: DiagInner) -> Option<ErrorGuaranteed> { + self.inner.borrow_mut().emit_diagnostic(diagnostic, self.tainted_with_errors) + } + + pub fn emit_artifact_notification(&self, path: &Path, artifact_type: &str) { + self.inner.borrow_mut().emitter.emit_artifact_notification(path, artifact_type); + } + + pub fn emit_future_breakage_report(&self) { + let inner = &mut *self.inner.borrow_mut(); + let diags = std::mem::take(&mut inner.future_breakage_diagnostics); + if !diags.is_empty() { + inner.emitter.emit_future_breakage_report(diags, &inner.registry); + } + } + + pub fn emit_unused_externs( + &self, + lint_level: rustc_lint_defs::Level, + loud: bool, + unused_externs: &[&str], + ) { + let mut inner = self.inner.borrow_mut(); + + // This "error" is an odd duck. + // - It's only produce with JSON output. + // - It's not emitted the usual way, via `emit_diagnostic`. + // - The `$message_type` field is "unused_externs" rather than the usual + // "diagnosic". + // + // We count it as a lint error because it has a lint level. The value + // of `loud` (which comes from "unused-externs" or + // "unused-externs-silent"), also affects whether it's treated like a + // hard error or not. + if loud && lint_level.is_error() { + // This `unchecked_error_guaranteed` is valid. It is where the + // `ErrorGuaranteed` for unused_extern errors originates. + #[allow(deprecated)] + inner.lint_err_guars.push(ErrorGuaranteed::unchecked_error_guaranteed()); + inner.panic_if_treat_err_as_bug(); + } + + inner.emitter.emit_unused_externs(lint_level, unused_externs) + } + + /// This methods steals all [`LintExpectationId`]s that are stored inside + /// [`DiagCtxtInner`] and indicate that the linked expectation has been fulfilled. + #[must_use] + pub fn steal_fulfilled_expectation_ids(&self) -> FxIndexSet<LintExpectationId> { + std::mem::take(&mut self.inner.borrow_mut().fulfilled_expectations) + } + + pub fn flush_delayed(&self) { + self.inner.borrow_mut().flush_delayed(); + } + + /// Used when trimmed_def_paths is called and we must produce a diagnostic + /// to justify its cost. + #[track_caller] + pub fn set_must_produce_diag(&self) { + assert!( + self.inner.borrow().must_produce_diag.is_none(), + "should only need to collect a backtrace once" + ); + self.inner.borrow_mut().must_produce_diag = Some(Backtrace::capture()); + } +} + +// This `impl` block contains only the public diagnostic creation/emission API. +// +// Functions beginning with `struct_`/`create_` create a diagnostic. Other +// functions create and emit a diagnostic all in one go. +impl<'a> DiagCtxtHandle<'a> { + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn struct_bug(self, msg: impl Into<Cow<'static, str>>) -> Diag<'a, BugAbort> { + Diag::new(self, Bug, msg.into()) + } + + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn bug(self, msg: impl Into<Cow<'static, str>>) -> ! { + self.struct_bug(msg).emit() + } + + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn struct_span_bug( + self, + span: impl Into<MultiSpan>, + msg: impl Into<Cow<'static, str>>, + ) -> Diag<'a, BugAbort> { + self.struct_bug(msg).with_span(span) + } + + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn span_bug(self, span: impl Into<MultiSpan>, msg: impl Into<Cow<'static, str>>) -> ! { + self.struct_span_bug(span, msg.into()).emit() + } + + #[track_caller] + pub fn create_bug(self, bug: impl Diagnostic<'a, BugAbort>) -> Diag<'a, BugAbort> { + bug.into_diag(self, Bug) + } + + #[track_caller] + pub fn emit_bug(self, bug: impl Diagnostic<'a, BugAbort>) -> ! { + self.create_bug(bug).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_fatal(self, msg: impl Into<DiagMessage>) -> Diag<'a, FatalAbort> { + Diag::new(self, Fatal, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn fatal(self, msg: impl Into<DiagMessage>) -> ! { + self.struct_fatal(msg).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_span_fatal( + self, + span: impl Into<MultiSpan>, + msg: impl Into<DiagMessage>, + ) -> Diag<'a, FatalAbort> { + self.struct_fatal(msg).with_span(span) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn span_fatal(self, span: impl Into<MultiSpan>, msg: impl Into<DiagMessage>) -> ! { + self.struct_span_fatal(span, msg).emit() + } + + #[track_caller] + pub fn create_fatal(self, fatal: impl Diagnostic<'a, FatalAbort>) -> Diag<'a, FatalAbort> { + fatal.into_diag(self, Fatal) + } + + #[track_caller] + pub fn emit_fatal(self, fatal: impl Diagnostic<'a, FatalAbort>) -> ! { + self.create_fatal(fatal).emit() + } + + #[track_caller] + pub fn create_almost_fatal( + self, + fatal: impl Diagnostic<'a, FatalError>, + ) -> Diag<'a, FatalError> { + fatal.into_diag(self, Fatal) + } + + #[track_caller] + pub fn emit_almost_fatal(self, fatal: impl Diagnostic<'a, FatalError>) -> FatalError { + self.create_almost_fatal(fatal).emit() + } + + // FIXME: This method should be removed (every error should have an associated error code). + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_err(self, msg: impl Into<DiagMessage>) -> Diag<'a> { + Diag::new(self, Error, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn err(self, msg: impl Into<DiagMessage>) -> ErrorGuaranteed { + self.struct_err(msg).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_span_err( + self, + span: impl Into<MultiSpan>, + msg: impl Into<DiagMessage>, + ) -> Diag<'a> { + self.struct_err(msg).with_span(span) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn span_err( + self, + span: impl Into<MultiSpan>, + msg: impl Into<DiagMessage>, + ) -> ErrorGuaranteed { + self.struct_span_err(span, msg).emit() + } + + #[track_caller] + pub fn create_err(self, err: impl Diagnostic<'a>) -> Diag<'a> { + err.into_diag(self, Error) + } + + #[track_caller] + pub fn emit_err(self, err: impl Diagnostic<'a>) -> ErrorGuaranteed { + self.create_err(err).emit() + } + + /// Ensures that an error is printed. See `Level::DelayedBug`. + // + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn delayed_bug(self, msg: impl Into<Cow<'static, str>>) -> ErrorGuaranteed { + Diag::<ErrorGuaranteed>::new(self, DelayedBug, msg.into()).emit() + } + + /// Ensures that an error is printed. See [`Level::DelayedBug`]. + /// + /// Note: this function used to be called `delay_span_bug`. It was renamed + /// to match similar functions like `span_err`, `span_warn`, etc. + // + // No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't + // user-facing. + #[track_caller] + pub fn span_delayed_bug( + self, + sp: impl Into<MultiSpan>, + msg: impl Into<Cow<'static, str>>, + ) -> ErrorGuaranteed { + Diag::<ErrorGuaranteed>::new(self, DelayedBug, msg.into()).with_span(sp).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_warn(self, msg: impl Into<DiagMessage>) -> Diag<'a, ()> { + Diag::new(self, Warning, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn warn(self, msg: impl Into<DiagMessage>) { + self.struct_warn(msg).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_span_warn( + self, + span: impl Into<MultiSpan>, + msg: impl Into<DiagMessage>, + ) -> Diag<'a, ()> { + self.struct_warn(msg).with_span(span) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn span_warn(self, span: impl Into<MultiSpan>, msg: impl Into<DiagMessage>) { + self.struct_span_warn(span, msg).emit() + } + + #[track_caller] + pub fn create_warn(self, warning: impl Diagnostic<'a, ()>) -> Diag<'a, ()> { + warning.into_diag(self, Warning) + } + + #[track_caller] + pub fn emit_warn(self, warning: impl Diagnostic<'a, ()>) { + self.create_warn(warning).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_note(self, msg: impl Into<DiagMessage>) -> Diag<'a, ()> { + Diag::new(self, Note, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn note(&self, msg: impl Into<DiagMessage>) { + self.struct_note(msg).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_span_note( + self, + span: impl Into<MultiSpan>, + msg: impl Into<DiagMessage>, + ) -> Diag<'a, ()> { + self.struct_note(msg).with_span(span) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn span_note(self, span: impl Into<MultiSpan>, msg: impl Into<DiagMessage>) { + self.struct_span_note(span, msg).emit() + } + + #[track_caller] + pub fn create_note(self, note: impl Diagnostic<'a, ()>) -> Diag<'a, ()> { + note.into_diag(self, Note) + } + + #[track_caller] + pub fn emit_note(self, note: impl Diagnostic<'a, ()>) { + self.create_note(note).emit() + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_help(self, msg: impl Into<DiagMessage>) -> Diag<'a, ()> { + Diag::new(self, Help, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_failure_note(self, msg: impl Into<DiagMessage>) -> Diag<'a, ()> { + Diag::new(self, FailureNote, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_allow(self, msg: impl Into<DiagMessage>) -> Diag<'a, ()> { + Diag::new(self, Allow, msg) + } + + #[rustc_lint_diagnostics] + #[track_caller] + pub fn struct_expect(self, msg: impl Into<DiagMessage>, id: LintExpectationId) -> Diag<'a, ()> { + Diag::new(self, Expect(id), msg) + } +} + +// Note: we prefer implementing operations on `DiagCtxt`, rather than +// `DiagCtxtInner`, whenever possible. This minimizes functions where +// `DiagCtxt::foo()` just borrows `inner` and forwards a call to +// `DiagCtxtInner::foo`. +impl DiagCtxtInner { + fn new(emitter: Box<DynEmitter>) -> Self { + Self { + flags: DiagCtxtFlags { can_emit_warnings: true, ..Default::default() }, + registry: Registry::new(&[]), + err_guars: Vec::new(), + lint_err_guars: Vec::new(), + delayed_bugs: Vec::new(), + deduplicated_err_count: 0, + deduplicated_warn_count: 0, + emitter, + must_produce_diag: None, + has_printed: false, + suppressed_expected_diag: false, + taught_diagnostics: Default::default(), + emitted_diagnostic_codes: Default::default(), + emitted_diagnostics: Default::default(), + stashed_diagnostics: Default::default(), + future_breakage_diagnostics: Vec::new(), + fulfilled_expectations: Default::default(), + ice_file: None, + } + } + + /// Emit all stashed diagnostics. + fn emit_stashed_diagnostics(&mut self) -> Option<ErrorGuaranteed> { + let mut guar = None; + let has_errors = !self.err_guars.is_empty(); + for (_, (diag, _guar)) in std::mem::take(&mut self.stashed_diagnostics).into_iter() { + if !diag.is_error() { + // Unless they're forced, don't flush stashed warnings when + // there are errors, to avoid causing warning overload. The + // stash would've been stolen already if it were important. + if !diag.is_force_warn() && has_errors { + continue; + } + } + guar = guar.or(self.emit_diagnostic(diag, None)); + } + guar + } + + // Return value is only `Some` if the level is `Error` or `DelayedBug`. + fn emit_diagnostic( + &mut self, + mut diagnostic: DiagInner, + taint: Option<&Cell<Option<ErrorGuaranteed>>>, + ) -> Option<ErrorGuaranteed> { + if diagnostic.has_future_breakage() { + // Future breakages aren't emitted if they're `Level::Allow` or + // `Level::Expect`, but they still need to be constructed and + // stashed below, so they'll trigger the must_produce_diag check. + assert_matches!(diagnostic.level, Error | Warning | Allow | Expect(_)); + self.future_breakage_diagnostics.push(diagnostic.clone()); + } + + // We call TRACK_DIAGNOSTIC with an empty closure for the cases that + // return early *and* have some kind of side-effect, except where + // noted. + match diagnostic.level { + Bug => {} + Fatal | Error => { + if self.treat_next_err_as_bug() { + // `Fatal` and `Error` can be promoted to `Bug`. + diagnostic.level = Bug; + } + } + DelayedBug => { + // Note that because we check these conditions first, + // `-Zeagerly-emit-delayed-bugs` and `-Ztreat-err-as-bug` + // continue to work even after we've issued an error and + // stopped recording new delayed bugs. + if self.flags.eagerly_emit_delayed_bugs { + // `DelayedBug` can be promoted to `Error` or `Bug`. + if self.treat_next_err_as_bug() { + diagnostic.level = Bug; + } else { + diagnostic.level = Error; + } + } else { + // If we have already emitted at least one error, we don't need + // to record the delayed bug, because it'll never be used. + return if let Some(guar) = self.has_errors() { + Some(guar) + } else { + // No `TRACK_DIAGNOSTIC` call is needed, because the + // incremental session is deleted if there is a delayed + // bug. This also saves us from cloning the diagnostic. + let backtrace = std::backtrace::Backtrace::capture(); + // This `unchecked_error_guaranteed` is valid. It is where the + // `ErrorGuaranteed` for delayed bugs originates. See + // `DiagCtxtInner::drop`. + #[allow(deprecated)] + let guar = ErrorGuaranteed::unchecked_error_guaranteed(); + self.delayed_bugs + .push((DelayedDiagInner::with_backtrace(diagnostic, backtrace), guar)); + Some(guar) + }; + } + } + ForceWarning(None) => {} // `ForceWarning(Some(...))` is below, with `Expect` + Warning => { + if !self.flags.can_emit_warnings { + // We are not emitting warnings. + if diagnostic.has_future_breakage() { + // The side-effect is at the top of this method. + TRACK_DIAGNOSTIC(diagnostic, &mut |_| None); + } + return None; + } + } + Note | Help | FailureNote => {} + OnceNote | OnceHelp => panic!("bad level: {:?}", diagnostic.level), + Allow => { + // Nothing emitted for allowed lints. + if diagnostic.has_future_breakage() { + // The side-effect is at the top of this method. + TRACK_DIAGNOSTIC(diagnostic, &mut |_| None); + self.suppressed_expected_diag = true; + } + return None; + } + Expect(expect_id) | ForceWarning(Some(expect_id)) => { + self.fulfilled_expectations.insert(expect_id); + if let Expect(_) = diagnostic.level { + // Nothing emitted here for expected lints. + TRACK_DIAGNOSTIC(diagnostic, &mut |_| None); + self.suppressed_expected_diag = true; + return None; + } + } + } + + TRACK_DIAGNOSTIC(diagnostic, &mut |mut diagnostic| { + if let Some(code) = diagnostic.code { + self.emitted_diagnostic_codes.insert(code); + } + + let already_emitted = { + let mut hasher = StableHasher::new(); + diagnostic.hash(&mut hasher); + let diagnostic_hash = hasher.finish(); + !self.emitted_diagnostics.insert(diagnostic_hash) + }; + + let is_error = diagnostic.is_error(); + let is_lint = diagnostic.is_lint.is_some(); + + // Only emit the diagnostic if we've been asked to deduplicate or + // haven't already emitted an equivalent diagnostic. + if !(self.flags.deduplicate_diagnostics && already_emitted) { + debug!(?diagnostic); + debug!(?self.emitted_diagnostics); + + let not_yet_emitted = |sub: &mut Subdiag| { + debug!(?sub); + if sub.level != OnceNote && sub.level != OnceHelp { + return true; + } + let mut hasher = StableHasher::new(); + sub.hash(&mut hasher); + let diagnostic_hash = hasher.finish(); + debug!(?diagnostic_hash); + self.emitted_diagnostics.insert(diagnostic_hash) + }; + diagnostic.children.retain_mut(not_yet_emitted); + if already_emitted { + let msg = "duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no`"; + diagnostic.sub(Note, msg, MultiSpan::new()); + } + + if is_error { + self.deduplicated_err_count += 1; + } else if matches!(diagnostic.level, ForceWarning(_) | Warning) { + self.deduplicated_warn_count += 1; + } + self.has_printed = true; + + self.emitter.emit_diagnostic(diagnostic, &self.registry); + } + + if is_error { + // If we have any delayed bugs recorded, we can discard them + // because they won't be used. (This should only occur if there + // have been no errors previously emitted, because we don't add + // new delayed bugs once the first error is emitted.) + if !self.delayed_bugs.is_empty() { + assert_eq!(self.lint_err_guars.len() + self.err_guars.len(), 0); + self.delayed_bugs.clear(); + self.delayed_bugs.shrink_to_fit(); + } + + // This `unchecked_error_guaranteed` is valid. It is where the + // `ErrorGuaranteed` for errors and lint errors originates. + #[allow(deprecated)] + let guar = ErrorGuaranteed::unchecked_error_guaranteed(); + if is_lint { + self.lint_err_guars.push(guar); + } else { + if let Some(taint) = taint { + taint.set(Some(guar)); + } + self.err_guars.push(guar); + } + self.panic_if_treat_err_as_bug(); + Some(guar) + } else { + None + } + }) + } + + fn treat_err_as_bug(&self) -> bool { + self.flags + .treat_err_as_bug + .is_some_and(|c| self.err_guars.len() + self.lint_err_guars.len() >= c.get()) + } + + // Use this one before incrementing `err_count`. + fn treat_next_err_as_bug(&self) -> bool { + self.flags + .treat_err_as_bug + .is_some_and(|c| self.err_guars.len() + self.lint_err_guars.len() + 1 >= c.get()) + } + + fn has_errors_excluding_lint_errors(&self) -> Option<ErrorGuaranteed> { + self.err_guars.get(0).copied().or_else(|| { + if let Some((_diag, guar)) = self + .stashed_diagnostics + .values() + .find(|(diag, guar)| guar.is_some() && diag.is_lint.is_none()) + { + *guar + } else { + None + } + }) + } + + fn has_errors(&self) -> Option<ErrorGuaranteed> { + self.err_guars.get(0).copied().or_else(|| self.lint_err_guars.get(0).copied()).or_else( + || { + if let Some((_diag, guar)) = + self.stashed_diagnostics.values().find(|(_diag, guar)| guar.is_some()) + { + *guar + } else { + None + } + }, + ) + } + + fn has_errors_or_delayed_bugs(&self) -> Option<ErrorGuaranteed> { + self.has_errors().or_else(|| self.delayed_bugs.get(0).map(|(_, guar)| guar).copied()) + } + + /// Translate `message` eagerly with `args` to `SubdiagMessage::Eager`. + fn eagerly_translate<'a>( + &self, + message: DiagMessage, + args: impl Iterator<Item = DiagArg<'a>>, + ) -> SubdiagMessage { + SubdiagMessage::Translated(Cow::from(self.eagerly_translate_to_string(message, args))) + } + + /// Translate `message` eagerly with `args` to `String`. + fn eagerly_translate_to_string<'a>( + &self, + message: DiagMessage, + args: impl Iterator<Item = DiagArg<'a>>, + ) -> String { + let args = crate::translation::to_fluent_args(args); + self.emitter.translate_message(&message, &args).map_err(Report::new).unwrap().to_string() + } + + fn eagerly_translate_for_subdiag( + &self, + diag: &DiagInner, + msg: impl Into<SubdiagMessage>, + ) -> SubdiagMessage { + let msg = diag.subdiagnostic_message_to_diagnostic_message(msg); + self.eagerly_translate(msg, diag.args.iter()) + } + + fn flush_delayed(&mut self) { + // Stashed diagnostics must be emitted before delayed bugs are flushed. + // Otherwise, we might ICE prematurely when errors would have + // eventually happened. + assert!(self.stashed_diagnostics.is_empty()); + + if !self.err_guars.is_empty() { + // If an error happened already. We shouldn't expose delayed bugs. + return; + } + + if self.delayed_bugs.is_empty() { + // Nothing to do. + return; + } + + let bugs: Vec<_> = + std::mem::take(&mut self.delayed_bugs).into_iter().map(|(b, _)| b).collect(); + + let backtrace = std::env::var_os("RUST_BACKTRACE").as_deref() != Some(OsStr::new("0")); + let decorate = backtrace || self.ice_file.is_none(); + let mut out = self + .ice_file + .as_ref() + .and_then(|file| std::fs::File::options().create(true).append(true).open(file).ok()); + + // Put the overall explanation before the `DelayedBug`s, to frame them + // better (e.g. separate warnings from them). Also, use notes, which + // don't count as errors, to avoid possibly triggering + // `-Ztreat-err-as-bug`, which we don't want. + let note1 = "no errors encountered even though delayed bugs were created"; + let note2 = "those delayed bugs will now be shown as internal compiler errors"; + self.emit_diagnostic(DiagInner::new(Note, note1), None); + self.emit_diagnostic(DiagInner::new(Note, note2), None); + + for bug in bugs { + if let Some(out) = &mut out { + _ = write!( + out, + "delayed bug: {}\n{}\n", + bug.inner + .messages + .iter() + .filter_map(|(msg, _)| msg.as_str()) + .collect::<String>(), + &bug.note + ); + } + + let mut bug = if decorate { bug.decorate(self) } else { bug.inner }; + + // "Undelay" the delayed bugs into plain bugs. + if bug.level != DelayedBug { + // NOTE(eddyb) not panicking here because we're already producing + // an ICE, and the more information the merrier. + // + // We are at the `DiagInner`/`DiagCtxtInner` level rather than + // the usual `Diag`/`DiagCtxt` level, so we must augment `bug` + // in a lower-level fashion. + bug.arg("level", bug.level); + let msg = crate::fluent_generated::errors_invalid_flushed_delayed_diagnostic_level; + let msg = self.eagerly_translate_for_subdiag(&bug, msg); // after the `arg` call + bug.sub(Note, msg, bug.span.primary_span().unwrap().into()); + } + bug.level = Bug; + + self.emit_diagnostic(bug, None); + } + + // Panic with `DelayedBugPanic` to avoid "unexpected panic" messages. + panic::panic_any(DelayedBugPanic); + } + + fn panic_if_treat_err_as_bug(&self) { + if self.treat_err_as_bug() { + let n = self.flags.treat_err_as_bug.map(|c| c.get()).unwrap(); + assert_eq!(n, self.err_guars.len() + self.lint_err_guars.len()); + if n == 1 { + panic!("aborting due to `-Z treat-err-as-bug=1`"); + } else { + panic!("aborting after {n} errors due to `-Z treat-err-as-bug={n}`"); + } + } + } +} + +struct DelayedDiagInner { + inner: DiagInner, + note: Backtrace, +} + +impl DelayedDiagInner { + fn with_backtrace(diagnostic: DiagInner, backtrace: Backtrace) -> Self { + DelayedDiagInner { inner: diagnostic, note: backtrace } + } + + fn decorate(self, dcx: &DiagCtxtInner) -> DiagInner { + // We are at the `DiagInner`/`DiagCtxtInner` level rather than the + // usual `Diag`/`DiagCtxt` level, so we must construct `diag` in a + // lower-level fashion. + let mut diag = self.inner; + let msg = match self.note.status() { + BacktraceStatus::Captured => crate::fluent_generated::errors_delayed_at_with_newline, + // Avoid the needless newline when no backtrace has been captured, + // the display impl should just be a single line. + _ => crate::fluent_generated::errors_delayed_at_without_newline, + }; + diag.arg("emitted_at", diag.emitted_at.clone()); + diag.arg("note", self.note); + let msg = dcx.eagerly_translate_for_subdiag(&diag, msg); // after the `arg` calls + diag.sub(Note, msg, diag.span.primary_span().unwrap_or(DUMMY_SP).into()); + diag + } +} + +/// | Level | is_error | EmissionGuarantee | Top-level | Sub | Used in lints? +/// | ----- | -------- | ----------------- | --------- | --- | -------------- +/// | Bug | yes | BugAbort | yes | - | - +/// | Fatal | yes | FatalAbort/FatalError[^star] | yes | - | - +/// | Error | yes | ErrorGuaranteed | yes | - | yes +/// | DelayedBug | yes | ErrorGuaranteed | yes | - | - +/// | ForceWarning | - | () | yes | - | lint-only +/// | Warning | - | () | yes | yes | yes +/// | Note | - | () | rare | yes | - +/// | OnceNote | - | () | - | yes | lint-only +/// | Help | - | () | rare | yes | - +/// | OnceHelp | - | () | - | yes | lint-only +/// | FailureNote | - | () | rare | - | - +/// | Allow | - | () | yes | - | lint-only +/// | Expect | - | () | yes | - | lint-only +/// +/// [^star]: `FatalAbort` normally, `FatalError` in the non-aborting "almost fatal" case that is +/// occasionally used. +/// +#[derive(Copy, PartialEq, Eq, Clone, Hash, Debug, Encodable, Decodable)] +pub enum Level { + /// For bugs in the compiler. Manifests as an ICE (internal compiler error) panic. + Bug, + + /// An error that causes an immediate abort. Used for things like configuration errors, + /// internal overflows, some file operation errors. + Fatal, + + /// An error in the code being compiled, which prevents compilation from finishing. This is the + /// most common case. + Error, + + /// This is a strange one: lets you register an error without emitting it. If compilation ends + /// without any other errors occurring, this will be emitted as a bug. Otherwise, it will be + /// silently dropped. I.e. "expect other errors are emitted" semantics. Useful on code paths + /// that should only be reached when compiling erroneous code. + DelayedBug, + + /// A `force-warn` lint warning about the code being compiled. Does not prevent compilation + /// from finishing. + /// + /// The [`LintExpectationId`] is used for expected lint diagnostics. In all other cases this + /// should be `None`. + ForceWarning(Option<LintExpectationId>), + + /// A warning about the code being compiled. Does not prevent compilation from finishing. + /// Will be skipped if `can_emit_warnings` is false. + Warning, + + /// A message giving additional context. + Note, + + /// A note that is only emitted once. + OnceNote, + + /// A message suggesting how to fix something. + Help, + + /// A help that is only emitted once. + OnceHelp, + + /// Similar to `Note`, but used in cases where compilation has failed. When printed for human + /// consumption, it doesn't have any kind of `note:` label. + FailureNote, + + /// Only used for lints. + Allow, + + /// Only used for lints. + Expect(LintExpectationId), +} + +impl fmt::Display for Level { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.to_str().fmt(f) + } +} + +impl Level { + fn color(self) -> ColorSpec { + let mut spec = ColorSpec::new(); + match self { + Bug | Fatal | Error | DelayedBug => { + spec.set_fg(Some(Color::Red)).set_intense(true); + } + ForceWarning(_) | Warning => { + spec.set_fg(Some(Color::Yellow)).set_intense(cfg!(windows)); + } + Note | OnceNote => { + spec.set_fg(Some(Color::Green)).set_intense(true); + } + Help | OnceHelp => { + spec.set_fg(Some(Color::Cyan)).set_intense(true); + } + FailureNote => {} + Allow | Expect(_) => unreachable!(), + } + spec + } + + pub fn to_str(self) -> &'static str { + match self { + Bug | DelayedBug => "error: internal compiler error", + Fatal | Error => "error", + ForceWarning(_) | Warning => "warning", + Note | OnceNote => "note", + Help | OnceHelp => "help", + FailureNote => "failure-note", + Allow | Expect(_) => unreachable!(), + } + } + + pub fn is_failure_note(&self) -> bool { + matches!(*self, FailureNote) + } + + // Can this level be used in a subdiagnostic message? + fn can_be_subdiag(&self) -> bool { + match self { + Bug | DelayedBug | Fatal | Error | ForceWarning(_) | FailureNote | Allow + | Expect(_) => false, + + Warning | Note | Help | OnceNote | OnceHelp => true, + } + } +} + +// FIXME(eddyb) this doesn't belong here AFAICT, should be moved to callsite. +pub fn elided_lifetime_in_path_suggestion( + source_map: &SourceMap, + n: usize, + path_span: Span, + incl_angl_brckt: bool, + insertion_span: Span, +) -> ElidedLifetimeInPathSubdiag { + let expected = ExpectedLifetimeParameter { span: path_span, count: n }; + // Do not try to suggest anything if generated by a proc-macro. + let indicate = source_map.is_span_accessible(insertion_span).then(|| { + let anon_lts = vec!["'_"; n].join(", "); + let suggestion = + if incl_angl_brckt { format!("<{anon_lts}>") } else { format!("{anon_lts}, ") }; + + IndicateAnonymousLifetime { span: insertion_span.shrink_to_hi(), count: n, suggestion } + }); + + ElidedLifetimeInPathSubdiag { expected, indicate } +} + +pub fn report_ambiguity_error<'a, G: EmissionGuarantee>( + diag: &mut Diag<'a, G>, + ambiguity: rustc_lint_defs::AmbiguityErrorDiag, +) { + diag.span_label(ambiguity.label_span, ambiguity.label_msg); + diag.note(ambiguity.note_msg); + diag.span_note(ambiguity.b1_span, ambiguity.b1_note_msg); + for help_msg in ambiguity.b1_help_msgs { + diag.help(help_msg); + } + diag.span_note(ambiguity.b2_span, ambiguity.b2_note_msg); + for help_msg in ambiguity.b2_help_msgs { + diag.help(help_msg); + } +} + +/// Grammatical tool for displaying messages to end users in a nice form. +/// +/// Returns "an" if the given string starts with a vowel, and "a" otherwise. +pub fn a_or_an(s: &str) -> &'static str { + let mut chars = s.chars(); + let Some(mut first_alpha_char) = chars.next() else { + return "a"; + }; + if first_alpha_char == '`' { + let Some(next) = chars.next() else { + return "a"; + }; + first_alpha_char = next; + } + if ["a", "e", "i", "o", "u", "&"].contains(&&first_alpha_char.to_lowercase().to_string()[..]) { + "an" + } else { + "a" + } +} + +#[derive(Clone, Copy, PartialEq, Hash, Debug)] +pub enum TerminalUrl { + No, + Yes, + Auto, +} diff --git a/compiler/rustc_errors/src/lock.rs b/compiler/rustc_errors/src/lock.rs new file mode 100644 index 00000000000..85199c34a14 --- /dev/null +++ b/compiler/rustc_errors/src/lock.rs @@ -0,0 +1,85 @@ +//! Bindings to acquire a global named lock. +//! +//! This is intended to be used to synchronize multiple compiler processes to +//! ensure that we can output complete errors without interleaving on Windows. +//! Note that this is currently only needed for allowing only one 32-bit MSVC +//! linker to execute at once on MSVC hosts, so this is only implemented for +//! `cfg(windows)`. Also note that this may not always be used on Windows, +//! only when targeting 32-bit MSVC. +//! +//! For more information about why this is necessary, see where this is called. + +use std::any::Any; + +#[cfg(windows)] +pub(crate) fn acquire_global_lock(name: &str) -> Box<dyn Any> { + use std::ffi::CString; + use std::io; + + use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_ABANDONED, WAIT_OBJECT_0}; + use windows::Win32::System::Threading::{ + CreateMutexA, INFINITE, ReleaseMutex, WaitForSingleObject, + }; + use windows::core::PCSTR; + + struct Handle(HANDLE); + + impl Drop for Handle { + fn drop(&mut self) { + unsafe { + // FIXME can panic here + CloseHandle(self.0).unwrap(); + } + } + } + + struct Guard(Handle); + + impl Drop for Guard { + fn drop(&mut self) { + unsafe { + // FIXME can panic here + ReleaseMutex((self.0).0).unwrap(); + } + } + } + + let cname = CString::new(name).unwrap(); + // Create a named mutex, with no security attributes and also not + // acquired when we create it. + // + // This will silently create one if it doesn't already exist, or it'll + // open up a handle to one if it already exists. + let mutex = unsafe { CreateMutexA(None, false, PCSTR::from_raw(cname.as_ptr().cast())) } + .unwrap_or_else(|_| panic!("failed to create global mutex named `{}`", name)); + let mutex = Handle(mutex); + + // Acquire the lock through `WaitForSingleObject`. + // + // A return value of `WAIT_OBJECT_0` means we successfully acquired it. + // + // A return value of `WAIT_ABANDONED` means that the previous holder of + // the thread exited without calling `ReleaseMutex`. This can happen, + // for example, when the compiler crashes or is interrupted via ctrl-c + // or the like. In this case, however, we are still transferred + // ownership of the lock so we continue. + // + // If an error happens.. well... that's surprising! + match unsafe { WaitForSingleObject(mutex.0, INFINITE) } { + WAIT_OBJECT_0 | WAIT_ABANDONED => (), + err => panic!( + "WaitForSingleObject failed on global mutex named `{}`: {} (ret={:x})", + name, + io::Error::last_os_error(), + err.0 + ), + } + + // Return a guard which will call `ReleaseMutex` when dropped. + Box::new(Guard(mutex)) +} + +#[cfg(not(windows))] +pub(crate) fn acquire_global_lock(_name: &str) -> Box<dyn Any> { + Box::new(()) +} diff --git a/compiler/rustc_errors/src/markdown/mod.rs b/compiler/rustc_errors/src/markdown/mod.rs new file mode 100644 index 00000000000..64576cdc8ca --- /dev/null +++ b/compiler/rustc_errors/src/markdown/mod.rs @@ -0,0 +1,77 @@ +//! A simple markdown parser that can write formatted text to the terminal +//! +//! Entrypoint is `MdStream::parse_str(...)` + +use std::io; + +use termcolor::{Buffer, BufferWriter, ColorChoice}; +mod parse; +mod term; + +/// An AST representation of a Markdown document +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MdStream<'a>(Vec<MdTree<'a>>); + +impl<'a> MdStream<'a> { + /// Parse a markdown string to a tokenstream + #[must_use] + pub fn parse_str(s: &str) -> MdStream<'_> { + parse::entrypoint(s) + } + + /// Write formatted output to a termcolor buffer + pub fn write_termcolor_buf(&self, buf: &mut Buffer) -> io::Result<()> { + term::entrypoint(self, buf) + } +} + +/// Create a termcolor buffer with the `Always` color choice +pub fn create_stdout_bufwtr() -> BufferWriter { + BufferWriter::stdout(ColorChoice::Always) +} + +/// A single tokentree within a Markdown document +#[derive(Clone, Debug, PartialEq)] +pub enum MdTree<'a> { + /// Leaf types + Comment(&'a str), + CodeBlock { + txt: &'a str, + lang: Option<&'a str>, + }, + CodeInline(&'a str), + Strong(&'a str), + Emphasis(&'a str), + Strikethrough(&'a str), + PlainText(&'a str), + /// [Foo](www.foo.com) or simple anchor <www.foo.com> + Link { + disp: &'a str, + link: &'a str, + }, + /// `[Foo link][ref]` + RefLink { + disp: &'a str, + id: Option<&'a str>, + }, + /// [ref]: www.foo.com + LinkDef { + id: &'a str, + link: &'a str, + }, + /// Break bewtween two paragraphs (double `\n`), not directly parsed but + /// added later + ParagraphBreak, + /// Break bewtween two lines (single `\n`) + LineBreak, + HorizontalRule, + Heading(u8, MdStream<'a>), + OrderedListItem(u16, MdStream<'a>), + UnorderedListItem(MdStream<'a>), +} + +impl<'a> From<Vec<MdTree<'a>>> for MdStream<'a> { + fn from(value: Vec<MdTree<'a>>) -> Self { + Self(value) + } +} diff --git a/compiler/rustc_errors/src/markdown/parse.rs b/compiler/rustc_errors/src/markdown/parse.rs new file mode 100644 index 00000000000..7a991a2ace7 --- /dev/null +++ b/compiler/rustc_errors/src/markdown/parse.rs @@ -0,0 +1,595 @@ +use std::{iter, mem, str}; + +use crate::markdown::{MdStream, MdTree}; + +/// Short aliases that we can use in match patterns. If an end pattern is not +/// included, this type may be variable +const ANC_E: &[u8] = b">"; +const ANC_S: &[u8] = b"<"; +const BRK: &[u8] = b"---"; +const CBK: &[u8] = b"```"; +const CIL: &[u8] = b"`"; +const CMT_E: &[u8] = b"-->"; +const CMT_S: &[u8] = b"<!--"; +const EMP_U: &[u8] = b"_"; +const EMP_A: &[u8] = b"*"; +const HDG: &[u8] = b"#"; +const LNK_CHARS: &str = "$-_.+!*'()/&?=:%"; +const LNK_E: &[u8] = b"]"; +const LNK_S: &[u8] = b"["; +const STG_U: &[u8] = b"__"; +const STG_A: &[u8] = b"**"; +const STK: &[u8] = b"~~"; + +/// Pattern replacements +const REPLACEMENTS: &[(&str, &str)] = &[ + ("(c)", "©"), + ("(C)", "©"), + ("(r)", "®"), + ("(R)", "®"), + ("(tm)", "™"), + ("(TM)", "™"), + (":crab:", "🦀"), + ("\n", " "), +]; + +/// `(extracted, remaining)` +type Parsed<'a> = (MdTree<'a>, &'a [u8]); +/// Output of a parse function +type ParseResult<'a> = Option<Parsed<'a>>; + +/// Parsing context +#[derive(Clone, Copy, Debug, PartialEq)] +struct Context { + /// If true, we are at a the topmost level (not recursing a nested tt) + top_block: bool, + /// Previous character + prev: Prev, +} + +/// Character class preceding this one +#[derive(Clone, Copy, Debug, PartialEq)] +enum Prev { + Newline, + /// Whitespace that is not a newline + Whitespace, + Escape, + Any, +} + +impl Default for Context { + /// Most common setting for non top-level parsing: not top block, not at + /// line start (yes leading whitespace, not escaped) + fn default() -> Self { + Self { top_block: false, prev: Prev::Whitespace } + } +} + +/// Flags to simple parser function +#[derive(Clone, Copy, Debug, PartialEq)] +enum ParseOpt { + /// Ignore escapes before closing pattern, trim content + TrimNoEsc, + None, +} + +/// Parse a buffer +pub(crate) fn entrypoint(txt: &str) -> MdStream<'_> { + let ctx = Context { top_block: true, prev: Prev::Newline }; + normalize(parse_recursive(txt.trim().as_bytes(), ctx), &mut Vec::new()) +} + +/// Parse a buffer with specified context +fn parse_recursive<'a>(buf: &'a [u8], ctx: Context) -> MdStream<'a> { + use ParseOpt as Po; + use Prev::{Escape, Newline, Whitespace}; + + let mut stream: Vec<MdTree<'a>> = Vec::new(); + let Context { top_block: top_blk, mut prev } = ctx; + + // wip_buf is our entire unprocessed (unpushed) buffer, loop_buf is our to + // check buffer that shrinks with each loop + let mut wip_buf = buf; + let mut loop_buf = wip_buf; + + while !loop_buf.is_empty() { + let next_prev = match loop_buf[0] { + b'\n' => Newline, + b'\\' => Escape, + x if x.is_ascii_whitespace() => Whitespace, + _ => Prev::Any, + }; + + let res: ParseResult<'_> = match (top_blk, prev) { + _ if loop_buf.starts_with(CMT_S) => { + parse_simple_pat(loop_buf, CMT_S, CMT_E, Po::TrimNoEsc, MdTree::Comment) + } + (true, Newline) if loop_buf.starts_with(CBK) => Some(parse_codeblock(loop_buf)), + _ if loop_buf.starts_with(CIL) => parse_codeinline(loop_buf), + (true, Newline | Whitespace) if loop_buf.starts_with(HDG) => parse_heading(loop_buf), + (true, Newline) if loop_buf.starts_with(BRK) => { + Some((MdTree::HorizontalRule, parse_to_newline(loop_buf).1)) + } + (_, Newline) if unordered_list_start(loop_buf) => Some(parse_unordered_li(loop_buf)), + (_, Newline | Whitespace) if loop_buf.starts_with(STG_U) => { + parse_simple_pat(loop_buf, STG_U, STG_U, Po::None, MdTree::Strong) + } + _ if loop_buf.starts_with(STG_A) => { + parse_simple_pat(loop_buf, STG_A, STG_A, Po::None, MdTree::Strong) + } + (_, Newline | Whitespace) if loop_buf.starts_with(EMP_U) => { + parse_simple_pat(loop_buf, EMP_U, EMP_U, Po::None, MdTree::Emphasis) + } + _ if loop_buf.starts_with(EMP_A) => { + parse_simple_pat(loop_buf, EMP_A, EMP_A, Po::None, MdTree::Emphasis) + } + _ if loop_buf.starts_with(STK) => { + parse_simple_pat(loop_buf, STK, STK, Po::None, MdTree::Strikethrough) + } + (_, Newline | Whitespace) if loop_buf.starts_with(ANC_S) => { + let tt_fn = |link| MdTree::Link { disp: link, link }; + let ret = parse_simple_pat(loop_buf, ANC_S, ANC_E, Po::None, tt_fn); + match ret { + Some((MdTree::Link { disp, .. }, _)) + if disp.chars().all(|ch| LNK_CHARS.contains(ch)) => + { + ret + } + _ => None, + } + } + (_, Newline) if ord_list_start(loop_buf).is_some() => Some(parse_ordered_li(loop_buf)), + _ if loop_buf.starts_with(LNK_S) => { + parse_any_link(loop_buf, top_blk && prev == Prev::Newline) + } + (_, Escape | _) => None, + }; + + if let Some((tree, rest)) = res { + // We found something: push our WIP and then push the found tree + let prev_buf = &wip_buf[..(wip_buf.len() - loop_buf.len())]; + if !prev_buf.is_empty() { + let prev_str = str::from_utf8(prev_buf).unwrap(); + stream.push(MdTree::PlainText(prev_str)); + } + stream.push(tree); + + wip_buf = rest; + loop_buf = rest; + } else { + // Just move on to the next character + loop_buf = &loop_buf[1..]; + // If we are at the end and haven't found anything, just push plain text + if loop_buf.is_empty() && !wip_buf.is_empty() { + let final_str = str::from_utf8(wip_buf).unwrap(); + stream.push(MdTree::PlainText(final_str)); + } + }; + + prev = next_prev; + } + + MdStream(stream) +} + +/// The simplest kind of patterns: data within start and end patterns +fn parse_simple_pat<'a, F>( + buf: &'a [u8], + start_pat: &[u8], + end_pat: &[u8], + opts: ParseOpt, + create_tt: F, +) -> ParseResult<'a> +where + F: FnOnce(&'a str) -> MdTree<'a>, +{ + let ignore_esc = matches!(opts, ParseOpt::TrimNoEsc); + let trim = matches!(opts, ParseOpt::TrimNoEsc); + let (txt, rest) = parse_with_end_pat(&buf[start_pat.len()..], end_pat, ignore_esc)?; + let mut txt = str::from_utf8(txt).unwrap(); + if trim { + txt = txt.trim(); + } + Some((create_tt(txt), rest)) +} + +/// Parse backtick-wrapped inline code. Accounts for >1 backtick sets +fn parse_codeinline(buf: &[u8]) -> ParseResult<'_> { + let seps = buf.iter().take_while(|ch| **ch == b'`').count(); + let (txt, rest) = parse_with_end_pat(&buf[seps..], &buf[..seps], true)?; + Some((MdTree::CodeInline(str::from_utf8(txt).unwrap()), rest)) +} + +/// Parse a codeblock. Accounts for >3 backticks and language specification +fn parse_codeblock(buf: &[u8]) -> Parsed<'_> { + // account for ````code```` style + let seps = buf.iter().take_while(|ch| **ch == b'`').count(); + let end_sep = &buf[..seps]; + let mut working = &buf[seps..]; + + // Handle "````rust" style language specifications + let next_ws_idx = working.iter().take_while(|ch| !ch.is_ascii_whitespace()).count(); + + let lang = if next_ws_idx > 0 { + // Munch the lang + let tmp = str::from_utf8(&working[..next_ws_idx]).unwrap(); + working = &working[next_ws_idx..]; + Some(tmp) + } else { + None + }; + + let mut end_pat = vec![b'\n']; + end_pat.extend(end_sep); + + // Find first end pattern with nothing else on its line + let mut found = None; + for idx in (0..working.len()).filter(|idx| working[*idx..].starts_with(&end_pat)) { + let (eol_txt, rest) = parse_to_newline(&working[(idx + end_pat.len())..]); + if !eol_txt.iter().any(u8::is_ascii_whitespace) { + found = Some((&working[..idx], rest)); + break; + } + } + + let (txt, rest) = found.unwrap_or((working, &[])); + let txt = str::from_utf8(txt).unwrap().trim_matches('\n'); + + (MdTree::CodeBlock { txt, lang }, rest) +} + +fn parse_heading(buf: &[u8]) -> ParseResult<'_> { + let level = buf.iter().take_while(|ch| **ch == b'#').count(); + let buf = &buf[level..]; + + if level > 6 || (buf.len() > 1 && !buf[0].is_ascii_whitespace()) { + // Enforce max 6 levels and whitespace following the `##` pattern + return None; + } + + let (txt, rest) = parse_to_newline(&buf[1..]); + let ctx = Context { top_block: false, prev: Prev::Whitespace }; + let stream = parse_recursive(txt, ctx); + + Some((MdTree::Heading(level.try_into().unwrap(), stream), rest)) +} + +/// Bulleted list +fn parse_unordered_li(buf: &[u8]) -> Parsed<'_> { + let (txt, rest) = get_indented_section(&buf[2..]); + let ctx = Context { top_block: false, prev: Prev::Whitespace }; + let stream = parse_recursive(trim_ascii_start(txt), ctx); + (MdTree::UnorderedListItem(stream), rest) +} + +/// Numbered list +fn parse_ordered_li(buf: &[u8]) -> Parsed<'_> { + let (num, pos) = ord_list_start(buf).unwrap(); // success tested in caller + let (txt, rest) = get_indented_section(&buf[pos..]); + let ctx = Context { top_block: false, prev: Prev::Whitespace }; + let stream = parse_recursive(trim_ascii_start(txt), ctx); + (MdTree::OrderedListItem(num, stream), rest) +} + +fn get_indented_section(buf: &[u8]) -> (&[u8], &[u8]) { + let mut lines = buf.split(|&byte| byte == b'\n'); + let mut end = lines.next().map_or(0, |line| line.len()); + for line in lines { + if let Some(first) = line.first() { + if unordered_list_start(line) || !first.is_ascii_whitespace() { + break; + } + } + end += line.len() + 1; + } + + (&buf[..end], &buf[end..]) +} + +fn unordered_list_start(mut buf: &[u8]) -> bool { + while let [b' ', rest @ ..] = buf { + buf = rest; + } + matches!(buf, [b'*' | b'-', b' ', ..]) +} + +/// Verify a valid ordered list start (e.g. `1.`) and parse it. Returns the +/// parsed number and offset of character after the dot. +fn ord_list_start(buf: &[u8]) -> Option<(u16, usize)> { + let pos = buf.iter().take(10).position(|ch| *ch == b'.')?; + let n = str::from_utf8(&buf[..pos]).ok()?; + if !buf.get(pos + 1)?.is_ascii_whitespace() { + return None; + } + n.parse::<u16>().ok().map(|v| (v, pos + 2)) +} + +/// Parse links. `can_be_def` indicates that a link definition is possible (top +/// level, located at the start of a line) +fn parse_any_link(buf: &[u8], can_be_def: bool) -> ParseResult<'_> { + let (bracketed, rest) = parse_with_end_pat(&buf[1..], LNK_E, true)?; + if rest.is_empty() { + return None; + } + + let disp = str::from_utf8(bracketed).unwrap(); + match (can_be_def, rest[0]) { + (true, b':') => { + let (link, tmp) = parse_to_newline(&rest[1..]); + let link = str::from_utf8(link).unwrap().trim(); + Some((MdTree::LinkDef { id: disp, link }, tmp)) + } + (_, b'(') => parse_simple_pat(rest, b"(", b")", ParseOpt::TrimNoEsc, |link| MdTree::Link { + disp, + link, + }), + (_, b'[') => parse_simple_pat(rest, b"[", b"]", ParseOpt::TrimNoEsc, |id| { + MdTree::RefLink { disp, id: Some(id) } + }), + _ => Some((MdTree::RefLink { disp, id: None }, rest)), + } +} + +/// Find and consume an end pattern, return `(match, residual)` +fn parse_with_end_pat<'a>( + buf: &'a [u8], + end_sep: &[u8], + ignore_esc: bool, +) -> Option<(&'a [u8], &'a [u8])> { + // Find positions that start with the end separator + for idx in (0..buf.len()).filter(|idx| buf[*idx..].starts_with(end_sep)) { + if !ignore_esc && idx > 0 && buf[idx - 1] == b'\\' { + continue; + } + return Some((&buf[..idx], &buf[idx + end_sep.len()..])); + } + None +} + +/// Return `(match, residual)` to end of line. The EOL is returned with the +/// residual. +fn parse_to_newline(buf: &[u8]) -> (&[u8], &[u8]) { + buf.iter().position(|ch| *ch == b'\n').map_or((buf, &[]), |pos| buf.split_at(pos)) +} + +/// Take a parsed stream and fix the little things +fn normalize<'a>(MdStream(stream): MdStream<'a>, linkdefs: &mut Vec<MdTree<'a>>) -> MdStream<'a> { + let mut new_stream = Vec::with_capacity(stream.len()); + let new_defs = stream.iter().filter(|tt| matches!(tt, MdTree::LinkDef { .. })); + linkdefs.extend(new_defs.cloned()); + + // Run plaintest expansions on types that need it, call this function on nested types + for item in stream { + match item { + MdTree::PlainText(txt) => expand_plaintext(txt, &mut new_stream, MdTree::PlainText), + MdTree::Strong(txt) => expand_plaintext(txt, &mut new_stream, MdTree::Strong), + MdTree::Emphasis(txt) => expand_plaintext(txt, &mut new_stream, MdTree::Emphasis), + MdTree::Strikethrough(txt) => { + expand_plaintext(txt, &mut new_stream, MdTree::Strikethrough); + } + MdTree::RefLink { disp, id } => new_stream.push(match_reflink(linkdefs, disp, id)), + MdTree::OrderedListItem(n, st) => { + new_stream.push(MdTree::OrderedListItem(n, normalize(st, linkdefs))); + } + MdTree::UnorderedListItem(st) => { + new_stream.push(MdTree::UnorderedListItem(normalize(st, linkdefs))); + } + MdTree::Heading(n, st) => new_stream.push(MdTree::Heading(n, normalize(st, linkdefs))), + _ => new_stream.push(item), + } + } + + // Remove non printing types, duplicate paragraph breaks, and breaks at start/end + new_stream.retain(|x| !matches!(x, MdTree::Comment(_) | MdTree::LinkDef { .. })); + new_stream.dedup_by(|r, l| matches!((r, l), (MdTree::ParagraphBreak, MdTree::ParagraphBreak))); + + if new_stream.first().is_some_and(is_break_ty) { + new_stream.remove(0); + } + if new_stream.last().is_some_and(is_break_ty) { + new_stream.pop(); + } + + // Remove paragraph breaks that shouldn't be there. w[1] is what will be + // removed in these cases. Note that these are the items to keep, not delete + // (for `retain`) + let to_keep: Vec<bool> = new_stream + .windows(3) + .map(|w| { + !((matches!(&w[1], MdTree::ParagraphBreak) + && matches!(should_break(&w[0], &w[2]), BreakRule::Always(1) | BreakRule::Never)) + || (matches!(&w[1], MdTree::PlainText(txt) if txt.trim().is_empty()) + && matches!( + should_break(&w[0], &w[2]), + BreakRule::Always(_) | BreakRule::Never + ))) + }) + .collect(); + let mut iter = iter::once(true).chain(to_keep).chain(iter::once(true)); + new_stream.retain(|_| iter.next().unwrap()); + + // Insert line or paragraph breaks where there should be some + let mut insertions = 0; + let to_insert: Vec<(usize, MdTree<'_>)> = new_stream + .windows(2) + .enumerate() + .filter_map(|(idx, w)| match should_break(&w[0], &w[1]) { + BreakRule::Always(1) => Some((idx, MdTree::LineBreak)), + BreakRule::Always(2) => Some((idx, MdTree::ParagraphBreak)), + _ => None, + }) + .map(|(idx, tt)| { + insertions += 1; + (idx + insertions, tt) + }) + .collect(); + to_insert.into_iter().for_each(|(idx, tt)| new_stream.insert(idx, tt)); + + MdStream(new_stream) +} + +/// Whether two types should or shouldn't have a paragraph break between them +#[derive(Clone, Copy, Debug, PartialEq)] +enum BreakRule { + Always(u8), + Never, + Optional, +} + +/// Blocks that automatically handle their own text wrapping +fn should_break(left: &MdTree<'_>, right: &MdTree<'_>) -> BreakRule { + use MdTree::*; + + match (left, right) { + // Separate these types with a single line + (HorizontalRule, _) + | (_, HorizontalRule) + | (OrderedListItem(_, _), OrderedListItem(_, _)) + | (UnorderedListItem(_), UnorderedListItem(_)) => BreakRule::Always(1), + // Condensed types shouldn't have an extra break on either side + (Comment(_) | ParagraphBreak | Heading(_, _), _) | (_, Comment(_) | ParagraphBreak) => { + BreakRule::Never + } + // Block types should always be separated by full breaks + (CodeBlock { .. } | OrderedListItem(_, _) | UnorderedListItem(_), _) + | (_, CodeBlock { .. } | Heading(_, _) | OrderedListItem(_, _) | UnorderedListItem(_)) => { + BreakRule::Always(2) + } + // Text types may or may not be separated by a break + ( + CodeInline(_) + | Strong(_) + | Emphasis(_) + | Strikethrough(_) + | PlainText(_) + | Link { .. } + | RefLink { .. } + | LinkDef { .. }, + CodeInline(_) + | Strong(_) + | Emphasis(_) + | Strikethrough(_) + | PlainText(_) + | Link { .. } + | RefLink { .. } + | LinkDef { .. }, + ) => BreakRule::Optional, + (LineBreak, _) | (_, LineBreak) => { + unreachable!("should have been removed during deduplication") + } + } +} + +/// Types that indicate some form of break +fn is_break_ty(val: &MdTree<'_>) -> bool { + matches!(val, MdTree::ParagraphBreak | MdTree::LineBreak) + // >1 break between paragraphs acts as a break + || matches!(val, MdTree::PlainText(txt) if txt.trim().is_empty()) +} + +/// Perform transformations to text. This splits paragraphs, replaces patterns, +/// and corrects newlines. +/// +/// To avoid allocating strings (and using a different heavier tt type), our +/// replace method means split into three and append each. For this reason, any +/// viewer should treat consecutive `PlainText` types as belonging to the same +/// paragraph. +fn expand_plaintext<'a>( + txt: &'a str, + stream: &mut Vec<MdTree<'a>>, + mut f: fn(&'a str) -> MdTree<'a>, +) { + if txt.is_empty() { + return; + } else if txt == "\n" { + if let Some(tt) = stream.last() { + let tmp = MdTree::PlainText(" "); + if should_break(tt, &tmp) == BreakRule::Optional { + stream.push(tmp); + } + } + return; + } + let mut queue1 = Vec::new(); + let mut queue2 = Vec::new(); + let stream_start_len = stream.len(); + for paragraph in txt.split("\n\n") { + if paragraph.is_empty() { + stream.push(MdTree::ParagraphBreak); + continue; + } + let paragraph = trim_extra_ws(paragraph); + + queue1.clear(); + queue1.push(paragraph); + + for (from, to) in REPLACEMENTS { + queue2.clear(); + for item in &queue1 { + for s in item.split(from) { + queue2.extend(&[s, to]); + } + if queue2.len() > 1 { + let _ = queue2.pop(); // remove last unnecessary intersperse + } + } + mem::swap(&mut queue1, &mut queue2); + } + + // Make sure we don't double whitespace + queue1.retain(|s| !s.is_empty()); + for idx in 0..queue1.len() { + queue1[idx] = trim_extra_ws(queue1[idx]); + if idx < queue1.len() - 1 + && queue1[idx].ends_with(char::is_whitespace) + && queue1[idx + 1].starts_with(char::is_whitespace) + { + queue1[idx] = queue1[idx].trim_end(); + } + } + stream.extend(queue1.iter().copied().filter(|txt| !txt.is_empty()).map(&mut f)); + stream.push(MdTree::ParagraphBreak); + } + + if stream.len() - stream_start_len > 1 { + let _ = stream.pop(); // remove last unnecessary intersperse + } +} + +/// Turn reflinks (links with reference IDs) into normal standalone links using +/// listed link definitions +fn match_reflink<'a>(linkdefs: &[MdTree<'a>], disp: &'a str, match_id: Option<&str>) -> MdTree<'a> { + let to_match = match_id.unwrap_or(disp); // Match with the display name if there isn't an id + for def in linkdefs { + if let MdTree::LinkDef { id, link } = def { + if *id == to_match { + return MdTree::Link { disp, link }; + } + } + } + MdTree::Link { disp, link: "" } // link not found +} + +/// If there is more than one whitespace char at start or end, trim the extras +fn trim_extra_ws(mut txt: &str) -> &str { + let start_ws = + txt.bytes().position(|ch| !ch.is_ascii_whitespace()).unwrap_or(txt.len()).saturating_sub(1); + txt = &txt[start_ws..]; + let end_ws = txt + .bytes() + .rev() + .position(|ch| !ch.is_ascii_whitespace()) + .unwrap_or(txt.len()) + .saturating_sub(1); + &txt[..txt.len() - end_ws] +} + +/// If there is more than one whitespace char at start, trim the extras +fn trim_ascii_start(buf: &[u8]) -> &[u8] { + let count = buf.iter().take_while(|ch| ch.is_ascii_whitespace()).count(); + &buf[count..] +} + +#[cfg(test)] +#[path = "tests/parse.rs"] +mod tests; diff --git a/compiler/rustc_errors/src/markdown/term.rs b/compiler/rustc_errors/src/markdown/term.rs new file mode 100644 index 00000000000..579e00b8b85 --- /dev/null +++ b/compiler/rustc_errors/src/markdown/term.rs @@ -0,0 +1,189 @@ +use std::cell::Cell; +use std::io::{self, Write}; + +use termcolor::{Buffer, Color, ColorSpec, WriteColor}; + +use crate::markdown::{MdStream, MdTree}; + +const DEFAULT_COLUMN_WIDTH: usize = 140; + +thread_local! { + /// Track the position of viewable characters in our buffer + static CURSOR: Cell<usize> = const { Cell::new(0) }; + /// Width of the terminal + static WIDTH: Cell<usize> = const { Cell::new(DEFAULT_COLUMN_WIDTH) }; +} + +/// Print to terminal output to a buffer +pub(crate) fn entrypoint(stream: &MdStream<'_>, buf: &mut Buffer) -> io::Result<()> { + #[cfg(not(test))] + if let Some((w, _)) = termize::dimensions() { + WIDTH.with(|c| c.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH))); + } + write_stream(stream, buf, None, 0)?; + buf.write_all(b"\n") +} + +/// Write the buffer, reset to the default style after each +fn write_stream( + MdStream(stream): &MdStream<'_>, + buf: &mut Buffer, + default: Option<&ColorSpec>, + indent: usize, +) -> io::Result<()> { + match default { + Some(c) => buf.set_color(c)?, + None => buf.reset()?, + } + + for tt in stream { + write_tt(tt, buf, indent)?; + if let Some(c) = default { + buf.set_color(c)?; + } + } + + buf.reset()?; + Ok(()) +} + +fn write_tt(tt: &MdTree<'_>, buf: &mut Buffer, indent: usize) -> io::Result<()> { + match tt { + MdTree::CodeBlock { txt, lang: _ } => { + buf.set_color(ColorSpec::new().set_dimmed(true))?; + buf.write_all(txt.as_bytes())?; + } + MdTree::CodeInline(txt) => { + buf.set_color(ColorSpec::new().set_dimmed(true))?; + write_wrapping(buf, txt, indent, None)?; + } + MdTree::Strong(txt) => { + buf.set_color(ColorSpec::new().set_bold(true))?; + write_wrapping(buf, txt, indent, None)?; + } + MdTree::Emphasis(txt) => { + buf.set_color(ColorSpec::new().set_italic(true))?; + write_wrapping(buf, txt, indent, None)?; + } + MdTree::Strikethrough(txt) => { + buf.set_color(ColorSpec::new().set_strikethrough(true))?; + write_wrapping(buf, txt, indent, None)?; + } + MdTree::PlainText(txt) => { + write_wrapping(buf, txt, indent, None)?; + } + MdTree::Link { disp, link } => { + write_wrapping(buf, disp, indent, Some(link))?; + } + MdTree::ParagraphBreak => { + buf.write_all(b"\n\n")?; + reset_cursor(); + } + MdTree::LineBreak => { + buf.write_all(b"\n")?; + reset_cursor(); + } + MdTree::HorizontalRule => { + (0..WIDTH.with(Cell::get)).for_each(|_| buf.write_all(b"-").unwrap()); + reset_cursor(); + } + MdTree::Heading(n, stream) => { + let mut cs = ColorSpec::new(); + cs.set_fg(Some(Color::Cyan)); + match n { + 1 => cs.set_intense(true).set_bold(true).set_underline(true), + 2 => cs.set_intense(true).set_underline(true), + 3 => cs.set_intense(true).set_italic(true), + 4.. => cs.set_underline(true).set_italic(true), + 0 => unreachable!(), + }; + write_stream(stream, buf, Some(&cs), 0)?; + buf.write_all(b"\n")?; + } + MdTree::OrderedListItem(n, stream) => { + let base = format!("{n}. "); + write_wrapping(buf, &format!("{base:<4}"), indent, None)?; + write_stream(stream, buf, None, indent + 4)?; + } + MdTree::UnorderedListItem(stream) => { + let base = "* "; + write_wrapping(buf, &format!("{base:<4}"), indent, None)?; + write_stream(stream, buf, None, indent + 4)?; + } + // Patterns popped in previous step + MdTree::Comment(_) | MdTree::LinkDef { .. } | MdTree::RefLink { .. } => unreachable!(), + } + + buf.reset()?; + + Ok(()) +} + +/// End of that block, just wrap the line +fn reset_cursor() { + CURSOR.with(|cur| cur.set(0)); +} + +/// Change to be generic on Write for testing. If we have a link URL, we don't +/// count the extra tokens to make it clickable. +fn write_wrapping<B: io::Write>( + buf: &mut B, + text: &str, + indent: usize, + link_url: Option<&str>, +) -> io::Result<()> { + let ind_ws = &b" "[..indent]; + let mut to_write = text; + if let Some(url) = link_url { + // This is a nonprinting prefix so we don't increment our cursor + write!(buf, "\x1b]8;;{url}\x1b\\")?; + } + CURSOR.with(|cur| { + loop { + if cur.get() == 0 { + buf.write_all(ind_ws)?; + cur.set(indent); + } + let ch_count = WIDTH.with(Cell::get) - cur.get(); + let mut iter = to_write.char_indices(); + let Some((end_idx, _ch)) = iter.nth(ch_count) else { + // Write entire line + buf.write_all(to_write.as_bytes())?; + cur.set(cur.get() + to_write.chars().count()); + break; + }; + + if let Some((break_idx, ch)) = to_write[..end_idx] + .char_indices() + .rev() + .find(|(_idx, ch)| ch.is_whitespace() || ['_', '-'].contains(ch)) + { + // Found whitespace to break at + if ch.is_whitespace() { + writeln!(buf, "{}", &to_write[..break_idx])?; + to_write = to_write[break_idx..].trim_start(); + } else { + // Break at a `-` or `_` separator + writeln!(buf, "{}", &to_write.get(..break_idx + 1).unwrap_or(to_write))?; + to_write = to_write.get(break_idx + 1..).unwrap_or_default().trim_start(); + } + } else { + // No whitespace, we need to just split + let ws_idx = + iter.find(|(_, ch)| ch.is_whitespace()).map_or(to_write.len(), |(idx, _)| idx); + writeln!(buf, "{}", &to_write[..ws_idx])?; + to_write = to_write.get(ws_idx + 1..).map_or("", str::trim_start); + } + cur.set(0); + } + if link_url.is_some() { + buf.write_all(b"\x1b]8;;\x1b\\")?; + } + + Ok(()) + }) +} + +#[cfg(test)] +#[path = "tests/term.rs"] +mod tests; diff --git a/compiler/rustc_errors/src/markdown/tests/input.md b/compiler/rustc_errors/src/markdown/tests/input.md new file mode 100644 index 00000000000..7d207fc4220 --- /dev/null +++ b/compiler/rustc_errors/src/markdown/tests/input.md @@ -0,0 +1,50 @@ +# H1 Heading [with a link][remote-link] + +H1 content: **some words in bold** and `so does inline code` + +## H2 Heading + +H2 content: _some words in italic_ + +### H3 Heading + +H3 content: ~~strikethrough~~ text + +#### H4 Heading + +H4 content: A [simple link](https://docs.rs) and a [remote-link]. + +--- + +A section break was above. We can also do paragraph breaks: + +(new paragraph) and unordered lists: + +- Item 1 in `code` +- Item 2 in _italics_ + +Or ordered: + +1. Item 1 in **bold** +2. Item 2 with some long lines that should wrap: Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Aenean ac mattis nunc. Phasellus elit quam, + pulvinar ac risus in, dictum vehicula turpis. Vestibulum neque est, accumsan + in cursus sit amet, dictum a nunc. Suspendisse aliquet, lorem eu eleifend + accumsan, magna neque sodales nisi, a aliquet lectus leo eu sem. + +--- + +## Code + +Both `inline code` and code blocks are supported: + +```rust +/// A rust enum +#[derive(Debug, PartialEq, Clone)] +enum Foo { + /// Start of line + Bar +} +``` + +[remote-link]: http://docs.rs diff --git a/compiler/rustc_errors/src/markdown/tests/output.stdout b/compiler/rustc_errors/src/markdown/tests/output.stdout new file mode 100644 index 00000000000..23c60d5c319 --- /dev/null +++ b/compiler/rustc_errors/src/markdown/tests/output.stdout @@ -0,0 +1,35 @@ +[0m[0m[1m[4m[38;5;14mH1 Heading [0m[0m[1m[4m[38;5;14m]8;;http://docs.rs\with a link]8;;\[0m[0m[1m[4m[38;5;14m[0m +[0mH1 content: [0m[0m[1msome words in bold[0m and [0m[0m[2mso does inline code[0m + +[0m[0m[4m[38;5;14mH2 Heading[0m[0m[4m[38;5;14m[0m +[0mH2 content: [0m[0m[3msome words in italic[0m + +[0m[0m[3m[38;5;14mH3 Heading[0m[0m[3m[38;5;14m[0m +[0mH3 content: [0m[0m[9mstrikethrough[0m text[0m + +[0m[0m[3m[4m[36mH4 Heading[0m[0m[3m[4m[36m[0m +[0mH4 content: A [0m]8;;https://docs.rs\simple link]8;;\[0m and a [0m]8;;http://docs.rs\remote-link]8;;\[0m.[0m +[0m--------------------------------------------------------------------------------------------------------------------------------------------[0m +[0mA section break was above. We can also do paragraph breaks:[0m + +[0m(new paragraph) and unordered lists:[0m + +[0m* [0mItem 1 in [0m[0m[2mcode[0m[0m[0m +[0m* [0mItem 2 in [0m[0m[3mitalics[0m[0m[0m + +[0mOr ordered:[0m + +[0m1. [0mItem 1 in [0m[0m[1mbold[0m[0m[0m +[0m2. [0mItem 2 with some long lines that should wrap: Lorem ipsum dolor sit amet,[0m consectetur adipiscing elit. Aenean ac mattis nunc. Phasellus + elit quam,[0m pulvinar ac risus in, dictum vehicula turpis. Vestibulum neque est, accumsan[0m in cursus sit amet, dictum a nunc. Suspendisse + aliquet, lorem eu eleifend[0m accumsan, magna neque sodales nisi, a aliquet lectus leo eu sem.[0m[0m[0m +[0m--------------------------------------------------------------------------------------------------------------------------------------------[0m +[0m[0m[4m[38;5;14mCode[0m[0m[4m[38;5;14m[0m +[0mBoth [0m[0m[2minline code[0m and code blocks are supported:[0m + +[0m[0m[2m/// A rust enum +#[derive(Debug, PartialEq, Clone)] +enum Foo { + /// Start of line + Bar +}[0m[0m diff --git a/compiler/rustc_errors/src/markdown/tests/parse.rs b/compiler/rustc_errors/src/markdown/tests/parse.rs new file mode 100644 index 00000000000..bfcb3de16fa --- /dev/null +++ b/compiler/rustc_errors/src/markdown/tests/parse.rs @@ -0,0 +1,366 @@ +use ParseOpt as PO; + +use super::*; + +#[test] +fn test_parse_simple() { + let buf = "**abcd** rest"; + let (t, r) = parse_simple_pat(buf.as_bytes(), b"**", b"**", PO::None, MdTree::Strong).unwrap(); + assert_eq!(t, MdTree::Strong("abcd")); + assert_eq!(r, b" rest"); + + // Escaping should fail + let buf = r"**abcd\** rest"; + let res = parse_simple_pat(buf.as_bytes(), b"**", b"**", PO::None, MdTree::Strong); + assert!(res.is_none()); +} + +#[test] +fn test_parse_comment() { + let opt = PO::TrimNoEsc; + let buf = "<!-- foobar! -->rest"; + let (t, r) = parse_simple_pat(buf.as_bytes(), CMT_S, CMT_E, opt, MdTree::Comment).unwrap(); + assert_eq!(t, MdTree::Comment("foobar!")); + assert_eq!(r, b"rest"); + + let buf = r"<!-- foobar! \-->rest"; + let (t, r) = parse_simple_pat(buf.as_bytes(), CMT_S, CMT_E, opt, MdTree::Comment).unwrap(); + assert_eq!(t, MdTree::Comment(r"foobar! \")); + assert_eq!(r, b"rest"); +} + +#[test] +fn test_parse_heading() { + let buf1 = "# Top level\nrest"; + let (t, r) = parse_heading(buf1.as_bytes()).unwrap(); + assert_eq!(t, MdTree::Heading(1, vec![MdTree::PlainText("Top level")].into())); + assert_eq!(r, b"\nrest"); + + let buf1 = "# Empty"; + let (t, r) = parse_heading(buf1.as_bytes()).unwrap(); + assert_eq!(t, MdTree::Heading(1, vec![MdTree::PlainText("Empty")].into())); + assert_eq!(r, b""); + + // Combo + let buf2 = "### Top `level` _woo_\nrest"; + let (t, r) = parse_heading(buf2.as_bytes()).unwrap(); + assert_eq!( + t, + MdTree::Heading( + 3, + vec![ + MdTree::PlainText("Top "), + MdTree::CodeInline("level"), + MdTree::PlainText(" "), + MdTree::Emphasis("woo"), + ] + .into() + ) + ); + assert_eq!(r, b"\nrest"); +} + +#[test] +fn test_parse_code_inline() { + let buf1 = "`abcd` rest"; + let (t, r) = parse_codeinline(buf1.as_bytes()).unwrap(); + assert_eq!(t, MdTree::CodeInline("abcd")); + assert_eq!(r, b" rest"); + + // extra backticks, newline + let buf2 = "```ab\ncd``` rest"; + let (t, r) = parse_codeinline(buf2.as_bytes()).unwrap(); + assert_eq!(t, MdTree::CodeInline("ab\ncd")); + assert_eq!(r, b" rest"); + + // test no escaping + let buf3 = r"`abcd\` rest"; + let (t, r) = parse_codeinline(buf3.as_bytes()).unwrap(); + assert_eq!(t, MdTree::CodeInline(r"abcd\")); + assert_eq!(r, b" rest"); +} + +#[test] +fn test_parse_code_block() { + let buf1 = "```rust\ncode\ncode\n```\nleftovers"; + let (t, r) = parse_codeblock(buf1.as_bytes()); + assert_eq!(t, MdTree::CodeBlock { txt: "code\ncode", lang: Some("rust") }); + assert_eq!(r, b"\nleftovers"); + + let buf2 = "`````\ncode\ncode````\n`````\nleftovers"; + let (t, r) = parse_codeblock(buf2.as_bytes()); + assert_eq!(t, MdTree::CodeBlock { txt: "code\ncode````", lang: None }); + assert_eq!(r, b"\nleftovers"); +} + +#[test] +fn test_parse_link() { + let simple = "[see here](docs.rs) other"; + let (t, r) = parse_any_link(simple.as_bytes(), false).unwrap(); + assert_eq!(t, MdTree::Link { disp: "see here", link: "docs.rs" }); + assert_eq!(r, b" other"); + + let simple_toplevel = "[see here](docs.rs) other"; + let (t, r) = parse_any_link(simple_toplevel.as_bytes(), true).unwrap(); + assert_eq!(t, MdTree::Link { disp: "see here", link: "docs.rs" }); + assert_eq!(r, b" other"); + + let reference = "[see here] other"; + let (t, r) = parse_any_link(reference.as_bytes(), true).unwrap(); + assert_eq!(t, MdTree::RefLink { disp: "see here", id: None }); + assert_eq!(r, b" other"); + + let reference_full = "[see here][docs-rs] other"; + let (t, r) = parse_any_link(reference_full.as_bytes(), false).unwrap(); + assert_eq!(t, MdTree::RefLink { disp: "see here", id: Some("docs-rs") }); + assert_eq!(r, b" other"); + + let reference_def = "[see here]: docs.rs\nother"; + let (t, r) = parse_any_link(reference_def.as_bytes(), true).unwrap(); + assert_eq!(t, MdTree::LinkDef { id: "see here", link: "docs.rs" }); + assert_eq!(r, b"\nother"); +} + +const IND1: &str = r"test standard + ind + ind2 +not ind"; +const IND2: &str = r"test end of stream + 1 + 2 +"; +const IND3: &str = r"test empty lines + 1 + 2 + +not ind"; + +#[test] +fn test_indented_section() { + let (t, r) = get_indented_section(IND1.as_bytes()); + assert_eq!(str::from_utf8(t).unwrap(), "test standard\n ind\n ind2"); + assert_eq!(str::from_utf8(r).unwrap(), "\nnot ind"); + + let (txt, rest) = get_indented_section(IND2.as_bytes()); + assert_eq!(str::from_utf8(txt).unwrap(), "test end of stream\n 1\n 2\n"); + assert_eq!(str::from_utf8(rest).unwrap(), ""); + + let (txt, rest) = get_indented_section(IND3.as_bytes()); + assert_eq!(str::from_utf8(txt).unwrap(), "test empty lines\n 1\n 2\n"); + assert_eq!(str::from_utf8(rest).unwrap(), "\nnot ind"); +} + +const HBT: &str = r"# Heading + +content"; + +#[test] +fn test_heading_breaks() { + let expected = vec![ + MdTree::Heading(1, vec![MdTree::PlainText("Heading")].into()), + MdTree::PlainText("content"), + ] + .into(); + let res = entrypoint(HBT); + assert_eq!(res, expected); +} + +const NL1: &str = r"start + +end"; +const NL2: &str = r"start + + +end"; +const NL3: &str = r"start + + + +end"; + +#[test] +fn test_newline_breaks() { + let expected = + vec![MdTree::PlainText("start"), MdTree::ParagraphBreak, MdTree::PlainText("end")].into(); + for (idx, check) in [NL1, NL2, NL3].iter().enumerate() { + let res = entrypoint(check); + assert_eq!(res, expected, "failed {idx}"); + } +} + +const WRAP: &str = "plain _italics +italics_"; + +#[test] +fn test_wrap_pattern() { + let expected = vec![ + MdTree::PlainText("plain "), + MdTree::Emphasis("italics"), + MdTree::Emphasis(" "), + MdTree::Emphasis("italics"), + ] + .into(); + let res = entrypoint(WRAP); + assert_eq!(res, expected); +} + +const WRAP_NOTXT: &str = r"_italics_ +**bold**"; + +#[test] +fn test_wrap_notxt() { + let expected = + vec![MdTree::Emphasis("italics"), MdTree::PlainText(" "), MdTree::Strong("bold")].into(); + let res = entrypoint(WRAP_NOTXT); + assert_eq!(res, expected); +} + +const MIXED_LIST: &str = r"start +- _italics item_ +<!-- comment --> +- **bold item** + second line [link1](foobar1) + third line [link2][link-foo] +- :crab: + extra indent +end +[link-foo]: foobar2 +"; + +#[test] +fn test_list() { + let expected = vec![ + MdTree::PlainText("start"), + MdTree::ParagraphBreak, + MdTree::UnorderedListItem(vec![MdTree::Emphasis("italics item")].into()), + MdTree::LineBreak, + MdTree::UnorderedListItem( + vec![ + MdTree::Strong("bold item"), + MdTree::PlainText(" second line "), + MdTree::Link { disp: "link1", link: "foobar1" }, + MdTree::PlainText(" third line "), + MdTree::Link { disp: "link2", link: "foobar2" }, + ] + .into(), + ), + MdTree::LineBreak, + MdTree::UnorderedListItem( + vec![MdTree::PlainText("🦀"), MdTree::PlainText(" extra indent")].into(), + ), + MdTree::ParagraphBreak, + MdTree::PlainText("end"), + ] + .into(); + let res = entrypoint(MIXED_LIST); + assert_eq!(res, expected); +} + +const SMOOSHED: &str = r#" +start +### heading +1. ordered item +```rust +println!("Hello, world!"); +``` +`inline` +``end`` +"#; + +#[test] +fn test_without_breaks() { + let expected = vec![ + MdTree::PlainText("start"), + MdTree::ParagraphBreak, + MdTree::Heading(3, vec![MdTree::PlainText("heading")].into()), + MdTree::OrderedListItem(1, vec![MdTree::PlainText("ordered item")].into()), + MdTree::ParagraphBreak, + MdTree::CodeBlock { txt: r#"println!("Hello, world!");"#, lang: Some("rust") }, + MdTree::ParagraphBreak, + MdTree::CodeInline("inline"), + MdTree::PlainText(" "), + MdTree::CodeInline("end"), + ] + .into(); + let res = entrypoint(SMOOSHED); + assert_eq!(res, expected); +} + +const CODE_STARTLINE: &str = r#" +start +`code` +middle +`more code` +end +"#; + +#[test] +fn test_code_at_start() { + let expected = vec![ + MdTree::PlainText("start"), + MdTree::PlainText(" "), + MdTree::CodeInline("code"), + MdTree::PlainText(" "), + MdTree::PlainText("middle"), + MdTree::PlainText(" "), + MdTree::CodeInline("more code"), + MdTree::PlainText(" "), + MdTree::PlainText("end"), + ] + .into(); + let res = entrypoint(CODE_STARTLINE); + assert_eq!(res, expected); +} + +#[test] +fn test_code_in_parens() { + let expected = + vec![MdTree::PlainText("("), MdTree::CodeInline("Foo"), MdTree::PlainText(")")].into(); + let res = entrypoint("(`Foo`)"); + assert_eq!(res, expected); +} + +const LIST_WITH_SPACE: &str = " +para + * l1 + * l2 +"; + +#[test] +fn test_list_with_space() { + let expected = vec![ + MdTree::PlainText("para"), + MdTree::ParagraphBreak, + MdTree::UnorderedListItem(vec![MdTree::PlainText("l1")].into()), + MdTree::LineBreak, + MdTree::UnorderedListItem(vec![MdTree::PlainText("l2")].into()), + ] + .into(); + let res = entrypoint(LIST_WITH_SPACE); + assert_eq!(res, expected); +} + +const SNAKE_CASE: &str = " +foo*bar* +foo**bar** +foo_bar_ +foo__bar__ +"; + +#[test] +fn test_snake_case() { + let expected = vec![ + MdTree::PlainText("foo"), + MdTree::Emphasis("bar"), + MdTree::PlainText(" "), + MdTree::PlainText("foo"), + MdTree::Strong("bar"), + MdTree::PlainText(" "), + MdTree::PlainText("foo_bar_"), + MdTree::PlainText(" "), + MdTree::PlainText("foo__bar__"), + ] + .into(); + let res = entrypoint(SNAKE_CASE); + assert_eq!(res, expected); +} diff --git a/compiler/rustc_errors/src/markdown/tests/term.rs b/compiler/rustc_errors/src/markdown/tests/term.rs new file mode 100644 index 00000000000..e025870f055 --- /dev/null +++ b/compiler/rustc_errors/src/markdown/tests/term.rs @@ -0,0 +1,91 @@ +use std::io::BufWriter; +use std::path::PathBuf; + +use termcolor::{BufferWriter, ColorChoice}; + +use super::*; + +const INPUT: &str = include_str!("input.md"); +const OUTPUT_PATH: &[&str] = + &[env!("CARGO_MANIFEST_DIR"), "src", "markdown", "tests", "output.stdout"]; + +const TEST_WIDTH: usize = 80; + +// We try to make some words long to create corner cases +const TXT: &str = r"Lorem ipsum dolor sit amet, consecteturadipiscingelit. +Fusce-id-urna-sollicitudin, pharetra nisl nec, lobortis tellus. In at +metus hendrerit, tincidunteratvel, ultrices turpis. Curabitur_risus_sapien, +porta-sed-nunc-sed, ultricesposuerelacus. Sed porttitor quis +dolor non venenatis. Aliquam ut. "; + +const WRAPPED: &str = r"Lorem ipsum dolor sit amet, consecteturadipiscingelit. Fusce-id-urna- +sollicitudin, pharetra nisl nec, lobortis tellus. In at metus hendrerit, +tincidunteratvel, ultrices turpis. Curabitur_risus_sapien, porta-sed-nunc-sed, +ultricesposuerelacus. Sed porttitor quis dolor non venenatis. Aliquam ut. Lorem + ipsum dolor sit amet, consecteturadipiscingelit. Fusce-id-urna- + sollicitudin, pharetra nisl nec, lobortis tellus. In at metus hendrerit, + tincidunteratvel, ultrices turpis. Curabitur_risus_sapien, porta-sed-nunc- + sed, ultricesposuerelacus. Sed porttitor quis dolor non venenatis. Aliquam + ut. Sample link lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, +consecteturadipiscingelit. Fusce-id-urna-sollicitudin, pharetra nisl nec, +lobortis tellus. In at metus hendrerit, tincidunteratvel, ultrices turpis. +Curabitur_risus_sapien, porta-sed-nunc-sed, ultricesposuerelacus. Sed porttitor +quis dolor non venenatis. Aliquam ut. "; + +#[test] +fn test_wrapping_write() { + WIDTH.with(|w| w.set(TEST_WIDTH)); + let mut buf = BufWriter::new(Vec::new()); + let txt = TXT.replace("-\n", "-").replace("_\n", "_").replace('\n', " ").replace(" ", ""); + write_wrapping(&mut buf, &txt, 0, None).unwrap(); + write_wrapping(&mut buf, &txt, 4, None).unwrap(); + write_wrapping( + &mut buf, + "Sample link lorem ipsum dolor sit amet. ", + 4, + Some("link-address-placeholder"), + ) + .unwrap(); + write_wrapping(&mut buf, &txt, 0, None).unwrap(); + let out = String::from_utf8(buf.into_inner().unwrap()).unwrap(); + let out = out + .replace("\x1b\\", "") + .replace('\x1b', "") + .replace("]8;;", "") + .replace("link-address-placeholder", ""); + + for line in out.lines() { + assert!(line.len() <= TEST_WIDTH, "line length\n'{line}'") + } + + assert_eq!(out, WRAPPED); +} + +#[test] +fn test_output() { + // Capture `--bless` when run via ./x + let bless = std::env::var_os("RUSTC_BLESS").is_some_and(|v| v != "0"); + let ast = MdStream::parse_str(INPUT); + let bufwtr = BufferWriter::stderr(ColorChoice::Always); + let mut buffer = bufwtr.buffer(); + ast.write_termcolor_buf(&mut buffer).unwrap(); + + let mut blessed = PathBuf::new(); + blessed.extend(OUTPUT_PATH); + + if bless { + std::fs::write(&blessed, buffer.into_inner()).unwrap(); + eprintln!("blessed output at {}", blessed.display()); + } else { + let output = buffer.into_inner(); + if std::fs::read(blessed).unwrap() != output { + // hack: I don't know any way to write bytes to the captured stdout + // that cargo test uses + let mut out = std::io::stdout(); + out.write_all(b"\n\nMarkdown output did not match. Expected:\n").unwrap(); + out.write_all(&output).unwrap(); + out.write_all(b"\n\n").unwrap(); + panic!("markdown output mismatch"); + } + } +} diff --git a/compiler/rustc_errors/src/registry.rs b/compiler/rustc_errors/src/registry.rs new file mode 100644 index 00000000000..baca7700d90 --- /dev/null +++ b/compiler/rustc_errors/src/registry.rs @@ -0,0 +1,23 @@ +use rustc_data_structures::fx::FxHashMap; + +use crate::ErrCode; + +#[derive(Debug)] +pub struct InvalidErrorCode; + +#[derive(Clone)] +pub struct Registry { + long_descriptions: FxHashMap<ErrCode, &'static str>, +} + +impl Registry { + pub fn new(long_descriptions: &[(ErrCode, &'static str)]) -> Registry { + Registry { long_descriptions: long_descriptions.iter().copied().collect() } + } + + /// Returns `InvalidErrorCode` if the code requested does not exist in the + /// registry. + pub fn try_find_description(&self, code: ErrCode) -> Result<&'static str, InvalidErrorCode> { + self.long_descriptions.get(&code).copied().ok_or(InvalidErrorCode) + } +} diff --git a/compiler/rustc_errors/src/snippet.rs b/compiler/rustc_errors/src/snippet.rs new file mode 100644 index 00000000000..8485d7087cf --- /dev/null +++ b/compiler/rustc_errors/src/snippet.rs @@ -0,0 +1,218 @@ +// Code for annotating snippets. + +use rustc_macros::{Decodable, Encodable}; + +use crate::{Level, Loc}; + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Line { + pub line_index: usize, + pub annotations: Vec<Annotation>, +} + +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Default)] +pub(crate) struct AnnotationColumn { + /// the (0-indexed) column for *display* purposes, counted in characters, not utf-8 bytes + pub display: usize, + /// the (0-indexed) column in the file, counted in characters, not utf-8 bytes. + /// + /// this may be different from `self.display`, + /// e.g. if the file contains hard tabs, because we convert tabs to spaces for error messages. + /// + /// for example: + /// ```text + /// (hard tab)hello + /// ^ this is display column 4, but file column 1 + /// ``` + /// + /// we want to keep around the correct file offset so that column numbers in error messages + /// are correct. (motivated by <https://github.com/rust-lang/rust/issues/109537>) + pub file: usize, +} + +impl AnnotationColumn { + pub(crate) fn from_loc(loc: &Loc) -> AnnotationColumn { + AnnotationColumn { display: loc.col_display, file: loc.col.0 } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct MultilineAnnotation { + pub depth: usize, + pub line_start: usize, + pub line_end: usize, + pub start_col: AnnotationColumn, + pub end_col: AnnotationColumn, + pub is_primary: bool, + pub label: Option<String>, + pub overlaps_exactly: bool, +} + +impl MultilineAnnotation { + pub(crate) fn increase_depth(&mut self) { + self.depth += 1; + } + + /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. + pub(crate) fn same_span(&self, other: &MultilineAnnotation) -> bool { + self.line_start == other.line_start + && self.line_end == other.line_end + && self.start_col == other.start_col + && self.end_col == other.end_col + } + + pub(crate) fn as_start(&self) -> Annotation { + Annotation { + start_col: self.start_col, + end_col: AnnotationColumn { + // these might not correspond to the same place anymore, + // but that's okay for our purposes + display: self.start_col.display + 1, + file: self.start_col.file + 1, + }, + is_primary: self.is_primary, + label: None, + annotation_type: AnnotationType::MultilineStart(self.depth), + } + } + + pub(crate) fn as_end(&self) -> Annotation { + Annotation { + start_col: AnnotationColumn { + // these might not correspond to the same place anymore, + // but that's okay for our purposes + display: self.end_col.display.saturating_sub(1), + file: self.end_col.file.saturating_sub(1), + }, + end_col: self.end_col, + is_primary: self.is_primary, + label: self.label.clone(), + annotation_type: AnnotationType::MultilineEnd(self.depth), + } + } + + pub(crate) fn as_line(&self) -> Annotation { + Annotation { + start_col: Default::default(), + end_col: Default::default(), + is_primary: self.is_primary, + label: None, + annotation_type: AnnotationType::MultilineLine(self.depth), + } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum AnnotationType { + /// Annotation under a single line of code + Singleline, + + // The Multiline type above is replaced with the following three in order + // to reuse the current label drawing code. + // + // Each of these corresponds to one part of the following diagram: + // + // x | foo(1 + bar(x, + // | _________^ < MultilineStart + // x | | y), < MultilineLine + // | |______________^ label < MultilineEnd + // x | z); + /// Annotation marking the first character of a fully shown multiline span + MultilineStart(usize), + /// Annotation marking the last character of a fully shown multiline span + MultilineEnd(usize), + /// Line at the left enclosing the lines of a fully shown multiline span + // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 + // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in + // `draw_multiline_line`. + MultilineLine(usize), +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Annotation { + /// Start column. + /// Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + pub start_col: AnnotationColumn, + + /// End column within the line (exclusive) + pub end_col: AnnotationColumn, + + /// Is this annotation derived from primary span + pub is_primary: bool, + + /// Optional label to display adjacent to the annotation. + pub label: Option<String>, + + /// Is this a single line, multiline or multiline span minimized down to a + /// smaller span. + pub annotation_type: AnnotationType, +} + +impl Annotation { + /// Whether this annotation is a vertical line placeholder. + pub(crate) fn is_line(&self) -> bool { + matches!(self.annotation_type, AnnotationType::MultilineLine(_)) + } + + /// Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + if self.end_col.display > self.start_col.display { + self.end_col.display - self.start_col.display + } else { + self.start_col.display - self.end_col.display + } + } + + pub(crate) fn has_label(&self) -> bool { + if let Some(ref label) = self.label { + // Consider labels with no text as effectively not being there + // to avoid weird output with unnecessary vertical lines, like: + // + // X | fn foo(x: u32) { + // | -------^------ + // | | | + // | | + // | + // + // Note that this would be the complete output users would see. + !label.is_empty() + } else { + false + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_type, + AnnotationType::MultilineStart(_) | AnnotationType::MultilineEnd(_) + ) + } +} + +#[derive(Debug)] +pub(crate) struct StyledString { + pub text: String, + pub style: Style, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)] +pub enum Style { + MainHeaderMsg, + HeaderMsg, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, + Level(Level), + Highlight, + Addition, + Removal, +} diff --git a/compiler/rustc_errors/src/styled_buffer.rs b/compiler/rustc_errors/src/styled_buffer.rs new file mode 100644 index 00000000000..5ca9e9b18f3 --- /dev/null +++ b/compiler/rustc_errors/src/styled_buffer.rs @@ -0,0 +1,151 @@ +// Code for creating styled buffers + +use crate::snippet::{Style, StyledString}; + +#[derive(Debug)] +pub(crate) struct StyledBuffer { + lines: Vec<Vec<StyledChar>>, +} + +#[derive(Debug, Clone)] +struct StyledChar { + chr: char, + style: Style, +} + +impl StyledChar { + const SPACE: Self = StyledChar::new(' ', Style::NoStyle); + + const fn new(chr: char, style: Style) -> Self { + StyledChar { chr, style } + } +} + +impl StyledBuffer { + pub(crate) fn new() -> StyledBuffer { + StyledBuffer { lines: vec![] } + } + + /// Returns content of `StyledBuffer` split by lines and line styles + pub(crate) fn render(&self) -> Vec<Vec<StyledString>> { + // Tabs are assumed to have been replaced by spaces in calling code. + debug_assert!(self.lines.iter().all(|r| !r.iter().any(|sc| sc.chr == '\t'))); + + let mut output: Vec<Vec<StyledString>> = vec![]; + let mut styled_vec: Vec<StyledString> = vec![]; + + for styled_line in &self.lines { + let mut current_style = Style::NoStyle; + let mut current_text = String::new(); + + for sc in styled_line { + if sc.style != current_style { + if !current_text.is_empty() { + styled_vec.push(StyledString { text: current_text, style: current_style }); + } + current_style = sc.style; + current_text = String::new(); + } + current_text.push(sc.chr); + } + if !current_text.is_empty() { + styled_vec.push(StyledString { text: current_text, style: current_style }); + } + + // We're done with the row, push and keep going + output.push(styled_vec); + + styled_vec = vec![]; + } + + output + } + + fn ensure_lines(&mut self, line: usize) { + if line >= self.lines.len() { + self.lines.resize(line + 1, Vec::new()); + } + } + + /// Sets `chr` with `style` for given `line`, `col`. + /// If `line` does not exist in our buffer, adds empty lines up to the given + /// and fills the last line with unstyled whitespace. + pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + self.ensure_lines(line); + if col >= self.lines[line].len() { + self.lines[line].resize(col + 1, StyledChar::SPACE); + } + self.lines[line][col] = StyledChar::new(chr, style); + } + + /// Sets `string` with `style` for given `line`, starting from `col`. + /// If `line` does not exist in our buffer, adds empty lines up to the given + /// and fills the last line with unstyled whitespace. + pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + let mut n = col; + for c in string.chars() { + self.putc(line, n, c, style); + n += 1; + } + } + + /// For given `line` inserts `string` with `style` before old content of that line, + /// adding lines if needed + pub(crate) fn prepend(&mut self, line: usize, string: &str, style: Style) { + self.ensure_lines(line); + let string_len = string.chars().count(); + + if !self.lines[line].is_empty() { + // Push the old content over to make room for new content + for _ in 0..string_len { + self.lines[line].insert(0, StyledChar::SPACE); + } + } + + self.puts(line, 0, string, style); + } + + /// For given `line` inserts `string` with `style` after old content of that line, + /// adding lines if needed + pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) { + if line >= self.lines.len() { + self.puts(line, 0, string, style); + } else { + let col = self.lines[line].len(); + self.puts(line, col, string, style); + } + } + + pub(crate) fn num_lines(&self) -> usize { + self.lines.len() + } + + /// Set `style` for `line`, `col_start..col_end` range if: + /// 1. That line and column range exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style_range( + &mut self, + line: usize, + col_start: usize, + col_end: usize, + style: Style, + overwrite: bool, + ) { + for col in col_start..col_end { + self.set_style(line, col, style, overwrite); + } + } + + /// Set `style` for `line`, `col` if: + /// 1. That line and column exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + fn set_style(&mut self, line: usize, col: usize, style: Style, overwrite: bool) { + if let Some(ref mut line) = self.lines.get_mut(line) { + if let Some(StyledChar { style: s, .. }) = line.get_mut(col) { + if overwrite || matches!(s, Style::NoStyle | Style::Quotation) { + *s = style; + } + } + } + } +} diff --git a/compiler/rustc_errors/src/tests.rs b/compiler/rustc_errors/src/tests.rs new file mode 100644 index 00000000000..376fd24d57b --- /dev/null +++ b/compiler/rustc_errors/src/tests.rs @@ -0,0 +1,191 @@ +use rustc_data_structures::sync::IntoDynSyncSend; +use rustc_error_messages::fluent_bundle::resolver::errors::{ReferenceKind, ResolverError}; +use rustc_error_messages::{DiagMessage, langid}; + +use crate::FluentBundle; +use crate::error::{TranslateError, TranslateErrorKind}; +use crate::fluent_bundle::*; +use crate::translation::Translate; + +struct Dummy { + bundle: FluentBundle, +} + +impl Translate for Dummy { + fn fluent_bundle(&self) -> Option<&FluentBundle> { + None + } + + fn fallback_fluent_bundle(&self) -> &FluentBundle { + &self.bundle + } +} + +fn make_dummy(ftl: &'static str) -> Dummy { + let resource = FluentResource::try_new(ftl.into()).expect("Failed to parse an FTL string."); + + let langid_en = langid!("en-US"); + + let mut bundle: FluentBundle = + IntoDynSyncSend(crate::fluent_bundle::bundle::FluentBundle::new_concurrent(vec![ + langid_en, + ])); + + bundle.add_resource(resource).expect("Failed to add FTL resources to the bundle."); + + Dummy { bundle } +} + +#[test] +fn wellformed_fluent() { + let dummy = make_dummy("mir_build_borrow_of_moved_value = borrow of moved value + .label = value moved into `{$name}` here + .occurs_because_label = move occurs because `{$name}` has type `{$ty}` which does not implement the `Copy` trait + .value_borrowed_label = value borrowed here after move + .suggestion = borrow this binding in the pattern to avoid moving the value"); + + let mut args = FluentArgs::new(); + args.set("name", "Foo"); + args.set("ty", "std::string::String"); + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("suggestion".into()), + ); + + assert_eq!( + dummy.translate_message(&message, &args).unwrap(), + "borrow this binding in the pattern to avoid moving the value" + ); + } + + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("value_borrowed_label".into()), + ); + + assert_eq!( + dummy.translate_message(&message, &args).unwrap(), + "value borrowed here after move" + ); + } + + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("occurs_because_label".into()), + ); + + assert_eq!( + dummy.translate_message(&message, &args).unwrap(), + "move occurs because `\u{2068}Foo\u{2069}` has type `\u{2068}std::string::String\u{2069}` which does not implement the `Copy` trait" + ); + + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("label".into()), + ); + + assert_eq!( + dummy.translate_message(&message, &args).unwrap(), + "value moved into `\u{2068}Foo\u{2069}` here" + ); + } + } +} + +#[test] +fn misformed_fluent() { + let dummy = make_dummy("mir_build_borrow_of_moved_value = borrow of moved value + .label = value moved into `{name}` here + .occurs_because_label = move occurs because `{$oops}` has type `{$ty}` which does not implement the `Copy` trait + .suggestion = borrow this binding in the pattern to avoid moving the value"); + + let mut args = FluentArgs::new(); + args.set("name", "Foo"); + args.set("ty", "std::string::String"); + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("value_borrowed_label".into()), + ); + + let err = dummy.translate_message(&message, &args).unwrap_err(); + assert!( + matches!( + &err, + TranslateError::Two { + primary: box TranslateError::One { + kind: TranslateErrorKind::PrimaryBundleMissing, + .. + }, + fallback: box TranslateError::One { + kind: TranslateErrorKind::AttributeMissing { attr: "value_borrowed_label" }, + .. + } + } + ), + "{err:#?}" + ); + assert_eq!( + format!("{err}"), + "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nthe attribute `value_borrowed_label` was missing\nhelp: add `.value_borrowed_label = <message>`\n" + ); + } + + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("label".into()), + ); + + let err = dummy.translate_message(&message, &args).unwrap_err(); + if let TranslateError::Two { + primary: box TranslateError::One { kind: TranslateErrorKind::PrimaryBundleMissing, .. }, + fallback: box TranslateError::One { kind: TranslateErrorKind::Fluent { errs }, .. }, + } = &err + && let [ + FluentError::ResolverError(ResolverError::Reference( + ReferenceKind::Message { id, .. } | ReferenceKind::Variable { id, .. }, + )), + ] = &**errs + && id == "name" + { + } else { + panic!("{err:#?}") + }; + assert_eq!( + format!("{err}"), + "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nargument `name` exists but was not referenced correctly\nhelp: try using `{$name}` instead\n" + ); + } + + { + let message = DiagMessage::FluentIdentifier( + "mir_build_borrow_of_moved_value".into(), + Some("occurs_because_label".into()), + ); + + let err = dummy.translate_message(&message, &args).unwrap_err(); + if let TranslateError::Two { + primary: box TranslateError::One { kind: TranslateErrorKind::PrimaryBundleMissing, .. }, + fallback: box TranslateError::One { kind: TranslateErrorKind::Fluent { errs }, .. }, + } = &err + && let [ + FluentError::ResolverError(ResolverError::Reference( + ReferenceKind::Message { id, .. } | ReferenceKind::Variable { id, .. }, + )), + ] = &**errs + && id == "oops" + { + } else { + panic!("{err:#?}") + }; + assert_eq!( + format!("{err}"), + "failed while formatting fluent string `mir_build_borrow_of_moved_value`: \nthe fluent string has an argument `oops` that was not found.\nhelp: the arguments `name` and `ty` are available\n" + ); + } +} diff --git a/compiler/rustc_errors/src/translation.rs b/compiler/rustc_errors/src/translation.rs new file mode 100644 index 00000000000..156f5e5d26e --- /dev/null +++ b/compiler/rustc_errors/src/translation.rs @@ -0,0 +1,130 @@ +use std::borrow::Cow; +use std::env; +use std::error::Report; + +pub use rustc_error_messages::FluentArgs; +use tracing::{debug, trace}; + +use crate::error::{TranslateError, TranslateErrorKind}; +use crate::snippet::Style; +use crate::{DiagArg, DiagMessage, FluentBundle}; + +/// Convert diagnostic arguments (a rustc internal type that exists to implement +/// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation. +/// +/// Typically performed once for each diagnostic at the start of `emit_diagnostic` and then +/// passed around as a reference thereafter. +pub fn to_fluent_args<'iter>(iter: impl Iterator<Item = DiagArg<'iter>>) -> FluentArgs<'static> { + let mut args = if let Some(size) = iter.size_hint().1 { + FluentArgs::with_capacity(size) + } else { + FluentArgs::new() + }; + + for (k, v) in iter { + args.set(k.clone(), v.clone()); + } + + args +} + +pub trait Translate { + /// Return `FluentBundle` with localized diagnostics for the locale requested by the user. If no + /// language was requested by the user then this will be `None` and `fallback_fluent_bundle` + /// should be used. + fn fluent_bundle(&self) -> Option<&FluentBundle>; + + /// Return `FluentBundle` with localized diagnostics for the default locale of the compiler. + /// Used when the user has not requested a specific language or when a localized diagnostic is + /// unavailable for the requested locale. + fn fallback_fluent_bundle(&self) -> &FluentBundle; + + /// Convert `DiagMessage`s to a string, performing translation if necessary. + fn translate_messages( + &self, + messages: &[(DiagMessage, Style)], + args: &FluentArgs<'_>, + ) -> Cow<'_, str> { + Cow::Owned( + messages + .iter() + .map(|(m, _)| self.translate_message(m, args).map_err(Report::new).unwrap()) + .collect::<String>(), + ) + } + + /// Convert a `DiagMessage` to a string, performing translation if necessary. + fn translate_message<'a>( + &'a self, + message: &'a DiagMessage, + args: &'a FluentArgs<'_>, + ) -> Result<Cow<'a, str>, TranslateError<'a>> { + trace!(?message, ?args); + let (identifier, attr) = match message { + DiagMessage::Str(msg) | DiagMessage::Translated(msg) => { + return Ok(Cow::Borrowed(msg)); + } + DiagMessage::FluentIdentifier(identifier, attr) => (identifier, attr), + }; + let translate_with_bundle = + |bundle: &'a FluentBundle| -> Result<Cow<'_, str>, TranslateError<'_>> { + let message = bundle + .get_message(identifier) + .ok_or(TranslateError::message(identifier, args))?; + let value = match attr { + Some(attr) => message + .get_attribute(attr) + .ok_or(TranslateError::attribute(identifier, args, attr))? + .value(), + None => message.value().ok_or(TranslateError::value(identifier, args))?, + }; + debug!(?message, ?value); + + let mut errs = vec![]; + let translated = bundle.format_pattern(value, Some(args), &mut errs); + debug!(?translated, ?errs); + if errs.is_empty() { + Ok(translated) + } else { + Err(TranslateError::fluent(identifier, args, errs)) + } + }; + + try { + match self.fluent_bundle().map(|b| translate_with_bundle(b)) { + // The primary bundle was present and translation succeeded + Some(Ok(t)) => t, + + // If `translate_with_bundle` returns `Err` with the primary bundle, this is likely + // just that the primary bundle doesn't contain the message being translated, so + // proceed to the fallback bundle. + Some(Err( + primary @ TranslateError::One { + kind: TranslateErrorKind::MessageMissing, .. + }, + )) => translate_with_bundle(self.fallback_fluent_bundle()) + .map_err(|fallback| primary.and(fallback))?, + + // Always yeet out for errors on debug (unless + // `RUSTC_TRANSLATION_NO_DEBUG_ASSERT` is set in the environment - this allows + // local runs of the test suites, of builds with debug assertions, to test the + // behaviour in a normal build). + Some(Err(primary)) + if cfg!(debug_assertions) + && env::var("RUSTC_TRANSLATION_NO_DEBUG_ASSERT").is_err() => + { + do yeet primary + } + + // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so + // just hide it and try with the fallback bundle. + Some(Err(primary)) => translate_with_bundle(self.fallback_fluent_bundle()) + .map_err(|fallback| primary.and(fallback))?, + + // The primary bundle is missing, proceed to the fallback bundle + None => translate_with_bundle(self.fallback_fluent_bundle()) + .map_err(|fallback| TranslateError::primary(identifier, args).and(fallback))?, + } + } + } +} |
