diff --git a/crates/cli/src/tests/query_test.rs b/crates/cli/src/tests/query_test.rs index 2fff0525..fdbdaab8 100644 --- a/crates/cli/src/tests/query_test.rs +++ b/crates/cli/src/tests/query_test.rs @@ -8,6 +8,7 @@ use tree_sitter::{ QueryCursorOptions, QueryError, QueryErrorKind, QueryPredicate, QueryPredicateArg, QueryProperty, Range, }; +use tree_sitter_generate::load_grammar_file; use unindent::Unindent; use super::helpers::{ @@ -5762,3 +5763,60 @@ fn test_query_allows_error_nodes_with_children() { assert_eq!(matches, &[(0, vec![("error", ".bar")])]); }); } + +#[test] +fn test_query_assertion_on_unreachable_node_with_child() { + // The `await_binding` rule is unreachable because it has a lower precedence than + // `identifier`, so we'll always reduce to an expression of type `identifier` + // instead whenever we see the token `await` followed by an identifier. + // + // A query that tries to capture the `await` token in the `await_binding` rule + // should not cause an assertion failure during query analysis. + let grammar = r#" +module.exports = grammar({ + name: "query_assertion_crash", + + rules: { + source_file: $ => repeat($.expression), + + expression: $ => choice( + $.await_binding, + $.await_expr, + $.equal_expr, + prec(3, $.identifier), + ), + + await_binding: $ => prec(1, seq('await', $.identifier, '=', $.expression)), + + await_expr: $ => prec(1, seq('await', $.expression)), + + equal_expr: $ => prec.right(2, seq($.expression, '=', $.expression)), + + identifier: _ => /[a-z]+/, + } +}); + "#; + + let file = tempfile::NamedTempFile::with_suffix(".js").unwrap(); + std::fs::write(file.path(), grammar).unwrap(); + + let grammar_json = load_grammar_file(file.path(), None).unwrap(); + + let (parser_name, parser_code) = generate_parser(&grammar_json).unwrap(); + + let language = get_test_language(&parser_name, &parser_code, None); + + let query_result = Query::new(&language, r#"(await_binding "await")"#); + + assert!(query_result.is_err()); + assert_eq!( + query_result.unwrap_err(), + QueryError { + kind: QueryErrorKind::Structure, + row: 0, + offset: 0, + column: 0, + message: ["(await_binding \"await\")", "^"].join("\n"), + } + ); +} diff --git a/lib/src/query.c b/lib/src/query.c index 90dd30b6..9ea255ac 100644 --- a/lib/src/query.c +++ b/lib/src/query.c @@ -1759,8 +1759,13 @@ static bool ts_query__analyze_patterns(TSQuery *self, unsigned *error_offset) { // If this pattern cannot match, store the pattern index so that it can be // returned to the caller. if (analysis.finished_parent_symbols.size == 0) { - ts_assert(analysis.final_step_indices.size > 0); - uint16_t impossible_step_index = *array_back(&analysis.final_step_indices); + uint16_t impossible_step_index; + if (analysis.final_step_indices.size > 0) { + impossible_step_index = *array_back(&analysis.final_step_indices); + } else { + // If there isn't a final step, then that means the parent step itself is unreachable. + impossible_step_index = parent_step_index; + } uint32_t j, impossible_exists; array_search_sorted_by(&self->step_offsets, .step_index, impossible_step_index, &j, &impossible_exists); if (j >= self->step_offsets.size) j = self->step_offsets.size - 1;