ruff_python_trivia/
comment_ranges.rs1use std::fmt::{Debug, Formatter};
2use std::ops::Deref;
3
4use itertools::Itertools;
5use rustc_hash::FxHashSet;
6
7use ruff_source_file::LineRanges;
8use ruff_text_size::{Ranged, TextRange, TextSize};
9
10use crate::{has_leading_content, has_trailing_content, is_python_whitespace};
11
12#[derive(Clone, Default)]
14pub struct TriviaRanges {
15 comments: CommentRanges,
16 parenthesized_expressions: ParenthesizedExpressions,
17}
18
19impl TriviaRanges {
20 pub fn new(
22 comments: CommentRanges,
23 parenthesized_expressions: ParenthesizedExpressions,
24 ) -> Self {
25 Self {
26 comments,
27 parenthesized_expressions,
28 }
29 }
30
31 pub fn comments(&self) -> &CommentRanges {
33 &self.comments
34 }
35
36 pub fn parenthesized(&self) -> &ParenthesizedExpressions {
38 &self.parenthesized_expressions
39 }
40}
41
42impl Debug for TriviaRanges {
43 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
44 self.comments.fmt(f)
45 }
46}
47
48#[derive(Clone, Default)]
50pub struct ParenthesizedExpressions {
51 ranges: FxHashSet<TextRange>,
52}
53
54impl ParenthesizedExpressions {
55 pub fn new(ranges: FxHashSet<TextRange>) -> Self {
57 Self { ranges }
58 }
59
60 pub fn contains(&self, range: TextRange) -> bool {
62 self.ranges.contains(&range)
63 }
64}
65
66#[derive(Clone, Default)]
69pub struct CommentRanges {
70 raw: Vec<TextRange>,
71}
72
73impl CommentRanges {
74 pub fn new(ranges: Vec<TextRange>) -> Self {
75 Self { raw: ranges }
76 }
77
78 pub fn intersects(&self, target: TextRange) -> bool {
80 self.raw
81 .binary_search_by(|range| {
82 if target.intersect(*range).is_some() {
83 std::cmp::Ordering::Equal
84 } else if range.end() < target.start() {
85 std::cmp::Ordering::Less
86 } else {
87 std::cmp::Ordering::Greater
88 }
89 })
90 .is_ok()
91 }
92
93 pub fn comments_in_range(&self, range: TextRange) -> &[TextRange] {
95 let start = self
96 .raw
97 .partition_point(|comment| comment.start() < range.start());
98 match self.raw[start..]
100 .iter()
101 .find_position(|comment| comment.end() > range.end())
102 {
103 Some((in_range, _element)) => &self.raw[start..start + in_range],
104 None => &self.raw[start..],
105 }
106 }
107
108 pub fn has_comments<T>(&self, node: &T, source: &str) -> bool
110 where
111 T: Ranged,
112 {
113 let start = if has_leading_content(node.start(), source) {
114 node.start()
115 } else {
116 source.line_start(node.start())
117 };
118 let end = if has_trailing_content(node.end(), source) {
119 node.end()
120 } else {
121 source.line_end(node.end())
122 };
123
124 self.intersects(TextRange::new(start, end))
125 }
126
127 pub fn block_comments(&self, source: &str) -> Vec<TextSize> {
158 let mut block_comments: Vec<TextSize> = Vec::new();
159
160 let mut current_block: Vec<TextSize> = Vec::new();
161 let mut current_block_column: Option<TextSize> = None;
162 let mut current_block_non_empty = false;
163
164 let mut prev_line_end = None;
165
166 for comment_range in &self.raw {
167 let offset = comment_range.start();
168 let line_start = source.line_start(offset);
169 let line_end = source.full_line_end(offset);
170 let column = offset - line_start;
171
172 if !Self::is_own_line(offset, source) {
174 if current_block.len() > 1 && current_block_non_empty {
176 block_comments.extend(current_block);
177 }
178 current_block = vec![];
179 current_block_column = None;
180 current_block_non_empty = false;
181 prev_line_end = Some(line_end);
182 continue;
183 }
184
185 if prev_line_end.is_some_and(|prev_line_end| {
188 source.contains_line_break(TextRange::new(prev_line_end, line_start))
189 }) {
190 if current_block.len() > 1 && current_block_non_empty {
192 block_comments.extend(current_block);
193 }
194
195 current_block = vec![offset];
197 current_block_column = Some(column);
198 current_block_non_empty = !Self::is_empty(*comment_range, source);
199 prev_line_end = Some(line_end);
200 continue;
201 }
202
203 if let Some(current_column) = current_block_column {
204 if column == current_column {
205 current_block.push(offset);
207 current_block_non_empty |= !Self::is_empty(*comment_range, source);
208 prev_line_end = Some(line_end);
209 } else {
210 if current_block.len() > 1 && current_block_non_empty {
212 block_comments.extend(current_block);
213 }
214
215 current_block = vec![offset];
217 current_block_column = Some(column);
218 current_block_non_empty = !Self::is_empty(*comment_range, source);
219 prev_line_end = Some(line_end);
220 }
221 } else {
222 if current_block.len() > 1 && current_block_non_empty {
224 block_comments.extend(current_block);
225 }
226
227 current_block = vec![offset];
229 current_block_column = Some(column);
230 current_block_non_empty = !Self::is_empty(*comment_range, source);
231 prev_line_end = Some(line_end);
232 }
233 }
234
235 if current_block.len() > 1 && current_block_non_empty {
237 block_comments.extend(current_block);
238 }
239
240 block_comments
241 }
242
243 fn is_empty(range: TextRange, source: &str) -> bool {
245 source[range].chars().skip(1).all(is_python_whitespace)
246 }
247
248 pub fn is_own_line(offset: TextSize, source: &str) -> bool {
250 let range = TextRange::new(source.line_start(offset), offset);
251 source[range].chars().all(is_python_whitespace)
252 }
253}
254
255impl Deref for CommentRanges {
256 type Target = [TextRange];
257
258 fn deref(&self) -> &Self::Target {
259 self.raw.as_slice()
260 }
261}
262
263impl Debug for CommentRanges {
264 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265 f.debug_tuple("CommentRanges").field(&self.raw).finish()
266 }
267}
268
269impl<'a> IntoIterator for &'a CommentRanges {
270 type Item = TextRange;
271 type IntoIter = std::iter::Copied<std::slice::Iter<'a, TextRange>>;
272
273 fn into_iter(self) -> Self::IntoIter {
274 self.raw.iter().copied()
275 }
276}