Skip to content

Commit 0b5d6ae

Browse files
author
mejrs
committed
Improve fluent error messages
1 parent 3b1c8a9 commit 0b5d6ae

File tree

3 files changed

+180
-67
lines changed

3 files changed

+180
-67
lines changed

compiler/rustc_errors/src/error.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use rustc_error_messages::{
2+
fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
3+
FluentArgs, FluentError,
4+
};
5+
use std::borrow::Cow;
6+
use std::error::Error;
7+
use std::fmt;
8+
9+
#[derive(Debug)]
10+
pub enum TranslateError<'args> {
11+
One {
12+
id: &'args Cow<'args, str>,
13+
args: &'args FluentArgs<'args>,
14+
kind: TranslateErrorKind<'args>,
15+
},
16+
Two {
17+
primary: Box<TranslateError<'args>>,
18+
fallback: Box<TranslateError<'args>>,
19+
},
20+
}
21+
22+
impl<'args> TranslateError<'args> {
23+
pub fn message(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
24+
Self::One { id, args, kind: TranslateErrorKind::MessageMissing }
25+
}
26+
pub fn primary(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
27+
Self::One { id, args, kind: TranslateErrorKind::PrimaryBundleMissing }
28+
}
29+
pub fn attribute(
30+
id: &'args Cow<'args, str>,
31+
args: &'args FluentArgs<'args>,
32+
attr: &'args str,
33+
) -> Self {
34+
Self::One { id, args, kind: TranslateErrorKind::AttributeMissing { attr } }
35+
}
36+
pub fn value(id: &'args Cow<'args, str>, args: &'args FluentArgs<'args>) -> Self {
37+
Self::One { id, args, kind: TranslateErrorKind::ValueMissing }
38+
}
39+
40+
pub fn fluent(
41+
id: &'args Cow<'args, str>,
42+
args: &'args FluentArgs<'args>,
43+
errs: Vec<FluentError>,
44+
) -> Self {
45+
Self::One { id, args, kind: TranslateErrorKind::Fluent { errs } }
46+
}
47+
48+
pub fn and(self, fallback: TranslateError<'args>) -> TranslateError<'args> {
49+
Self::Two { primary: Box::new(self), fallback: Box::new(fallback) }
50+
}
51+
}
52+
53+
#[derive(Debug)]
54+
pub enum TranslateErrorKind<'args> {
55+
MessageMissing,
56+
PrimaryBundleMissing,
57+
AttributeMissing { attr: &'args str },
58+
ValueMissing,
59+
Fluent { errs: Vec<FluentError> },
60+
}
61+
62+
impl fmt::Display for TranslateError<'_> {
63+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64+
use TranslateErrorKind::*;
65+
66+
match self {
67+
Self::One { id, args, kind } => {
68+
writeln!(f, "\nfailed while formatting fluent string `{id}`: ")?;
69+
match kind {
70+
MessageMissing => writeln!(f, "message was missing")?,
71+
PrimaryBundleMissing => writeln!(f, "the primary bundle was missing")?,
72+
AttributeMissing { attr } => writeln!(f, "the attribute `{attr}` was missing")?,
73+
ValueMissing => writeln!(f, "the value was missing")?,
74+
Fluent { errs } => {
75+
for err in errs {
76+
match err {
77+
FluentError::ResolverError(ResolverError::Reference(
78+
ReferenceKind::Message { id, .. }
79+
| ReferenceKind::Variable { id, .. },
80+
)) => {
81+
if args.iter().any(|(arg_id, _)| arg_id == id) {
82+
writeln!(
83+
f,
84+
"argument `{id}` exists but was not referenced correctly"
85+
)?;
86+
writeln!(f, "help: try using `{{${id}}}` instead")?;
87+
} else {
88+
writeln!(
89+
f,
90+
"the fluent string has an argument `{id}` that was not found."
91+
)?;
92+
let vars: Vec<&str> =
93+
args.iter().map(|(a, _v)| a).collect();
94+
match &*vars {
95+
[] => writeln!(f, "help: no arguments are available")?,
96+
[one] => writeln!(
97+
f,
98+
"help: the argument `{one}` is available"
99+
)?,
100+
[first, middle @ .., last] => {
101+
write!(f, "help: the arguments `{first}`")?;
102+
for a in middle {
103+
write!(f, ", `{a}`")?;
104+
}
105+
writeln!(f, " and `{last}` are available")?;
106+
}
107+
}
108+
}
109+
}
110+
_ => writeln!(f, "{err}")?,
111+
}
112+
}
113+
}
114+
}
115+
}
116+
// If someone cares about primary bundles, they'll probably notice it's missing
117+
// regardless or will be using `debug_assertions`
118+
// so we skip the arm below this one to avoid confusing the regular user.
119+
Self::Two { primary: box Self::One { kind: PrimaryBundleMissing, .. }, fallback } => {
120+
fmt::Display::fmt(fallback, f)?;
121+
}
122+
Self::Two { primary, fallback } => {
123+
writeln!(
124+
f,
125+
"first, fluent formatting using the primary bundle failed:\n {primary}\n \
126+
while attempting to recover by using the fallback bundle instead, another error occurred:\n{fallback}"
127+
)?;
128+
}
129+
}
130+
Ok(())
131+
}
132+
}
133+
134+
impl Error for TranslateError<'_> {}

