unnecessary_fold: lint folding over an Option's iterator#17445
unnecessary_fold: lint folding over an Option's iterator#17445bugprone wants to merge 2 commits into
Conversation
|
Thanks for the pull request, and welcome! You should hear from one of our reviewers after this PR gets at least 2 reviews from the community. Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (
|
This comment has been minimized.
This comment has been minimized.
Folding over an Option's iterator visits zero or one items, which is map_or in disguise: opt.iter().fold(init, |acc, x| f(acc, x)) is opt.as_ref().map_or(init, |x| f(init, x)) (as_mut for iter_mut, plain map_or for into_iter, which consumes the Option). This extends unnecessary_fold rather than adding a new lint, and only runs when none of the existing any/all/sum/product cases fired, so existing behavior is unchanged. Accumulator uses in the closure body are substituted with the init expression; the suggestion is machine-applicable when the accumulator is unused or init is a literal or a Copy binding, downgraded to MaybeIncorrect when substituting duplicates a re-evaluated expression, and omitted entirely when it would move a non-Copy binding twice.
f41dbf8 to
8150f30
Compare
| // non-trivial init expression: lint, but the suggestion must not duplicate | ||
| // the side effect (not machine-applicable) | ||
| fn compute() -> i32 { | ||
| 42 | ||
| } | ||
| let _ = opt.as_ref().map_or(compute(), |x| compute() + x); | ||
| //~^ unnecessary_fold |
There was a problem hiding this comment.
compute() is evaluated here twice, which can return a different result each time if the function returns a different value on each call, although its marked as MaybeIncorrect, suggesting this can still change the code behavior.
There was a problem hiding this comment.
Agreed — resolved in 413edb1 by no longer linting this case at all: substituting a call re-evaluates it only in the Some case, so there is no correct machine fix. Every emitted suggestion is now MachineApplicable.
| return; | ||
| } | ||
| }, | ||
| _ => Applicability::MaybeIncorrect, |
There was a problem hiding this comment.
The other folds handle only literals, so why not keep that behavior too? Since handling other cases can be unsafe.
There was a problem hiding this comment.
Done in 413edb1, with one deliberate extension: literals plus bindings of Copy types. The issue's motivating example folds with a binding init (opt.iter().fold(my_val, ...)), and reading a Copy binding twice is exactly as safe as duplicating a literal — no move, no side effects. Everything else no longer lints. Happy to narrow to literals only if you or the reviewer prefer; it's a one-line change.
| if !splice_ok { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Aren't we linting here but giving no suggestion? is this intentional? why not avoid linting at all?
There was a problem hiding this comment.
It was intentional (lint the smell even when we can't fix it), but you're right that it's inconsistent with how the rest of this lint behaves — gone in 413edb1. Cases without a correct suggestion are no longer linted.
…turn Restrict the Option-fold case to init expressions that can be duplicated into the closure without changing behavior: literals and bindings of Copy types. Arbitrary expressions (re-evaluated only in the Some case) and non-Copy bindings (moved twice) are no longer linted at all, so every emitted suggestion is machine-applicable and the MaybeIncorrect and suggestion-less paths are gone. Also replace the fired flag with an early return from the extracted check_standard_fold.
|
Thanks for the review! All four points addressed in 413edb1:
|
|
I would personally prefer to have a back and forth with a human if possible, I don't mind if you use LLMs as a tool to help implement an initial draft or to help you understand concepts, but I would prefer to not be the first person to have read this code and I'd much rather discuss things directly with you (even if the implementation isn't right) than debate an LLM. |
|
@Gri-ffin I strongly agree with you. English isn't my first language, so I mainly use an LLM to help with writing. I believe every decision is mine. Btw, I'll try to use my own words. |
Fixes #1658
Folding over an
Option's iterator visits zero or one items, which ismap_orin disguise:(
as_mut()foriter_mut(), plainmap_orforinto_iter(), which consumes theOption.)This extends
unnecessary_foldrather than adding a new lint, and only runs when none of the existingany/all/sum/productcases fired, so existing behavior (and the existing test expectations) are unchanged.Suggestion details:
initexpression, parenthesized when neededinitis a literal or aCopybindingMaybeIncorrectwhen substituting duplicates an arbitrary expression (it becomes evaluated only in theSomecase, which changes behavior for side-effectinginit)Copybinding twice — no compiling suggestion exists without restructuring (covered inunnecessary_fold_unfixable.rs)changelog: [
unnecessary_fold]: lintfoldover anOption's iterator and suggestmap_or