Handle common errors in Fluent code

finchie
Jul 11, 2024, 9:31 AM
2SITVDYW6KANM24QXRHVSBL6S77UHKJLOSOHSUZQBJFL5NAAGQYAC

Dependencies

  • [2] QFPQZR4K Refactor `fluent_embed`
  • [3] NO3PDO7P Refactor `fluent_embed` to support structs
  • [4] RLX6XPNZ Return an error when user provides an exact path
  • [5] 2XQ6ZB4W Store multiple locales in a single `Group`
  • [6] 5TEX4MNU Split `fluent_embed` into `group` and `parse` modules
  • [7] SHNZZSZG Create `cli_macros` shim crate
  • [8] BQ6N55O7 Refactor how `Group` stores messages
  • [9] F5LG7WEN Emit compilation errors from Fluent source code
  • [10] ROSR4HD5 Parse captured glob as locale
  • [*] O77KA6C4 Create `fluent_embed` crate

Change contents

  • replacement in fluent_embed/src/lib.rs at line 33
    [3.7002][3.7002:7046]()
    #[error("Error in Fluent source code")]
    [3.7002]
    [3.7046]
    #[error("invalid Fluent source code")]
  • edit in fluent_embed/src/lib.rs at line 62
    [3.5096]
    [3.5096]
    // TODO: return an error instead of panic
  • edit in fluent_embed/src/lib.rs at line 78
    [2.634][2.634:664]()
    // let context = todo!();
  • replacement in fluent_embed/src/fluent/mod.rs at line 6
    [2.1144][2.1144:1168]()
    use miette::Diagnostic;
    [2.1144]
    [2.1168]
    use miette::{Diagnostic, NamedSource, SourceSpan};
  • replacement in fluent_embed/src/fluent/mod.rs at line 15
    [2.1271][2.1271:1320]()
    #[diagnostic(transparent)]
    #[error(transparent)]
    [2.1271]
    [2.1320]
    #[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
    [2.1337][2.1337:1390]()
    InvalidReference(#[from] ast::InvalidReference),
    [2.1337]
    [2.1390]
    #[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
    [2.1461][2.1461:1506]()
    syntax_tree: &'context Resource<String>,
    [2.1461]
    [2.1506]
    source: &'context SourceFile,
    root_id: &'context str,
  • edit in fluent_embed/src/fluent/mod.rs at line 66
    [2.1546][2.1546:1571]()
    path: &'context str,
  • replacement in fluent_embed/src/fluent/mod.rs at line 79
    [2.1926][2.1926:2056]()
    // TODO: return an error instead of panic
    let syntax_tree = fluent_syntax::parser::parse(file_contents).unwrap();
    [2.1926]
    [2.2056]
    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` errors
    let 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
    [2.2984]
    [2.2984]
    }
    /// Calculate the byte offset of the serialized
    fn source_with_message_offset(&self, id: &str) -> (NamedSource<String>, usize) {
    // Find the message position in the AST
    let 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 offset
    let 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
    [2.3507][2.3507:3551]()
    syntax_tree: &self.syntax_tree,
    [2.3507]
    [2.3551]
    source: &self,
    root_id: id,
  • edit in fluent_embed/src/fluent/mod.rs at line 188
    [2.3742][2.3742:3796]()
    // TODO: return an error instead of panic
  • edit in fluent_embed/src/fluent/mod.rs at line 189
    [2.3850][2.3850:3898]()
    path: &self.path.to_string_lossy(),
  • replacement in fluent_embed/src/fluent/group.rs at line 3
    [2.4076][2.4076:4118]()
    use std::collections::{HashMap, HashSet};
    [2.4076]
    [2.4118]
    use std::collections::HashMap;
  • edit in fluent_embed/src/fluent/group.rs at line 7
    [3.417]
    [3.461]
    use miette::SourceSpan;
  • replacement in fluent_embed/src/fluent/group.rs at line 30
    [3.4960][2.4608:4851](),[2.4851][2.4851:5025]()
    // Collect all keys used in the Fluent source code
    let 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 panic
    let canonical_resource = locales.get(&canonical_locale).unwrap();
    // TODO: return an error instead of panic
    [3.4960]
    [2.5025]
    // Make sure the canonical locale exists
    let 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
    [2.5091][2.5091:5171]()
    assert_eq!(keys, HashSet::from_iter(canonical_resource.message_ids()));
    [2.5091]
    [3.1818]
    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
    [3.2217][2.6482:6573]()
    use fluent_syntax::ast::{Entry, Expression, InlineExpression, PatternElement, VariantKey};
    [3.2217]
    [3.2329]
    use fluent_syntax::ast::{Expression, InlineExpression, PatternElement, VariantKey};
  • replacement in fluent_embed/src/fluent/ast.rs at line 6
    [3.2386][3.2386:2437]()
    use miette::{Diagnostic, NamedSource, SourceSpan};
    [3.2386]
    [3.182]
    use miette::SourceSpan;
  • edit in fluent_embed/src/fluent/ast.rs at line 9
    [3.2460][3.2460:2461](),[3.2461][3.2461:2720]()
    #[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 String
    let 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,
    },
    );
    [3.4668]
    [3.5265]
    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
    [3.5266][3.5266:5352]()
    let location = source_string.find(&format!("${}", id.name)).unwrap();
    [3.5266]
    [3.5352]
    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!(
    [3.5353]
    [3.5607]
    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
    [3.5990][3.5990:6011]()
    }));
    [3.5990]
    [3.6011]
    });
  • replacement in cli_macros/src/lib.rs at line 85
    [3.12754][3.12754:12920]()
    match error {
    FluentError::InvalidReference(invalid_reference) => {
    eprintln!("{:?}", miette::Error::new(invalid_reference));
    }
    }
    [3.12754]
    [3.12920]
    eprintln!("{:?}", miette::Error::new(error));