Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lint precedence possible ambiguity between closure and method call #14421

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

samueltardieu
Copy link
Contributor

@samueltardieu samueltardieu commented Mar 16, 2025

|x: String| {x}.clone() applies clone() to {x}, while |x: String| -> String {x}.clone() applies .clone() to the closure because a closure with an explicit return type requires a block as its body.

This extends the precedence lint to suggest using parentheses around the closure definition in the second case, when .clone() would apply to the whole closure.

changelog: [precedence]: warn about ambiguity when a closure is used as a method call receiver

Related discussion

@rustbot
Copy link
Collaborator

rustbot commented Mar 16, 2025

r? @Alexendoo

rustbot has assigned @Alexendoo.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Mar 16, 2025
}

fn closure_method_call() {
let f = move |x: W| { { x }.clone() };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the lint would conflict with the following lint for which lang FCP has been proposed:

Probably what I'd suggest instead is annotating the return type to make the outer block braces required:

Suggested change
let f = move |x: W| { { x }.clone() };
let f = move |x: W| -> _ { { x }.clone() };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As rust-lang/rust#136906 (comment) mentions rustfmt will remove the extra braces from |x: W| { { x }.clone() } so we definitely can't suggest that

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a proposal about how to handle that described here and here.

Comment on lines +56 to +62
let f = (move |x: W| -> _ { x }).clone();
assert!(matches!(f(W(0)), W(0)));
//~^^ precedence
Copy link

@traviscross traviscross Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let f = (move |x: W| -> _ { x }).clone();
assert!(matches!(f(W(0)), W(0)));
//~^^ precedence

I'd prefer we not do this one. In my model, the one that's "wrong" is the |x| { .. }.something() case, because it's reasonable to assume that opening brace triggers the parsing of a block, when it doesn't. This one, by contrast, does the "right" thing, and with the annotated return type there, which then requires those braces, I think it's reasonably clear to the reader that the closure body must terminate with the closing brace.

That is, by linting against the one that's "wrong", as above, and suggesting the return type be annotated to make it unambiguous, probably we've solved the problem.

There's also a practical reason I don't want us pushing the parens here. I'm interested in us later implementing IntoFuture for thunk async closures. This would then allow writing async || -> _ { .. }.into_future(), and I think it'd be better ergonomics for those to not require a set of parens around them to please clippy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one, by contrast, does the "right" thing

I find async || -> _ { .. }.into_future() quite ambiguous and would have a hard time parsing it if I saw it in code, so I am in favor of requiring more explicit parentheses here. Or rather, I'd probably write this as IntoFuture::into_future(async || { ... }).

Copy link

@traviscross traviscross Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I find that || -> _ { .. }(), which we accept without linting, feels the same to me, so I'd prefer to lint against both or neither.

And for both, I feel like they look worse in abstract form than they would often in practice, e.g. with the block split over multiple lines or the return type ascribed in other ways.

Copy link

@traviscross traviscross Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broader point is that || -> _ { .. }// anything here shouldn't feel ambiguous to us because the only thing that can follow the -> _ is a block, so it's completely unambiguous where that block and therefore that closure body ends.

The only reason that it might feel ambiguous to us (I claim) is because of what we did with || { .., where the opening brace doesn't force block parsing. I would like us to move away from that. Perhaps we could lint hard against anything that relies on it. Maybe over an edition we could change the rule.

My hope is that in pushing in that direction, || -> _ { .. }// anything here won't feel ambiguous to us any longer.

Copy link
Member

@RalfJung RalfJung Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

|| -> _ { .. }// anything here shouldn't feel ambiguous to us because the only thing that can follow the -> _ is a block

The fact that -> _ affects parsing like that is arcane knowledge most people don't have. I heard about it the first time ~2 weeks ago. The entire point of this PR is to make it so people can parse code correctly regardless of whether they possess this arcane knowledge or not.

The only reason that it might feel ambiguous to us (I claim) is because of what we did with || { .., where the opening brace doesn't force block parsing. I would like us to move away from that. Perhaps we could lint hard against anything that relies on it. Maybe over an edition we could change the rule.

Ah, so you are saying we could make || -> _ { actually consistent with || {? Yeah that seems nice, and should definitely be possible with an edition transition.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly.

On the other, all I'm saying is that people know that || -> u64 42 doesn't work, and I don't think people expect it to, and so I think their normal intuitions about blocks kick in. What messes this up and makes it arcane is the other one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normal intuitions about blocks kick in

My intuition is surprised whenever these end of block cases pop up, for example that the parens are required in

fn f() -> Y {
    (unsafe { x } as Y)
}

Similarly, the current parsing of |x: String| {x}.clone() matches my intuition

@Alexendoo Alexendoo added the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label Mar 30, 2025
A discrepancy between the `cmake` version available on the runners and
the one required by the `aws-lc-sys` dependency prevents the crate from
building.
`|x: String| {x}.clone()` applies `clone()` to {x}, while
`|x: String| -> String {x}.clone()` applies `.clone()` to
the closure because a closure with an explicit return type
requires a block.

This extends the `precedence` lint to suggest using parentheses
around the closure definition when the closure would be cloned,
as in `(|x: String| -> String {x}).clone()`.
@samueltardieu
Copy link
Contributor Author

The current state of the proposed change right now is to suggests using parentheses when the method call would apply to the closure itself, and only in this case. That would be, in the top example, (|x: String| -> String { x }).clone().

@samueltardieu samueltardieu removed the I-nominated Issue: Nominated to be discussed at the next Clippy meeting label Apr 1, 2025
@traviscross
Copy link

There's also a proposal on the table here for rustfmt to disambiguate closures that start with an inner block and then have an ambiguous end.

With that in place the following

fn main() {
    _ = || { { 0 }.clone() };
}

is formatted like this.

fn main() {
    _ = || {
        { 0 }.clone()
    };

@RalfJung
Copy link
Member

RalfJung commented Apr 2, 2025

There's also a proposal on the table rust-lang/rust#136906 (comment) for rustfmt to disambiguate closures that start with an inner block and then have an ambiguous end.

The original form there is already disambiguated, so I don't think this helps with the problem this lint is trying to address.

EDIT: Oh I see this is about now having rustfmt work against disambiguation here. We'd still need the lint, but the lint can now suggest to add braces since rustfmt will not remove them any more. Is that what you mean?

@traviscross
Copy link

We'd still need the lint, but the lint can now suggest to add braces since rustfmt will not remove them any more. Is that what you mean?

That's right.

@samueltardieu
Copy link
Contributor Author

There's also a proposal on the table rust-lang/rust#136906 (comment) for rustfmt to disambiguate closures that start with an inner block and then have an ambiguous end.

The original form there is already disambiguated, so I don't think this helps with the problem this lint is trying to address.

EDIT: Oh I see this is about now having rustfmt work against disambiguation here. We'd still need the lint, but the lint can now suggest to add braces since rustfmt will not remove them any more. Is that what you mean?

@samueltardieu
Copy link
Contributor Author

Sorry, closed by mistake and reopened.

I'll mark this PR as blocked until the rustfmt one is closed. If it's merged, I'll readd add-ing brackets around the block.

@rustbot label +S-blocked +S-waiting-on-author -S-waiting-on-review

@rustbot rustbot added S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Apr 2, 2025
@samueltardieu samueltardieu removed the S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) label Apr 2, 2025
@Alexendoo
Copy link
Member

Doesn't need to be considered blocked, we can revisit the braces case at a later date separately

@samueltardieu
Copy link
Contributor Author

Doesn't need to be considered blocked, we can revisit the braces case at a later date separately

Sure.
@rustbot label -S-blocked +S-waiting-on-review

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties and removed S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work labels Apr 2, 2025
@Alexendoo
Copy link
Member

Did a small survey with some GitHub searches, I managed to find three cases in the wild:

Interestingly the last one has (|| {}).foo() and || -> _ {}.foo() in the same file

So an in context comparison would be

        |stream: ParseStream| -> syn::Result<Self> { Self::parse(stream) }.parse2(input)
        (|stream: ParseStream| -> syn::Result<Self> { Self::parse(stream) }).parse2(input)

|| -> _ { .. }() is much more common https://github.com/search?q=lang%3Ars+%2F%5C%7C+-%3E.*%5C%7B.*%5C%7D%5C%28%5B%5E%7B%7D%5Cn%5D*%24%2F&type=code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants