fix(query): prevent infinite loop with + and ? quantifiers

**Problem:** A query with a `?` quantifier followed by a `+` quantifier
would hang at 100% CPU usage while iterating through a tree, regardless
of the source content.

**Solution:** Collect all quantifiers in one step, and then add the
required repeat/optional step logic *after* we have determined the
composite quantifier we need to use for the current step.
This commit is contained in:
Riley Bruins 2025-11-22 17:36:37 -08:00 committed by Will Lillis
parent d64b863030
commit 829733a35e
2 changed files with 57 additions and 27 deletions

View file

@ -5032,6 +5032,26 @@ fn test_query_quantified_captures() {
("comment.documentation", "// quuz"),
],
},
Row {
description: "multiple quantifiers should not hang query parsing",
language: get_language("c"),
code: indoc! {"
// foo
// bar
// baz
"},
pattern: r"
((comment) ?+ @comment)
",
// This should be identical to the `*` quantifier.
captures: &[
("comment", "// foo"),
("comment", "// foo"),
("comment", "// foo"),
("comment", "// bar"),
("comment", "// baz"),
],
},
];
allocations::record(|| {

View file

@ -2736,12 +2736,6 @@ static TSQueryError ts_query__parse_pattern(
stream_advance(stream);
stream_skip_whitespace(stream);
QueryStep repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false);
repeat_step.alternative_index = starting_step_index;
repeat_step.is_pass_through = true;
repeat_step.alternative_is_immediate = true;
array_push(&self->steps, repeat_step);
}
// Parse the zero-or-more repetition operator.
@ -2750,21 +2744,6 @@ static TSQueryError ts_query__parse_pattern(
stream_advance(stream);
stream_skip_whitespace(stream);
QueryStep repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false);
repeat_step.alternative_index = starting_step_index;
repeat_step.is_pass_through = true;
repeat_step.alternative_is_immediate = true;
array_push(&self->steps, repeat_step);
// Stop when `step->alternative_index` is `NONE` or it points to
// `repeat_step` or beyond. Note that having just been pushed,
// `repeat_step` occupies slot `self->steps.size - 1`.
QueryStep *step = array_get(&self->steps, starting_step_index);
while (step->alternative_index != NONE && step->alternative_index < self->steps.size - 1) {
step = array_get(&self->steps, step->alternative_index);
}
step->alternative_index = self->steps.size;
}
// Parse the optional operator.
@ -2773,12 +2752,6 @@ static TSQueryError ts_query__parse_pattern(
stream_advance(stream);
stream_skip_whitespace(stream);
QueryStep *step = array_get(&self->steps, starting_step_index);
while (step->alternative_index != NONE && step->alternative_index < self->steps.size) {
step = array_get(&self->steps, step->alternative_index);
}
step->alternative_index = self->steps.size;
}
// Parse an '@'-prefixed capture pattern
@ -2822,6 +2795,43 @@ static TSQueryError ts_query__parse_pattern(
}
}
QueryStep repeat_step;
QueryStep *step;
switch (quantifier) {
case TSQuantifierOneOrMore:
repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false);
repeat_step.alternative_index = starting_step_index;
repeat_step.is_pass_through = true;
repeat_step.alternative_is_immediate = true;
array_push(&self->steps, repeat_step);
break;
case TSQuantifierZeroOrMore:
repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false);
repeat_step.alternative_index = starting_step_index;
repeat_step.is_pass_through = true;
repeat_step.alternative_is_immediate = true;
array_push(&self->steps, repeat_step);
// Stop when `step->alternative_index` is `NONE` or it points to
// `repeat_step` or beyond. Note that having just been pushed,
// `repeat_step` occupies slot `self->steps.size - 1`.
step = array_get(&self->steps, starting_step_index);
while (step->alternative_index != NONE && step->alternative_index < self->steps.size - 1) {
step = array_get(&self->steps, step->alternative_index);
}
step->alternative_index = self->steps.size;
break;
case TSQuantifierZeroOrOne:
step = array_get(&self->steps, starting_step_index);
while (step->alternative_index != NONE && step->alternative_index < self->steps.size) {
step = array_get(&self->steps, step->alternative_index);
}
step->alternative_index = self->steps.size;
break;
default:
break;
}
capture_quantifiers_mul(capture_quantifiers, quantifier);
return 0;