From ede3fdae20d672cc0a0102995dea5e2e91c9717e Mon Sep 17 00:00:00 2001 From: Peter Toulouras <40574038+PeterToulouras@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:21:08 -0700 Subject: [PATCH] fix(aff): consult AF aliases for numeric flag types decode_flagset took a fast lane for `FLAG num` that decoded the field as a literal numeric flag without checking the AF alias table. With both `FLAG num` and an AF table, a `.dic` entry like `word/1` produced the literal flag {1} instead of expanding alias #1, so aliased affix rules never matched. An AF table should take priority over the flag type, as Hunspell does in HashMgr::load_tables. Gate the fast lane on `aliases.is_empty()` alone; the existing alias-index lookup then covers numeric dictionaries too, and out-of-range or non-integer fields still fall through to the literal decode. I hit this with the Korean dictionary, whose josa suffix rules are all referenced through AF aliases, so every suffixed word was wrongly flagged as misspelled. --- CHANGELOG.md | 6 ++++++ src/aff/parser.rs | 46 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e8eb7..29a0b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `SIMPLIFIEDTRIPLE`). * `NOSPLITSUGS` is now honored: the suggester no longer proposes splitting a word into two when the dictionary sets this flag. +* Fixed flag decoding for dictionaries that combine `FLAG num` with an `AF` + alias table. A flag field is now resolved as an alias index rather than + decoded as a literal numeric flag, so alias references expand correctly + (matching Hunspell and Nuspell). Previously affected dictionaries such as + Korean, whose suffix rules are referenced through `AF` aliases, reported + every suffixed word as misspelled. ### Performance diff --git a/src/aff/parser.rs b/src/aff/parser.rs index 7bba57a..5afa8e2 100644 --- a/src/aff/parser.rs +++ b/src/aff/parser.rs @@ -1185,8 +1185,11 @@ fn decode_flagset( flag_type: FlagType, aliases: &[FlagSet], ) -> core::result::Result { - // Fast lane for numeric flag-types and empty aliases. - if matches!(flag_type, FlagType::Numeric) || aliases.is_empty() { + // Fast lane when there are no flag aliases. An `AF` alias table takes + // priority over the flag type: a flagset field is decoded as a 1-based alias + // index even under `FLAG num`. Hunspell: + // + if aliases.is_empty() { return parse_flags_from_str(flag_type, input); } @@ -1851,34 +1854,57 @@ mod test { #[test] fn decode_flagset_test() { - let aliases = &[flagset![1], flagset![2], flagset![3, 4]]; + // Alias values differ from their indices so resolving an index is + // distinguishable from decoding a literal flag. + let aliases = &[flagset![100], flagset![200, 300]]; + // With an alias table a flagset field is a 1-based alias index. // NOTE: 1-indexing. assert_eq!( - flagset![1], + flagset![100], decode_flagset("1", FlagType::default(), aliases).unwrap() ); assert_eq!( - flagset![2], + flagset![200, 300], decode_flagset("2", FlagType::default(), aliases).unwrap() ); + + // The alias table takes priority over the flag type: `FLAG num` resolves + // the index rather than decoding the literal flag {1}. + assert_eq!( + flagset![100], + decode_flagset("1", FlagType::Numeric, aliases).unwrap() + ); assert_eq!( - flagset![3, 4], + flagset![200, 300], + decode_flagset("2", FlagType::Numeric, aliases).unwrap() + ); + + // An out-of-range index falls back to decoding a literal flag. + assert_eq!( + flagset!['3' as u16], decode_flagset("3", FlagType::default(), aliases).unwrap() ); assert_eq!( - flagset!['a' as u16], - decode_flagset("a", FlagType::default(), aliases).unwrap() + flagset![3], + decode_flagset("3", FlagType::Numeric, aliases).unwrap() ); + // A non-integer field can't be an index, so it's decoded literally. assert_eq!( - flagset![1], - decode_flagset("1", FlagType::Numeric, aliases).unwrap() + flagset!['a' as u16], + decode_flagset("a", FlagType::default(), aliases).unwrap() ); + + // Without an alias table the field is always decoded literally. assert_eq!( flagset!['1' as u16], decode_flagset("1", FlagType::default(), &[]).unwrap() ); + assert_eq!( + flagset![1], + decode_flagset("1", FlagType::Numeric, &[]).unwrap() + ); } #[test]