diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py index 12e167d413..3677fedb70 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py @@ -152,4 +152,54 @@ lambda *x\ x: x ) +lambda: ( # comment + x) +( + lambda: # comment + x +) + +( + lambda: + # comment + x +) + +( + lambda # comment + : + x +) + +( + lambda + # comment + : + x +) + +( + lambda: # comment + ( # comment + x + ) +) + +( + lambda # 1 + # 2 + x # 3 + # 4 + : # 5 + # 6 + x +) + +( + lambda + x, + # comment + y: + z +) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index af762ce921..6af0efcab3 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -229,6 +229,7 @@ fn handle_enclosed_comment<'a>( } AnyNodeRef::ExprUnaryOp(unary_op) => handle_unary_op_comment(comment, unary_op, locator), AnyNodeRef::ExprNamedExpr(_) => handle_named_expr_comment(comment, locator), + AnyNodeRef::ExprLambda(lambda) => handle_lambda_comment(comment, lambda, locator), AnyNodeRef::ExprDict(_) => handle_dict_unpacking_comment(comment, locator) .or_else(|comment| handle_bracketed_end_of_line_comment(comment, locator)) .or_else(|comment| handle_key_value_comment(comment, locator)), @@ -1687,6 +1688,119 @@ fn handle_named_expr_comment<'a>( } } +/// Handles comments around the `:` token in a lambda expression. +/// +/// For parameterized lambdas, both the comments between the `lambda` and the parameters, and the +/// comments between the parameters and the body, are considered dangling, as is the case for all +/// of the following: +/// +/// ```python +/// ( +/// lambda # 1 +/// # 2 +/// x +/// : # 3 +/// # 4 +/// y +/// ) +/// ``` +/// +/// For non-parameterized lambdas, all comments before the body are considered dangling, as is the +/// case for all of the following: +/// +/// ```python +/// ( +/// lambda # 1 +/// # 2 +/// : # 3 +/// # 4 +/// y +/// ) +/// ``` +fn handle_lambda_comment<'a>( + comment: DecoratedComment<'a>, + lambda: &'a ast::ExprLambda, + locator: &Locator, +) -> CommentPlacement<'a> { + if let Some(parameters) = lambda.parameters.as_deref() { + // Comments between the `lambda` and the parameters are dangling on the lambda: + // ```python + // ( + // lambda # comment + // x: + // y + // ) + // ``` + if comment.start() < parameters.start() { + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + + // Comments between the parameters and the body are dangling on the lambda: + // ```python + // ( + // lambda x: # comment + // y + // ) + // ``` + if parameters.end() < comment.start() && comment.start() < lambda.body.start() { + // If the value is parenthesized, and the comment is within the parentheses, it should + // be a leading comment on the value, not a dangling comment in the lambda, as in: + // ```python + // ( + // lambda x: ( # comment + // y + // ) + // ) + // ``` + let tokenizer = SimpleTokenizer::new( + locator.contents(), + TextRange::new(parameters.end(), comment.start()), + ); + if tokenizer + .skip_trivia() + .any(|token| token.kind == SimpleTokenKind::LParen) + { + return CommentPlacement::Default(comment); + } + + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + } else { + // Comments between the lambda and the body are dangling on the lambda: + // ```python + // ( + // lambda: # comment + // y + // ) + // ``` + if comment.start() < lambda.body.start() { + // If the value is parenthesized, and the comment is within the parentheses, it should + // be a leading comment on the value, not a dangling comment in the lambda, as in: + // ```python + // ( + // lambda: ( # comment + // y + // ) + // ) + // ``` + let tokenizer = SimpleTokenizer::new( + locator.contents(), + TextRange::new(lambda.start(), comment.start()), + ); + if tokenizer + .skip_trivia() + .any(|token| token.kind == SimpleTokenKind::LParen) + { + return CommentPlacement::Default(comment); + } + + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + } + + CommentPlacement::Default(comment) +} + /// Attach trailing end-of-line comments on the operator as dangling comments on the enclosing /// node. /// diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index 3de50e7668..9e1e7f9402 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,6 +1,7 @@ use ruff_formatter::write; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::ExprLambda; +use ruff_text_size::Ranged; use crate::comments::{dangling_comments, SourceComment}; use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; @@ -24,28 +25,40 @@ impl FormatNodeRule for FormatExprLambda { write!(f, [token("lambda")])?; if let Some(parameters) = parameters { + // In this context, a dangling comment can either be a comment between the `lambda` the + // parameters, or a comment between the parameters and the body. + let (dangling_before_parameters, dangling_after_parameters) = dangling + .split_at(dangling.partition_point(|comment| comment.end() < parameters.start())); + + if dangling_before_parameters.is_empty() { + write!(f, [space()])?; + } else { + write!(f, [dangling_comments(dangling_before_parameters)])?; + } + write!( f, - [ - space(), - parameters - .format() - .with_options(ParametersParentheses::Never), - ] + [parameters + .format() + .with_options(ParametersParentheses::Never)] )?; - } - write!(f, [token(":")])?; + write!(f, [token(":")])?; - if dangling.is_empty() { - write!(f, [space()])?; + if dangling_after_parameters.is_empty() { + write!(f, [space()])?; + } else { + write!(f, [dangling_comments(dangling_after_parameters)])?; + } } else { - write!(f, [dangling_comments(dangling)])?; - } + write!(f, [token(":")])?; - // Insert hard line break if body has leading comment to ensure consistent formatting - if comments.has_leading(body.as_ref()) { - write!(f, [hard_line_break()])?; + // In this context, a dangling comment is a comment between the `lambda` and the body. + if dangling.is_empty() { + write!(f, [space()])?; + } else { + write!(f, [dangling_comments(dangling)])?; + } } write!(f, [body.format()]) diff --git a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs index 2ec759ab5f..1b0cd194e8 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -17,7 +17,7 @@ impl FormatNodeRule for FormatExprNamedExpr { range: _, } = item; - // This context, a dangling comment is an end-of-line comment on the same line as the `:=`. + // This context, a dangling comment is a comment between the `:=` and the value. let comments = f.context().comments().clone(); let dangling = comments.dangling(item); diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap index 7585e7f371..8f615a6511 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__lambda.py.snap @@ -158,7 +158,57 @@ lambda *x\ x: x ) +lambda: ( # comment + x) +( + lambda: # comment + x +) + +( + lambda: + # comment + x +) + +( + lambda # comment + : + x +) + +( + lambda + # comment + : + x +) + +( + lambda: # comment + ( # comment + x + ) +) + +( + lambda # 1 + # 2 + x # 3 + # 4 + : # 5 + # 6 + x +) + +( + lambda + x, + # comment + y: + z +) ``` ## Output @@ -220,8 +270,7 @@ lambda x: lambda y: lambda z: ( # Trailing a = ( - lambda: - # Dangling + lambda: # Dangling 1 ) @@ -268,20 +317,18 @@ lambda a, /, c: a # Dangling comments without parameters. ( - lambda: + lambda: # 3 + None +) + +( + lambda: # 3 None ) ( - lambda: - # 3 - None -) - -( - lambda: - # 1 + lambda: # 1 # 2 # 3 # 4 @@ -289,30 +336,83 @@ lambda a, /, c: a ) ( - lambda # comment + lambda + # comment *x: x ) ( - lambda # comment 1 + lambda + # comment 1 # comment 2 - *x: + *x: # comment 3 x ) ( - lambda # comment 1 + lambda # comment 1 # comment 2 - *x: x # comment 3 + *x: # comment 3 + x ) lambda *x: x ( - lambda # comment + lambda + # comment *x: x ) + +lambda: ( # comment + x +) + +( + lambda: # comment + x +) + +( + lambda: + # comment + x +) + +( + lambda: # comment + x +) + +( + lambda: + # comment + x +) + +( + lambda: # comment + ( # comment + x + ) +) + +( + lambda # 1 + # 2 + x: # 3 + # 4 + # 5 + # 6 + x +) + +( + lambda x, + # comment + y: z +) ```