use std::collections::{BTreeMap, HashSet};
use crate::extract::ExtractResult;
use crate::guard::Guard;
use crate::po::{PoIndex, PoKey};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AliveVia {
Literal,
Guard,
Whitelist,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Alive(AliveVia),
Suspect,
Dead,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyVerdict {
pub key: PoKey,
pub status: Status,
}
#[derive(Debug, Default)]
pub struct Usage {
pub literals: HashSet<String>,
pub guards: Vec<Guard>,
pub blind: BTreeMap<String, usize>,
pub source_literals: HashSet<String>,
}
impl Usage {
pub fn add(&mut self, lang_label: &str, result: ExtractResult) {
self.literals.extend(result.literals);
self.guards.extend(result.guards);
if result.blind > 0 {
*self.blind.entry(lang_label.to_string()).or_default() += result.blind;
}
self.source_literals.extend(result.source_literals);
}
}
#[derive(Debug)]
pub struct LivenessReport {
pub verdicts: Vec<KeyVerdict>,
pub blind: BTreeMap<String, usize>,
}
impl LivenessReport {
pub fn dead(&self) -> impl Iterator<Item = &KeyVerdict> {
self.verdicts.iter().filter(|v| v.status == Status::Dead)
}
pub fn suspect(&self) -> impl Iterator<Item = &KeyVerdict> {
self.verdicts.iter().filter(|v| v.status == Status::Suspect)
}
pub fn dead_count(&self) -> usize {
self.dead().count()
}
pub fn suspect_count(&self) -> usize {
self.suspect().count()
}
pub fn alive_count(&self) -> usize {
self.verdicts.len() - self.dead_count() - self.suspect_count()
}
pub fn total_blind(&self) -> usize {
self.blind.values().sum()
}
}
fn key_forms(key: &PoKey) -> Vec<&str> {
let mut forms = vec![key.msgid.as_str()];
if let Some(plural) = &key.msgid_plural {
forms.push(plural.as_str());
}
forms
}
pub fn classify(index: &PoIndex, usage: &Usage, whitelist: &HashSet<String>) -> LivenessReport {
let verdicts = index
.keys()
.map(|key| KeyVerdict {
key: key.clone(),
status: classify_key(key, usage, whitelist),
})
.collect();
LivenessReport {
verdicts,
blind: usage.blind.clone(),
}
}
fn classify_key(key: &PoKey, usage: &Usage, whitelist: &HashSet<String>) -> Status {
let forms = key_forms(key);
if forms.iter().any(|f| whitelist.contains(*f)) {
return Status::Alive(AliveVia::Whitelist);
}
if forms.iter().any(|f| usage.literals.contains(*f)) {
return Status::Alive(AliveVia::Literal);
}
if usage
.guards
.iter()
.any(|g| forms.iter().any(|f| g.matches(f)))
{
return Status::Alive(AliveVia::Guard);
}
if forms.iter().any(|f| usage.source_literals.contains(*f)) {
return Status::Suspect;
}
Status::Dead
}
#[cfg(test)]
mod tests {
use super::*;
fn key(msgid: &str) -> PoKey {
PoKey {
msgctxt: None,
msgid: msgid.to_string(),
msgid_plural: None,
}
}
fn plural_key(msgid: &str, plural: &str) -> PoKey {
PoKey {
msgctxt: None,
msgid: msgid.to_string(),
msgid_plural: Some(plural.to_string()),
}
}
#[test]
fn classifies_three_cases() {
let index = PoIndex::from_keys([
key("used_literal"),
key("cf_subtype_color"),
key("truly_dead"),
]);
let mut usage = Usage::default();
usage.literals.insert("used_literal".to_string());
usage.guards.push(Guard::Prefix("cf_subtype_".to_string()));
let report = classify(&index, &usage, &HashSet::new());
let verdict = |id: &str| {
report
.verdicts
.iter()
.find(|v| v.key.msgid == id)
.unwrap()
.status
};
assert_eq!(verdict("used_literal"), Status::Alive(AliveVia::Literal));
assert_eq!(verdict("cf_subtype_color"), Status::Alive(AliveVia::Guard));
assert_eq!(verdict("truly_dead"), Status::Dead);
assert_eq!(report.dead_count(), 1);
assert_eq!(report.alive_count(), 2);
}
#[test]
fn source_literal_only_is_suspect() {
let index = PoIndex::from_keys([
key("data_table_key"),
key("never_anywhere"),
key("also_called"),
]);
let mut usage = Usage::default();
usage
.source_literals
.extend(["data_table_key".to_string(), "also_called".to_string()]);
usage.literals.insert("also_called".to_string());
let report = classify(&index, &usage, &HashSet::new());
let verdict = |id: &str| {
report
.verdicts
.iter()
.find(|v| v.key.msgid == id)
.unwrap()
.status
};
assert_eq!(verdict("data_table_key"), Status::Suspect);
assert_eq!(verdict("never_anywhere"), Status::Dead);
assert_eq!(verdict("also_called"), Status::Alive(AliveVia::Literal));
assert_eq!(report.suspect_count(), 1);
assert_eq!(report.dead_count(), 1);
assert_eq!(report.alive_count(), 1);
}
#[test]
fn whitelist_keeps_key_alive() {
let index = PoIndex::from_keys([key("dynamic.from.db")]);
let usage = Usage::default();
let whitelist: HashSet<String> = ["dynamic.from.db".to_string()].into_iter().collect();
let report = classify(&index, &usage, &whitelist);
assert_eq!(
report.verdicts[0].status,
Status::Alive(AliveVia::Whitelist)
);
}
#[test]
fn plural_key_alive_via_plural_form() {
let index = PoIndex::from_keys([plural_key("1 apple", "%d apples")]);
let mut usage = Usage::default();
usage.literals.insert("%d apples".to_string());
let report = classify(&index, &usage, &HashSet::new());
assert_eq!(report.verdicts[0].status, Status::Alive(AliveVia::Literal));
}
#[test]
fn blind_summary_aggregates_per_language() {
let mut usage = Usage::default();
usage.add(
"js",
ExtractResult {
blind: 1,
..Default::default()
},
);
usage.add(
"js",
ExtractResult {
blind: 1,
..Default::default()
},
);
usage.add(
"twig",
ExtractResult {
blind: 1,
..Default::default()
},
);
let report = classify(&PoIndex::from_keys([]), &usage, &HashSet::new());
assert_eq!(report.blind.get("js"), Some(&2));
assert_eq!(report.blind.get("twig"), Some(&1));
assert_eq!(report.total_blind(), 3);
}
}