Don't format trailing comma for lambda arguments (#5946)

**Summary** lambda arguments don't have parentheses, so they shouldn't
get a magic trailing comma either. This fixes some unstable formatting

**Test Plan** Added a regression test.

89 (from previously 145) instances of unstable formatting remaining.

```
$ cargo run --bin ruff_dev --release -- format-dev --stability-check --error-file formatter-ecosystem-errors.txt --multi-project target/checkouts > formatter-ecosystem-progress.txt
$ rg "Unstable formatting" target/formatter-ecosystem-errors.txt | wc -l
89
```

Closes #5892
This commit is contained in:
konsti 2023-07-27 12:32:10 +02:00 committed by Charlie Marsh
parent 40f54375cb
commit 06d9ff9577
5 changed files with 126 additions and 28 deletions

View file

@ -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<Arguments> 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<AnyNodeRef>, 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
}