fix(lib): continue search for later named siblings in

`ts_tree_cursor_current_status`

By terminating early on `has_later_siblings`, `has_later_named_siblings`
was incorrectly reported as `false` in some cases. This led to the
execution of some queries to terminate early.

Also remove some dead branches inside `ts_tree_cursor_current_status`.

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
This commit is contained in:
Newosko 2026-08-07 09:52:44 +02:00 committed by Will Lillis
parent 003b10c280
commit 308aee0c90
4 changed files with 133 additions and 4 deletions

View file

@ -6485,3 +6485,64 @@ export default grammar({
assert_query_matches(&language, &query, source, &[(0, vec![("tuple", "()")])]);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_repeat() {
let language = get_test_fixture_language("last_child_anchor_past_hidden_repeat");
let source = "T a.b.c\nL a.b.c\nN a!.b!.c\n";
let query = Query::new(
&language,
"
(trailing_sep (name) @last .)
(leading_sep (name) @last .)
(trailing_named (name) @last .)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("last", "c")]),
(1, vec![("last", "c")]),
(2, vec![("last", "c")]),
],
);
let query = Query::new(
&language,
"
(trailing_sep . (name) @first)
(trailing_sep (name) @a . (name) @b)
",
)
.unwrap();
assert_query_matches(
&language,
&query,
source,
&[
(0, vec![("first", "a")]),
(1, vec![("a", "a"), ("b", "b")]),
(1, vec![("a", "b"), ("b", "c")]),
],
);
}
#[test]
fn test_last_child_anchor_looks_past_hidden_node() {
allocations::record(|| {
let language = get_language("c");
let query = Query::new(&language, "(translation_unit (_) @last .)").unwrap();
let source = "enum E { A };\nint x;\nint y;\n";
assert_query_matches(&language, &query, source, &[(0, vec![("last", "int y;")])]);
});
}

View file

@ -557,8 +557,10 @@ void ts_tree_cursor_current_status(
(*supertype_count)++;
}
// Determine if the current node has later siblings.
if (!*has_later_siblings) {
// Determine if the current node has later siblings. A later *anonymous*
// sibling settles `has_later_siblings` but says nothing about later *named*
// siblings.
if (!*has_later_named_siblings) {
unsigned sibling_count = parent_entry->subtree->ptr->child_count;
unsigned structural_child_index = entry->structural_child_index;
if (!ts_subtree_extra(*entry->subtree)) structural_child_index++;
@ -570,14 +572,12 @@ void ts_tree_cursor_current_status(
);
if (sibling_metadata.visible) {
*has_later_siblings = true;
if (*has_later_named_siblings) break;
if (sibling_metadata.named) {
*has_later_named_siblings = true;
break;
}
} else if (ts_subtree_visible_child_count(sibling) > 0) {
*has_later_siblings = true;
if (*has_later_named_siblings) break;
if (sibling.ptr->named_child_count > 0) {
*has_later_named_siblings = true;
break;

View file

@ -0,0 +1,43 @@
=========================================
Repeat ending with an anonymous separator
=========================================
T a.b.c
---
(source_file
(trailing_sep
(name)
(name)
(name)))
===============================
Repeat ending with a named node
===============================
L a.b.c
---
(source_file
(leading_sep
(name)
(name)
(name)))
============================================================
Repeat ending with an anonymous separator, preceded by a tag
============================================================
N a!.b!.c
---
(source_file
(trailing_named
(name)
(tag)
(name)
(tag)
(name)))

View file

@ -0,0 +1,25 @@
// These three rules all parse to a flat run of `name` children, but `repeat`
// builds a different hidden `_repeat1` node for each of them, so a query's
// trailing `.` anchor has to walk past a different set of hidden nodes to
// decide whether a `name` is the last *named* child of its rule.
export default grammar({
name: 'last_child_anchor_past_hidden_repeat',
rules: {
source_file: $ => repeat(choice($.trailing_sep, $.leading_sep, $.trailing_named)),
// The hidden repeat ends with an anonymous node, and a named node follows it.
trailing_sep: $ => seq('T', repeat(seq($.name, '.')), $.name),
// The hidden repeat ends with a named node.
leading_sep: $ => seq('L', $.name, repeat(seq('.', $.name))),
// Same shape as `trailing_sep`, but a named node sits inside the repeat
// ahead of the anonymous separator.
trailing_named: $ => seq('N', repeat(seq($.name, $.tag, '.')), $.name),
tag: _ => '!',
name: _ => /[a-z]+/,
}
});