mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-14 12:29:11 +00:00

<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> Fixes #6611 ## Summary This lint rule spots comments that are _intended_ to suppress or enable the formatter, but will be ignored by the Ruff formatter. We borrow some functions the formatter uses for determining comment placement / putting them in context within an AST. The analysis function uses an AST visitor to visit each comment and attach it to the AST. It then uses that context to check: 1. Is this comment in an expression? 2. Does this comment have bad placement? (e.g. a `# fmt: skip` above a function instead of at the end of a line) 3. Is this comment redundant? 4. Does this comment actually suppress any code? 5. Does this comment have ambiguous placement? (e.g. a `# fmt: off` above an `else:` block) If any of these are true, a violation is thrown. The reported reason depends on the order of the above check-list: in other words, a `# fmt: skip` comment on its own line within a list expression will be reported as being in an expression, since that reason takes priority. The lint suggests removing the comment as an unsafe fix, regardless of the reason. ## Test Plan A snapshot test has been created.
240 lines
6.8 KiB
Rust
240 lines
6.8 KiB
Rust
use std::fmt::{Debug, Formatter, Write};
|
|
|
|
use itertools::Itertools;
|
|
|
|
use ruff_formatter::SourceCode;
|
|
use ruff_text_size::Ranged;
|
|
|
|
use crate::comments::node_key::NodeRefEqualityKey;
|
|
use crate::comments::{CommentsMap, SourceComment};
|
|
|
|
/// Prints a debug representation of [`SourceComment`] that includes the comment's text
|
|
pub(crate) struct DebugComment<'a> {
|
|
comment: &'a SourceComment,
|
|
source_code: SourceCode<'a>,
|
|
}
|
|
|
|
impl<'a> DebugComment<'a> {
|
|
pub(super) fn new(comment: &'a SourceComment, source_code: SourceCode<'a>) -> Self {
|
|
Self {
|
|
comment,
|
|
source_code,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for DebugComment<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let mut strut = f.debug_struct("SourceComment");
|
|
|
|
strut
|
|
.field("text", &self.comment.slice.text(self.source_code))
|
|
.field("position", &self.comment.line_position)
|
|
.field("formatted", &self.comment.formatted.get());
|
|
|
|
strut.finish()
|
|
}
|
|
}
|
|
|
|
/// Pretty-printed debug representation of [`Comments`].
|
|
pub(crate) struct DebugComments<'a> {
|
|
comments: &'a CommentsMap<'a>,
|
|
source_code: SourceCode<'a>,
|
|
}
|
|
|
|
impl<'a> DebugComments<'a> {
|
|
pub(super) fn new(comments: &'a CommentsMap, source_code: SourceCode<'a>) -> Self {
|
|
Self {
|
|
comments,
|
|
source_code,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Debug for DebugComments<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let mut map = f.debug_map();
|
|
|
|
for node in self.comments.keys().sorted_by_key(|key| key.node().start()) {
|
|
map.entry(
|
|
&NodeKindWithSource {
|
|
key: *node,
|
|
source: self.source_code,
|
|
},
|
|
&DebugNodeComments {
|
|
comments: self.comments,
|
|
source_code: self.source_code,
|
|
key: *node,
|
|
},
|
|
);
|
|
}
|
|
|
|
map.finish()
|
|
}
|
|
}
|
|
|
|
/// Prints the source code up to the first new line character. Truncates the text if it exceeds 40 characters.
|
|
struct NodeKindWithSource<'a> {
|
|
key: NodeRefEqualityKey<'a>,
|
|
source: SourceCode<'a>,
|
|
}
|
|
|
|
impl Debug for NodeKindWithSource<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
struct TruncatedSource<'a>(&'a str);
|
|
|
|
impl Debug for TruncatedSource<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.write_char('`')?;
|
|
let first_line = if let Some(line_end_pos) = self.0.find(['\n', '\r']) {
|
|
&self.0[..line_end_pos]
|
|
} else {
|
|
self.0
|
|
};
|
|
|
|
if first_line.len() > 40 {
|
|
let (head, rest) = first_line.split_at(27);
|
|
|
|
f.write_str(head)?;
|
|
f.write_str("...")?;
|
|
|
|
// Take the last 10 characters
|
|
let tail = &rest[rest.len().saturating_sub(10)..];
|
|
f.write_str(tail)?;
|
|
} else {
|
|
f.write_str(first_line)?;
|
|
}
|
|
|
|
if first_line.len() < self.0.len() {
|
|
f.write_str("\u{23ce}")?;
|
|
}
|
|
|
|
f.write_char('`')
|
|
}
|
|
}
|
|
|
|
let kind = self.key.node().kind();
|
|
let source = self.source.slice(self.key.node().range()).text(self.source);
|
|
|
|
f.debug_struct("Node")
|
|
.field("kind", &kind)
|
|
.field("range", &self.key.node().range())
|
|
.field("source", &TruncatedSource(source))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
struct DebugNodeComments<'a> {
|
|
comments: &'a CommentsMap<'a>,
|
|
source_code: SourceCode<'a>,
|
|
key: NodeRefEqualityKey<'a>,
|
|
}
|
|
|
|
impl Debug for DebugNodeComments<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_map()
|
|
.entry(
|
|
&"leading",
|
|
&DebugNodeCommentSlice {
|
|
node_comments: self.comments.leading(&self.key),
|
|
source_code: self.source_code,
|
|
},
|
|
)
|
|
.entry(
|
|
&"dangling",
|
|
&DebugNodeCommentSlice {
|
|
node_comments: self.comments.dangling(&self.key),
|
|
source_code: self.source_code,
|
|
},
|
|
)
|
|
.entry(
|
|
&"trailing",
|
|
&DebugNodeCommentSlice {
|
|
node_comments: self.comments.trailing(&self.key),
|
|
source_code: self.source_code,
|
|
},
|
|
)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
struct DebugNodeCommentSlice<'a> {
|
|
node_comments: &'a [SourceComment],
|
|
source_code: SourceCode<'a>,
|
|
}
|
|
|
|
impl Debug for DebugNodeCommentSlice<'_> {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
let mut list = f.debug_list();
|
|
|
|
for comment in self.node_comments {
|
|
list.entry(&comment.debug(self.source_code));
|
|
}
|
|
|
|
list.finish()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_debug_snapshot;
|
|
|
|
use ruff_formatter::SourceCode;
|
|
use ruff_python_ast::AnyNode;
|
|
use ruff_python_ast::{StmtBreak, StmtContinue};
|
|
use ruff_python_trivia::{CommentLinePosition, CommentRanges};
|
|
use ruff_text_size::{TextRange, TextSize};
|
|
|
|
use crate::comments::map::MultiMap;
|
|
use crate::comments::{Comments, CommentsMap, SourceComment};
|
|
|
|
#[test]
|
|
fn debug() {
|
|
let continue_statement = AnyNode::from(StmtContinue {
|
|
range: TextRange::default(),
|
|
});
|
|
|
|
let break_statement = AnyNode::from(StmtBreak {
|
|
range: TextRange::default(),
|
|
});
|
|
|
|
let source = r"# leading comment
|
|
continue; # trailing
|
|
# break leading
|
|
break;
|
|
";
|
|
|
|
let source_code = SourceCode::new(source);
|
|
|
|
let mut comments_map: CommentsMap = MultiMap::new();
|
|
|
|
comments_map.push_leading(
|
|
continue_statement.as_ref().into(),
|
|
SourceComment::new(
|
|
source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))),
|
|
CommentLinePosition::OwnLine,
|
|
),
|
|
);
|
|
|
|
comments_map.push_trailing(
|
|
continue_statement.as_ref().into(),
|
|
SourceComment::new(
|
|
source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))),
|
|
CommentLinePosition::EndOfLine,
|
|
),
|
|
);
|
|
|
|
comments_map.push_leading(
|
|
break_statement.as_ref().into(),
|
|
SourceComment::new(
|
|
source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))),
|
|
CommentLinePosition::OwnLine,
|
|
),
|
|
);
|
|
|
|
let comment_ranges = CommentRanges::default();
|
|
let comments = Comments::new(comments_map, &comment_ranges);
|
|
|
|
assert_debug_snapshot!(comments.debug(source_code));
|
|
}
|
|
}
|