Skip to content

Commit 34ba3a7

Browse files
alecthomasclaude
andcommitted
fix: guard against zero-width match loops in the lexer
A rule matching the empty string without changing the state stack made no progress and iterated forever (e.g. pattern a* at a non-matching position). Such matches now emit an error token for the current rune and advance, mirroring the no-match path. All lexer golden tests are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5b89a37 commit 34ba3a7

2 files changed

Lines changed: 21 additions & 0 deletions

File tree

regexp.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,24 @@ func (l *LexerState) Iterator() iter.Seq[Token] { // nolint: gocognit
249249
l.Groups = groups
250250
l.NamedGroups = namedGroups
251251
l.Pos += utf8.RuneCountInString(groups[0])
252+
progressed := groups[0] != ""
252253
if rule.Mutator != nil {
254+
depth, top := len(l.Stack), l.State
253255
if err := rule.Mutator.Mutate(l); err != nil {
254256
panic(err)
255257
}
258+
progressed = progressed || len(l.Stack) != depth || len(l.Stack) == 0 || l.Stack[len(l.Stack)-1] != top
256259
}
257260
if rule.Type != nil {
258261
l.pushIterator(rule.Type.Emit(l.Groups, l))
259262
}
263+
if !progressed {
264+
// A zero-width match that did not change state will never advance.
265+
l.Pos++
266+
if !yield(Token{Error, string(l.Text[l.Pos-1 : l.Pos])}) {
267+
return
268+
}
269+
}
260270
}
261271
if !l.drainIteratorStack(yield) {
262272
return

regexp_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ func TestMatchingAtStart(t *testing.T) {
5454
slices.Collect(it))
5555
}
5656

57+
func TestZeroWidthMatchTerminates(t *testing.T) {
58+
l := Coalesce(mustNewLexer(t, &Config{}, Rules{ // nolint: forbidigo
59+
"root": {
60+
{`a*`, Text, nil},
61+
},
62+
}))
63+
it, err := l.Tokenise(nil, "b")
64+
assert.NoError(t, err)
65+
assert.Equal(t, []Token{{Error, "b"}}, slices.Collect(it))
66+
}
67+
5768
func TestEnsureLFOption(t *testing.T) {
5869
l := Coalesce(mustNewLexer(t, &Config{}, Rules{ // nolint: forbidigo
5970
"root": {

0 commit comments

Comments
 (0)