-
Notifications
You must be signed in to change notification settings - Fork 901
/
suppression_comment_visitor.rs
217 lines (192 loc) · 7.9 KB
/
suppression_comment_visitor.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use std::iter::Peekable;
use ruff_python_ast::{
helpers::comment_indentation_after,
visitor::preorder::{self, PreorderVisitor, TraversalSignal},
AnyNodeRef, Suite,
};
use ruff_python_trivia::{
indentation_at_offset, CommentLinePosition, SimpleTokenizer, SuppressionKind,
};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
#[derive(Clone, Copy, Debug)]
pub(super) struct SuppressionComment {
pub(super) range: TextRange,
pub(super) kind: SuppressionKind,
}
/// Visitor that captures AST data for suppression comments. This uses a similar approach
/// to `CommentsVisitor` in the formatter crate.
pub(super) struct SuppressionCommentVisitor<
'src,
'builder,
I: Iterator<Item = SuppressionComment> + 'src,
> {
comments: Peekable<I>,
parents: Vec<AnyNodeRef<'src>>,
preceding_node: Option<AnyNodeRef<'src>>,
builder: &'builder mut (dyn CaptureSuppressionComment<'src> + 'src),
locator: &'src Locator<'src>,
}
impl<'src, 'builder, I> SuppressionCommentVisitor<'src, 'builder, I>
where
I: Iterator<Item = SuppressionComment> + 'src,
{
pub(super) fn new(
comment_ranges: I,
builder: &'builder mut (dyn CaptureSuppressionComment<'src> + 'src),
locator: &'src Locator<'src>,
) -> Self {
Self {
comments: comment_ranges.peekable(),
parents: Vec::default(),
preceding_node: Option::default(),
builder,
locator,
}
}
pub(super) fn visit(mut self, suite: &'src Suite) {
self.visit_body(suite.as_slice());
}
fn can_skip(&mut self, node_end: TextSize) -> bool {
self.comments
.peek()
.map_or(true, |next| next.range.start() >= node_end)
}
}
impl<'ast, I> PreorderVisitor<'ast> for SuppressionCommentVisitor<'ast, '_, I>
where
I: Iterator<Item = SuppressionComment> + 'ast,
{
fn enter_node(&mut self, node: AnyNodeRef<'ast>) -> TraversalSignal {
let node_range = node.range();
let enclosing_node = self.parents.last().copied();
// Process all remaining comments that end before this node's start position.
// If the `preceding` node is set, then it process all comments ending after the `preceding` node
// and ending before this node's start position
while let Some(SuppressionComment { range, kind }) = self.comments.peek().copied() {
// Exit if the comment is enclosed by this node or comes after it
if range.end() > node_range.start() {
break;
}
let line_position = CommentLinePosition::for_range(range, self.locator.contents());
let data = SuppressionCommentData {
enclosing: enclosing_node,
preceding: self.preceding_node,
following: Some(node),
line_position,
kind,
range,
};
self.builder.capture(data);
self.comments.next();
}
// From here on, we're inside of `node`, meaning, we're passed the preceding node.
self.preceding_node = None;
self.parents.push(node);
if self.can_skip(node_range.end()) {
TraversalSignal::Skip
} else {
TraversalSignal::Traverse
}
}
fn leave_node(&mut self, node: AnyNodeRef<'ast>) {
self.parents.pop();
let node_end = node.end();
// Process all comments that start after the `preceding` node and end before this node's end.
while let Some(SuppressionComment { range, kind }) = self.comments.peek().copied() {
let line_position = CommentLinePosition::for_range(range, self.locator.contents());
if range.start() >= node_end {
let between = TextRange::new(node_end, range.start());
// Check if a non-trivial token exists between the end of this node and the start of the comment.
// If it doesn't, that means this comment could possibly be a trailing comment that comes after the
// end of this node.
// For example:
// ```
// def func(x):
// pass # fmt: skip
// ```
// We want to make sure that `# fmt: skip` is associated with the `pass` statement,
// even though it comes after the end of that node.
if SimpleTokenizer::new(self.locator.contents(), between)
.skip_trivia()
.next()
.is_some()
{
break;
}
// If the comment is on its own line, it could still be a trailing comment if it has a greater
// level of indentation compared to this node. For example:
// ```
// def func(x):
// # fmt: off
// pass
// # fmt: on
// def func2(y):
// pass
// ```
// We want `# fmt: on` to be considered a trailing comment of `func(x)` instead of a leading comment
// on `func2(y)`.
if line_position.is_own_line() {
let comment_indent = comment_indentation_after(node, range, self.locator);
let node_indent = TextSize::of(
indentation_at_offset(node.start(), self.locator).unwrap_or_default(),
);
if node_indent >= comment_indent {
break;
}
}
}
let data = SuppressionCommentData {
enclosing: Some(node),
preceding: self.preceding_node,
following: None,
line_position,
kind,
range,
};
self.builder.capture(data);
self.comments.next();
}
self.preceding_node = Some(node);
}
fn visit_body(&mut self, body: &'ast [ruff_python_ast::Stmt]) {
match body {
[] => {
// no-op
}
[only] => {
self.visit_stmt(only);
}
[first, .., last] => {
if self.can_skip(last.end()) {
// Skip traversing the body when there's no comment between the first and last statement.
// It is still necessary to visit the first statement to process all comments between
// the previous node and the first statement.
self.visit_stmt(first);
self.preceding_node = Some(last.into());
} else {
preorder::walk_body(self, body);
}
}
}
}
}
#[derive(Clone, Debug)]
pub(super) struct SuppressionCommentData<'src> {
/// The AST node that encloses the comment. If `enclosing` is `None`, this comment is a top-level statement.
pub(super) enclosing: Option<AnyNodeRef<'src>>,
/// An AST node that comes directly before the comment. A child of `enclosing`.
pub(super) preceding: Option<AnyNodeRef<'src>>,
/// An AST node that comes directly after the comment. A child of `enclosing`.
pub(super) following: Option<AnyNodeRef<'src>>,
/// The line position of the comment - it can either be on its own line, or at the end of a line.
pub(super) line_position: CommentLinePosition,
/// Whether this comment is `fmt: off`, `fmt: on`, or `fmt: skip` (or `yapf disable` / `yapf enable`)
pub(super) kind: SuppressionKind,
/// The range of text that makes up the comment. This includes the `#` prefix.
pub(super) range: TextRange,
}
pub(super) trait CaptureSuppressionComment<'src> {
/// This is the entrypoint for the capturer to analyze the next comment.
fn capture(&mut self, comment: SuppressionCommentData<'src>);
}