Merge pull request #1473

Spell check: don't check words longer than 100 characters (fixes #1175)
This commit is contained in:
Brandon Thomas
2026-10-30 15:48:35 -04:00
+24
View File
@@ -356,7 +356,14 @@ impl Dictionary {
}
/// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass.
/// Words longer than 100 characters are not checked (they pass).
pub fn check(&self, word: &str) -> bool {
// Compound splitting tries every split point, so checking costs O(n²) in the word's
// length: a 100,000-character "word" took seconds per check under a compounding
// dictionary, on every edit (#1175).
if word.chars().nth(MAX_CHECK_CHARS).is_some() {
return true;
}
if word.contains('\u{2019}') {
return self.check(&word.replace('\u{2019}', "'"));
}
@@ -553,6 +560,9 @@ pub fn find_in(dirs: &[PathBuf], lang: &str) -> Option<(PathBuf, PathBuf)> {
/// Longest word `Dictionary::suggest` generates corrections for.
const MAX_SUGGEST_CHARS: usize = 64;
/// Longest word `Dictionary::check` checks, as Hunspell's word length limit.
const MAX_CHECK_CHARS: usize = 100;
type Cache = Mutex<HashMap<String, Option<Arc<Dictionary>>>>;
fn cache() -> &'static Cache {
@@ -843,6 +853,20 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string()));
}
/// #1175: compound splitting is quadratic in the word's length, so a word longer than
/// `MAX_CHECK_CHARS` is not checked; shorter ones still are.
#[test]
fn a_word_longer_than_the_check_limit_is_not_checked() {
let d = Dictionary::parse(b"COMPOUNDFLAG X\nCOMPOUNDMIN 3\n", b"2\nfoot/X\nball/X\n".as_slice());
assert!(d.check("footballfoot"), "a compound is checked");
let under: String = "footb".repeat(MAX_CHECK_CHARS / 5);
assert_eq!(under.chars().count(), MAX_CHECK_CHARS);
assert!(!d.check(&under), "a word at the limit is still checked");
// Unchecked, not seconds of compound splitting.
assert!(d.check(&"footb".repeat(20_000)));
assert!(d.check(&format!("{under}x")));
}
#[test]
fn compounds_join_flagged_parts() {
let aff = "COMPOUNDFLAG X\nCOMPOUNDMIN 3\nONLYINCOMPOUND O\nSFX S Y 1\nSFX S 0 en .\n";