Skip to content

Commit 1ecdc36

Browse files
committed
Yeast: Fix matching against extras like comments
1 parent e3b3888 commit 1ecdc36

2 files changed

Lines changed: 47 additions & 5 deletions

File tree

shared/yeast/src/query.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,15 @@ impl QueryListElem {
178178
let Some(child) = remaining_children.next() else {
179179
return Ok(false);
180180
};
181-
if skip_unnamed {
182-
let node = ast.get_node(child).unwrap();
183-
if !node.is_named() {
184-
continue;
185-
}
181+
let node = ast.get_node(child).unwrap();
182+
// Skip tree-sitter `extras` (e.g. comments) during
183+
// positional matching: they are conceptually invisible
184+
// between siblings, mirroring tree-sitter query semantics.
185+
if node.is_extra() {
186+
continue;
187+
}
188+
if skip_unnamed && !node.is_named() {
189+
continue;
186190
}
187191
let snapshot = matches.clone();
188192
if sub_query.do_match(ast, child, matches)? {

shared/yeast/tests/test.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,44 @@ fn test_query_no_match() {
274274
assert!(!matched);
275275
}
276276

277+
#[test]
278+
fn test_query_skips_extras_in_positional_match() {
279+
// Regression test: positional wildcards `(_)` must not bind to
280+
// tree-sitter `extras` (e.g. comments) during forward-scan; extras
281+
// are conceptually invisible between siblings, matching tree-sitter
282+
// query semantics. Without this, a later rule that translates a
283+
// captured comment to nothing (a common idiom, e.g.
284+
// `(comment) => ()` in Swift) leaves the capture's match-list empty
285+
// and causes the transform to fail with "Variable X has 0 matches".
286+
let runner = Runner::new(tree_sitter_ruby::LANGUAGE.into(), &[]);
287+
let ast = runner.run("[1, # comment\n2]").unwrap();
288+
289+
// Navigate to the `array` node: program -> array.
290+
let mut cursor = AstCursor::new(&ast);
291+
cursor.goto_first_child();
292+
let array_id = cursor.node_id();
293+
assert_eq!(ast.get_node(array_id).unwrap().kind(), "array");
294+
295+
// Two positional wildcards should bind to the two integers, skipping
296+
// the comment that sits between them.
297+
let query = yeast::query!((array (_) @a (_) @b));
298+
let mut captures = yeast::captures::Captures::new();
299+
let matched = query.do_match(&ast, array_id, &mut captures).unwrap();
300+
assert!(matched);
301+
assert_eq!(
302+
ast.get_node(captures.get_var("a").unwrap())
303+
.unwrap()
304+
.kind(),
305+
"integer"
306+
);
307+
assert_eq!(
308+
ast.get_node(captures.get_var("b").unwrap())
309+
.unwrap()
310+
.kind(),
311+
"integer"
312+
);
313+
}
314+
277315
#[test]
278316
fn test_reachable_nodes_excludes_orphaned_rewrite_nodes() {
279317
let lang: tree_sitter::Language = tree_sitter_ruby::LANGUAGE.into();

0 commit comments

Comments
 (0)