compiler/rustc_errors/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
#![feature(never_type)]
1212
#![feature(result_option_inspect)]
1313
#![feature(rustc_attrs)]
14+
#![feature(yeet_expr)]
15+
#![feature(try_blocks)]
16+
#![feature(box_patterns)]
17+
#![feature(error_reporter)]
1418
#![allow(incomplete_features)]
1519

1620
#[macro_use]
@@ -55,6 +59,7 @@ mod diagnostic;
5559
mod diagnostic_builder;
5660
mod diagnostic_impls;
5761
pub mod emitter;
62+
pub mod error;
5863
pub mod json;
5964
mod lock;
6065
pub mod registry;
Lines changed: 41 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
use crate::error::TranslateError;
12
use crate::snippet::Style;
23
use crate::{DiagnosticArg, DiagnosticMessage, FluentBundle};
34
use rustc_data_structures::sync::Lrc;
4-
use rustc_error_messages::{
5-
fluent_bundle::resolver::errors::{ReferenceKind, ResolverError},
6-
FluentArgs, FluentError,
7-
};
5+
use rustc_error_messages::FluentArgs;
86
use std::borrow::Cow;
7+
use std::error::Report;
98

109
/// Convert diagnostic arguments (a rustc internal type that exists to implement
1110
/// `Encodable`/`Decodable`) into `FluentArgs` which is necessary to perform translation.
@@ -63,75 +62,50 @@ pub trait Translate {
6362
}
6463
DiagnosticMessage::FluentIdentifier(identifier, attr) => (identifier, attr),
6564
};
65+
let translate_with_bundle =
66+
|bundle: &'a FluentBundle| -> Result<Cow<'_, str>, TranslateError<'_>> {
67+
let message = bundle
68+
.get_message(identifier)
69+
.ok_or(TranslateError::message(identifier, args))?;
70+
let value = match attr {
71+
Some(attr) => message
72+
.get_attribute(attr)
73+
.ok_or(TranslateError::attribute(identifier, args, attr))?
74+
.value(),
75+
None => message.value().ok_or(TranslateError::value(identifier, args))?,
76+
};
77+
debug!(?message, ?value);
6678

