Coverage Report

Created: 2026-10-28 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}