fix(lib): account for unreachable patterns with children

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
This commit is contained in:
Amaan Qureshi 2025-09-23 03:38:16 -04:00 committed by Amaan Qureshi
parent f26bd44a43
commit 90ee433c9b
2 changed files with 65 additions and 2 deletions

View file

@ -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"),
}
);
}

View file

@ -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;