fix(query): correctly set a state's skipped_quantifier flag when a

zero quantifier skip is performed.

The zero-skip branch currently sets skipped_quantifier unconditionally.
That flag is correct only when the quantified step and its skip target
are _siblings_ at the same query depth. Otherwise, setting it allows for
a "leak" and disables unrelated, "outer" anchors.
This commit is contained in:
Will Lillis 2026-08-09 16:08:05 -05:00
parent 5c2533bb7b
commit 42f33fe2f8
2 changed files with 47 additions and 1 deletions

View file

@ -1268,6 +1268,48 @@ function foo() {}
});
}
#[test]
fn test_query_matches_with_anchor_after_nested_zero_quantifier() {
allocations::record(|| {
let language = get_language("javascript");
let query = Query::new(
&language,
r#"
(_
(field_definition
property: (_) @name
value: (_)? @value
) @field
.
";" @semicolon
)
"#,
)
.unwrap();
assert_query_matches(
&language,
&query,
"class Foo { bar; baz = 0; }",
&[
(
0,
vec![("field", "bar"), ("name", "bar"), ("semicolon", ";")],
),
(
0,
vec![
("field", "baz = 0"),
("name", "baz"),
("value", "0"),
("semicolon", ";"),
],
),
],
);
});
}
#[test]
fn test_query_matches_with_last_child_anchor_after_optional() {
allocations::record(|| {

View file

@ -4481,10 +4481,14 @@ static inline bool ts_query_cursor__advance(
// nothing. How an adjacent anchor behaves then depends on where it sat:
if (child_step->alternative_is_skip) {
if (!child_step->is_immediate) {
QueryStep *skip_target = array_get(
&self->query->steps,
child_step->alternative_index
);
// No leading anchor on the skipped step, so an immediately-following
// anchor on the skip target is vacuous (`Q* . B` with zero `Q` lets
// `B` match anywhere).
copy->skipped_quantifier = true;
copy->skipped_quantifier = skip_target->depth == child_step->depth;
} else if (
array_get(&self->query->steps, child_state->step_index - 1)->depth <
child_step->depth