diff --git a/cli/src/tests/query_test.rs b/cli/src/tests/query_test.rs index b80b891e..6e722aef 100644 --- a/cli/src/tests/query_test.rs +++ b/cli/src/tests/query_test.rs @@ -5442,3 +5442,81 @@ fn test_wildcard_behavior_before_anchor() { ], ); } + +#[test] +fn test_pattern_alternatives_follow_last_child_constraint() { + let language = get_language("rust"); + let mut parser = Parser::new(); + parser.set_language(&language).unwrap(); + + let code = " +fn f() { + if a {} // <- should NOT match + if b {} +}"; + + let tree = parser.parse(code, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let query = Query::new( + &language, + "(block + [ + (type_cast_expression) + (expression_statement) + ] @last + . + )", + ) + .unwrap(); + + let matches = { + let root_node = tree.root_node(); + let matches = cursor.matches(&query, root_node, code.as_bytes()); + collect_matches(matches, &query, code) + .into_iter() + .map(|(i, m)| { + ( + i, + m.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(), + ) + }) + .collect::>() + }; + + let flipped_query = Query::new( + &language, + "(block + [ + (expression_statement) + (type_cast_expression) + ] @last + . + )", + ) + .unwrap(); + + let flipped_matches = { + let root_node = tree.root_node(); + let matches = cursor.matches(&flipped_query, root_node, code.as_bytes()); + collect_matches(matches, &flipped_query, code) + .into_iter() + .map(|(i, m)| { + ( + i, + m.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(), + ) + }) + .collect::>() + }; + + assert_eq!( + matches, + vec![(0, vec![(String::from("last"), String::from("if b {}"))])] + ); + assert_eq!(matches, flipped_matches); +} diff --git a/lib/src/query.c b/lib/src/query.c index 14fa0ab9..a594342f 100644 --- a/lib/src/query.c +++ b/lib/src/query.c @@ -2491,7 +2491,20 @@ static TSQueryError ts_query__parse_pattern( capture_quantifiers_delete(&child_capture_quantifiers); return TSQueryErrorSyntax; } - self->steps.contents[last_child_step_index].is_last_child = true; + // Mark this step *and* its alternatives as the last child of the parent. + QueryStep *last_child_step = &self->steps.contents[last_child_step_index]; + last_child_step->is_last_child = true; + if (last_child_step->alternative_index != NONE) { + QueryStep *alternative_step = &self->steps.contents[last_child_step->alternative_index]; + alternative_step->is_last_child = true; + while ( + alternative_step->alternative_index != NONE && + alternative_step->alternative_index < self->steps.size + ) { + alternative_step = &self->steps.contents[alternative_step->alternative_index]; + alternative_step->is_last_child = true; + } + } } if (negated_field_count) {