Handle common errors in Fluent code
Dependencies
- [2]
QFPQZR4KRefactor `fluent_embed` - [3]
NO3PDO7PRefactor `fluent_embed` to support structs - [4]
RLX6XPNZReturn an error when user provides an exact path - [5]
2XQ6ZB4WStore multiple locales in a single `Group` - [6]
5TEX4MNUSplit `fluent_embed` into `group` and `parse` modules - [7]
SHNZZSZGCreate `cli_macros` shim crate - [8]
BQ6N55O7Refactor how `Group` stores messages - [9]
F5LG7WENEmit compilation errors from Fluent source code - [10]
ROSR4HD5Parse captured glob as locale - [*]
O77KA6C4Create `fluent_embed` crate
Change contents
- replacement in fluent_embed/src/lib.rs at line 33
#[error("Error in Fluent source code")]#[error("invalid Fluent source code")] - edit in fluent_embed/src/lib.rs at line 62
// TODO: return an error instead of panic - edit in fluent_embed/src/lib.rs at line 78
// let context = todo!(); - replacement in fluent_embed/src/fluent/mod.rs at line 6
use miette::Diagnostic;use miette::{Diagnostic, NamedSource, SourceSpan}; - replacement in fluent_embed/src/fluent/mod.rs at line 15
#[diagnostic(transparent)]#[error(transparent)]#[error("couldn't parse Fluent source code")]pub struct ParserError {#[label("{kind}")]span: SourceSpan,kind: fluent_syntax::parser::ErrorKind,}#[derive(Diagnostic, Debug, Error)] - replacement in fluent_embed/src/fluent/mod.rs at line 24
InvalidReference(#[from] ast::InvalidReference),#[error("the reference `${fluent_name}` doesn't match a Rust field `{rust_name}`")]InvalidReference {fluent_name: String,rust_name: String,#[source_code]source_code: NamedSource<String>,#[label("This references `{rust_name}` which doesn't exist")]span: SourceSpan,#[help]valid_references: String,},#[error("missing canonical locale `{canonical_locale}`")]MissingCanonicalLocale {canonical_locale: String,#[help]matched_locales: String,},#[error(r#"message "{unexpected_key}" from `{locale}` is not in the canonical `{canonical_locale}` locale"#)]#[help("the canonical locale must include all keys!")]UnexpectedKey {unexpected_key: String,locale: String,canonical_locale: String,#[source_code]source_code: NamedSource<String>,#[label("This key isn't in the `{canonical_locale}` locale")]span: SourceSpan,},#[error("unable to parse Fluent source code")]ParserErrors {#[source_code]source_code: NamedSource<String>,#[related]related: Vec<ParserError>,}, - replacement in fluent_embed/src/fluent/mod.rs at line 63
syntax_tree: &'context Resource<String>,source: &'context SourceFile,root_id: &'context str, - edit in fluent_embed/src/fluent/mod.rs at line 66
path: &'context str, - replacement in fluent_embed/src/fluent/mod.rs at line 79
// TODO: return an error instead of paniclet syntax_tree = fluent_syntax::parser::parse(file_contents).unwrap();let syntax_tree = match fluent_syntax::parser::parse(file_contents.clone()) {Ok(syntax_tree) => syntax_tree,Err((_partial_syntax_tree, parser_errors)) => {// Map the `fluent_syntax` errors to `miette::Diagnostic` errorslet related = parser_errors.into_iter().map(|error| ParserError {span: SourceSpan::from(error.pos),kind: error.kind,}).collect();return Err(Error::ParserErrors {source_code: NamedSource::new(path.to_string_lossy(), file_contents),related,});}}; - edit in fluent_embed/src/fluent/mod.rs at line 122
}/// Calculate the byte offset of the serializedfn source_with_message_offset(&self, id: &str) -> (NamedSource<String>, usize) {// Find the message position in the ASTlet ast_index = self.syntax_tree.body.iter().position(|entry| {if let Entry::Message(message) = entry {message.id.name == id} else {false}}).unwrap();let options = fluent_syntax::serializer::Options {// Make sure to include all source code in error snippet, even if marked as "junk"with_junk: true,};// Serialize everything before this message to get the byte offsetlet source_before_message = fluent_syntax::serializer::serialize_with_options(&Resource {body: self.syntax_tree.body[0..ast_index].to_vec(),},options,);let byte_offset = source_before_message.len();let source_after_offset = fluent_syntax::serializer::serialize_with_options(&Resource {body: self.syntax_tree.body[ast_index..].to_vec(),},options,);let source = format!("{source_before_message}{source_after_offset}");let named_source = NamedSource::new(self.path.to_string_lossy(), source);(named_source, byte_offset) - replacement in fluent_embed/src/fluent/mod.rs at line 184
syntax_tree: &self.syntax_tree,source: &self,root_id: id, - edit in fluent_embed/src/fluent/mod.rs at line 188
// TODO: return an error instead of panic - edit in fluent_embed/src/fluent/mod.rs at line 189
path: &self.path.to_string_lossy(), - replacement in fluent_embed/src/fluent/group.rs at line 3
use std::collections::{HashMap, HashSet};use std::collections::HashMap; - edit in fluent_embed/src/fluent/group.rs at line 7
use miette::SourceSpan; - replacement in fluent_embed/src/fluent/group.rs at line 30
// Collect all keys used in the Fluent source codelet keys: HashSet<&str> =HashSet::from_iter(locales.values().flat_map(|resource| resource.message_ids()));// Collect all keys from the canonical resource// TODO: return an error instead of paniclet canonical_resource = locales.get(&canonical_locale).unwrap();// TODO: return an error instead of panic// Make sure the canonical locale existslet canonical_resource = if let Some(canonical_resource) = locales.get(&canonical_locale) {canonical_resource} else {return Err(Error::MissingCanonicalLocale {canonical_locale: canonical_locale.to_string(),matched_locales: format!("the following locales were found:\n{}",locales.keys().map(|locale| format!("- {locale}")).collect::<Vec<_>>().join("\n")),});}; - replacement in fluent_embed/src/fluent/group.rs at line 48
assert_eq!(keys, HashSet::from_iter(canonical_resource.message_ids()));let canonical_keys: Vec<&str> = canonical_resource.message_ids().collect();for (locale, source_file) in &locales {// Find any keys in `locale_keys` missing from `canonical_keys`let mut unexpected_keys = source_file.message_ids().filter(|id: &&str| !canonical_keys.contains(id));if let Some(unexpected_key) = unexpected_keys.next() {let (src, message_offset) = source_file.source_with_message_offset(unexpected_key);return Err(Error::UnexpectedKey {unexpected_key: unexpected_key.to_string(),locale: locale.to_string(),canonical_locale: canonical_locale.to_string(),source_code: src,span: SourceSpan::new(message_offset.into(), unexpected_key.len()),});}} - replacement in fluent_embed/src/fluent/ast.rs at line 4
use fluent_syntax::ast::{Entry, Expression, InlineExpression, PatternElement, VariantKey};use fluent_syntax::ast::{Expression, InlineExpression, PatternElement, VariantKey}; - replacement in fluent_embed/src/fluent/ast.rs at line 6
use miette::{Diagnostic, NamedSource, SourceSpan};use miette::SourceSpan; - edit in fluent_embed/src/fluent/ast.rs at line 9
#[derive(Diagnostic, Debug, Error)]#[error("Field doesn't exist")]pub struct InvalidReference {#[source_code]src: NamedSource<String>,#[label("Can't find any Rust fields with this name")]span: SourceSpan,#[help]help: String,} - replacement in fluent_embed/src/fluent/ast.rs at line 100[3.4668]→[2.8673:8741](∅→∅),[2.8741]→[3.4894:4981](∅→∅),[3.4894]→[3.4894:4981](∅→∅),[3.4981]→[2.8742:8792](∅→∅),[2.8792]→[3.5018:5265](∅→∅),[3.5018]→[3.5018:5265](∅→∅)
// Serialize the Fluent file AST back into a Stringlet source_string = fluent_syntax::serializer::serialize_with_options(&message_context.syntax_tree,fluent_syntax::serializer::Options {// Make sure to include all source code in error snippet, even if marked as "junk"with_junk: true,},);let (source_code, offset) = message_context.source.source_with_message_offset(message_context.root_id); - replacement in fluent_embed/src/fluent/ast.rs at line 104
let location = source_string.find(&format!("${}", id.name)).unwrap();let location = source_code.inner()[offset..].find(&format!("${}", id.name)).unwrap()+ offset; - replacement in fluent_embed/src/fluent/ast.rs at line 109[3.5353]→[2.8793:8863](∅→∅),[2.8863]→[2.8863:8951](∅→∅),[2.8951]→[3.5501:5572](∅→∅),[3.5501]→[3.5501:5572](∅→∅),[3.5572]→[3.5572:5607](∅→∅)
return Err(Error::InvalidReference(InvalidReference {src: NamedSource::new(message_context.path, source_string.clone()),span: (location..location + id.name.len()).into(),help: format!(return Err(Error::InvalidReference {fluent_name: id.name.clone(),rust_name: id.name.to_snake_case(),source_code,span: SourceSpan::new(location.into(), id.name.len()),valid_references: format!( - replacement in fluent_embed/src/fluent/ast.rs at line 124
}));}); - replacement in cli_macros/src/lib.rs at line 85
match error {FluentError::InvalidReference(invalid_reference) => {eprintln!("{:?}", miette::Error::new(invalid_reference));}}eprintln!("{:?}", miette::Error::new(error));