use std::collections::{HashMap, HashSet};
use std::fmt;
use fluent_bundle::{FluentArgs, FluentResource, FluentValue};
use fluent_syntax::ast;
use crate::phrasebook::{entry_id, new_bundle, parse};
use crate::{Catalog, Lang, Source};
pub fn catalog(catalog: &Catalog) -> Result<(), Problems> {
let mut problems = Problems::default();
let mut langs = HashSet::new();
for source in catalog.sources() {
let lang = source.lang();
let mut report = |message: String| problems.push(catalog, lang, message);
if !langs.insert(lang) {
report("a second source for this language, which is ignored".to_owned());
continue;
}
check_source(catalog, source, &mut report);
}
problems.into_result()
}
fn check_source(catalog: &Catalog, source: &Source, report: &mut dyn FnMut(String)) {
let resource = parse(source, |line, error| {
report(format!("syntax error on line {line}: {error}"));
});
let mut seen = HashSet::new();
for (key, name) in resource.entries().filter_map(entry_id) {
if !seen.insert(key.clone()) {
report(format!("`{key}` is defined twice"));
}
if !crate::phrasebook::owns(catalog, name) {
let prefix = catalog.prefix().unwrap_or_default();
report(format!(
"`{key}` does not start with the catalog's prefix `{prefix}`"
));
}
}
let mut variables = Vec::new();
for entry in resource.entries() {
if let ast::Entry::Message(message) = entry {
if let Some(pattern) = &message.value {
pattern_variables(pattern, &mut variables);
}
for attribute in &message.attributes {
pattern_variables(&attribute.value, &mut variables);
}
}
}
let mut args = FluentArgs::new();
for name in variables {
args.set(name.to_owned(), FluentValue::from(1));
}
let ids = resource
.entries()
.filter_map(|entry| match entry {
ast::Entry::Message(m) => Some(m.id.name.to_owned()),
_ => None,
})
.collect::<Vec<_>>();
let mut bundle = new_bundle(source.lang(), false, false);
let _ = bundle.add_resource(resource);
let mut reported = HashSet::new();
for id in ids {
if !reported.insert(id.clone()) {
continue;
}
let Some(message) = bundle.get_message(&id) else {
continue;
};
let patterns = message
.value()
.map(|p| (None, p))
.into_iter()
.chain(message.attributes().map(|a| (Some(a.id()), a.value())));
for (attribute, pattern) in patterns {
let mut errors = Vec::new();
let _ = bundle.format_pattern(pattern, Some(&args), &mut errors);
let name = match attribute {
Some(attribute) => format!("{id}.{attribute}"),
None => id.clone(),
};
for error in errors {
report(format!("`{name}` does not format: {error}"));
}
}
}
}
pub fn parity(catalog: &Catalog, lang: Lang) -> Result<(), Problems> {
parity_with(catalog, lang, &[])
}
pub fn parity_with(catalog: &Catalog, lang: Lang, libraries: &[&Catalog]) -> Result<(), Problems> {
let mut problems = Problems::default();
let Some(default) = catalog.default_lang() else {
problems.push(catalog, lang, "the catalog has no sources".to_owned());
return problems.into_result();
};
if catalog.source(lang).is_none() {
problems.push(catalog, lang, "no source for this language".to_owned());
return problems.into_result();
}
let expected = ids(catalog, default);
let actual = ids(catalog, lang);
let expected_set = expected.iter().collect::<HashSet<_>>();
let actual_set = actual.iter().collect::<HashSet<_>>();
let library_ids = libraries
.iter()
.filter_map(|library| Some(ids(library, library.default_lang()?)))
.flatten()
.collect::<HashSet<_>>();
for id in &expected {
if !actual_set.contains(id) {
problems.push(
catalog,
lang,
format!("`{id}` is missing (defined in {default})"),
);
}
}
for id in &actual {
if expected_set.contains(id) || library_ids.contains(id) {
continue;
}
let message = if libraries.is_empty() {
format!("`{id}` is not defined in {default}")
} else {
format!("`{id}` is defined neither in {default} nor in any given library")
};
problems.push(catalog, lang, message);
}
problems.into_result()
}
pub fn all(catalog: &Catalog, libraries: &[&Catalog]) -> Result<(), Problems> {
let mut problems = Problems::default();
problems.extend(self::catalog(catalog));
let default = catalog.default_lang();
let mut seen = HashSet::new();
for lang in catalog.languages() {
if Some(lang) != default && seen.insert(lang) {
problems.extend(parity_with(catalog, lang, libraries));
}
}
problems.into_result()
}
pub fn all_ids(catalog: &Catalog) -> Vec<String> {
let mut seen = HashSet::new();
let mut out = Vec::new();
for lang in catalog.languages() {
for id in ids(catalog, lang) {
if seen.insert(id.clone()) {
out.push(id);
}
}
}
out
}
pub fn coverage<'a>(
catalog: &Catalog,
lang: Lang,
asked: impl IntoIterator<Item = &'a str>,
libraries: &[&Catalog],
) -> Result<(), Problems> {
let mut problems = Problems::default();
if catalog.source(lang).is_none() {
problems.push(catalog, lang, "no source for this language".to_owned());
return problems.into_result();
}
let defined = ids(catalog, lang);
let defined_set = defined.iter().map(String::as_str).collect::<HashSet<_>>();
let library_ids = libraries
.iter()
.flat_map(|library| all_ids(library))
.collect::<HashSet<_>>();
let mut asked_set = HashSet::new();
for id in asked {
if !asked_set.insert(id) {
continue;
}
if !defined_set.contains(id) && !library_ids.contains(id) {
problems.push(
catalog,
lang,
format!("`{id}` is asked for but not defined"),
);
}
}
for id in &defined {
if !asked_set.contains(id.as_str()) && !library_ids.contains(id) {
problems.push(
catalog,
lang,
format!("`{id}` is defined but never asked for"),
);
}
}
problems.into_result()
}
pub fn defines<'a>(
catalog: &Catalog,
lang: Lang,
ids: impl IntoIterator<Item = &'a str>,
libraries: &[&Catalog],
) -> Result<(), Problems> {
let mut problems = Problems::default();
let defined = self::ids(catalog, lang);
let defined = defined.iter().map(String::as_str).collect::<HashSet<_>>();
let library_ids = libraries
.iter()
.flat_map(|library| all_ids(library))
.collect::<HashSet<_>>();
let mut seen = HashSet::new();
for id in ids {
if seen.insert(id) && !defined.contains(id) && !library_ids.contains(id) {
problems.push(
catalog,
lang,
format!("`{id}` is asked for but not defined"),
);
}
}
problems.into_result()
}
pub fn ids(catalog: &Catalog, lang: Lang) -> Vec<String> {
let Some(source) = catalog.source(lang) else {
return Vec::new();
};
let resource: FluentResource = parse(source, |_, _| {});
let mut seen = HashMap::new();
let mut out = Vec::new();
for entry in resource.entries() {
if let ast::Entry::Message(m) = entry
&& seen.insert(m.id.name, ()).is_none()
{
out.push(m.id.name.to_owned());
}
}
out
}
fn pattern_variables<'s>(pattern: &ast::Pattern<&'s str>, out: &mut Vec<&'s str>) {
for element in &pattern.elements {
if let ast::PatternElement::Placeable { expression } = element {
expression_variables(expression, out);
}
}
}
fn expression_variables<'s>(expression: &ast::Expression<&'s str>, out: &mut Vec<&'s str>) {
match expression {
ast::Expression::Select { selector, variants } => {
inline_variables(selector, out);
for variant in variants {
pattern_variables(&variant.value, out);
}
}
ast::Expression::Inline(inline) => inline_variables(inline, out),
}
}
fn inline_variables<'s>(inline: &ast::InlineExpression<&'s str>, out: &mut Vec<&'s str>) {
match inline {
ast::InlineExpression::VariableReference { id } => {
if !out.contains(&id.name) {
out.push(id.name);
}
}
ast::InlineExpression::FunctionReference { arguments, .. } => {
for argument in &arguments.positional {
inline_variables(argument, out);
}
for argument in &arguments.named {
inline_variables(&argument.value, out);
}
}
ast::InlineExpression::Placeable { expression } => expression_variables(expression, out),
_ => {}
}
}
#[derive(Default, Clone, PartialEq, Eq)]
pub struct Problems {
problems: Vec<Problem>,
}
impl Problems {
fn push(&mut self, catalog: &Catalog, lang: Lang, message: String) {
self.problems.push(Problem {
catalog: catalog.name().to_owned(),
lang,
message,
});
}
fn extend(&mut self, result: Result<(), Problems>) {
if let Err(other) = result {
self.problems.extend(other.problems);
}
}
fn into_result(self) -> Result<(), Problems> {
if self.problems.is_empty() {
Ok(())
} else {
Err(self)
}
}
pub fn iter(&self) -> impl Iterator<Item = &Problem> {
self.problems.iter()
}
}
impl<'a> IntoIterator for &'a Problems {
type Item = &'a Problem;
type IntoIter = std::slice::Iter<'a, Problem>;
fn into_iter(self) -> Self::IntoIter {
self.problems.iter()
}
}
impl fmt::Display for Problems {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (n, problem) in self.problems.iter().enumerate() {
if n > 0 {
writeln!(f)?;
}
problem.fmt(f)?;
}
Ok(())
}
}
impl fmt::Debug for Problems {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
fmt::Display::fmt(self, f)
}
}
impl core::error::Error for Problems {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Problem {
catalog: String,
lang: Lang,
message: String,
}
impl Problem {
pub fn catalog(&self) -> &str {
&self.catalog
}
pub fn lang(&self) -> Lang {
self.lang
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}: {}", self.catalog, self.lang, self.message)
}
}