use std::collections::{HashMap, HashSet};
use crate::build::LangBundle;
use crate::build::typed::{ElementKind, Id, Message, RefKind, VarType};
use crate::ftl_refs::{RefsIncompat, check_refs};
#[derive(Debug)]
pub struct Analyzed {
pub common: HashSet<Id>,
pub warnings: Vec<String>,
}
impl Analyzed {
pub fn from(langs: &[LangBundle], default: &LangBundle) -> Self {
let others: Vec<(&LangBundle, HashMap<&Id, &Message>)> = langs
.iter()
.filter(|l| l.language_id != default.language_id)
.map(|l| {
let mut by_id: HashMap<&Id, &Message> = HashMap::new();
for m in &l.messages {
by_id.entry(&m.id).or_insert(m);
}
(l, by_id)
})
.collect();
let mut common = HashSet::new();
let mut warnings = Vec::new();
for contract in &default.messages {
let id = &contract.id;
let mut missing_in: Vec<String> = Vec::new();
let mut incompatible_in: Vec<String> = Vec::new();
for (lang, by_id) in &others {
match by_id.get(id) {
None => missing_in.push(lang.language_id.clone()),
Some(msg) if !compatible(contract, msg) => {
incompatible_in
.push(format!("{} ({}:{})", lang.language_id, msg.file, msg.line));
}
Some(_) => {}
}
}
if !missing_in.is_empty() {
warnings.push(format!(
"{}:{}: {id} is not generated — missing from locale(s): {}",
contract.file,
contract.line,
missing_in.join(", "),
));
} else if !incompatible_in.is_empty() {
warnings.push(format!(
"{}:{}: {id} is not generated — incompatible variables, \
Boolean selectors or elements in locale(s): {}",
contract.file,
contract.line,
incompatible_in.join(", "),
));
} else {
common.insert(id.clone());
}
}
warnings.extend(orphan_warnings(&others, default));
warnings.sort();
Self { common, warnings }
}
}
fn orphan_warnings(
others: &[(&LangBundle, HashMap<&Id, &Message>)],
default: &LangBundle,
) -> Vec<String> {
let default_ids: HashSet<&Id> = default.messages.iter().map(|m| &m.id).collect();
let mut orphans: HashMap<Id, Vec<String>> = HashMap::new();
for (lang, _) in others {
for msg in &lang.messages {
if !default_ids.contains(&msg.id) {
orphans
.entry(msg.id.clone())
.or_default()
.push(lang.language_id.clone());
}
}
}
orphans
.into_iter()
.map(|(id, mut langs)| {
langs.sort();
format!(
"{id} is not generated — present in locale(s) {} but missing from \
the default locale '{}'",
langs.join(", "),
default.language_id,
)
})
.collect()
}
fn compatible(contract: &Message, other: &Message) -> bool {
let vars: Vec<&str> = contract.variables.iter().map(|v| v.id.as_str()).collect();
let bool_vars: Vec<&str> = contract
.variables
.iter()
.filter(|v| v.typ == VarType::Bool)
.map(|v| v.id.as_str())
.collect();
let elements: Vec<(&str, RefKind)> = contract
.elements
.iter()
.map(|e| {
let kind = match e.kind {
ElementKind::Variable => RefKind::Variable,
ElementKind::Term => RefKind::Term,
};
(e.name.as_str(), kind)
})
.collect();
check_refs(
&vars,
&bool_vars,
&elements,
&other.pattern_refs,
&other.selectors,
)
.is_ok()
}
pub(crate) fn default_contract_errors(default: &LangBundle) -> Vec<String> {
let mut errors = Vec::new();
for msg in &default.messages {
let vars: Vec<&str> = msg.variables.iter().map(|v| v.id.as_str()).collect();
let bool_vars: Vec<&str> = msg
.variables
.iter()
.filter(|v| v.typ == VarType::Bool)
.map(|v| v.id.as_str())
.collect();
let elements: Vec<(&str, RefKind)> = msg
.elements
.iter()
.map(|e| {
let kind = match e.kind {
ElementKind::Variable => RefKind::Variable,
ElementKind::Term => RefKind::Term,
};
(e.name.as_str(), kind)
})
.collect();
let Err(incompatibilities) = check_refs(
&vars,
&bool_vars,
&elements,
&msg.pattern_refs,
&msg.selectors,
) else {
continue;
};
for incompatibility in incompatibilities {
match incompatibility {
RefsIncompat::BoolSelectorMismatch { variable, found } => errors.push(format!(
"{}:{}: Boolean selector ${variable} in {} has keys [{}] — expected [true] and [false]",
msg.file,
msg.line,
msg.id,
found.join(", "),
)),
RefsIncompat::BoolReferenceOutsideSelector { variable } => errors.push(format!(
"{}:{}: Boolean variable ${variable} in {} is referenced outside a [true]/[false] selector",
msg.file, msg.line, msg.id,
)),
RefsIncompat::UnknownVariable { .. } | RefsIncompat::ElementMismatch { .. } => {}
}
}
}
errors.sort();
errors
}