Rollup merge of #106427 - mejrs:translation_errors, r=davidtwco

Improve fluent error messages

These have been really frustrating me while migrating diagnostics.
This commit is contained in:
nils 2023-01-11 17:30:54 +01:00 committed by GitHub
commit 73476554e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 407 additions and 82 deletions

View file

@ -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::<Vec<_>>();
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 {

View file

@ -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<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<'_> {}

View file

@ -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,
)

View file

@ -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]
@ -41,6 +45,7 @@ use rustc_span::HashStableContext;
use rustc_span::{Loc, Span};
use std::borrow::Cow;
use std::error::Report;
use std::fmt;
use std::hash::Hash;
use std::num::NonZeroUsize;
@ -54,11 +59,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;
@ -616,7 +624,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;

View file

@ -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<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");
#[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 = <message>`\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"
);
}
}

View file

@ -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::<String>(),
messages
.iter()
.map(|(m, _)| self.translate_message(m, args).map_err(Report::new).unwrap())
.collect::<String>(),
)
}
@ -55,83 +57,56 @@ pub trait Translate {
&'a self,
message: &'a DiagnosticMessage,
args: &'a FluentArgs<'_>,
) -> Cow<'_, str> {
) -> Result<Cow<'_, str>, 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<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 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()?,
};
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.
let mut help_messages = vec![];
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"));
}
_ => {}
}
}
panic!(
"Encountered errors while formatting message for `{identifier}`\n\
help: {}\n\
attr: `{attr:?}`\n\
args: `{args:?}`\n\
errors: `{errs:?}`",
help_messages.join("\nhelp: ")
);
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))
}
};
translated
})
.expect("failed to find message in primary or fallback fluent bundles")
try {
match self.fluent_bundle().map(|b| translate_with_bundle(b)) {
// The primary bundle was present and translation succeeded
Some(Ok(t)) => t,
// Always yeet out for errors on debug
Some(Err(primary)) if cfg!(debug_assertions) => do yeet primary,
// 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))?,
// 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))?,
}
}
}
}

View file

@ -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() {