Add dangling comment handling for lambda expressions (#7493)

## Summary

This PR adds dangling comment handling for `lambda` expressions. In
short, comments around the `lambda` and the `:` are all considered
dangling. Comments that come between the `lambda` and the `:` may be
moved after the colon for simplicity (this is an odd position for a
comment anyway), unless they also precede the lambda parameters, in
which case they're formatted before the parameters.

Closes https://github.com/astral-sh/ruff/issues/7470.

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |
This commit is contained in:
Charlie Marsh 2023-09-19 15:23:51 -04:00 committed by GitHub
parent e07670ad97
commit 4c4eceee36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 310 additions and 33 deletions

View file

@ -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
)

View file

@ -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.
///

View file

@ -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<ExprLambda> 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()])

View file

@ -17,7 +17,7 @@ impl FormatNodeRule<ExprNamedExpr> 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);

View file

@ -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
)
```