67-
let translate_with_bundle = |bundle: &'a FluentBundle| -> Option<(Cow<'_, str>, Vec<_>)> {
68-
let message = bundle.get_message(identifier)?;
69-
let value = match attr {
70-
Some(attr) => message.get_attribute(attr)?.value(),
71-
None => message.value()?,
79+
let mut errs = vec![];
80+
let translated = bundle.format_pattern(value, Some(args), &mut errs);
81+
debug!(?translated, ?errs);
82+
if errs.is_empty() {
83+
Ok(translated)
84+
} else {
85+
Err(TranslateError::fluent(identifier, args, errs))
86+
}
7287
};
73-
debug!(?message, ?value);
74-
75-
let mut errs = vec![];
76-
let translated = bundle.format_pattern(value, Some(args), &mut errs);
77-
debug!(?translated, ?errs);
78-
Some((translated, errs))
79-
};
8088

81-
self.fluent_bundle()
82-
.and_then(|bundle| translate_with_bundle(bundle))
83-
// If `translate_with_bundle` returns `None` with the primary bundle, this is likely
84-
// just that the primary bundle doesn't contain the message being translated, so
85-
// proceed to the fallback bundle.
86-
//
87-
// However, when errors are produced from translation, then that means the translation
88-
// is broken (e.g. `{$foo}` exists in a translation but `foo` isn't provided).
89-
//
90-
// In debug builds, assert so that compiler devs can spot the broken translation and
91-
// fix it..
92-
.inspect(|(_, errs)| {
93-
debug_assert!(
94-
errs.is_empty(),
95-
"identifier: {:?}, attr: {:?}, args: {:?}, errors: {:?}",
96-
identifier,
97-
attr,
98-
args,
99-
errs
100-
);
101-
})
102-
// ..otherwise, for end users, an error about this wouldn't be useful or actionable, so
103-
// just hide it and try with the fallback bundle.
104-
.filter(|(_, errs)| errs.is_empty())
105-
.or_else(|| translate_with_bundle(self.fallback_fluent_bundle()))
106-
.map(|(translated, errs)| {
107-
// Always bail out for errors with the fallback bundle.
89+
let ret: Result<Cow<'_, str>, TranslateError<'_>> = try {
90+
match self.fluent_bundle().map(|b| translate_with_bundle(b)) {
91+
// The primary bundle was present and translation succeeded
92+
Some(Ok(t)) => t,
10893

109-
let mut help_messages = vec![];
94+
// Always yeet out for errors on debug
95+
Some(Err(primary)) if cfg!(debug_assertions) => do yeet primary,
11096

111-
if !errs.is_empty() {
112-
for error in &errs {
113-
match error {
114-
FluentError::ResolverError(ResolverError::Reference(
115-
ReferenceKind::Message { id, .. },
116-
)) if args.iter().any(|(arg_id, _)| arg_id == id) => {
117-
help_messages.push(format!("Argument `{id}` exists but was not referenced correctly. Try using `{{${id}}}` instead"));
118-
}
119-
_ => {}
120-
}
121-
}
97+
// If `translate_with_bundle` returns `Err` with the primary bundle, this is likely
98+
// just that the primary bundle doesn't contain the message being translated or
99+
// something else went wrong) so proceed to the fallback bundle.
100+
Some(Err(primary)) => translate_with_bundle(self.fallback_fluent_bundle())
101+
.map_err(|fallback| primary.and(fallback))?,
122102

123-
panic!(
124-
"Encountered errors while formatting message for `{identifier}`\n\
125-
help: {}\n\
126-
attr: `{attr:?}`\n\
127-
args: `{args:?}`\n\
128-
errors: `{errs:?}`",
129-
help_messages.join("\nhelp: ")
130-
);
131-
}
132-
133-
translated
134-
})
103+
// The primary bundle is missing, proceed to the fallback bundle
104+
None => translate_with_bundle(self.fallback_fluent_bundle())
105+
.map_err(|fallback| TranslateError::primary(identifier, args).and(fallback))?,
106+
}
107+
};
108+
ret.map_err(Report::new)
135109
.expect("failed to find message in primary or fallback fluent bundles")
136110
}
137111
}

0 commit comments

Comments
 (0)