diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index c45bb251e62e32..8cc49c616767d7 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2313,6 +2313,14 @@ impl Parameters { && self.vararg.is_none() && self.kwarg.is_none() } + + pub fn len(&self) -> usize { + self.posonlyargs.len() + + self.args.len() + + usize::from(self.vararg.is_some()) + + self.kwonlyargs.len() + + usize::from(self.kwarg.is_some()) + } } /// An alternative type of AST `arg`. This is used for each function argument that might have a default value. diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.options.json new file mode 100644 index 00000000000000..92e14f0a45903b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/lambda.options.json @@ -0,0 +1,8 @@ +[ + { + "preview": "disabled" + }, + { + "preview": "enabled" + } +] 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 3677fedb708b54..41007d3381476f 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 @@ -125,6 +125,13 @@ def f( *x: x ) +( + lambda + # comment + *x, + **y: x +) + ( lambda # comment 1 @@ -135,6 +142,17 @@ def f( x ) +( + lambda + # comment 1 + * + # comment 2 + x, + **y: + # comment 3 + x +) + ( lambda # comment 1 * # comment 2 @@ -142,6 +160,14 @@ def f( x ) +( + lambda # comment 1 + * # comment 2 + x, + y: # comment 3 + x +) + lambda *x\ :x @@ -196,6 +222,17 @@ def f( x ) +( + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + ( lambda x, @@ -203,3 +240,93 @@ def f( y: z ) + + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z +] # Trailing +# Trailing + +lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d + +# Regression tests for https://github.com/astral-sh/ruff/issues/8179 +def a(): + return b( + c, + d, + e, + f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs + ), + ) + +def a(): + return b( + c, + d, + e, + f=lambda self, araa, kkkwargs,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + args,kwargs, + e=1, f=2, g=2: d, + g = 10 + ) + diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 01caed9e6854d8..c63a03db9bfdd6 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -43,6 +43,14 @@ impl<'a> PyFormatContext<'a> { pub(crate) fn comments(&self) -> &Comments<'a> { &self.comments } + + pub(crate) const fn is_preview(&self) -> bool { + self.options.preview().is_enabled() + } + + pub(crate) const fn is_stable(&self) -> bool { + !self.is_preview() + } } impl FormatContext for PyFormatContext<'_> { diff --git a/crates/ruff_python_formatter/src/expression/expr_lambda.rs b/crates/ruff_python_formatter/src/expression/expr_lambda.rs index f2487fd6a488a8..02b314e9d4af5a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_lambda.rs +++ b/crates/ruff_python_formatter/src/expression/expr_lambda.rs @@ -1,10 +1,11 @@ -use ruff_formatter::write; +use ruff_formatter::{format_args, write}; use ruff_python_ast::AnyNodeRef; use ruff_python_ast::ExprLambda; use ruff_text_size::Ranged; -use crate::comments::{dangling_comments, SourceComment}; -use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses}; +use crate::comments::{dangling_comments, leading_comments, SourceComment}; +use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses, Parenthesize}; +use crate::expression::{has_own_parentheses, maybe_parenthesize_expression}; use crate::other::parameters::ParametersParentheses; use crate::prelude::*; @@ -25,31 +26,49 @@ 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 + // In this context, a dangling comment can either be a comment between the `lambda` and 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, - [parameters - .format() - .with_options(ParametersParentheses::Never)] - )?; + group(&format_with(|f: &mut PyFormatter| { + if f.context().node_level().is_parenthesized() + && (parameters.len() > 1 || !dangling_before_parameters.is_empty()) + { + let end_of_line_start = dangling_before_parameters + .partition_point(|comment| comment.line_position().is_end_of_line()); + let (same_line_comments, own_line_comments) = + dangling_before_parameters.split_at(end_of_line_start); - write!(f, [token(":")])?; + dangling_comments(same_line_comments).fmt(f)?; - if dangling_after_parameters.is_empty() { - write!(f, [space()])?; - } else { - write!(f, [dangling_comments(dangling_after_parameters)])?; - } + soft_block_indent(&format_args![ + leading_comments(own_line_comments), + parameters + .format() + .with_options(ParametersParentheses::Never), + ]) + .fmt(f) + } else { + parameters + .format() + .with_options(ParametersParentheses::Never) + .fmt(f) + }?; + + token(":").fmt(f)?; + + if dangling_after_parameters.is_empty() { + space().fmt(f) + } else { + dangling_comments(dangling_after_parameters).fmt(f) + } + })) + .fmt(f)?; } else { write!(f, [token(":")])?; @@ -61,7 +80,12 @@ impl FormatNodeRule for FormatExprLambda { } } - write!(f, [body.format()]) + // Avoid parenthesizing lists, dictionaries, etc. + if f.context().is_stable() || has_own_parentheses(body, f.context()).is_some() { + body.format().fmt(f) + } else { + maybe_parenthesize_expression(body, item, Parenthesize::IfBreaksOrIfRequired).fmt(f) + } } fn fmt_dangling_comments( 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 3fe831c3a0b9ad..aaf71fcbfa7102 100644 --- a/crates/ruff_python_formatter/src/expression/expr_named_expr.rs +++ b/crates/ruff_python_formatter/src/expression/expr_named_expr.rs @@ -69,6 +69,7 @@ impl NeedsParentheses for ExprNamedExpr { || parent.is_stmt_delete() || parent.is_stmt_for() || parent.is_stmt_function_def() + || parent.is_expr_lambda() { OptionalParentheses::Always } else { diff --git a/crates/ruff_python_formatter/src/options.rs b/crates/ruff_python_formatter/src/options.rs index b39c7b6ff79c64..eac037b3c067d0 100644 --- a/crates/ruff_python_formatter/src/options.rs +++ b/crates/ruff_python_formatter/src/options.rs @@ -108,7 +108,7 @@ impl PyFormatOptions { self.line_ending } - pub fn preview(&self) -> PreviewMode { + pub const fn preview(&self) -> PreviewMode { self.preview } diff --git a/crates/ruff_python_formatter/src/other/parameters.rs b/crates/ruff_python_formatter/src/other/parameters.rs index 5e67e579de7288..0a50f538f4ad51 100644 --- a/crates/ruff_python_formatter/src/other/parameters.rs +++ b/crates/ruff_python_formatter/src/other/parameters.rs @@ -102,7 +102,15 @@ impl FormatNodeRule for FormatParameters { dangling.split_at(parenthesis_comments_end); let format_inner = format_with(|f: &mut PyFormatter| { - let separator = format_with(|f| write!(f, [token(","), soft_line_break_or_space()])); + let separator = format_with(|f: &mut PyFormatter| { + token(",").fmt(f)?; + + if f.context().node_level().is_parenthesized() { + soft_line_break_or_space().fmt(f) + } else { + space().fmt(f) + } + }); let mut joiner = f.join_with(separator); let mut last_node: Option = None; @@ -232,23 +240,19 @@ impl FormatNodeRule for FormatParameters { Ok(()) }); - let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); - - let num_parameters = posonlyargs.len() - + args.len() - + usize::from(vararg.is_some()) - + kwonlyargs.len() - + usize::from(kwarg.is_some()); + let num_parameters = item.len(); if self.parentheses == ParametersParentheses::Never { - write!(f, [group(&format_inner), dangling_comments(dangling)]) + write!(f, [format_inner, dangling_comments(dangling)]) } else if num_parameters == 0 { + let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); // No parameters, format any dangling comments between `()` write!(f, [empty_parenthesized("(", dangling, ")")]) } else { // Intentionally avoid `parenthesized`, which groups the entire formatted contents. // We want parameters to be grouped alongside return types, one level up, so we // format them "inline" here. + let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); write!( f, [ 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 8f615a65114234..214018e1737980 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 @@ -131,6 +131,13 @@ lambda a, /, c: a *x: x ) +( + lambda + # comment + *x, + **y: x +) + ( lambda # comment 1 @@ -141,6 +148,17 @@ lambda a, /, c: a x ) +( + lambda + # comment 1 + * + # comment 2 + x, + **y: + # comment 3 + x +) + ( lambda # comment 1 * # comment 2 @@ -148,6 +166,14 @@ lambda a, /, c: a x ) +( + lambda # comment 1 + * # comment 2 + x, + y: # comment 3 + x +) + lambda *x\ :x @@ -202,6 +228,17 @@ lambda: ( # comment x ) +( + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + ( lambda x, @@ -209,9 +246,109 @@ lambda: ( # comment y: z ) + + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z +] # Trailing +# Trailing + +lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), e=1, f=2, g=2: d + +# Regression tests for https://github.com/astral-sh/ruff/issues/8179 +def a(): + return b( + c, + d, + e, + f=lambda self, *args, **kwargs: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs + ), + ) + +def a(): + return b( + c, + d, + e, + f=lambda self, araa, kkkwargs,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + args,kwargs, + e=1, f=2, g=2: d, + g = 10 + ) + +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +magic-trailing-comma = Respect +preview = Disabled ``` -## Output ```py # Leading lambda x: x # Trailing @@ -275,8 +412,10 @@ a = ( ) a = ( - lambda x, # Dangling - y: 1 + lambda + x, # Dangling + y + : 1 ) # Regression test: lambda empty arguments ranges were too long, leading to unstable @@ -337,23 +476,54 @@ lambda a, /, c: a ( lambda - # comment - *x: x + # comment + *x + : x ) ( lambda - # comment 1 - # comment 2 - *x: + # comment + *x, + **y + : x +) + +( + lambda + # comment 1 + # comment 2 + *x + : + # comment 3 + x +) + +( + lambda + # comment 1 + # comment 2 + *x, + **y + : # comment 3 x ) ( lambda # comment 1 - # comment 2 - *x: # comment 3 + # comment 2 + *x + : # comment 3 + x +) + +( + lambda # comment 1 + # comment 2 + *x, + y + : # comment 3 x ) @@ -361,8 +531,9 @@ lambda *x: x ( lambda - # comment - *x: x + # comment + *x + : x ) lambda: ( # comment @@ -400,8 +571,9 @@ lambda: ( # comment ( lambda # 1 - # 2 - x: # 3 + # 2 + x + : # 3 # 4 # 5 # 6 @@ -409,10 +581,481 @@ lambda: ( # comment ) ( - lambda x, + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + +( + lambda + x, + # comment + y + : z +) + + +# Leading +lambda x: ( + lambda y: lambda z: x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z # Trailing +) # Trailing + + +# Leading +lambda x: lambda y: lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z, +] # Trailing +# Trailing + +lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs +), e=1, f=2, g=2: d + + +# Regression tests for https://github.com/astral-sh/ruff/issues/8179 +def a(): + return b( + c, + d, + e, + f=lambda + self, + *args, + **kwargs + : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), + ) + + +def a(): + return b( + c, + d, + e, + f=lambda + self, + araa, + kkkwargs, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + args, + kwargs, + e=1, + f=2, + g=2 + : d, + g=10, + ) +``` + + +### Output 2 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +magic-trailing-comma = Respect +preview = Enabled +``` + +```py +# Leading +lambda x: x # Trailing +# Trailing + +# Leading +lambda x, y: x # Trailing +# Trailing + +# Leading +lambda x, y: x, y # Trailing +# Trailing + +# Leading +lambda x, /, y: x # Trailing +# Trailing + +# Leading +lambda x: lambda y: lambda z: x # Trailing +# Trailing + +# Leading +lambda x: lambda y: lambda z: (x, y, z) # Trailing +# Trailing + +# Leading +lambda x: lambda y: lambda z: (x, y, z) # Trailing +# Trailing + +# Leading +lambda x: ( + lambda y: ( + lambda z: (x, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, y, z) + ) +) # Trailing +# Trailing + +a = ( + lambda: # Dangling + 1 +) + +a = ( + lambda + x, # Dangling + y + : 1 +) + +# Regression test: lambda empty arguments ranges were too long, leading to unstable +# formatting +( + lambda: ( # + ), +) + + +# lambda arguments don't have parentheses, so we never add a magic trailing comma ... +def f( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = lambda x: ( + y + ), +): + pass + + +# ...but we do preserve a trailing comma after the arguments +a = lambda b,: 0 + +lambda a,: 0 +lambda *args,: 0 +lambda **kwds,: 0 +lambda a, *args,: 0 +lambda a, **kwds,: 0 +lambda *args, b,: 0 +lambda *, b,: 0 +lambda *args, **kwds,: 0 +lambda a, *args, b,: 0 +lambda a, *, b,: 0 +lambda a, *args, **kwds,: 0 +lambda *args, b, **kwds,: 0 +lambda *, b, **kwds,: 0 +lambda a, *args, b, **kwds,: 0 +lambda a, *, b, **kwds,: 0 +lambda a, /: a +lambda a, /, c: a + +# Dangling comments without parameters. +( + lambda: # 3 + None +) + +( + lambda: + # 3 + None +) + +( + lambda: # 1 + # 2 + # 3 + # 4 + None # 5 +) + +( + lambda + # comment + *x + : x +) + +( + lambda + # comment + *x, + **y + : x +) + +( + lambda + # comment 1 + # comment 2 + *x + : + # comment 3 + x +) + +( + lambda + # comment 1 + # comment 2 + *x, + **y + : + # comment 3 + x +) + +( + lambda # comment 1 + # comment 2 + *x + : # comment 3 + x +) + +( + lambda # comment 1 + # comment 2 + *x, + y + : # comment 3 + x +) + +lambda *x: x + +( + lambda + # comment + *x + : x +) + +lambda: ( # comment + x +) + +( + lambda: # comment + x +) + +( + lambda: + # comment + x +) + +( + lambda: # comment + x +) + +( + lambda: # comment - y: z + x +) + +( + lambda: # comment + ( # comment + x + ) ) + +( + lambda # 1 + # 2 + x + : # 3 + # 4 + # 5 + # 6 + x +) + +( + lambda # 1 + # 2 + x, # 3 + # 4 + y + : # 5 + # 6 + x +) + +( + lambda + x, + # comment + y + : z +) + + +# Leading +lambda x: ( + lambda y: ( + lambda z: ( + x + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + y + + z + ) + ) # Trailing +) # Trailing + + +# Leading +lambda x: ( + lambda y: ( + lambda z: [ + x, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + y, + z, + ] + ) +) # Trailing +# Trailing + +lambda self, araa, kkkwargs=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + *args, **kwargs +), e=1, f=2, g=2: d + + +# Regression tests for https://github.com/astral-sh/ruff/issues/8179 +def a(): + return b( + c, + d, + e, + f=lambda + self, + *args, + **kwargs + : aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(*args, **kwargs), + ) + + +def a(): + return b( + c, + d, + e, + f=lambda + self, + araa, + kkkwargs, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, + args, + kwargs, + e=1, + f=2, + g=2 + : d, + g=10, + ) ```