diff --git a/crates/cli/src/tests/query_test.rs b/crates/cli/src/tests/query_test.rs index 1baf1e471..abba409da 100644 --- a/crates/cli/src/tests/query_test.rs +++ b/crates/cli/src/tests/query_test.rs @@ -1258,6 +1258,45 @@ function foo() {} }); } +#[test] +fn test_query_matches_with_last_child_anchor_after_optional() { + allocations::record(|| { + let language = get_language("c"); + let query = Query::new( + &language, + "(preproc_if (preproc_def)+ @def . (preproc_else)? @else .)", + ) + .unwrap(); + + // The optional `(preproc_else)?` is absent, so the trailing anchor's + // last-child requirement transfers to the last `preproc_def`. A trailing + // comment means the def is not the last child, so nothing matches. + assert_query_matches( + &language, + &query, + " +#if X +#define A +// c +#endif +", + &[], + ); + + // With the def as the last child, the (else-less) match is allowed. + assert_query_matches( + &language, + &query, + " +#if X +#define A +#endif +", + &[(0, vec![("def", "#define A\n")])], + ); + }); +} + #[test] fn test_query_matches_with_last_named_child() { allocations::record(|| { diff --git a/lib/src/query.c b/lib/src/query.c index 23db6ca64..c47101dd9 100644 --- a/lib/src/query.c +++ b/lib/src/query.c @@ -4450,6 +4450,17 @@ static inline bool ts_query_cursor__advance( k--; } + // A `?`/`*` zero-skip past a step that carries a trailing last-child + // anchor transfers that requirement to the last matched node. The + // skip is only valid if that node really is the last named child. + if ( + child_step->alternative_is_skip && + child_step->is_last_child && + has_later_named_siblings + ) { + continue; + } + QueryState *copy = ts_query_cursor__copy_state(self, &child_state); if (copy) { LOG(