mirror of
https://github.com/tree-sitter/tree-sitter.git
synced 2026-09-10 07:26:23 -04:00
fix(query): make an anchor after a zero-matched quantifier vacuous
A `.` anchor between a quantified pattern and a following node, e.g. `(parent (comment)* @c . (decl))`, was treated as a leading anchor when the quantifier matched zero. The zero-skip jumps a state directly onto the anchored step, where `is_immediate` was enforced even though no sibling had matched before it. Name that skip edge (`alternative_is_skip`) and, when a state follows it, record that it skipped the quantifier (`skipped_quantifier`). The immediate anchor is then supressoed for that state's next match.
This commit is contained in:
parent
966dc52557
commit
7e7f2584d1
|
|
@ -1222,6 +1222,42 @@ fn test_query_matches_with_immediate_siblings() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_anchor_after_zero_quantifier() {
|
||||
allocations::record(|| {
|
||||
let language = get_language("javascript");
|
||||
let query = Query::new(
|
||||
&language,
|
||||
"(program (comment)* @doc . (function_declaration name: (identifier) @name))",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// No comments and the function is not the first child. An anchor after a
|
||||
// zero-matched quantifier is vacuous, so the function still matches.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
class X {}
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("name", "foo")])],
|
||||
);
|
||||
|
||||
// With at least one comment the anchor applies, so the comments must
|
||||
// immediately precede the function.
|
||||
assert_query_matches(
|
||||
&language,
|
||||
&query,
|
||||
"
|
||||
// c
|
||||
function foo() {}
|
||||
",
|
||||
&[(0, vec![("doc", "// c"), ("name", "foo")])],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_matches_with_last_named_child() {
|
||||
allocations::record(|| {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ typedef struct {
|
|||
* - `is_pass_through` - Indicates that state has no matching logic of its own,
|
||||
* and exists only to split a state. One copy of the state advances immediately
|
||||
* to the next step, and one moves to the alternative step.
|
||||
* - `alternative_is_skip` - Indicates that this step's `alternative_index` is the
|
||||
* forward skip introduced by a `?` or `*` quantifier (the branch taken when the
|
||||
* quantifier matches zero occurrences). For a state that follows it, an
|
||||
* immediately-following anchor is vacuous.
|
||||
* - `is_inside_alternation` - Indicates that state is inside an alternation.
|
||||
* Currently only written to quantifier steps, read by logic that maintains
|
||||
* correctness for quantifiers inside alternations.
|
||||
|
|
@ -115,6 +119,7 @@ typedef struct {
|
|||
bool root_pattern_guaranteed: 1;
|
||||
bool parent_pattern_guaranteed: 1;
|
||||
bool is_missing: 1;
|
||||
bool alternative_is_skip: 1;
|
||||
} QueryStep;
|
||||
|
||||
/*
|
||||
|
|
@ -210,6 +215,7 @@ typedef struct {
|
|||
bool has_in_progress_alternatives: 1;
|
||||
bool dead: 1;
|
||||
bool needs_parent: 1;
|
||||
bool skipped_quantifier: 1;
|
||||
} QueryState;
|
||||
|
||||
typedef Array(QueryState) QueryStateList;
|
||||
|
|
@ -2992,6 +2998,7 @@ static TSQueryError ts_query__parse_pattern(
|
|||
step = array_get(&self->steps, step->alternative_index);
|
||||
}
|
||||
step->alternative_index = self->steps.size;
|
||||
step->alternative_is_skip = true;
|
||||
break;
|
||||
case TSQuantifierZeroOrOne:
|
||||
step = array_get(&self->steps, starting_step_index);
|
||||
|
|
@ -2999,6 +3006,7 @@ static TSQueryError ts_query__parse_pattern(
|
|||
step = array_get(&self->steps, step->alternative_index);
|
||||
}
|
||||
step->alternative_index = self->steps.size;
|
||||
step->alternative_is_skip = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -3827,6 +3835,7 @@ static void ts_query_cursor__add_state(
|
|||
.has_in_progress_alternatives = false,
|
||||
.needs_parent = step->depth == 1,
|
||||
.dead = false,
|
||||
.skipped_quantifier = false,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -4267,7 +4276,7 @@ static inline bool ts_query_cursor__advance(
|
|||
node_does_match = symbol == step->symbol && (!step->is_missing || is_missing);
|
||||
}
|
||||
bool later_sibling_can_match = has_later_siblings;
|
||||
if ((step->is_immediate && is_named) || state->seeking_immediate_match) {
|
||||
if ((step->is_immediate && is_named && !state->skipped_quantifier) || state->seeking_immediate_match) {
|
||||
later_sibling_can_match = false;
|
||||
}
|
||||
if (step->is_last_child && has_later_named_siblings) {
|
||||
|
|
@ -4410,6 +4419,9 @@ static inline bool ts_query_cursor__advance(
|
|||
} else {
|
||||
state->seeking_immediate_match = false;
|
||||
}
|
||||
// The zero-skip's vacuous-anchor exemption only covers the immediate
|
||||
// step it lands on. Once the state advances, a later anchor is normal.
|
||||
state->skipped_quantifier = false;
|
||||
|
||||
if (stop_on_definite_step && next_step->root_pattern_guaranteed) did_match = true;
|
||||
|
||||
|
|
@ -4454,6 +4466,11 @@ static inline bool ts_query_cursor__advance(
|
|||
if (child_step->is_pass_through) {
|
||||
copy->seeking_immediate_match = true;
|
||||
}
|
||||
// Taking a `?`/`*` zero-skip means the quantified subpattern matched
|
||||
// nothing, so an immediately-following anchor is vacuous for this copy.
|
||||
if (child_step->alternative_is_skip) {
|
||||
copy->skipped_quantifier = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue