From 5ddf71b9f4a7a88a194e45148b7eabe99fb55090 Mon Sep 17 00:00:00 2001 From: Will Lillis Date: Sat, 29 Aug 2026 17:44:18 -0400 Subject: [PATCH] fix(rust)!: expose `QueryMatch::captures` as a method The field handed out a slice whose lifetime outlived the streaming-iterator loan it came from. An accessor reborrowing through `&self` bounds it correctly. BREAKING CHANGE: In the rust bindings, replace `m.captures` with `m.captures()`. --- crates/cli/src/query.rs | 4 +- crates/cli/src/tests/helpers/query_helpers.rs | 4 +- crates/cli/src/tests/query_test.rs | 40 +++++++++---------- crates/cli/src/tests/text_provider_test.rs | 2 +- crates/highlight/src/highlight.rs | 14 +++---- crates/tags/src/tags.rs | 4 +- lib/binding_rust/lib.rs | 7 +++- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/crates/cli/src/query.rs b/crates/cli/src/query.rs index 258fa8557..fa14ebe16 100644 --- a/crates/cli/src/query.rs +++ b/crates/cli/src/query.rs @@ -75,7 +75,7 @@ pub fn query_file_at_path( if opts.ordered_captures { let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice()); while let Some((mat, capture_index)) = captures.next() { - let capture = mat.captures[*capture_index]; + let capture = mat.captures()[*capture_index]; let capture_name = &query.capture_names()[capture.index as usize]; if !opts.quiet && !should_test { writeln!( @@ -102,7 +102,7 @@ pub fn query_file_at_path( if !opts.quiet && !should_test { writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; } - for capture in m.captures { + for capture in m.captures() { let start = capture.node.start_position(); let end = capture.node.end_position(); let capture_name = &query.capture_names()[capture.index as usize]; diff --git a/crates/cli/src/tests/helpers/query_helpers.rs b/crates/cli/src/tests/helpers/query_helpers.rs index a09b13985..bd348dc53 100644 --- a/crates/cli/src/tests/helpers/query_helpers.rs +++ b/crates/cli/src/tests/helpers/query_helpers.rs @@ -336,7 +336,7 @@ pub fn collect_matches<'a>( while let Some(m) = matches.next() { result.push(( m.pattern_index, - format_captures(m.captures.iter().into_streaming_iter_ref(), query, source), + format_captures(m.captures().iter().into_streaming_iter_ref(), query, source), )); } result @@ -347,7 +347,7 @@ pub fn collect_captures<'a>( query: &'a Query, source: &'a str, ) -> Vec<(&'a str, &'a str)> { - format_captures(captures.map(|(m, i)| m.captures[*i]), query, source) + format_captures(captures.map(|(m, i)| m.captures()[*i]), query, source) } fn format_captures<'a>( diff --git a/crates/cli/src/tests/query_test.rs b/crates/cli/src/tests/query_test.rs index 7056f2175..c9357e059 100644 --- a/crates/cli/src/tests/query_test.rs +++ b/crates/cli/src/tests/query_test.rs @@ -2918,7 +2918,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() { ); while let Some(mat) = match_iter.next() { - if let Some(capture) = mat.captures.first() { + if let Some(capture) = mat.captures().first() { matches.push(capture.node.kind()); } } @@ -2934,7 +2934,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() { ); while let Some(mat) = match_iter.next() { - if let Some(capture) = mat.captures.first() { + if let Some(capture) = mat.captures().first() { matches.push(capture.node.kind()); } } @@ -2950,7 +2950,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() { ); while let Some(mat) = match_iter.next() { - if let Some(capture) = mat.captures.first() { + if let Some(capture) = mat.captures().first() { matches.push(capture.node.kind()); } } @@ -3008,7 +3008,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() { let mut results = Vec::new(); let mut first_five = captures.by_ref().take(5); while let Some((mat, capture_ix)) = first_five.next() { - let capture = mat.captures[*capture_ix]; + let capture = mat.captures()[*capture_ix]; results.push(( query.capture_names()[capture.index as usize], &source[capture.node.byte_range()], @@ -3031,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() { results.clear(); captures.set_byte_range(source.find("Ok").unwrap()..source.len()); while let Some((mat, capture_ix)) = captures.next() { - let capture = mat.captures[*capture_ix]; + let capture = mat.captures()[*capture_ix]; results.push(( query.capture_names()[capture.index as usize], &source[capture.node.byte_range()], @@ -3330,7 +3330,7 @@ fn test_query_matches_with_captured_wildcard_at_root() { while let Some(m) = match_iter.next() { let captures = m - .captures + .captures() .iter() .map(|c| { ( @@ -4311,7 +4311,7 @@ fn test_query_captures_with_matches_removed() { let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes()); while let Some((m, i)) = captures.next() { - let capture = m.captures[*i]; + let capture = m.captures()[*i]; let text = capture.node.utf8_text(source.as_bytes()).unwrap(); if text == "a" { m.remove(); @@ -4356,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() { let mut captured_strings = Vec::new(); let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes()); while let Some((m, i)) = captures.next() { - let capture = m.captures[*i]; + let capture = m.captures()[*i]; let text = capture.node.utf8_text(source.as_bytes()).unwrap(); if text == "as" { m.remove(); @@ -4397,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() { let mut cursor = QueryCursor::new(); let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes()); - assert_eq!(captures.next().unwrap().0.captures[0].index, 0); - assert_eq!(captures.next().unwrap().0.captures[0].index, 0); - assert_eq!(captures.next().unwrap().0.captures[0].index, 0); + assert_eq!(captures.next().unwrap().0.captures()[0].index, 0); + assert_eq!(captures.next().unwrap().0.captures()[0].index, 0); + assert_eq!(captures.next().unwrap().0.captures()[0].index, 0); assert!(captures.next().is_none()); assert!(captures.next().is_none()); assert!(captures.next().is_none()); drop(captures); let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes()); - assert_eq!(matches.next().unwrap().captures[0].index, 0); - assert_eq!(matches.next().unwrap().captures[0].index, 0); - assert_eq!(matches.next().unwrap().captures[0].index, 0); + assert_eq!(matches.next().unwrap().captures()[0].index, 0); + assert_eq!(matches.next().unwrap().captures()[0].index, 0); + assert_eq!(matches.next().unwrap().captures()[0].index, 0); assert!(matches.next().is_none()); assert!(matches.next().is_none()); assert!(matches.next().is_none()); @@ -4588,7 +4588,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() { .matches(&query, node, source.as_bytes()) .next() .unwrap() - .captures[0] + .captures()[0] .node } @@ -4609,7 +4609,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() { .next() .unwrap() .0 - .captures[0] + .captures()[0] .node } @@ -4819,7 +4819,7 @@ fn test_query_random() { let transformed_match = Match { last_node: None, captures: mat - .captures + .captures() .iter() .map(|c| (query.capture_names()[c.index as usize], c.node)) .collect::>(), @@ -5833,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() { let mut len_1 = false; while let Some(m) = matches.next() { - if m.captures.len() == 3 { + if m.captures().len() == 3 { len_3 = true; } - if m.captures.len() == 1 { + if m.captures().len() == 1 { len_1 = true; } } @@ -5898,7 +5898,7 @@ fn test_query_max_start_depth_more() { let query = Query::new(&language, "(compound_statement) @capture").unwrap(); let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes()); - let node = matches.next().unwrap().captures[0].node; + let node = matches.next().unwrap().captures()[0].node; assert_eq!(node.kind(), "compound_statement"); for row in rows { diff --git a/crates/cli/src/tests/text_provider_test.rs b/crates/cli/src/tests/text_provider_test.rs index d103a9140..ee01b22c3 100644 --- a/crates/cli/src/tests/text_provider_test.rs +++ b/crates/cli/src/tests/text_provider_test.rs @@ -30,7 +30,7 @@ fn tree_query>(tree: &Tree, text: impl TextProvider, language: let mut cursor = QueryCursor::new(); let mut captures = cursor.captures(&query, tree.root_node(), text); let (match_, idx) = captures.next().unwrap(); - let capture = match_.captures[*idx]; + let capture = match_.captures()[*idx]; assert_eq!(capture.index as usize, *idx); assert_eq!("comment", capture.node.kind()); } diff --git a/crates/highlight/src/highlight.rs b/crates/highlight/src/highlight.rs index 804019a3f..6a583c407 100644 --- a/crates/highlight/src/highlight.rs +++ b/crates/highlight/src/highlight.rs @@ -800,7 +800,7 @@ impl<'a> HighlightIterLayer<'a> { let next_start = self .captures .peek() - .map(|(m, i)| m.captures[*i].node.start_byte()); + .map(|(m, i)| m.captures()[*i].node.start_byte()); let next_end = self.highlight_end_stack.last().copied(); match (next_start, next_end) { (Some(start), Some(end)) => { @@ -926,7 +926,7 @@ where let range; let layer = &mut self.layers[0]; if let Some((next_match, capture_index)) = layer.captures.peek() { - let next_capture = next_match.captures[*capture_index]; + let next_capture = next_match.captures()[*capture_index]; range = next_capture.node.byte_range(); // If any previous highlight ends before this node starts, then before @@ -950,7 +950,7 @@ where } let (mut match_, capture_index) = layer.captures.next().unwrap(); - let mut capture = match_.captures[capture_index]; + let mut capture = match_.captures()[capture_index]; // If this capture represents an injection, then process the injection. if match_.pattern_index < layer.config.locals_pattern_index { @@ -1037,7 +1037,7 @@ where let scope = layer.scope_stack.last_mut().unwrap(); let mut value_range = 0..0; - for capture in match_.captures { + for capture in match_.captures() { if Some(capture.index) == layer.config.local_def_value_capture_index { value_range = capture.node.byte_range(); } @@ -1080,7 +1080,7 @@ where // Continue processing any additional matches for the same node. if let Some((next_match, next_capture_index)) = layer.captures.peek() { - let next_capture = next_match.captures[*next_capture_index]; + let next_capture = next_match.captures()[*next_capture_index]; if next_capture.node == capture.node { capture = next_capture; match_ = layer.captures.next().unwrap().0; @@ -1110,7 +1110,7 @@ where // captures are guaranteed to be for highlighting, not injections or // local variables. while let Some((next_match, next_capture_index)) = layer.captures.peek() { - let next_capture = next_match.captures[*next_capture_index]; + let next_capture = next_match.captures()[*next_capture_index]; if next_capture.node == capture.node { let following_match = layer.captures.next().unwrap().0; // If the current node was found to be a local variable, then ignore @@ -1323,7 +1323,7 @@ fn injection_for_match<'a>( let mut language_name = None; let mut content_node = None; - for capture in query_match.captures { + for capture in query_match.captures() { let index = Some(capture.index); if index == language_capture_index { language_name = capture.node.utf8_text(source).ok(); diff --git a/crates/tags/src/tags.rs b/crates/tags/src/tags.rs index 3d7f4eddb..34d14a5fc 100644 --- a/crates/tags/src/tags.rs +++ b/crates/tags/src/tags.rs @@ -375,7 +375,7 @@ where let pattern_info = &self.config.pattern_info[mat.pattern_index]; if mat.pattern_index < self.config.tags_pattern_index { - for capture in mat.captures { + for capture in mat.captures() { let index = Some(capture.index); let range = capture.node.byte_range(); if index == self.config.local_scope_capture_index { @@ -405,7 +405,7 @@ where let mut docs_adjacent_node = None; let mut is_ignored = false; - for capture in mat.captures { + for capture in mat.captures() { let index = Some(capture.index); if index == self.config.ignore_capture_index { diff --git a/lib/binding_rust/lib.rs b/lib/binding_rust/lib.rs index 90b3b5a41..f84ac36fc 100644 --- a/lib/binding_rust/lib.rs +++ b/lib/binding_rust/lib.rs @@ -401,7 +401,7 @@ pub struct QueryPredicate { /// A match of a [`Query`] to a particular set of [`Node`]s. pub struct QueryMatch<'cursor, 'tree> { pub pattern_index: usize, - pub captures: &'cursor [QueryCapture<'tree>], + captures: &'cursor [QueryCapture<'tree>], id: u32, cursor: *mut ffi::TSQueryCursor, } @@ -3434,6 +3434,11 @@ impl<'tree> QueryMatch<'_, 'tree> { self.id } + #[must_use] + pub const fn captures(&self) -> &[QueryCapture<'tree>] { + self.captures + } + #[doc(alias = "ts_query_cursor_remove_match")] pub fn remove(&self) { unsafe { ffi::ts_query_cursor_remove_match(self.cursor, self.id) }