fix(utils): findAndReplace infinite loop on non-global regex + tests (+28)

Prevention work surfaced a real latent bug: findAndReplace looped forever
(OOM) on any non-global regex with a match — `match` was only reassigned
inside `if (regex.global)`, so a non-global regex never advanced. Fixed by
treating a non-global regex as a single match (`match = regex.global ?
regex.exec(text) : null`) and added a regression test. Latent in practice
(all current callers pass global regexes), but a crash waiting to happen.

New suites (tsx + node:test), verified empirically:
- utils/findAndReplace (10, incl. the regression)
- utils/AsyncSearch (9): normalize + matchQuery (the timer-based class is
  skipped — needs window.performance/setTimeout, unavailable in node)
- utils/ASCIILexicalTable (10): orderKeys gap-filling + invariants

Full suite now 103 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 11:15:39 -04:00
co-authored by Claude Opus 4.8
parent ae1d30bc5a
commit a926487f5e
4 changed files with 256 additions and 1 deletions
+4 -1
View File
@@ -19,7 +19,10 @@ export const findAndReplace = <ReplaceReturnType, ConvertReturnType>(
result.push(replace(match, result.length));
lastEnd = match.index + match[0].length;
if (regex.global) match = regex.exec(text);
// A non-global regex always returns the same first match from exec() (its
// lastIndex never advances), so re-running it would loop forever. Treat a
// non-global regex as a single match and stop after processing it.
match = regex.global ? regex.exec(text) : null;
}
result.push(convertPart(text.slice(lastEnd), result.length));