diff --git a/crates/proof/src/affix.rs b/crates/proof/src/affix.rs index 8bab96c2..723fdd9e 100644 --- a/crates/proof/src/affix.rs +++ b/crates/proof/src/affix.rs @@ -485,6 +485,11 @@ fn cap_try(s: &str) -> String { out } +/// `form`, unless it is empty: no word is, and `FULLSTRIP` can strip a stem down to nothing. +fn non_empty(form: String) -> Option { + Some(form).filter(|f| !f.is_empty()) +} + /// `0` means empty in Hunspell's strip and append fields. fn zero_empty(s: &str) -> String { if s == "0" { String::new() } else { s.to_string() } @@ -1553,11 +1558,15 @@ impl AffixDict { continue; } let form = format!("{base}{}", r.add); + if form.is_empty() { + continue; + } let circ = self.cont_holds(r, self.circumfix); if r.cross { cross_sfx.push((form.clone(), circ)); } - if r.cont != 0 && queued.insert((form.clone(), r.cont)) { + // A circumfix suffix needs its prefix, so what it ends is never chained without one. + if r.cont != 0 && !circ && queued.insert((form.clone(), r.cont)) { chain.push((form.clone(), r.cont)); } // A circumfix suffix or one that needs another affix is no word on its own. @@ -1589,6 +1598,9 @@ impl AffixDict { continue; } let next = format!("{base}{}", r.add); + if next.is_empty() { + continue; + } if r.cont != 0 && queued.insert((next.clone(), r.cont)) { chain.push((next.clone(), r.cont)); } @@ -1613,7 +1625,7 @@ impl AffixDict { } let circ = self.cont_holds(r, self.circumfix); if !circ && !self.cont_holds(r, self.need_affix) { - out.push(format!("{}{rest}", r.add)); + out.extend(non_empty(format!("{}{rest}", r.add))); } if r.cross { // Every suffix form looked at counts against the work budget, matching or not. @@ -1626,7 +1638,7 @@ impl AffixDict { continue; } if let Some(rest) = s.strip_prefix(&*r.strip) { - out.push(format!("{}{rest}", r.add)); + out.extend(non_empty(format!("{}{rest}", r.add))); } } } @@ -3113,4 +3125,33 @@ x/12 assert_eq!(forms, ["x", "xy", "z"]); assert!(d.contains("z")); } + + /// `forms()` never lists an empty form, which `FULLSTRIP` can produce when a suffix replaces + /// the whole stem with nothing, at the first suffix or further along a chain. + #[test] + fn forms_never_list_an_empty_word() { + let aff = "SET UTF-8\nFULLSTRIP\nSFX A Y 2\nSFX A x 0 .\nSFX A 0 y/B .\nSFX B Y 1\nSFX B xy 0 .\n"; + let d = AffixDict::from_hunspell(aff, "1\nx/A\n", &[]).unwrap(); + let mut forms = d.forms("x"); + forms.sort(); + assert_eq!(forms, ["x", "xy"]); + assert!(!d.contains("")); + } + + /// A circumfix suffix in the middle of a suffix chain still needs its prefix: `workable` + /// carries the circumfix, so neither it nor `workables` is a word without `ne`. + #[test] + fn circumfix_suffix_inside_a_chain_still_needs_its_prefix() { + let aff = "SET UTF-8\nCIRCUMFIX c\nPFX P Y 1\nPFX P 0 ne/c .\nSFX A Y 1\nSFX A 0 able/Bc .\nSFX B Y 1\nSFX B 0 s .\n"; + let d = AffixDict::from_hunspell(aff, "1\nwork/AP\n", &[]).unwrap(); + let mut forms = d.forms("work"); + forms.sort(); + assert_eq!(forms, ["neworkable", "work"]); + for f in &forms { + assert!(d.contains(f), "{f}"); + } + for w in ["workable", "workables", "nework"] { + assert!(!d.contains(w), "{w}"); + } + } }