diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs index 0ca200abe19fb..7f01df321010b 100644 --- a/compiler/rustc_errors/src/emitter.rs +++ b/compiler/rustc_errors/src/emitter.rs @@ -28,6 +28,7 @@ use rustc_error_messages::{FluentArgs, SpanLabel}; use rustc_span::hygiene::{ExpnKind, MacroKind}; use std::borrow::Cow; use std::cmp::{max, min, Reverse}; +use std::error::Report; use std::io::prelude::*; use std::io::{self, IsTerminal}; use std::iter; @@ -250,7 +251,7 @@ pub trait Emitter: Translate { let mut primary_span = diag.span.clone(); let suggestions = diag.suggestions.as_deref().unwrap_or(&[]); if let Some((sugg, rest)) = suggestions.split_first() { - let msg = self.translate_message(&sugg.msg, fluent_args); + 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 @@ -1325,7 +1326,7 @@ impl EmitterWriter { // very *weird* formats // see? for (text, style) in msg.iter() { - let text = self.translate_message(text, args); + let text = self.translate_message(text, args).map_err(Report::new).unwrap(); let lines = text.split('\n').collect::>(); if lines.len() > 1 { for (i, line) in lines.iter().enumerate() { @@ -1387,7 +1388,7 @@ impl EmitterWriter { label_width += 2; } for (text, _) in msg.iter() { - let text = self.translate_message(text, args); + let text = self.translate_message(text, args).map_err(Report::new).unwrap(); // Account for newlines to align output to its label. for (line, text) in normalize_whitespace(&text).lines().enumerate() { buffer.append( @@ -2301,7 +2302,9 @@ impl FileWithAnnotatedLines { hi.col_display += 1; } - let label = label.as_ref().map(|m| emitter.translate_message(m, args).to_string()); + let label = label.as_ref().map(|m| { + emitter.translate_message(m, args).map_err(Report::new).unwrap().to_string() + }); if lo.line != hi.line { let ml = MultilineAnnotation { diff --git a/compiler/rustc_errors/src/error.rs b/compiler/rustc_errors/src/error.rs new file mode 100644 index 0000000000000..ec0a2fe8cd8d0 --- /dev/null +++ b/compiler/rustc_errors/src/error.rs @@ -0,0 +1,137 @@ +use rustc_error_messages::{ + fluent_bundle::resolver::errors::{ReferenceKind, ResolverError}, + FluentArgs, FluentError, +}; +use std::borrow::Cow; +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +pub enum TranslateError<'args> { + One { + id: &'args Cow<'args, str>, + args: &'args FluentArgs<'args>, + kind: TranslateErrorKind<'args>, + }, + Two { + primary: Box>, + fallback: Box>, + }, +} + +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, + ) -> 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 }, +} + +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} = `")?; + } + 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 index a37073d8fa32a..dc38b8725ad1e 100644 --- a/compiler/rustc_errors/src/json.rs +++ b/compiler/rustc_errors/src/json.rs @@ -24,6 +24,7 @@ use rustc_data_structures::sync::Lrc; use rustc_error_messages::FluentArgs; use rustc_span::hygiene::ExpnData; use rustc_span::Span; +use std::error::Report; use std::io::{self, Write}; use std::path::Path; use std::sync::{Arc, Mutex}; @@ -321,7 +322,8 @@ impl Diagnostic { fn from_errors_diagnostic(diag: &crate::Diagnostic, je: &JsonEmitter) -> Diagnostic { let args = to_fluent_args(diag.args()); let sugg = diag.suggestions.iter().flatten().map(|sugg| { - let translated_message = je.translate_message(&sugg.msg, &args); + let translated_message = + je.translate_message(&sugg.msg, &args).map_err(Report::new).unwrap(); Diagnostic { message: translated_message.to_string(), code: None, @@ -411,7 +413,10 @@ impl DiagnosticSpan { Self::from_span_etc( span.span, span.is_primary, - span.label.as_ref().map(|m| je.translate_message(m, args)).map(|m| m.to_string()), + span.label + .as_ref() + .map(|m| je.translate_message(m, args).unwrap()) + .map(|m| m.to_string()), suggestion, je, ) diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs index 136c360201e61..0b5bfdc8e8985 100644 --- a/compiler/rustc_errors/src/lib.rs +++ b/compiler/rustc_errors/src/lib.rs @@ -11,6 +11,10 @@ #![feature(never_type)] #![feature(result_option_inspect)] #![feature(rustc_attrs)] +#![feature(yeet_expr)] +#![feature(try_blocks)] +#![feature(box_patterns)] +#![feature(error_reporter)] #![allow(incomplete_features)] #[macro_use] @@ -42,6 +46,7 @@ use rustc_span::{Loc, Span}; use std::any::Any; use std::borrow::Cow; +use std::error::Report; use std::fmt; use std::hash::Hash; use std::num::NonZeroUsize; @@ -55,11 +60,14 @@ mod diagnostic; mod diagnostic_builder; mod diagnostic_impls; pub mod emitter; +pub mod error; pub mod json; mod lock; pub mod registry; mod snippet; mod styled_buffer; +#[cfg(test)] +mod tests; pub mod translation; pub use diagnostic_builder::IntoDiagnostic; @@ -622,7 +630,14 @@ impl Handler { ) -> SubdiagnosticMessage { let inner = self.inner.borrow(); let args = crate::translation::to_fluent_args(args); - SubdiagnosticMessage::Eager(inner.emitter.translate_message(&message, &args).to_string()) + SubdiagnosticMessage::Eager( + inner + .emitter + .translate_message(&message, &args) + .map_err(Report::new) + .unwrap() + .to_string(), + ) } // This is here to not allow mutation of flags; diff --git a/compiler/rustc_errors/src/tests.rs b/compiler/rustc_errors/src/tests.rs new file mode 100644 index 0000000000000..52103e4609770 --- /dev/null +++ b/compiler/rustc_errors/src/tests.rs @@ -0,0 +1,188 @@ +use crate::error::{TranslateError, TranslateErrorKind}; +use crate::fluent_bundle::*; +use crate::translation::Translate; +use crate::FluentBundle; +use rustc_data_structures::sync::Lrc; +use rustc_error_messages::fluent_bundle::resolver::errors::{ReferenceKind, ResolverError}; +use rustc_error_messages::langid; +use rustc_error_messages::DiagnosticMessage; + +struct Dummy { + bundle: FluentBundle, +} + +impl Translate for Dummy { + fn fluent_bundle(&self) -> Option<&Lrc> { + 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"); + + #[cfg(parallel_compiler)] + let mut bundle = FluentBundle::new_concurrent(vec![langid_en]); + + #[cfg(not(parallel_compiler))] + let mut bundle = FluentBundle::new(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 = DiagnosticMessage::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 = DiagnosticMessage::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 = DiagnosticMessage::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 = DiagnosticMessage::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 = DiagnosticMessage::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 = `\n" + ); + } + + { + let message = DiagnosticMessage::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 = DiagnosticMessage::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 index afd660ff1bf1f..addfc9726ca44 100644 --- a/compiler/rustc_errors/src/translation.rs +++ b/compiler/rustc_errors/src/translation.rs @@ -1,11 +1,10 @@ +use crate::error::TranslateError; use crate::snippet::Style; use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle}; use rustc_data_structures::sync::Lrc; -use rustc_error_messages::{ - fluent_bundle::resolver::errors::{ReferenceKind, ResolverError}, - FluentArgs, FluentError, -}; +use rustc_error_messages::FluentArgs; use std::borrow::Cow; +use std::error::Report; /// Convert diagnostic arguments (a rustc internal type that exists to implement /// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation. @@ -46,7 +45,10 @@ pub trait Translate { args: &FluentArgs<'_>, ) -> Cow<'_, str> { Cow::Owned( - messages.iter().map(|(m, _)| self.translate_message(m, args)).collect::(), + messages + .iter() + .map(|(m, _)| self.translate_message(m, args).map_err(Report::new).unwrap()) + .collect::(), ) } @@ -55,83 +57,56 @@ pub trait Translate { &'a self, message: &'a DiagnosticMessage, args: &'a FluentArgs<'_>, - ) -> Cow<'_, str> { + ) -> Result, TranslateError<'_>> { trace!(?message, ?args); let (identifier, attr) = match message { DiagnosticMessage::Str(msg) | DiagnosticMessage::Eager(msg) => { - return Cow::Borrowed(msg); + return Ok(Cow::Borrowed(msg)); } DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr), }; + let translate_with_bundle = + |bundle: &'a FluentBundle| -> Result, 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 translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> { - let message = bundle.get_message(identifier)?; - let value = match attr { - Some(attr) => message.get_attribute(attr)?.value(), - None => 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)) + } }; - debug!(?message, ?value); - - let mut errs = vec![]; - let translated = bundle.format_pattern(value, Some(args), &mut errs); - debug!(?translated, ?errs); - Some((translated, errs)) - }; - self.fluent_bundle() - .and_then(|bundle| translate_with_bundle(bundle)) - // If `translate_with_bundle` returns `None` 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. - // - // However, when errors are produced from translation, then that means the translation - // is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided). - // - // In debug builds, assert so that compiler devs can spot the broken translation and - // fix it.. - .inspect(|(_, errs)| { - debug_assert!( - errs.is_empty(), - "identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}", - identifier, - attr, - args, - errs - ); - }) - // ..otherwise, for end users, an error about this wouldn't be useful or actionable, so - // just hide it and try with the fallback bundle. - .filter(|(_, errs)| errs.is_empty()) - .or_else(|| translate_with_bundle(self.fallback_fluent_bundle())) - .map(|(translated, errs)| { - // Always bail out for errors with the fallback bundle. + try { + match self.fluent_bundle().map(|b| translate_with_bundle(b)) { + // The primary bundle was present and translation succeeded + Some(Ok(t)) => t, - let mut help_messages = vec![]; + // Always yeet out for errors on debug + Some(Err(primary)) if cfg!(debug_assertions) => do yeet primary, - if !errs.is_empty() { - for error in &errs { - match error { - FluentError::ResolverError(ResolverError::Reference( - ReferenceKind::Message { id, .. }, - )) if args.iter().any(|(arg_id, _)| arg_id == id) => { - help_messages.push(format!("Argument `{id}` exists but was not referenced correctly. Try using `{{${id}}}` instead")); - } - _ => {} - } - } + // 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 or + // something else went wrong) so proceed to the fallback bundle. + Some(Err(primary)) => translate_with_bundle(self.fallback_fluent_bundle()) + .map_err(|fallback| primary.and(fallback))?, - panic!( - "Encountered errors while formatting message for `{identifier}`\n\ - help: {}\n\ - attr: `{attr:?}`\n\ - args: `{args:?}`\n\ - errors: `{errs:?}`", - help_messages.join("\nhelp: ") - ); - } - - translated - }) - .expect("failed to find message in primary or fallback fluent bundles") + // 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))?, + } + } } } diff --git a/src/librustdoc/passes/lint/check_code_block_syntax.rs b/src/librustdoc/passes/lint/check_code_block_syntax.rs index 5aa4f238b2d14..7158355ffdacc 100644 --- a/src/librustdoc/passes/lint/check_code_block_syntax.rs +++ b/src/librustdoc/passes/lint/check_code_block_syntax.rs @@ -156,7 +156,9 @@ impl Emitter for BufferEmitter { let mut buffer = self.buffer.borrow_mut(); let fluent_args = to_fluent_args(diag.args()); - let translated_main_message = self.translate_message(&diag.message[0].0, &fluent_args); + let translated_main_message = self + .translate_message(&diag.message[0].0, &fluent_args) + .unwrap_or_else(|e| panic!("{e}")); buffer.messages.push(format!("error from rustc: {}", translated_main_message)); if diag.is_error() {