Skip to content

unnecessary_fold: lint folding over an Option's iterator#17445

Open
bugprone wants to merge 2 commits into
rust-lang:masterfrom
bugprone:unnecessary-fold-option
Open

unnecessary_fold: lint folding over an Option's iterator#17445
bugprone wants to merge 2 commits into
rust-lang:masterfrom
bugprone:unnecessary-fold-option

Conversation

@bugprone

@bugprone bugprone commented Jul 22, 2026

Copy link
Copy Markdown

Fixes #1658

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 (and the existing test expectations) are unchanged.

Suggestion details:

  • accumulator uses in the closure body are substituted with the init expression, parenthesized when needed
  • machine-applicable when the accumulator is unused, or init is a literal or a Copy binding
  • MaybeIncorrect when substituting duplicates an arbitrary expression (it becomes evaluated only in the Some case, which changes behavior for side-effecting init)
  • omitted entirely when it would move a non-Copy binding twice — no compiling suggestion exists without restructuring (covered in unnecessary_fold_unfixable.rs)
  • the whole suggestion is spliced from source text, so it bails out of suggesting when any part comes from a different syntax context

changelog: [unnecessary_fold]: lint fold over an Option's iterator and suggest map_or

@rustbot rustbot added the S-waiting-on-community-reviews Status: This is awaiting for positive reviews from the community before a maintainer is assigned. label Jul 22, 2026
@rustbot

rustbot commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

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 (S-waiting-on-review and S-waiting-on-author) stays updated, invoking these commands when appropriate:

  • @rustbot author: the review is finished, PR author should check the comments and take action accordingly
  • @rustbot review: the author is ready for a review, this PR will be queued again in the reviewer's queue

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Jul 22, 2026
@rustbot

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.
@bugprone
bugprone force-pushed the unnecessary-fold-option branch from f41dbf8 to 8150f30 Compare July 22, 2026 23:34

@Gri-ffin Gri-ffin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also why not move the logic to a function then have something like this?

if check_standard_fold(...) {
    return;
}
check_fold_option(...);

or something similar instead of tracking state using fired?

View changes since this review

Comment thread tests/ui/unnecessary_fold.fixed Outdated
Comment on lines +228 to +234
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The other folds handle only literals, so why not keep that behavior too? Since handling other cases can be unsafe.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +339 to +341
if !splice_ok {
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Aren't we linting here but giving no suggestion? is this intentional? why not avoid linting at all?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Author

Thanks for the review! All four points addressed in 413edb1:

  • replaced the fired flag with an early return from an extracted check_standard_fold
  • the Option case now only lints when init is a literal or a binding of a Copy type, so every suggestion is MachineApplicable — the MaybeIncorrect tier, the suggestion-less path, and the unnecessary_fold_unfixable test are all gone

@Gri-ffin

Gri-ffin commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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.

@bugprone

bugprone commented Jul 24, 2026

Copy link
Copy Markdown
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-community-reviews Status: This is awaiting for positive reviews from the community before a maintainer is assigned. 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.

don't fold(..) an Option when you can map_or(..)

3 participants