diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py index 5dc7d1af44..bb4425afbb 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py @@ -80,6 +80,21 @@ f( # oddly placed own line comment dict() ) +f( + session, + b=1, + **(# oddly placed own line comment + dict() + ) +) +f( + session, + b=1, + **( + # oddly placed own line comment + dict() + ) +) # Don't add a magic trailing comma when there is only one entry # Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 @@ -205,3 +220,25 @@ aaa = ( () .bbbbbbbbbbbbbbbb ) + +# Comments around keywords +f(x= # comment + 1) + +f(x # comment + = + 1) + +f(x= + # comment + 1) + +f(x=(# comment + 1 +)) + + +f(x=( + # comment + 1 +)) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index c506054b46..0a50bc0674 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -191,7 +191,10 @@ fn handle_enclosed_comment<'a>( locator, ) } - AnyNodeRef::Keyword(_) => handle_dict_unpacking_comment(comment, locator), + AnyNodeRef::Keyword(keyword) => handle_keyword_comment(comment, keyword, locator), + AnyNodeRef::PatternKeyword(pattern_keyword) => { + handle_pattern_keyword_comment(comment, pattern_keyword, locator) + } AnyNodeRef::ExprNamedExpr(_) => handle_named_expr_comment(comment, locator), AnyNodeRef::ExprDict(_) => handle_dict_unpacking_comment(comment, locator) .or_else(|comment| handle_bracketed_end_of_line_comment(comment, locator)), @@ -940,6 +943,74 @@ fn handle_leading_class_with_decorators_comment<'a>( CommentPlacement::Default(comment) } +/// Handles comments between a keyword's identifier and value: +/// ```python +/// func( +/// x # dangling +/// = # dangling +/// # dangling +/// 1, +/// ** # dangling +/// y +/// ) +/// ``` +fn handle_keyword_comment<'a>( + comment: DecoratedComment<'a>, + keyword: &'a ast::Keyword, + locator: &Locator, +) -> CommentPlacement<'a> { + let start = keyword.arg.as_ref().map_or(keyword.start(), Ranged::end); + + // If the comment is parenthesized, it should be attached to the value: + // ```python + // func( + // x=( # comment + // 1 + // ) + // ) + // ``` + let mut tokenizer = + SimpleTokenizer::new(locator.contents(), TextRange::new(start, comment.start())); + if tokenizer.any(|token| token.kind == SimpleTokenKind::LParen) { + return CommentPlacement::Default(comment); + } + + CommentPlacement::leading(comment.enclosing_node(), comment) +} + +/// Handles comments between a pattern keyword's identifier and value: +/// ```python +/// case Point2D( +/// x # dangling +/// = # dangling +/// # dangling +/// 1 +/// ) +/// ``` +fn handle_pattern_keyword_comment<'a>( + comment: DecoratedComment<'a>, + pattern_keyword: &'a ast::PatternKeyword, + locator: &Locator, +) -> CommentPlacement<'a> { + // If the comment is parenthesized, it should be attached to the value: + // ```python + // case Point2D( + // x=( # comment + // 1 + // ) + // ) + // ``` + let mut tokenizer = SimpleTokenizer::new( + locator.contents(), + TextRange::new(pattern_keyword.attr.end(), comment.start()), + ); + if tokenizer.any(|token| token.kind == SimpleTokenKind::LParen) { + return CommentPlacement::Default(comment); + } + + CommentPlacement::leading(comment.enclosing_node(), comment) +} + /// Handles comments between `**` and the variable name in dict unpacking /// It attaches these to the appropriate value node. /// @@ -954,10 +1025,7 @@ fn handle_dict_unpacking_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - debug_assert!(matches!( - comment.enclosing_node(), - AnyNodeRef::ExprDict(_) | AnyNodeRef::Keyword(_) - )); + debug_assert!(matches!(comment.enclosing_node(), AnyNodeRef::ExprDict(_))); // no node after our comment so we can't be between `**` and the name (node) let Some(following) = comment.following_node() else { @@ -980,7 +1048,7 @@ fn handle_dict_unpacking_comment<'a>( // if the remaining tokens from the previous node are exactly `**`, // re-assign the comment to the one that follows the stars if tokens.any(|token| token.kind == SimpleTokenKind::DoubleStar) { - CommentPlacement::trailing(following, comment) + CommentPlacement::leading(following, comment) } else { CommentPlacement::Default(comment) } diff --git a/crates/ruff_python_formatter/src/expression/expr_dict.rs b/crates/ruff_python_formatter/src/expression/expr_dict.rs index 851898ee87..d53e6b6b5f 100644 --- a/crates/ruff_python_formatter/src/expression/expr_dict.rs +++ b/crates/ruff_python_formatter/src/expression/expr_dict.rs @@ -40,6 +40,8 @@ impl Format> for KeyValuePair<'_> { ])] ) } else { + // TODO(charlie): Make these dangling comments on the `ExprDict`, and identify them + // dynamically, so as to avoid the parent rendering its child's comments. let comments = f.context().comments().clone(); let leading_value_comments = comments.leading(self.value); write!( diff --git a/crates/ruff_python_formatter/src/other/keyword.rs b/crates/ruff_python_formatter/src/other/keyword.rs index ed5ef0cca6..914a467ef8 100644 --- a/crates/ruff_python_formatter/src/other/keyword.rs +++ b/crates/ruff_python_formatter/src/other/keyword.rs @@ -13,10 +13,10 @@ impl FormatNodeRule for FormatKeyword { arg, value, } = item; + // Comments after the `=` or `**` are reassigned as leading comments on the value. if let Some(arg) = arg { write!(f, [arg.format(), text("="), value.format()]) } else { - // Comments after the stars are reassigned as trailing value comments write!(f, [text("**"), value.format()]) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap index e3771e0389..a8877d445c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap @@ -86,6 +86,21 @@ f( # oddly placed own line comment dict() ) +f( + session, + b=1, + **(# oddly placed own line comment + dict() + ) +) +f( + session, + b=1, + **( + # oddly placed own line comment + dict() + ) +) # Don't add a magic trailing comma when there is only one entry # Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 @@ -211,6 +226,28 @@ aaa = ( () .bbbbbbbbbbbbbbbb ) + +# Comments around keywords +f(x= # comment + 1) + +f(x # comment + = + 1) + +f(x= + # comment + 1) + +f(x=(# comment + 1 +)) + + +f(x=( + # comment + 1 +)) ``` ## Output @@ -288,13 +325,29 @@ f("aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaa f( session, b=1, - **dict(), # oddly placed end-of-line comment + # oddly placed end-of-line comment + **dict(), ) f( session, b=1, - **dict(), # oddly placed own line comment + **dict(), +) +f( + session, + b=1, + **( # oddly placed own line comment + dict() + ), +) +f( + session, + b=1, + **( + # oddly placed own line comment + dict() + ), ) # Don't add a magic trailing comma when there is only one entry @@ -408,6 +461,36 @@ aaa = ( bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # awkward comment )().bbbbbbbbbbbbbbbb + +# Comments around keywords +f( + # comment + x=1 +) + +f( + # comment + x=1 +) + +f( + # comment + x=1 +) + +f( + x=( # comment + 1 + ) +) + + +f( + x=( + # comment + 1 + ) +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap index a5b6c5e896..a1e535b1c2 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap @@ -80,22 +80,26 @@ x={ # dangling end of line comment { **a, # leading - **b, # middle # trailing + # middle + **b, # trailing } { - **b # middle with single item + # middle with single item + **b } { # before - **b, # between + # between + **b, } { **a, # comment before preceding node's comma # before - **b, # between + # between + **b, } {} diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap index 8a23c2f6d7..2ca2c18fa8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__match.py.snap @@ -985,18 +985,18 @@ match pattern_match_class: ... case A( - b=# b + # b # c - 2 # d + b=2 # d # e ): pass case A( # a - b=# b + # b # c - 2 # d + b=2 # d # e ): pass