From 42f33fe2f8ddef5617a8536723c5d2b8a19a615e Mon Sep 17 00:00:00 2001 From: Will Lillis Date: Sun, 9 Aug 2026 16:08:05 -0500 Subject: [PATCH] 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. --- crates/cli/src/tests/query_test.rs | 42 ++++++++++++++++++++++++++++++ lib/src/query.c | 6 ++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/tests/query_test.rs b/crates/cli/src/tests/query_test.rs index 23b8f7762..7056f2175 100644 --- a/crates/cli/src/tests/query_test.rs +++ b/crates/cli/src/tests/query_test.rs @@ -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(|| { diff --git a/lib/src/query.c b/lib/src/query.c index 25c408f28..04aa5dea1 100644 --- a/lib/src/query.c +++ b/lib/src/query.c @@ -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