<!-- 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.
1011 lines
28 KiB
Rust
1011 lines
28 KiB
Rust
//! Types for extracting and representing comments of a syntax tree.
|
|
//!
|
|
//! Most programming languages support comments allowing programmers to document their programs.
|
|
//! Comments are different from other syntax because programming languages allow comments in almost any position,
|
|
//! giving programmers great flexibility on where they can write comments:
|
|
//!
|
|
//! ```javascript
|
|
//! /**
|
|
//! * Documentation comment
|
|
//! */
|
|
//! async /* comment */ function Test () // line comment
|
|
//! {/*inline*/}
|
|
//! ```
|
|
//!
|
|
//! This flexibility makes formatting comments challenging because:
|
|
//! * The formatter must consistently place comments so that re-formatting the output yields the same result,
|
|
//! and does not create invalid syntax (line comments).
|
|
//! * It is essential that formatters place comments close to the syntax the programmer intended to document.
|
|
//! However, the lack of rules regarding where comments are allowed and what syntax they document requires
|
|
//! the use of heuristics to infer the documented syntax.
|
|
//!
|
|
//! This module tries to strike a balance between placing comments as closely as possible to their source location
|
|
//! and reducing the complexity of formatting comments. It does so by associating comments per node rather than a token.
|
|
//! This greatly reduces the combinations of possible comment positions, but turns out to be, in practice,
|
|
//! sufficiently precise to keep comments close to their source location.
|
|
//!
|
|
//! Luckily, Python doesn't support inline comments, which simplifying the problem significantly.
|
|
//!
|
|
//! ## Node comments
|
|
//!
|
|
//! Comments are associated per node but get further distinguished on their location related to that node:
|
|
//!
|
|
//! ### Leading Comments
|
|
//!
|
|
//! A comment at the start of a node
|
|
//!
|
|
//! ```python
|
|
//! # Leading comment of the statement
|
|
//! print("test");
|
|
//!
|
|
//! [ # Leading comment of a
|
|
//! a
|
|
//! ];
|
|
//! ```
|
|
//!
|
|
//! ### Dangling Comments
|
|
//!
|
|
//! A comment that is neither at the start nor the end of a node.
|
|
//!
|
|
//! ```python
|
|
//! [
|
|
//! # I'm between two brackets. There are no nodes
|
|
//! ];
|
|
//! ```
|
|
//!
|
|
//! ### Trailing Comments
|
|
//!
|
|
//! A comment at the end of a node.
|
|
//!
|
|
//! ```python
|
|
//! [
|
|
//! a, # trailing comment of a
|
|
//! b, c
|
|
//! ];
|
|
//! ```
|
|
//!
|
|
//! ## Limitations
|
|
//! Limiting the placement of comments to leading, dangling, or trailing node comments reduces complexity inside the formatter but means,
|
|
//! that the formatter's possibility of where comments can be formatted depends on the AST structure.
|
|
//!
|
|
//! For example, *`RustPython`* doesn't create a node for the `/` operator separating positional only arguments from the other arguments.
|
|
//!
|
|
//! ```python
|
|
//! def test(
|
|
//! a,
|
|
//! /, # The following arguments are positional or named arguments
|
|
//! b
|
|
//! ):
|
|
//! pass
|
|
//! ```
|
|
//!
|
|
//! Because *`RustPython`* doesn't create a Node for the `/` argument, it is impossible to associate any
|
|
//! comments with it. Meaning, the default behaviour is to associate the `# The following ...` comment
|
|
//! with the `b` argument, which is incorrect. This limitation can be worked around by implementing
|
|
//! a custom rule to associate comments for `/` as *dangling comments* of the `Arguments` node and then
|
|
//! implement custom formatting inside of the arguments formatter.
|
|
//!
|
|
//! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic,
|
|
|
|
use std::cell::Cell;
|
|
use std::fmt::Debug;
|
|
use std::rc::Rc;
|
|
|
|
pub(crate) use format::{
|
|
dangling_comments, dangling_node_comments, dangling_open_parenthesis_comments,
|
|
leading_alternate_branch_comments, leading_comments, leading_node_comments, trailing_comments,
|
|
};
|
|
use ruff_formatter::{SourceCode, SourceCodeSlice};
|
|
use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal};
|
|
use ruff_python_ast::AnyNodeRef;
|
|
use ruff_python_trivia::{CommentLinePosition, CommentRanges, SuppressionKind};
|
|
use ruff_source_file::Locator;
|
|
use ruff_text_size::{Ranged, TextRange};
|
|
pub(crate) use visitor::collect_comments;
|
|
|
|
use crate::comments::debug::{DebugComment, DebugComments};
|
|
use crate::comments::map::{LeadingDanglingTrailing, MultiMap};
|
|
use crate::comments::node_key::NodeRefEqualityKey;
|
|
use crate::comments::visitor::{CommentsMapBuilder, CommentsVisitor};
|
|
|
|
mod debug;
|
|
pub(crate) mod format;
|
|
mod map;
|
|
mod node_key;
|
|
mod placement;
|
|
mod visitor;
|
|
|
|
/// A comment in the source document.
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub(crate) struct SourceComment {
|
|
/// The location of the comment in the source document.
|
|
slice: SourceCodeSlice,
|
|
/// Whether the comment has been formatted or not.
|
|
formatted: Cell<bool>,
|
|
line_position: CommentLinePosition,
|
|
}
|
|
|
|
impl SourceComment {
|
|
fn new(slice: SourceCodeSlice, position: CommentLinePosition) -> Self {
|
|
Self {
|
|
slice,
|
|
line_position: position,
|
|
formatted: Cell::new(false),
|
|
}
|
|
}
|
|
|
|
/// Returns the location of the comment in the original source code.
|
|
/// Allows retrieving the text of the comment.
|
|
pub(crate) const fn slice(&self) -> &SourceCodeSlice {
|
|
&self.slice
|
|
}
|
|
|
|
pub(crate) const fn line_position(&self) -> CommentLinePosition {
|
|
self.line_position
|
|
}
|
|
|
|
/// Marks the comment as formatted
|
|
pub(crate) fn mark_formatted(&self) {
|
|
self.formatted.set(true);
|
|
}
|
|
|
|
/// Marks the comment as not-formatted
|
|
pub(crate) fn mark_unformatted(&self) {
|
|
self.formatted.set(false);
|
|
}
|
|
|
|
/// If the comment has already been formatted
|
|
pub(crate) fn is_formatted(&self) -> bool {
|
|
self.formatted.get()
|
|
}
|
|
|
|
pub(crate) fn is_unformatted(&self) -> bool {
|
|
!self.is_formatted()
|
|
}
|
|
|
|
/// Returns a nice debug representation that prints the source code for every comment (and not just the range).
|
|
pub(crate) fn debug<'a>(&'a self, source_code: SourceCode<'a>) -> DebugComment<'a> {
|
|
DebugComment::new(self, source_code)
|
|
}
|
|
|
|
pub(crate) fn is_suppression_off_comment(&self, text: &str) -> bool {
|
|
SuppressionKind::is_suppression_off(self.text(text), self.line_position)
|
|
}
|
|
|
|
pub(crate) fn is_suppression_on_comment(&self, text: &str) -> bool {
|
|
SuppressionKind::is_suppression_on(self.text(text), self.line_position)
|
|
}
|
|
|
|
fn text<'a>(&self, text: &'a str) -> &'a str {
|
|
self.slice.text(SourceCode::new(text))
|
|
}
|
|
}
|
|
|
|
impl Ranged for SourceComment {
|
|
#[inline]
|
|
fn range(&self) -> TextRange {
|
|
self.slice.range()
|
|
}
|
|
}
|
|
|
|
type CommentsMap<'a> = MultiMap<NodeRefEqualityKey<'a>, SourceComment>;
|
|
|
|
/// The comments of a syntax tree stored by node.
|
|
///
|
|
/// Cloning `comments` is cheap as it only involves bumping a reference counter.
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct Comments<'a> {
|
|
/// The implementation uses an [Rc] so that [Comments] has a lifetime independent from the [crate::Formatter].
|
|
/// Independent lifetimes are necessary to support the use case where a (formattable object)[crate::Format]
|
|
/// iterates over all comments, and writes them into the [crate::Formatter] (mutably borrowing the [crate::Formatter] and in turn its context).
|
|
///
|
|
/// ```block
|
|
/// for leading in f.context().comments().leading_comments(node) {
|
|
/// ^
|
|
/// |- Borrows comments
|
|
/// write!(f, [comment(leading.piece.text())])?;
|
|
/// ^
|
|
/// |- Mutably borrows the formatter, state, context, and comments (if comments aren't cloned)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// The use of an `Rc` solves this problem because we can cheaply clone `comments` before iterating.
|
|
///
|
|
/// ```block
|
|
/// let comments = f.context().comments().clone();
|
|
/// for leading in comments.leading_comments(node) {
|
|
/// write!(f, [comment(leading.piece.text())])?;
|
|
/// }
|
|
/// ```
|
|
data: Rc<CommentsData<'a>>,
|
|
}
|
|
|
|
impl<'a> Comments<'a> {
|
|
fn new(comments: CommentsMap<'a>, comment_ranges: &'a CommentRanges) -> Self {
|
|
Self {
|
|
data: Rc::new(CommentsData {
|
|
comments,
|
|
comment_ranges,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Effectively a [`Default`] implementation that works around the lifetimes for tests
|
|
#[cfg(test)]
|
|
pub(crate) fn from_ranges(comment_ranges: &'a CommentRanges) -> Self {
|
|
Self {
|
|
data: Rc::new(CommentsData {
|
|
comments: CommentsMap::default(),
|
|
comment_ranges,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn ranges(&self) -> &'a CommentRanges {
|
|
self.data.comment_ranges
|
|
}
|
|
|
|
/// Extracts the comments from the AST.
|
|
pub(crate) fn from_ast(
|
|
root: impl Into<AnyNodeRef<'a>>,
|
|
source_code: SourceCode<'a>,
|
|
comment_ranges: &'a CommentRanges,
|
|
) -> Self {
|
|
fn collect_comments<'a>(
|
|
root: AnyNodeRef<'a>,
|
|
source_code: SourceCode<'a>,
|
|
comment_ranges: &'a CommentRanges,
|
|
) -> Comments<'a> {
|
|
let map = if comment_ranges.is_empty() {
|
|
CommentsMap::new()
|
|
} else {
|
|
let mut builder =
|
|
CommentsMapBuilder::new(Locator::new(source_code.as_str()), comment_ranges);
|
|
CommentsVisitor::new(source_code, comment_ranges, &mut builder).visit(root);
|
|
builder.finish()
|
|
};
|
|
|
|
Comments::new(map, comment_ranges)
|
|
}
|
|
|
|
collect_comments(root.into(), source_code, comment_ranges)
|
|
}
|
|
|
|
/// Returns `true` if the given `node` has any comments.
|
|
#[inline]
|
|
pub(crate) fn has<T>(&self, node: T) -> bool
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.data
|
|
.comments
|
|
.has(&NodeRefEqualityKey::from_ref(node.into()))
|
|
}
|
|
|
|
/// Returns `true` if the given `node` has any [leading comments](self#leading-comments).
|
|
#[inline]
|
|
pub(crate) fn has_leading<T>(&self, node: T) -> bool
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
!self.leading(node).is_empty()
|
|
}
|
|
|
|
/// Returns the `node`'s [leading comments](self#leading-comments).
|
|
#[inline]
|
|
pub(crate) fn leading<T>(&self, node: T) -> &[SourceComment]
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.data
|
|
.comments
|
|
.leading(&NodeRefEqualityKey::from_ref(node.into()))
|
|
}
|
|
|
|
/// Returns `true` if node has any [dangling comments](self#dangling-comments).
|
|
pub(crate) fn has_dangling<T>(&self, node: T) -> bool
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
!self.dangling(node).is_empty()
|
|
}
|
|
|
|
/// Returns the [dangling comments](self#dangling-comments) of `node`
|
|
pub(crate) fn dangling<T>(&self, node: T) -> &[SourceComment]
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.data
|
|
.comments
|
|
.dangling(&NodeRefEqualityKey::from_ref(node.into()))
|
|
}
|
|
|
|
/// Returns the `node`'s [trailing comments](self#trailing-comments).
|
|
#[inline]
|
|
pub(crate) fn trailing<T>(&self, node: T) -> &[SourceComment]
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.data
|
|
.comments
|
|
.trailing(&NodeRefEqualityKey::from_ref(node.into()))
|
|
}
|
|
|
|
/// Returns `true` if the given `node` has any [trailing comments](self#trailing-comments).
|
|
#[inline]
|
|
pub(crate) fn has_trailing<T>(&self, node: T) -> bool
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
!self.trailing(node).is_empty()
|
|
}
|
|
|
|
/// Returns `true` if the given `node` has any [trailing own line comments](self#trailing-comments).
|
|
#[inline]
|
|
pub(crate) fn has_trailing_own_line<T>(&self, node: T) -> bool
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.trailing(node)
|
|
.iter()
|
|
.any(|comment| comment.line_position().is_own_line())
|
|
}
|
|
|
|
/// Returns an iterator over the [leading](self#leading-comments) and [trailing comments](self#trailing-comments) of `node`.
|
|
pub(crate) fn leading_trailing<T>(&self, node: T) -> impl Iterator<Item = &SourceComment>
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
let comments = self.leading_dangling_trailing(node);
|
|
comments.leading.iter().chain(comments.trailing)
|
|
}
|
|
|
|
/// Returns an iterator over the [leading](self#leading-comments), [dangling](self#dangling-comments), and [trailing](self#trailing) comments of `node`.
|
|
pub(crate) fn leading_dangling_trailing<T>(&self, node: T) -> LeadingDanglingTrailingComments
|
|
where
|
|
T: Into<AnyNodeRef<'a>>,
|
|
{
|
|
self.data
|
|
.comments
|
|
.leading_dangling_trailing(&NodeRefEqualityKey::from_ref(node.into()))
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) fn assert_all_formatted(&self, _source_code: SourceCode) {}
|
|
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn assert_all_formatted(&self, source_code: SourceCode) {
|
|
use std::fmt::Write;
|
|
|
|
let mut output = String::new();
|
|
let unformatted_comments = self
|
|
.data
|
|
.comments
|
|
.all_parts()
|
|
.filter(|c| !c.formatted.get());
|
|
|
|
for comment in unformatted_comments {
|
|
// SAFETY: Writing to a string never fails.
|
|
writeln!(output, "{:#?}", comment.debug(source_code)).unwrap();
|
|
}
|
|
|
|
assert!(
|
|
output.is_empty(),
|
|
"The following comments have not been formatted.\n{output}"
|
|
);
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[cfg(not(debug_assertions))]
|
|
pub(crate) fn mark_verbatim_node_comments_formatted(&self, _node: AnyNodeRef) {}
|
|
|
|
/// Marks the comments of a node printed in verbatim (suppressed) as formatted.
|
|
///
|
|
/// Marks the dangling comments and the comments of all descendants as formatted.
|
|
/// The leading and trailing comments are not marked as formatted because they are formatted
|
|
/// normally if `node` is the first or last node of a suppression range.
|
|
#[cfg(debug_assertions)]
|
|
pub(crate) fn mark_verbatim_node_comments_formatted(&self, node: AnyNodeRef) {
|
|
for dangling in self.dangling(node) {
|
|
dangling.mark_formatted();
|
|
}
|
|
|
|
node.visit_preorder(&mut MarkVerbatimCommentsAsFormattedVisitor(self));
|
|
}
|
|
|
|
/// Returns an object that implements [Debug] for nicely printing the [`Comments`].
|
|
pub(crate) fn debug(&'a self, source_code: SourceCode<'a>) -> DebugComments<'a> {
|
|
DebugComments::new(&self.data.comments, source_code)
|
|
}
|
|
}
|
|
|
|
pub(crate) type LeadingDanglingTrailingComments<'a> = LeadingDanglingTrailing<'a, SourceComment>;
|
|
|
|
impl LeadingDanglingTrailingComments<'_> {
|
|
/// Returns `true` if the struct has any [leading comments](self#leading-comments).
|
|
#[inline]
|
|
pub(crate) fn has_leading(&self) -> bool {
|
|
!self.leading.is_empty()
|
|
}
|
|
|
|
/// Returns `true` if the struct has any [trailing comments](self#trailing-comments).
|
|
#[inline]
|
|
pub(crate) fn has_trailing(&self) -> bool {
|
|
!self.trailing.is_empty()
|
|
}
|
|
|
|
/// Returns `true` if the struct has any [trailing own line comments](self#trailing-comments).
|
|
#[inline]
|
|
pub(crate) fn has_trailing_own_line(&self) -> bool {
|
|
self.trailing
|
|
.iter()
|
|
.any(|comment| comment.line_position().is_own_line())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CommentsData<'a> {
|
|
comments: CommentsMap<'a>,
|
|
|
|
/// We need those for backwards lexing
|
|
comment_ranges: &'a CommentRanges,
|
|
}
|
|
|
|
struct MarkVerbatimCommentsAsFormattedVisitor<'a>(&'a Comments<'a>);
|
|
|
|
impl<'a> PreorderVisitor<'a> for MarkVerbatimCommentsAsFormattedVisitor<'a> {
|
|
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
|
for comment in self.0.leading_dangling_trailing(node) {
|
|
comment.mark_formatted();
|
|
}
|
|
|
|
TraversalSignal::Traverse
|
|
}
|
|
}
|
|
|
|
pub(crate) fn has_skip_comment(trailing_comments: &[SourceComment], source: &str) -> bool {
|
|
trailing_comments.iter().any(|comment| {
|
|
comment.line_position().is_end_of_line()
|
|
&& matches!(
|
|
SuppressionKind::from_comment(comment.text(source)),
|
|
Some(SuppressionKind::Skip | SuppressionKind::Off)
|
|
)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_debug_snapshot;
|
|
|
|
use ruff_formatter::SourceCode;
|
|
use ruff_python_ast::{Mod, PySourceType};
|
|
use ruff_python_index::tokens_and_ranges;
|
|
use ruff_python_parser::{parse_tokens, AsMode};
|
|
use ruff_python_trivia::CommentRanges;
|
|
|
|
use crate::comments::Comments;
|
|
|
|
struct CommentsTestCase<'a> {
|
|
module: Mod,
|
|
comment_ranges: CommentRanges,
|
|
source_code: SourceCode<'a>,
|
|
}
|
|
|
|
impl<'a> CommentsTestCase<'a> {
|
|
fn from_code(source: &'a str) -> Self {
|
|
let source_code = SourceCode::new(source);
|
|
let source_type = PySourceType::Python;
|
|
let (tokens, comment_ranges) =
|
|
tokens_and_ranges(source, source_type).expect("Expect source to be valid Python");
|
|
let parsed = parse_tokens(tokens, source, source_type.as_mode())
|
|
.expect("Expect source to be valid Python");
|
|
|
|
CommentsTestCase {
|
|
source_code,
|
|
module: parsed,
|
|
comment_ranges,
|
|
}
|
|
}
|
|
|
|
fn to_comments(&self) -> Comments {
|
|
Comments::from_ast(&self.module, self.source_code, &self.comment_ranges)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn base_test() {
|
|
let source = r#"
|
|
# Function Leading comment
|
|
def test(x, y):
|
|
if x == y: # if statement end of line comment
|
|
print("Equal")
|
|
|
|
# Leading comment
|
|
elif x < y:
|
|
print("Less")
|
|
else:
|
|
print("Greater")
|
|
|
|
# own line comment
|
|
|
|
test(10, 20)
|
|
"#;
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn only_comments() {
|
|
let source = r"
|
|
# Some comment
|
|
|
|
# another comment
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_file() {
|
|
let source = r"";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn dangling_comment() {
|
|
let source = r"
|
|
def test(
|
|
# Some comment
|
|
):
|
|
pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn parenthesized_expression() {
|
|
let source = r"
|
|
a = ( # Trailing comment
|
|
10 + # More comments
|
|
3
|
|
)
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn parenthesized_trailing_comment() {
|
|
let source = r"(
|
|
a
|
|
# comment
|
|
)
|
|
";
|
|
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn trailing_function_comment() {
|
|
let source = r#"
|
|
def test(x, y):
|
|
if x == y:
|
|
pass
|
|
elif x < y:
|
|
print("Less")
|
|
else:
|
|
print("Greater")
|
|
|
|
# trailing `else` comment
|
|
|
|
# Trailing `if` statement comment
|
|
|
|
def other(y, z):
|
|
if y == z:
|
|
pass
|
|
# Trailing `pass` comment
|
|
# Trailing `if` statement comment
|
|
|
|
class Test:
|
|
def func():
|
|
pass
|
|
# Trailing `func` function comment
|
|
|
|
test(10, 20)
|
|
"#;
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn trailing_comment_after_single_statement_body() {
|
|
let source = r#"
|
|
if x == y: pass
|
|
|
|
# Test
|
|
print("test")
|
|
"#;
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn if_elif_else_comments() {
|
|
let source = r"
|
|
if x == y:
|
|
pass # trailing `pass` comment
|
|
# Root `if` trailing comment
|
|
|
|
# Leading elif comment
|
|
elif x < y:
|
|
pass
|
|
# `elif` trailing comment
|
|
# Leading else comment
|
|
else:
|
|
pass
|
|
# `else` trailing comment
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn if_elif_if_else_comments() {
|
|
let source = r"
|
|
if x == y:
|
|
pass
|
|
elif x < y:
|
|
if x < 10:
|
|
pass
|
|
# `elif` trailing comment
|
|
# Leading else comment
|
|
else:
|
|
pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn try_except_finally_else() {
|
|
let source = r#"
|
|
try:
|
|
pass
|
|
# trailing try comment
|
|
# leading handler comment
|
|
except Exception as ex:
|
|
pass
|
|
# Trailing except comment
|
|
# leading else comment
|
|
else:
|
|
pass
|
|
# trailing else comment
|
|
# leading finally comment
|
|
finally:
|
|
print("Finally!")
|
|
# Trailing finally comment
|
|
"#;
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn try_except() {
|
|
let source = r#"
|
|
def test():
|
|
try:
|
|
pass
|
|
# trailing try comment
|
|
# leading handler comment
|
|
except Exception as ex:
|
|
pass
|
|
# Trailing except comment
|
|
|
|
# Trailing function comment
|
|
|
|
print("Next statement");
|
|
"#;
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
// Issue: Match cases
|
|
#[test]
|
|
fn match_cases() {
|
|
let source = r#"def make_point_3d(pt):
|
|
match pt:
|
|
# Leading `case(x, y)` comment
|
|
case (x, y):
|
|
return Point3d(x, y, 0)
|
|
# Trailing `case(x, y) comment
|
|
# Leading `case (x, y, z)` comment
|
|
case (x, y, z):
|
|
if x < y:
|
|
print("if")
|
|
else:
|
|
print("else")
|
|
# Trailing else comment
|
|
# trailing case comment
|
|
case Point2d(x, y):
|
|
return Point3d(x, y, 0)
|
|
case _:
|
|
raise TypeError("not a point we support")
|
|
# Trailing last case comment
|
|
# Trailing match comment
|
|
# After match comment
|
|
|
|
print("other")
|
|
"#;
|
|
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn leading_most_outer() {
|
|
let source = r"
|
|
# leading comment
|
|
x
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
// Comment should be attached to the statement
|
|
#[test]
|
|
fn trailing_most_outer() {
|
|
let source = r"
|
|
x # trailing comment
|
|
y # trailing last node
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn trailing_most_outer_nested() {
|
|
let source = r"
|
|
x + (
|
|
3 # trailing comment
|
|
) # outer
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn trailing_after_comma() {
|
|
let source = r"
|
|
def test(
|
|
a, # Trailing comment for argument `a`
|
|
b,
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn positional_argument_only_comment() {
|
|
let source = r"
|
|
def test(
|
|
a, # trailing positional comment
|
|
# Positional arguments only after here
|
|
/, # trailing positional argument comment.
|
|
# leading b comment
|
|
b,
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn positional_argument_only_leading_comma_comment() {
|
|
let source = r"
|
|
def test(
|
|
a # trailing positional comment
|
|
# Positional arguments only after here
|
|
,/, # trailing positional argument comment.
|
|
# leading b comment
|
|
b,
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn positional_argument_only_comment_without_following_node() {
|
|
let source = r"
|
|
def test(
|
|
a, # trailing positional comment
|
|
# Positional arguments only after here
|
|
/, # trailing positional argument comment.
|
|
# Trailing on new line
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn non_positional_arguments_with_defaults() {
|
|
let source = r"
|
|
def test(
|
|
a=10 # trailing positional comment
|
|
# Positional arguments only after here
|
|
,/, # trailing positional argument comment.
|
|
# leading comment for b
|
|
b=20
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn non_positional_arguments_slash_on_same_line() {
|
|
let source = r"
|
|
def test(a=10,/, # trailing positional argument comment.
|
|
# leading comment for b
|
|
b=20
|
|
): pass
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_expression_left_operand_comment() {
|
|
let source = r"
|
|
a = (
|
|
5
|
|
# trailing left comment
|
|
+
|
|
# leading right comment
|
|
3
|
|
)
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn binary_expression_left_operand_trailing_end_of_line_comment() {
|
|
let source = r"
|
|
a = (
|
|
5 # trailing left comment
|
|
+ # trailing operator comment
|
|
# leading right comment
|
|
3
|
|
)
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_binary_expression() {
|
|
let source = r"
|
|
a = (
|
|
(5 # trailing left comment
|
|
*
|
|
2)
|
|
+ # trailing operator comment
|
|
# leading right comment
|
|
3
|
|
)
|
|
";
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn while_trailing_end_of_line_comment() {
|
|
let source = r"while True:
|
|
if something.changed:
|
|
do.stuff() # trailing comment
|
|
";
|
|
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
|
|
#[test]
|
|
fn while_trailing_else_end_of_line_comment() {
|
|
let source = r"while True:
|
|
pass
|
|
else: # trailing comment
|
|
pass
|
|
";
|
|
|
|
let test_case = CommentsTestCase::from_code(source);
|
|
|
|
let comments = test_case.to_comments();
|
|
|
|
assert_debug_snapshot!(comments.debug(test_case.source_code));
|
|
}
|
|
}
|