rustpython_ruff_python_trivia/comments.rs
1use ruff_text_size::TextRange;
2
3use crate::{PythonWhitespace, is_python_whitespace};
4
5#[derive(Copy, Clone, Eq, PartialEq, Debug)]
6pub enum SuppressionKind {
7 /// A `fmt: off` or `yapf: disable` comment
8 Off,
9 /// A `fmt: on` or `yapf: enable` comment
10 On,
11 /// A `fmt: skip` comment
12 Skip,
13}
14
15impl SuppressionKind {
16 /// Attempts to identify the `kind` of a `comment`.
17 /// The comment string should be the full line with the comment on it.
18 pub fn from_comment(comment: &str) -> Option<Self> {
19 // Match against `# fmt: on`, `# fmt: off`, `# yapf: disable`, and `# yapf: enable`, which
20 // must be on their own lines.
21 let trimmed = comment
22 .strip_prefix('#')
23 .unwrap_or(comment)
24 .trim_whitespace();
25 if let Some(command) = trimmed.strip_prefix("fmt:") {
26 match command.trim_whitespace_start() {
27 "off" => return Some(Self::Off),
28 "on" => return Some(Self::On),
29 "skip" => return Some(Self::Skip),
30 _ => {}
31 }
32 } else if let Some(command) = trimmed.strip_prefix("yapf:") {
33 match command.trim_whitespace_start() {
34 "disable" => return Some(Self::Off),
35 "enable" => return Some(Self::On),
36 _ => {}
37 }
38 }
39
40 // Search for `# fmt: skip` comments, which can be interspersed with other comments (e.g.,
41 // `# fmt: skip # noqa: E501`).
42 for segment in comment.split('#') {
43 let trimmed = segment.trim_whitespace();
44 if let Some(command) = trimmed.strip_prefix("fmt:") {
45 if command.trim_whitespace_start() == "skip" {
46 return Some(SuppressionKind::Skip);
47 }
48 }
49 }
50
51 None
52 }
53
54 /// Returns true if this comment is a `fmt: off` or `yapf: disable` own line suppression comment.
55 pub fn is_suppression_on(slice: &str, position: CommentLinePosition) -> bool {
56 position.is_own_line() && matches!(Self::from_comment(slice), Some(Self::On))
57 }
58
59 /// Returns true if this comment is a `fmt: on` or `yapf: enable` own line suppression comment.
60 pub fn is_suppression_off(slice: &str, position: CommentLinePosition) -> bool {
61 position.is_own_line() && matches!(Self::from_comment(slice), Some(Self::Off))
62 }
63}
64/// The position of a comment in the source text.
65#[derive(Debug, Copy, Clone, Eq, PartialEq)]
66pub enum CommentLinePosition {
67 /// A comment that is on the same line as the preceding token and is separated by at least one line break from the following token.
68 ///
69 /// # Examples
70 ///
71 /// ## End of line
72 ///
73 /// ```python
74 /// a; # comment
75 /// b;
76 /// ```
77 ///
78 /// `# comment` is an end of line comments because it is separated by at least one line break from the following token `b`.
79 /// Comments that not only end, but also start on a new line are [`OwnLine`](CommentLinePosition::OwnLine) comments.
80 EndOfLine,
81
82 /// A Comment that is separated by at least one line break from the preceding token.
83 ///
84 /// # Examples
85 ///
86 /// ```python
87 /// a;
88 /// # comment
89 /// b;
90 /// ```
91 ///
92 /// `# comment` line comments because they are separated by one line break from the preceding token `a`.
93 OwnLine,
94}
95
96impl CommentLinePosition {
97 pub const fn is_own_line(self) -> bool {
98 matches!(self, Self::OwnLine)
99 }
100
101 pub const fn is_end_of_line(self) -> bool {
102 matches!(self, Self::EndOfLine)
103 }
104
105 /// Finds the line position of a comment given a range over a valid
106 /// comment.
107 pub fn for_range(comment_range: TextRange, source_code: &str) -> Self {
108 let before = &source_code[TextRange::up_to(comment_range.start())];
109
110 for c in before.chars().rev() {
111 match c {
112 '\n' | '\r' => {
113 break;
114 }
115 c if is_python_whitespace(c) => continue,
116 _ => return Self::EndOfLine,
117 }
118 }
119 Self::OwnLine
120 }
121}