/rust/registry/src/index.crates.io-1949cf8c6b5b557f/oxc_ast-0.155.0/src/trivia.rs
Line | Count | Source |
1 | | //! Trivia such as comments and irregular whitespaces |
2 | | //! |
3 | | //! This module provides utilities for working with source code comments, |
4 | | //! including efficient range-based queries and iteration over comment collections. |
5 | | |
6 | | use std::{ |
7 | | cmp::Ordering, |
8 | | iter::FusedIterator, |
9 | | ops::{Bound, RangeBounds}, |
10 | | }; |
11 | | |
12 | | use oxc_span::Span; |
13 | | |
14 | | use crate::ast::comment::*; |
15 | | |
16 | | /// Create an iterator over comments within a given range |
17 | | /// |
18 | | /// Returns a double-ended iterator that yields comments whose starting positions |
19 | | /// fall within the specified range bounds. |
20 | | /// |
21 | | /// # Arguments |
22 | | /// |
23 | | /// * `comments` - A slice of comments sorted by starting position |
24 | | /// * `range` - The range of positions to filter comments by |
25 | | /// |
26 | | /// # Examples |
27 | | /// |
28 | | /// ```ignore |
29 | | /// let comments_in_range = comments_range(&all_comments, 10..50); |
30 | | /// for comment in comments_in_range { |
31 | | /// // Process comments starting between positions 10 and 50 |
32 | | /// } |
33 | | /// ``` |
34 | 0 | pub fn comments_range<R>(comments: &[Comment], range: R) -> CommentsRange<'_> |
35 | 0 | where |
36 | 0 | R: RangeBounds<u32>, |
37 | | { |
38 | 0 | CommentsRange::new(comments, range.start_bound().cloned(), range.end_bound().cloned()) |
39 | 0 | } |
40 | | |
41 | | /// Check if there are any comments within a given span |
42 | | /// |
43 | | /// Returns `true` if at least one comment starts within the specified span. |
44 | | /// |
45 | | /// # Arguments |
46 | | /// |
47 | | /// * `comments` - A slice of comments sorted by starting position |
48 | | /// * `span` - The span to check for comments |
49 | | /// |
50 | | /// # Examples |
51 | | /// |
52 | | /// ```ignore |
53 | | /// if has_comments_between(&comments, node.span) { |
54 | | /// // Handle nodes that have comments within their span |
55 | | /// } |
56 | | /// ``` |
57 | 0 | pub fn has_comments_between(comments: &[Comment], span: Span) -> bool { |
58 | 0 | comments_range(comments, span.start..span.end).count() > 0 |
59 | 0 | } |
60 | | |
61 | | /// Check if a position falls within any comment |
62 | | /// |
63 | | /// Returns `true` if the specified position is inside any comment's span. |
64 | | /// The start position is included, but the end position is excluded. |
65 | | /// |
66 | | /// Uses binary search for efficient lookup in O(log n) time. |
67 | | /// |
68 | | /// # Arguments |
69 | | /// |
70 | | /// * `comments` - A slice of comments sorted by starting position |
71 | | /// * `pos` - The position to check |
72 | | /// |
73 | | /// # Examples |
74 | | /// |
75 | | /// ```ignore |
76 | | /// // Comment spans from position 10 to 20 |
77 | | /// assert!(is_inside_comment(&comments, 15)); // Inside comment |
78 | | /// assert!(!is_inside_comment(&comments, 25)); // Outside comment |
79 | | /// ``` |
80 | 0 | pub fn is_inside_comment(comments: &[Comment], pos: u32) -> bool { |
81 | 0 | get_comment_at(comments, pos).is_some() |
82 | 0 | } |
83 | | |
84 | | /// Get the comment containing a position, if any |
85 | | /// |
86 | | /// Returns a reference to the comment if the specified position is inside any |
87 | | /// comment's span. The start position is included, but the end position is excluded. |
88 | | /// |
89 | | /// Uses binary search for efficient lookup in O(log n) time. |
90 | | /// |
91 | | /// # Arguments |
92 | | /// |
93 | | /// * `comments` - A slice of comments sorted by starting position |
94 | | /// * `pos` - The position to check |
95 | | /// |
96 | | /// # Examples |
97 | | /// |
98 | | /// ```ignore |
99 | | /// // Comment spans from position 10 to 20 |
100 | | /// if let Some(comment) = get_comment_at(&comments, 15) { |
101 | | /// // Position is inside this comment |
102 | | /// println!("Comment spans {:?}", comment.span); |
103 | | /// } |
104 | | /// ``` |
105 | 0 | pub fn get_comment_at(comments: &[Comment], pos: u32) -> Option<&Comment> { |
106 | 0 | comments |
107 | 0 | .binary_search_by(|c| { |
108 | 0 | if pos < c.span.start { |
109 | 0 | Ordering::Greater |
110 | 0 | } else if pos >= c.span.end { |
111 | 0 | Ordering::Less |
112 | | } else { |
113 | 0 | Ordering::Equal |
114 | | } |
115 | 0 | }) |
116 | 0 | .ok() |
117 | 0 | .map(|idx| &comments[idx]) |
118 | 0 | } |
119 | | |
120 | | /// Double-ended iterator over a range of comments, by starting position |
121 | | /// |
122 | | /// This iterator efficiently filters comments based on their starting positions, |
123 | | /// using binary search to skip comments outside the range. It supports both |
124 | | /// forward and backward iteration. |
125 | | /// |
126 | | /// The iterator is created using [`comments_range`] and yields references to |
127 | | /// comments whose `span.start` falls within the specified range bounds. |
128 | | pub struct CommentsRange<'c> { |
129 | | comments: &'c [Comment], |
130 | | range: (Bound<u32>, Bound<u32>), |
131 | | current_start: usize, |
132 | | current_end: usize, |
133 | | } |
134 | | |
135 | | impl<'c> CommentsRange<'c> { |
136 | | /// Create a new iterator over comments in the specified range |
137 | | /// |
138 | | /// Uses `partition_point` to efficiently skip comments outside the range, |
139 | | /// avoiding unnecessary iteration over comments that won't be yielded. |
140 | 0 | fn new(comments: &'c [Comment], start: Bound<u32>, end: Bound<u32>) -> Self { |
141 | | // Directly skip all comments that are already known to start |
142 | | // outside the requested range. |
143 | 0 | let partition_start = { |
144 | 0 | let range_start = match start { |
145 | 0 | Bound::Unbounded => 0, |
146 | 0 | Bound::Included(x) => x, |
147 | 0 | Bound::Excluded(x) => x.saturating_add(1), |
148 | | }; |
149 | 0 | comments.partition_point(|comment| comment.span.start < range_start) |
150 | | }; |
151 | 0 | let partition_end = { |
152 | 0 | let range_end = match end { |
153 | 0 | Bound::Unbounded => u32::MAX, |
154 | 0 | Bound::Included(x) => x, |
155 | 0 | Bound::Excluded(x) => x.saturating_sub(1), |
156 | | }; |
157 | 0 | comments.partition_point(|comment| comment.span.start <= range_end) |
158 | | }; |
159 | 0 | Self { |
160 | 0 | comments, |
161 | 0 | range: (start, end), |
162 | 0 | current_start: partition_start, |
163 | 0 | current_end: partition_end, |
164 | 0 | } |
165 | 0 | } |
166 | | } |
167 | | |
168 | | impl<'c> Iterator for CommentsRange<'c> { |
169 | | type Item = &'c Comment; |
170 | | |
171 | 0 | fn next(&mut self) -> Option<Self::Item> { |
172 | 0 | if self.current_start < self.current_end { |
173 | 0 | for comment in &self.comments[self.current_start..self.current_end] { |
174 | 0 | self.current_start = self.current_start.saturating_add(1); |
175 | 0 | if self.range.contains(&comment.span.start) { |
176 | 0 | return Some(comment); |
177 | 0 | } |
178 | | } |
179 | 0 | } |
180 | 0 | None |
181 | 0 | } |
182 | | |
183 | 0 | fn size_hint(&self) -> (usize, Option<usize>) { |
184 | 0 | let max_remaining = self.current_end.saturating_sub(self.current_start); |
185 | 0 | (0, Some(max_remaining)) |
186 | 0 | } |
187 | | } |
188 | | |
189 | | impl DoubleEndedIterator for CommentsRange<'_> { |
190 | 0 | fn next_back(&mut self) -> Option<Self::Item> { |
191 | 0 | if self.current_start < self.current_end { |
192 | 0 | for comment in self.comments[self.current_start..self.current_end].iter().rev() { |
193 | 0 | self.current_end = self.current_end.saturating_sub(1); |
194 | 0 | if self.range.contains(&comment.span.start) { |
195 | 0 | return Some(comment); |
196 | 0 | } |
197 | | } |
198 | 0 | } |
199 | 0 | None |
200 | 0 | } |
201 | | } |
202 | | |
203 | | impl FusedIterator for CommentsRange<'_> {} |
204 | | |
205 | | #[cfg(test)] |
206 | | mod test { |
207 | | use super::*; |
208 | | |
209 | | #[test] |
210 | | fn test_comments_range() { |
211 | | let comments = vec![ |
212 | | Comment::new(0, 4, CommentKind::Line), |
213 | | Comment::new(5, 9, CommentKind::Line), |
214 | | Comment::new(10, 13, CommentKind::Line), |
215 | | Comment::new(14, 17, CommentKind::Line), |
216 | | Comment::new(18, 23, CommentKind::Line), |
217 | | ] |
218 | | .into_boxed_slice(); |
219 | | let full_len = comments.len(); |
220 | | assert_eq!(comments_range(&comments, ..).count(), full_len); |
221 | | assert_eq!(comments_range(&comments, 1..).count(), full_len.saturating_sub(1)); |
222 | | assert_eq!(comments_range(&comments, ..18).count(), full_len.saturating_sub(1)); |
223 | | assert_eq!(comments_range(&comments, ..=18).count(), full_len); |
224 | | } |
225 | | |
226 | | #[test] |
227 | | fn test_is_inside_comment() { |
228 | | let comments = vec![ |
229 | | Comment::new(0, 4, CommentKind::Line), |
230 | | Comment::new(10, 20, CommentKind::SingleLineBlock), |
231 | | ] |
232 | | .into_boxed_slice(); |
233 | | |
234 | | assert!(is_inside_comment(&comments, 2)); |
235 | | assert!(!is_inside_comment(&comments, 5)); |
236 | | assert!(is_inside_comment(&comments, 15)); |
237 | | assert!(!is_inside_comment(&comments, 21)); |
238 | | } |
239 | | |
240 | | #[test] |
241 | | fn test_get_comment_at() { |
242 | | let comments = vec![ |
243 | | Comment::new(0, 4, CommentKind::Line), |
244 | | Comment::new(10, 20, CommentKind::SingleLineBlock), |
245 | | ] |
246 | | .into_boxed_slice(); |
247 | | |
248 | | // Inside first comment |
249 | | let comment = get_comment_at(&comments, 2); |
250 | | let comment = comment.expect("Expected a comment at position 2"); |
251 | | assert_eq!(comment.span.start, 0); |
252 | | assert_eq!(comment.span.end, 4); |
253 | | |
254 | | // Between comments |
255 | | assert!(get_comment_at(&comments, 5).is_none()); |
256 | | |
257 | | // Inside second comment |
258 | | let comment = get_comment_at(&comments, 15); |
259 | | let comment = comment.expect("Expected a comment at position 15"); |
260 | | assert_eq!(comment.span.start, 10); |
261 | | assert_eq!(comment.span.end, 20); |
262 | | |
263 | | // After all comments |
264 | | assert!(get_comment_at(&comments, 21).is_none()); |
265 | | |
266 | | // Boundary cases: start positions are included |
267 | | assert!(get_comment_at(&comments, 0).is_some()); // Start of first comment |
268 | | assert!(get_comment_at(&comments, 10).is_some()); // Start of second comment |
269 | | |
270 | | // Boundary cases: end positions are excluded |
271 | | assert!(get_comment_at(&comments, 4).is_none()); // End of first comment |
272 | | assert!(get_comment_at(&comments, 20).is_none()); // End of second comment |
273 | | } |
274 | | } |