From 5e9c2c825e57da7a612338a6e121ccc9f39930b2 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 11:50:23 -0500 Subject: [PATCH 1/6] Fix comment placement in lambda parameters Summary -- This PR makes two changes to comment placement in lambda paramters. First, we now insert a line break if the first parameter has a leading comment: ```py ( lambda * # comment 2 x: x ) ( lambda # comment 2 *x: x ) ( lambda # comment 2 *x: x ) ``` Note the missing space in the output from main. This case is currently unstable on main. Also note that the new formatting is more consistent with our stable formatting in cases where the lambda has its own dangling comment: ```py ( lambda # comment 1 * # comment 2 x: x ) ( lambda # comment 1 # comment 2 *x: x ) ``` and when a parameter without a comment precedes the split `*x`: ```py ( lambda y, * # comment 2 x: x ) ( lambda y, # comment 2 *x: x ) ``` Second, this PR modifies the comment placement such that `# comment 2` in these outputs is still a leading comment on the parameter. This is also not the case on main, where it becomes a dangling lambda comment. This doesn't cause any instability that I'm aware of on main, but it does cause problems when trying to adjust the placement of dangling lambda comments in #21385. Changing the placement in this way should not affect the formatting here. Test Plan -- New lambda tests, plus existing tests covering the cases above with multiple comments around the parameters (see lambda.py 122-143, and 122-205 or so more broadly) From 8a97f33f6251341f4b946e183201a26b8ba9bf19 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 11:22:26 -0500 Subject: [PATCH 2/6] add unstable test case the first of these is unstable because it formats as: ```py ( lambda # comment 2 *x: x ) ``` without the second space before the comment. As a leading comment on the `*x` parameter, it should instead format like: ```py ( lambda # comment 2 *x: x ) ``` which is also consistent with its formatting in the presence of an additional dangling comment on the lambda itself. this is the second test case added here, which formats (stably) as: ```py ( lambda # comment 1 # comment 2 *x: x ) ``` --- .../test/fixtures/ruff/expression/lambda.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 4a7090ff1329e9..72602ebea7081a 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 @@ -228,3 +228,16 @@ def a(): g = 10 ) +( + lambda + * # comment 2 + x: + x +) + +( + lambda # comment 1 + * # comment 2 + x: + x +) From 4e10b29571addf00eb469f03dd7bb381f2444b0c Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 11:28:17 -0500 Subject: [PATCH 3/6] insert a hard line break if the first parameter has leading comments --- .../src/expression/expr_lambda.rs | 10 ++++++- .../format@expression__lambda.py.snap | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index c5890fba248c0c..99d649b31b9233 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -32,7 +32,15 @@ impl FormatNodeRule for FormatExprLambda { .split_at(dangling.partition_point(|comment| comment.end() < parameters.start())); if dangling_before_parameters.is_empty() { - write!(f, [space()])?; + if parameters + .iter() + .next() + .is_some_and(|parameter| comments.has_leading(parameter.as_parameter())) + { + hard_line_break().fmt(f)?; + } else { + write!(f, [space()])?; + } } else { write!(f, [dangling_comments(dangling_before_parameters)])?; } 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 03332c6f924e43..71b2ad26f1e938 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 @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.py -snapshot_kind: text --- ## Input ```python @@ -235,6 +234,19 @@ def a(): g = 10 ) +( + lambda + * # comment 2 + x: + x +) + +( + lambda # comment 1 + * # comment 2 + x: + x +) ``` ## Output @@ -473,4 +485,17 @@ def a(): g=2: d, g=10, ) + + +( + lambda + # comment 2 + *x: x +) + +( + lambda # comment 1 + # comment 2 + *x: x +) ``` From 001cdeeafc8259aac6701114091949bafce864ef Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 11:29:26 -0500 Subject: [PATCH 4/6] add another test case --- .../test/fixtures/ruff/expression/lambda.py | 8 ++++++++ .../snapshots/format@expression__lambda.py.snap | 15 +++++++++++++++ 2 files changed, 23 insertions(+) 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 72602ebea7081a..660d5644e9df0e 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 @@ -241,3 +241,11 @@ def a(): x: x ) + +( + lambda # comment 1 + y, + * # comment 2 + x: + x +) 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 71b2ad26f1e938..3009dfaefccefb 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 @@ -247,6 +247,14 @@ def a(): x: x ) + +( + lambda # comment 1 + y, + * # comment 2 + x: + x +) ``` ## Output @@ -498,4 +506,11 @@ def a(): # comment 2 *x: x ) + +( + lambda # comment 1 + y, + # comment 2 + *x: x +) ``` From f71c5eebb7a67b5e247a62b50af4b19de5fef598 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 11:49:54 -0500 Subject: [PATCH 5/6] add docs --- .../src/expression/expr_lambda.rs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index 99d649b31b9233..335f1123234662 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -32,6 +32,60 @@ impl FormatNodeRule for FormatExprLambda { .split_at(dangling.partition_point(|comment| comment.end() < parameters.start())); if dangling_before_parameters.is_empty() { + // If the first parameter has a leading comment, insert a hard line break. This + // comment is associated as a leading comment on the first parameter: + // + // ```py + // ( + // lambda + // * # comment + // x: + // x + // ) + // ``` + // + // so a hard line break is needed to avoid formatting it like: + // + // ```py + // ( + // lambda # comment + // *x: x + // ) + // ``` + // + // which is unstable because it's missing the second space before the comment. + // + // Inserting the line break causes it to format like: + // + // ```py + // ( + // lambda + // # comment + // *x :x + // ) + // ``` + // + // which is also consistent with the formatting in the presence of an actual + // dangling comment on the lambda: + // + // ```py + // ( + // lambda # comment 1 + // * # comment 2 + // x: + // x + // ) + // ``` + // + // formats to: + // + // ```py + // ( + // lambda # comment 1 + // # comment 2 + // *x: x + // ) + // ``` if parameters .iter() .next() From 0ea93347d636c0b0143bb7d14abc25543695b0c5 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Tue, 9 Dec 2025 12:22:13 -0500 Subject: [PATCH 6/6] make own-line comments before lambda parameters leading on parameter --- .../src/comments/placement.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index f28f9b18a868a9..28397b6dcfcf80 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -1816,7 +1816,7 @@ fn handle_lambda_comment<'a>( source: &str, ) -> CommentPlacement<'a> { if let Some(parameters) = lambda.parameters.as_deref() { - // Comments between the `lambda` and the parameters are dangling on the lambda: + // End-of-line comments between the `lambda` and the parameters are dangling on the lambda: // ```python // ( // lambda # comment @@ -1824,8 +1824,24 @@ fn handle_lambda_comment<'a>( // y // ) // ``` + // + // But own-line comments are leading on the first parameter, if it exists: + // ```python + // ( + // lambda + // # comment + // x: + // y + // ) + // ``` if comment.start() < parameters.start() { - return CommentPlacement::dangling(comment.enclosing_node(), comment); + return if let Some(first) = parameters.iter().next() + && comment.line_position().is_own_line() + { + CommentPlacement::leading(first.as_parameter(), comment) + } else { + CommentPlacement::dangling(comment.enclosing_node(), comment) + }; } // Comments between the parameters and the body are dangling on the lambda: