use std::collections::HashSet;
use fluent_syntax::ast;
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Ref {
pub name: String,
pub kind: RefKind,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum RefKind {
Variable,
Term,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct Selector {
pub variable: String,
pub keys: Vec<String>,
}
impl Selector {
pub fn has_bool_keys(&self) -> bool {
self.keys.len() == 2
&& self.keys.iter().any(|key| key == "true")
&& self.keys.iter().any(|key| key == "false")
}
}
pub fn find_refs(pattern: &ast::Pattern<&str>) -> Vec<Ref> {
find_refs_and_selectors(pattern).0
}
pub fn find_refs_and_selectors(pattern: &ast::Pattern<&str>) -> (Vec<Ref>, Vec<Selector>) {
let mut collector = Collector::default();
collector.pattern(pattern);
(collector.refs, collector.selectors)
}
#[derive(Default)]
struct Collector {
refs: Vec<Ref>,
selectors: Vec<Selector>,
}
impl Collector {
fn pattern(&mut self, pattern: &ast::Pattern<&str>) {
for element in &pattern.elements {
if let ast::PatternElement::Placeable { expression } = element {
self.expression(expression);
}
}
}
fn expression(&mut self, expression: &ast::Expression<&str>) {
match expression {
ast::Expression::Inline(inline) => self.inline(inline),
ast::Expression::Select { selector, variants } => {
if let Some(variable) = selector_variable(selector) {
self.selectors.push(Selector {
variable: variable.to_owned(),
keys: variants
.iter()
.map(|variant| match variant.key {
ast::VariantKey::Identifier { name } => name.to_owned(),
ast::VariantKey::NumberLiteral { value } => value.to_owned(),
})
.collect(),
});
}
self.inline(selector);
for variant in variants {
self.pattern(&variant.value);
}
}
}
}
fn inline(&mut self, inline: &ast::InlineExpression<&str>) {
match inline {
ast::InlineExpression::VariableReference { id } => self.refs.push(Ref {
name: id.name.to_owned(),
kind: RefKind::Variable,
}),
ast::InlineExpression::TermReference { id, arguments, .. } => {
self.refs.push(Ref {
name: id.name.to_owned(),
kind: RefKind::Term,
});
if let Some(arguments) = arguments {
self.call_arguments(arguments);
}
}
ast::InlineExpression::FunctionReference { arguments, .. } => {
self.call_arguments(arguments)
}
ast::InlineExpression::Placeable { expression } => self.expression(expression),
ast::InlineExpression::StringLiteral { .. }
| ast::InlineExpression::NumberLiteral { .. }
| ast::InlineExpression::MessageReference { .. } => {}
}
}
fn call_arguments(&mut self, arguments: &ast::CallArguments<&str>) {
for positional in &arguments.positional {
self.inline(positional);
}
for named in &arguments.named {
self.inline(&named.value);
}
}
}
fn selector_variable<'a>(inline: &'a ast::InlineExpression<&'a str>) -> Option<&'a str> {
match inline {
ast::InlineExpression::VariableReference { id } => Some(id.name),
ast::InlineExpression::Placeable { expression } => match expression.as_ref() {
ast::Expression::Inline(inline) => selector_variable(inline),
ast::Expression::Select { .. } => None,
},
_ => None,
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum RefsIncompat {
UnknownVariable { variable: String },
ElementMismatch {
expected: Vec<(String, RefKind)>,
found: Vec<(String, RefKind)>,
},
BoolSelectorMismatch {
variable: String,
found: Vec<String>,
},
BoolReferenceOutsideSelector { variable: String },
}
pub fn check_refs(
vars: &[&str],
bool_vars: &[&str],
elements: &[(&str, RefKind)],
refs: &[Ref],
selectors: &[Selector],
) -> Result<(), Vec<RefsIncompat>> {
let mut incompatibilities = Vec::new();
if !elements.is_empty() {
let element_names: Vec<&str> = elements.iter().map(|(n, _)| *n).collect();
let found: Vec<(String, RefKind)> = refs
.iter()
.filter(|r| element_names.contains(&r.name.as_str()))
.map(|r| (r.name.clone(), r.kind))
.collect();
let expected: Vec<(String, RefKind)> =
elements.iter().map(|(n, k)| (n.to_string(), *k)).collect();
if expected != found {
incompatibilities.push(RefsIncompat::ElementMismatch { expected, found });
}
let mut unknown = HashSet::new();
for r in refs {
if r.kind == RefKind::Variable
&& !element_names.contains(&r.name.as_str())
&& !vars.contains(&r.name.as_str())
&& unknown.insert(r.name.as_str())
{
incompatibilities.push(RefsIncompat::UnknownVariable {
variable: r.name.clone(),
});
}
}
} else {
let mut unknown = HashSet::new();
for r in refs {
if r.kind == RefKind::Variable
&& !vars.contains(&r.name.as_str())
&& unknown.insert(r.name.as_str())
{
incompatibilities.push(RefsIncompat::UnknownVariable {
variable: r.name.clone(),
});
}
}
}
for variable in bool_vars {
let reference_count = refs
.iter()
.filter(|r| r.kind == RefKind::Variable && r.name == *variable)
.count();
let selector_count = selectors
.iter()
.filter(|selector| selector.variable == *variable)
.count();
if reference_count != selector_count {
incompatibilities.push(RefsIncompat::BoolReferenceOutsideSelector {
variable: (*variable).to_owned(),
});
}
}
for selector in selectors
.iter()
.filter(|selector| bool_vars.contains(&selector.variable.as_str()))
{
if !selector.has_bool_keys() {
incompatibilities.push(RefsIncompat::BoolSelectorMismatch {
variable: selector.variable.clone(),
found: selector.keys.clone(),
});
}
}
if incompatibilities.is_empty() {
Ok(())
} else {
Err(incompatibilities)
}
}