Skip to main content

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                && command.trim_whitespace_start() == "skip"
46            {
47                return Some(SuppressionKind::Skip);
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}