From 34f31b9a4c4845f468dfc7e74fc422142998b42c Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 01:06:48 +0000 Subject: [PATCH] Refactor proof for strict Clippy checks --- crates/proof/src/cs/suggest.rs | 216 +++++++++++++++++++-------------- crates/proof/src/lang.rs | 120 +++++++++--------- 2 files changed, 183 insertions(+), 153 deletions(-) diff --git a/crates/proof/src/cs/suggest.rs b/crates/proof/src/cs/suggest.rs index 4e5ecd17..7ab0d149 100644 --- a/crates/proof/src/cs/suggest.rs +++ b/crates/proof/src/cs/suggest.rs @@ -180,97 +180,13 @@ pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { let input = word.to_lowercase(); let chars: Vec = input.chars().collect(); let mut col = Collector { d, input: input.clone(), tested: HashSet::new(), found: HashMap::new(), seq: 0 }; - // 1. Letter case. - if d.contains(&input) && word != input { - col.keep(input.clone(), COST_CASE); - } else if d.contains(&capitalize(&input)) && word != capitalize(&input) { - col.keep(capitalize(&input), COST_CASE); - } - // 2. Accents and other MAP slips. let map = d.map(); - let alts: Vec> = chars.iter().map(|&c| alternatives(c, map)).collect(); - let mut variants = Vec::new(); - map_variants(&chars, &alts, 0, 0, 0, &mut Vec::with_capacity(n), &mut variants); - variants.sort_by_key(|v| v.1); - for (v, cost) in &variants { - col.offer(v, *cost); - } - // 3. Common confusions. - for (from, to) in d.rep() { - if from.is_empty() { - continue; - } - for (i, _) in input.match_indices(from.as_str()) { - if let (Some(a), Some(b)) = (input.get(..i), input.get(i + from.len()..)) { - col.offer(&format!("{a}{to}{b}"), COST_REP); - } - } - } - // 4. A missing space; both halves must be dictionary words. - for (i, _) in input.char_indices().skip(1) { - let (Some(a), Some(b)) = (input.get(..i), input.get(i..)) else { continue }; - let long = |part: &str| part.chars().count() >= 3; - let cost = if long(a) && long(b) { COST_SPLIT } else { COST_SPLIT_RARE }; - col.offer(&format!("{a} {b}"), cost); - } - // 5. Keyboard neighbours from the installed dictionary's KEY rows. - for row in d.key().split('|').take(32) { - let keys: Vec = row.chars().take(128).collect(); - for pair in keys.windows(2) { - let [a, b] = pair else { continue }; - for (i, &c) in chars.iter().enumerate() { - let replacement = if c == *a { - Some(*b) - } else if c == *b { - Some(*a) - } else { - None - }; - if let Some(replacement) = replacement { - let mut v = chars.clone(); - if let Some(x) = v.get_mut(i) { - *x = replacement; - } - col.offer(&v.iter().collect::(), COST_KEY); - } - } - } - } - // One-letter edits use only the runtime TRY alphabet, with bounded work. - let mut letters: Vec = d.try_chars().chars().filter(|c| c.is_lowercase()).take(128).collect(); - letters.sort_unstable(); - letters.dedup(); - let s = |v: &[char]| v.iter().collect::(); - for i in 0..n.saturating_sub(1) { - if chars.get(i) != chars.get(i + 1) { - let mut v = chars.clone(); - v.swap(i, i + 1); - col.offer(&s(&v), COST_SWAP); - } - } - for i in 0..n { - let mut v = chars.clone(); - v.remove(i); - col.offer(&s(&v), COST_DROP_OR_ADD); - } - for i in 0..=n { - for &c in &letters { - let mut v = chars.clone(); - v.insert(i, c); - col.offer(&s(&v), COST_DROP_OR_ADD); - } - } - for i in 0..n { - for &c in &letters { - if chars.get(i) != Some(&c) { - let mut v = chars.clone(); - if let Some(x) = v.get_mut(i) { - *x = c; - } - col.offer(&s(&v), COST_MISTYPE); - } - } - } + add_case_suggestions(&mut col, &input, word); + add_map_suggestions(&mut col, &chars, map); + add_replacement_suggestions(&mut col, &input, d.rep()); + add_split_suggestions(&mut col, &input); + add_keyboard_suggestions(&mut col, &chars, d.key()); + add_typing_suggestions(&mut col, &chars, d.try_chars()); // 6. Further off: scan the stems that start like the word. if col.count_within(COST_MISTYPE) < max { scan(&mut col, &chars); @@ -297,6 +213,126 @@ pub fn suggest_in(d: &AffixDict, word: &str, max: usize) -> Vec { out } +fn add_case_suggestions(col: &mut Collector, input: &str, word: &str) { + if col.d.contains(input) && word != input { + col.keep(input.to_string(), COST_CASE); + } else { + let capitalized = capitalize(input); + if col.d.contains(&capitalized) && word != capitalized { + col.keep(capitalized, COST_CASE); + } + } +} + +fn add_map_suggestions(col: &mut Collector, chars: &[char], map: &[Vec]) { + let alternatives: Vec> = chars.iter().map(|&c| alternatives(c, map)).collect(); + let mut variants = Vec::new(); + map_variants(chars, &alternatives, 0, 0, 0, &mut Vec::with_capacity(chars.len()), &mut variants); + variants.sort_by_key(|variant| variant.1); + for (variant, cost) in variants { + col.offer(&variant, cost); + } +} + +fn add_replacement_suggestions(col: &mut Collector, input: &str, replacements: &[(String, String)]) { + for (from, to) in replacements { + if from.is_empty() { + continue; + } + for (index, _) in input.match_indices(from) { + if let (Some(prefix), Some(suffix)) = (input.get(..index), input.get(index + from.len()..)) { + col.offer(&format!("{prefix}{to}{suffix}"), COST_REP); + } + } + } +} + +fn add_split_suggestions(col: &mut Collector, input: &str) { + for (index, _) in input.char_indices().skip(1) { + let (Some(first), Some(second)) = (input.get(..index), input.get(index..)) else { continue }; + let cost = if first.chars().count() >= 3 && second.chars().count() >= 3 { COST_SPLIT } else { COST_SPLIT_RARE }; + col.offer(&format!("{first} {second}"), cost); + } +} + +fn add_keyboard_suggestions(col: &mut Collector, chars: &[char], key_rows: &str) { + for row in key_rows.split('|').take(32) { + let keys: Vec = row.chars().take(128).collect(); + for pair in keys.windows(2) { + let [first, second] = pair else { continue }; + for (index, &character) in chars.iter().enumerate() { + let replacement = if character == *first { + Some(*second) + } else if character == *second { + Some(*first) + } else { + None + }; + if let Some(replacement) = replacement { + let mut candidate = chars.to_vec(); + if let Some(character) = candidate.get_mut(index) { + *character = replacement; + } + col.offer(&candidate.iter().collect::(), COST_KEY); + } + } + } + } +} + +fn add_typing_suggestions(col: &mut Collector, chars: &[char], try_chars: &str) { + let mut letters: Vec = try_chars.chars().filter(|character| character.is_lowercase()).take(128).collect(); + letters.sort_unstable(); + letters.dedup(); + let word = |candidate: &[char]| candidate.iter().collect::(); + add_swap_suggestions(col, chars, &word); + add_drop_suggestions(col, chars, &word); + add_insert_suggestions(col, chars, &letters, &word); + add_mistype_suggestions(col, chars, &letters, &word); +} + +fn add_swap_suggestions(col: &mut Collector, chars: &[char], word: &impl Fn(&[char]) -> String) { + for index in 0..chars.len().saturating_sub(1) { + if chars.get(index) != chars.get(index + 1) { + let mut candidate = chars.to_vec(); + candidate.swap(index, index + 1); + col.offer(&word(&candidate), COST_SWAP); + } + } +} + +fn add_drop_suggestions(col: &mut Collector, chars: &[char], word: &impl Fn(&[char]) -> String) { + for index in 0..chars.len() { + let mut candidate = chars.to_vec(); + candidate.remove(index); + col.offer(&word(&candidate), COST_DROP_OR_ADD); + } +} + +fn add_insert_suggestions(col: &mut Collector, chars: &[char], letters: &[char], word: &impl Fn(&[char]) -> String) { + for index in 0..=chars.len() { + for &letter in letters { + let mut candidate = chars.to_vec(); + candidate.insert(index, letter); + col.offer(&word(&candidate), COST_DROP_OR_ADD); + } + } +} + +fn add_mistype_suggestions(col: &mut Collector, chars: &[char], letters: &[char], word: &impl Fn(&[char]) -> String) { + for index in 0..chars.len() { + for &letter in letters { + if chars.get(index) != Some(&letter) { + let mut candidate = chars.to_vec(); + if let Some(character) = candidate.get_mut(index) { + *character = letter; + } + col.offer(&word(&candidate), COST_MISTYPE); + } + } + } +} + /// Round 6: rank the stems starting with the word's first letter (or a MAP relative of it, either /// case) by shared letter pairs, then keep the forms of the best within two typos. fn scan(col: &mut Collector, chars: &[char]) { diff --git a/crates/proof/src/lang.rs b/crates/proof/src/lang.rs index 2520a835..8f681408 100644 --- a/crates/proof/src/lang.rs +++ b/crates/proof/src/lang.rs @@ -276,84 +276,69 @@ mod tests { } } + fn assert_tags(tags: &[&str], expected: ProofLang) { + for tag in tags { + assert_eq!(ProofLang::from_tag(Some(tag)), expected, "{tag}"); + } + } + #[test] - fn tags_resolve() { - for t in ["pl", "pl-PL", "PL-pl", "pl_PL", "pl_PL.UTF-8", " pl-PL ", "pol"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Pl, "{t}"); - } - for t in ["pt", "pt-BR", "PT_br", "pt-BR.UTF-8", " pt-PT ", "por"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Pt, "{t}"); - } - for t in ["it", "it-IT", "IT_it", "it-CH", "it-CH.UTF-8", " it-IT ", "ita"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::It, "{t}"); - } - for t in ["cs", "cs-CZ", "CS-cz", "cs_CZ", " ces ", "CES", "cze", "CZE"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Cs, "{t}"); - } + fn tags_resolve_primary_languages() { + assert_tags(&["pl", "pl-PL", "PL-pl", "pl_PL", "pl_PL.UTF-8", " pl-PL ", "pol"], ProofLang::Pl); + assert_tags(&["pt", "pt-BR", "PT_br", "pt-BR.UTF-8", " pt-PT ", "por"], ProofLang::Pt); + assert_tags(&["it", "it-IT", "IT_it", "it-CH", "it-CH.UTF-8", " it-IT ", "ita"], ProofLang::It); + assert_tags(&["cs", "cs-CZ", "CS-cz", "cs_CZ", " ces ", "CES", "cze", "CZE"], ProofLang::Cs); + assert_tags(&["id", "id-ID", "ID-id", "id_ID", "id_ID.UTF-8", " id-ID ", "ind", "in", "in_ID"], ProofLang::Id); assert_eq!(ProofLang::Cs.code(), "cs"); assert!(ProofLang::Cs.is_proofed() && ProofLang::PROOFED.contains(&ProofLang::Cs)); - for t in ["id", "id-ID", "ID-id", "id_ID", "id_ID.UTF-8", " id-ID ", "ind", "in", "in_ID"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Id, "{t}"); - } assert_eq!(ProofLang::Id.code(), "id"); assert!(ProofLang::Id.is_proofed() && ProofLang::PROOFED.contains(&ProofLang::Id)); - for t in ["en", "en-US", "EN_us", "en-Latn-US", "en-PH", "en-001", "eng", "en_US.UTF-8", "en-x-gb"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::En, "{t}"); - } - for t in [ - "en-GB", - "EN_gb", - "en_GB.UTF-8", - "en-AU", - "en_AU", - "en-NZ", - "en-IE", - "en-ZA", - "en-IN", - "en-Latn-GB", - "en-UK", - "en-150", - "eng-GB", - "en_GB@euro", - ] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::EnGb, "{t}"); - } - for t in ["en-CA", "en_CA", "EN-ca", "en_CA.UTF-8", "en-Latn-CA"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::EnCa, "{t}"); - } - for l in [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa] { - assert!(l.is_english() && l.is_proofed() && ProofLang::PROOFED.contains(&l), "{l:?}"); - assert_eq!(l.code(), "en"); + } + + #[test] + fn tags_resolve_english_regions() { + assert_tags(&["en", "en-US", "EN_us", "en-Latn-US", "en-PH", "en-001", "eng", "en_US.UTF-8", "en-x-gb"], ProofLang::En); + assert_tags( + &[ + "en-GB", + "EN_gb", + "en_GB.UTF-8", + "en-AU", + "en_AU", + "en-NZ", + "en-IE", + "en-ZA", + "en-IN", + "en-Latn-GB", + "en-UK", + "en-150", + "eng-GB", + "en_GB@euro", + ], + ProofLang::EnGb, + ); + assert_tags(&["en-CA", "en_CA", "EN-ca", "en_CA.UTF-8", "en-Latn-CA"], ProofLang::EnCa); + for language in [ProofLang::En, ProofLang::EnGb, ProofLang::EnCa] { + assert!(language.is_english() && language.is_proofed() && ProofLang::PROOFED.contains(&language), "{language:?}"); + assert_eq!(language.code(), "en"); } assert!(!ProofLang::Pl.is_english() && !ProofLang::Other.is_english()); // The region is the first two-letter or three-digit subtag: a script or variant isn't one. assert_eq!(ProofLang::from_tag(Some("en-Brai")), ProofLang::En); assert_eq!(ProofLang::from_tag(Some("en-GB-oxendict")), ProofLang::EnGb); - for t in ["bg", "bg-BG", "BG_bg", "bg_BG.UTF-8", "bul"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Bg, "{t}"); - } - for t in ["tr", "tr-TR", "tr-CY", "TR_tr", "tur"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Tr, "{t}"); - } - for t in ["de", "de-DE", "de-CH", "DE_at", "deu", "ger"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::De, "{t}"); - } + } + + #[test] + fn tags_resolve_additional_locales_and_defaults() { + assert_tags(&["bg", "bg-BG", "BG_bg", "bg_BG.UTF-8", "bul"], ProofLang::Bg); + assert_tags(&["tr", "tr-TR", "tr-CY", "TR_tr", "tur"], ProofLang::Tr); + assert_tags(&["de", "de-DE", "de-CH", "DE_at", "deu", "ger"], ProofLang::De); + assert_tags(&["ru", "ru-RU", "RU-ru", "ru_RU", "ru_RU.UTF-8", " ru-RU ", "rus"], ProofLang::Ru); assert!(!ProofLang::De.is_proofed()); - for t in ["fr-FR", "dex", "x-none", "ja-JP", "plx", "bgx", "trx", "csx", "idx", "inx", "esx", "spax", "e", "-"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Other, "{t}"); - } - assert_eq!(ProofLang::from_tag(None), ProofLang::En); - assert_eq!(ProofLang::from_tag(Some("")), ProofLang::En); - assert_eq!(ProofLang::from_tag(Some(" ")), ProofLang::En); - assert!(ProofLang::Pl.is_proofed() && !ProofLang::Other.is_proofed()); - // Bulgarian has hyphenation only: no spelling or grammar marks. assert!(!ProofLang::Bg.is_proofed()); assert_eq!(ProofLang::Bg.code(), "bg"); assert!(ProofLang::Pt.is_proofed() && !ProofLang::Other.is_proofed()); assert!(ProofLang::Tr.is_proofed()); - for t in ["ru", "ru-RU", "RU-ru", "ru_RU", "ru_RU.UTF-8", " ru-RU ", "rus"] { - assert_eq!(ProofLang::from_tag(Some(t)), ProofLang::Ru, "{t}"); - } assert!(ProofLang::Ru.is_proofed()); assert_eq!(ProofLang::Ru.code(), "ru"); assert!(ProofLang::PROOFED.contains(&ProofLang::Ru)); @@ -365,6 +350,15 @@ mod tests { assert_eq!(ProofLang::Hr.code(), "hr"); assert_eq!(ProofLang::Tr.code(), "tr"); assert!(ProofLang::PROOFED.contains(&ProofLang::Pt), "every proofed language is listed"); + assert_eq!(ProofLang::from_tag(None), ProofLang::En); + assert_eq!(ProofLang::from_tag(Some("")), ProofLang::En); + assert_eq!(ProofLang::from_tag(Some(" ")), ProofLang::En); + assert!(ProofLang::Pl.is_proofed() && !ProofLang::Other.is_proofed()); + } + + #[test] + fn tags_reject_unknown_or_partial_language_codes() { + assert_tags(&["fr-FR", "dex", "x-none", "ja-JP", "plx", "bgx", "trx", "csx", "idx", "inx", "esx", "spax", "e", "-"], ProofLang::Other); } /// #3360, #3553: Spanish is proofed, by region: Spain (and no region), Mexico, Uruguay, and