From b0bcb812b1c397247cecc6e6397a68f5d68ad3d2 Mon Sep 17 00:00:00 2001 From: Kharec Date: Thu, 15 Oct 2026 17:56:48 +0200 Subject: [PATCH] fix: bound the spelling checker's word cache The remembered answers are keyed on user input and never evicted: a long session could grow the map without bound. Clear it at 10000 words and let it refill - a script repeats the same words, so re-checks are cheap. --- internal/spell/checker.go | 9 +++++++++ internal/spell/spell_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/internal/spell/checker.go b/internal/spell/checker.go index f50876c..5a33ab5 100644 --- a/internal/spell/checker.go +++ b/internal/spell/checker.go @@ -90,6 +90,12 @@ func (c *Checker) Add(word string) { func normalize(w string) string { return strings.ReplaceAll(w, "’", "'") } +// cacheLimit bounds the remembered answers, which are keyed on user input: +// a long session of typos would grow the map forever otherwise. When full, +// the cache is cleared and refilled; a script repeats the same words, so +// re-checks are cheap. +const cacheLimit = 10000 + // Check reports whether word is correct. Words with digits ("3rd", "R2D2") // and single letters are always accepted. func (c *Checker) Check(word string) bool { @@ -104,6 +110,9 @@ func (c *Checker) Check(word string) bool { } ok, found := c.cache[word] if !found { + if len(c.cache) >= cacheLimit { + clear(c.cache) + } ok = c.dict.Check(word) c.cache[word] = ok } diff --git a/internal/spell/spell_test.go b/internal/spell/spell_test.go index 3337c2b..14c8c33 100644 --- a/internal/spell/spell_test.go +++ b/internal/spell/spell_test.go @@ -180,6 +180,35 @@ func TestLatin1(t *testing.T) { } } +func TestCheckerCacheBound(t *testing.T) { + d, err := Parse([]byte("SET UTF-8\n"), []byte("1\nknown\n")) + if err != nil { + t.Fatal(err) + } + c := NewChecker(d, nil) + // letters encodes n in bijective base 26: distinct pure-letter words. + letters := func(n int) string { + var w []rune + for { + w = append([]rune{rune('a' + n%26)}, w...) + if n < 26 { + return string(w) + } + n = n/26 - 1 + } + } + for i := range 2 * cacheLimit { + c.Check(letters(i)) + } + if len(c.cache) > cacheLimit { + t.Errorf("cache holds %d words, limit %d", len(c.cache), cacheLimit) + } + // The answers are unchanged by a clear. + if !c.Check("known") || c.Check("knownn") { + t.Error("wrong answers after the cache cleared") + } +} + func TestWords(t *testing.T) { got := Words("Élise's well-known R2D2, l’homme… 'quoted' x") want := []Word{