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 15a59fe11c..a52616daae 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 @@ -66,3 +66,12 @@ a = ( # 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 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py index 1c138f163d..b91681ace8 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic_trailing_comma.py @@ -20,3 +20,10 @@ "seventh entry", "eighth entry", ) + +# Regression test: Respect setting in Arguments formatting +def f(a): pass +def g(a,): pass + +x1 = lambda y: 1 +x2 = lambda y,: 1 diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index d667b5190b..15e7e7e58a 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -5,7 +5,7 @@ use ruff_text_size::{TextRange, TextSize}; use ruff_formatter::{format_args, write, FormatRuleWithOptions}; use ruff_python_ast::node::{AnyNodeRef, AstNode}; -use ruff_python_trivia::{first_non_trivia_token, SimpleToken, SimpleTokenKind, SimpleTokenizer}; +use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer}; use crate::comments::{ dangling_comments, leading_comments, leading_node_comments, trailing_comments, @@ -160,34 +160,31 @@ impl FormatNodeRule for FormatArguments { joiner.finish()?; - write!(f, [if_group_breaks(&text(","))])?; + // Functions use the regular magic trailing comma logic, lambdas may or may not have + // a trailing comma but it's just preserved without any magic. + // ```python + // # Add magic trailing comma if its expands + // def f(a): pass + // # Expands if magic trailing comma setting is respect, otherwise remove the comma + // def g(a,): pass + // # Never expands + // x1 = lambda y: 1 + // # Never expands, the comma is always preserved + // x2 = lambda y,: 1 + // ``` + if self.parentheses == ArgumentsParentheses::SkipInsideLambda { + // For lambdas (no parentheses), preserve the trailing comma. It doesn't + // behave like a magic trailing comma, it's just preserved + if has_trailing_comma(item, last_node, f.context().source()) { + write!(f, [text(",")])?; + } + } else { + write!(f, [if_group_breaks(&text(","))])?; - // Expand the group if the source has a trailing *magic* comma. - if let Some(last_node) = last_node { - let ends_with_pos_only_argument_separator = !posonlyargs.is_empty() - && args.is_empty() - && vararg.is_none() - && kwonlyargs.is_empty() - && kwarg.is_none(); - - let maybe_comma_token = if ends_with_pos_only_argument_separator { - // `def a(b, c, /): ... ` - let mut tokens = - SimpleTokenizer::starts_at(last_node.end(), f.context().source()) - .skip_trivia(); - - let comma = tokens.next(); - assert!(matches!(comma, Some(SimpleToken { kind: SimpleTokenKind::Comma, .. })), "The last positional only argument must be separated by a `,` from the positional only arguments separator `/` but found '{comma:?}'."); - - let slash = tokens.next(); - assert!(matches!(slash, Some(SimpleToken { kind: SimpleTokenKind::Slash, .. })), "The positional argument separator must be present for a function that has positional only arguments but found '{slash:?}'."); - - tokens.next() - } else { - first_non_trivia_token(last_node.end(), f.context().source()) - }; - - if maybe_comma_token.map_or(false, |token| token.kind() == SimpleTokenKind::Comma) { + if f.options().magic_trailing_comma().is_respect() + && has_trailing_comma(item, last_node, f.context().source()) + { + // Make the magic trailing comma expand the group write!(f, [hard_line_break()])?; } } @@ -591,3 +588,33 @@ pub(crate) enum ArgumentSeparatorCommentLocation { StarLeading, StarTrailing, } + +fn has_trailing_comma(arguments: &Arguments, last_node: Option, source: &str) -> bool { + // No nodes, no trailing comma + let Some(last_node) = last_node else { + return false; + }; + + let ends_with_pos_only_argument_separator = !arguments.posonlyargs.is_empty() + && arguments.args.is_empty() + && arguments.vararg.is_none() + && arguments.kwonlyargs.is_empty() + && arguments.kwarg.is_none(); + + let mut tokens = SimpleTokenizer::starts_at(last_node.end(), source).skip_trivia(); + // `def a(b, c, /): ... ` + // The slash lacks its own node + if ends_with_pos_only_argument_separator { + let comma = tokens.next(); + assert!(matches!(comma, Some(SimpleToken { kind: SimpleTokenKind::Comma, .. })), "The last positional only argument must be separated by a `,` from the positional only arguments separator `/` but found '{comma:?}'."); + + let slash = tokens.next(); + assert!(matches!(slash, Some(SimpleToken { kind: SimpleTokenKind::Slash, .. })), "The positional argument separator must be present for a function that has positional only arguments but found '{slash:?}'."); + } + + tokens + .next() + .expect("There must be a token after the argument list") + .kind() + == SimpleTokenKind::Comma +} 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 59326cf719..fbc6c3ac76 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 @@ -72,6 +72,15 @@ a = ( # 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 ``` ## Output @@ -143,6 +152,17 @@ a = ( 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 ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap index f3613924cc..f0beff4915 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@skip_magic_trailing_comma.py.snap @@ -26,6 +26,13 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/skip_magic "seventh entry", "eighth entry", ) + +# Regression test: Respect setting in Arguments formatting +def f(a): pass +def g(a,): pass + +x1 = lambda y: 1 +x2 = lambda y,: 1 ``` ## Outputs @@ -56,6 +63,21 @@ magic-trailing-comma = Respect "seventh entry", "eighth entry", ) + + +# Regression test: Respect setting in Arguments formatting +def f(a): + pass + + +def g( + a, +): + pass + + +x1 = lambda y: 1 +x2 = lambda y,: 1 ``` @@ -82,6 +104,19 @@ magic-trailing-comma = Ignore "seventh entry", "eighth entry", ) + + +# Regression test: Respect setting in Arguments formatting +def f(a): + pass + + +def g(a): + pass + + +x1 = lambda y: 1 +x2 = lambda y,: 1 ```