Skip to content

Commit 7e16f9f

Browse files
authored
Rollup merge of #99696 - WaffleLapkin:uplift, r=fee1-dead
Uplift `clippy::for_loops_over_fallibles` lint into rustc This PR, as the title suggests, uplifts [`clippy::for_loops_over_fallibles`] lint into rustc. This lint warns for code like this: ```rust for _ in Some(1) {} for _ in Ok::<_, ()>(1) {} ``` i.e. directly iterating over `Option` and `Result` using `for` loop. There are a number of suggestions that this PR adds (on top of what clippy suggested): 1. If the argument (? is there a better name for that expression) of a `for` loop is a `.next()` call, then we can suggest removing it (or rather replacing with `.by_ref()` to allow iterator being used later) ```rust for _ in iter.next() {} // turns into for _ in iter.by_ref() {} ``` 2. (otherwise) We can suggest using `while let`, this is useful for non-iterator, iterator-like things like [async] channels ```rust for _ in rx.recv() {} // turns into while let Some(_) = rx.recv() {} ``` 3. If the argument type is `Result<impl IntoIterator, _>` and the body has a `Result<_, _>` type, we can suggest using `?` ```rust for _ in f() {} // turns into for _ in f()? {} ``` 4. To preserve the original behavior and clear intent, we can suggest using `if let` ```rust for _ in f() {} // turns into if let Some(_) = f() {} ``` (P.S. `Some` and `Ok` are interchangeable depending on the type) I still feel that the lint wording/look is somewhat off, so I'll be happy to hear suggestions (on how to improve suggestions :D)! Resolves #99272 [`clippy::for_loops_over_fallibles`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles
2 parents e495b37 + 9d4edff commit 7e16f9f

26 files changed

+381
-366
lines changed

compiler/rustc_ast/src/visit.rs

+6-8
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,12 @@ pub trait Visitor<'ast>: Sized {
244244

245245
#[macro_export]
246246
macro_rules! walk_list {
247-
($visitor: expr, $method: ident, $list: expr) => {
248-
for elem in $list {
249-
$visitor.$method(elem)
250-
}
251-
};
252-
($visitor: expr, $method: ident, $list: expr, $($extra_args: expr),*) => {
253-
for elem in $list {
254-
$visitor.$method(elem, $($extra_args,)*)
247+
($visitor: expr, $method: ident, $list: expr $(, $($extra_args: expr),* )?) => {
248+
{
249+
#[cfg_attr(not(bootstrap), allow(for_loops_over_fallibles))]
250+
for elem in $list {
251+
$visitor.$method(elem $(, $($extra_args,)* )?)
252+
}
255253
}
256254
}
257255
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use crate::{LateContext, LateLintPass, LintContext};
2+
3+
use hir::{Expr, Pat};
4+
use rustc_errors::{Applicability, DelayDm};
5+
use rustc_hir as hir;
6+
use rustc_infer::traits::TraitEngine;
7+
use rustc_infer::{infer::TyCtxtInferExt, traits::ObligationCause};
8+
use rustc_middle::ty::{self, List};
9+
use rustc_span::{sym, Span};
10+
use rustc_trait_selection::traits::TraitEngineExt;
11+
12+
declare_lint! {
13+
/// The `for_loops_over_fallibles` lint checks for `for` loops over `Option` or `Result` values.
14+
///
15+
/// ### Example
16+
///
17+
/// ```rust
18+
/// let opt = Some(1);
19+
/// for x in opt { /* ... */}
20+
/// ```
21+
///
22+
/// {{produces}}
23+
///
24+
/// ### Explanation
25+
///
26+
/// Both `Option` and `Result` implement `IntoIterator` trait, which allows using them in a `for` loop.
27+
/// `for` loop over `Option` or `Result` will iterate either 0 (if the value is `None`/`Err(_)`)
28+
/// or 1 time (if the value is `Some(_)`/`Ok(_)`). This is not very useful and is more clearly expressed
29+
/// via `if let`.
30+
///
31+
/// `for` loop can also be accidentally written with the intention to call a function multiple times,
32+
/// while the function returns `Some(_)`, in these cases `while let` loop should be used instead.
33+
///
34+
/// The "intended" use of `IntoIterator` implementations for `Option` and `Result` is passing them to
35+
/// generic code that expects something implementing `IntoIterator`. For example using `.chain(option)`
36+
/// to optionally add a value to an iterator.
37+
pub FOR_LOOPS_OVER_FALLIBLES,
38+
Warn,
39+
"for-looping over an `Option` or a `Result`, which is more clearly expressed as an `if let`"
40+
}
41+
42+
declare_lint_pass!(ForLoopsOverFallibles => [FOR_LOOPS_OVER_FALLIBLES]);
43+
44+
impl<'tcx> LateLintPass<'tcx> for ForLoopsOverFallibles {
45+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
46+
let Some((pat, arg)) = extract_for_loop(expr) else { return };
47+
48+
let ty = cx.typeck_results().expr_ty(arg);
49+
50+
let &ty::Adt(adt, substs) = ty.kind() else { return };
51+
52+
let (article, ty, var) = match adt.did() {
53+
did if cx.tcx.is_diagnostic_item(sym::Option, did) => ("an", "Option", "Some"),
54+
did if cx.tcx.is_diagnostic_item(sym::Result, did) => ("a", "Result", "Ok"),
55+
_ => return,
56+
};
57+
58+
let msg = DelayDm(|| {
59+
format!(
60+
"for loop over {article} `{ty}`. This is more readably written as an `if let` statement",
61+
)
62+
});
63+
64+
cx.struct_span_lint(FOR_LOOPS_OVER_FALLIBLES, arg.span, msg, |lint| {
65+
if let Some(recv) = extract_iterator_next_call(cx, arg)
66+
&& let Ok(recv_snip) = cx.sess().source_map().span_to_snippet(recv.span)
67+
{
68+
lint.span_suggestion(
69+
recv.span.between(arg.span.shrink_to_hi()),
70+
format!("to iterate over `{recv_snip}` remove the call to `next`"),
71+
".by_ref()",
72+
Applicability::MaybeIncorrect
73+
);
74+
} else {
75+
lint.multipart_suggestion_verbose(
76+
format!("to check pattern in a loop use `while let`"),
77+
vec![
78+
// NB can't use `until` here because `expr.span` and `pat.span` have different syntax contexts
79+
(expr.span.with_hi(pat.span.lo()), format!("while let {var}(")),
80+
(pat.span.between(arg.span), format!(") = ")),
81+
],
82+
Applicability::MaybeIncorrect
83+
);
84+
}
85+
86+
if suggest_question_mark(cx, adt, substs, expr.span) {
87+
lint.span_suggestion(
88+
arg.span.shrink_to_hi(),
89+
"consider unwrapping the `Result` with `?` to iterate over its contents",
90+
"?",
91+
Applicability::MaybeIncorrect,
92+
);
93+
}
94+
95+
lint.multipart_suggestion_verbose(
96+
"consider using `if let` to clear intent",
97+
vec![
98+
// NB can't use `until` here because `expr.span` and `pat.span` have different syntax contexts
99+
(expr.span.with_hi(pat.span.lo()), format!("if let {var}(")),
100+
(pat.span.between(arg.span), format!(") = ")),
101+
],
102+
Applicability::MaybeIncorrect,
103+
)
104+
})
105+
}
106+
}
107+
108+
fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)> {
109+
if let hir::ExprKind::DropTemps(e) = expr.kind
110+
&& let hir::ExprKind::Match(iterexpr, [arm], hir::MatchSource::ForLoopDesugar) = e.kind
111+
&& let hir::ExprKind::Call(_, [arg]) = iterexpr.kind
112+
&& let hir::ExprKind::Loop(block, ..) = arm.body.kind
113+
&& let [stmt] = block.stmts
114+
&& let hir::StmtKind::Expr(e) = stmt.kind
115+
&& let hir::ExprKind::Match(_, [_, some_arm], _) = e.kind
116+
&& let hir::PatKind::Struct(_, [field], _) = some_arm.pat.kind
117+
{
118+
Some((field.pat, arg))
119+
} else {
120+
None
121+
}
122+
}
123+
124+
fn extract_iterator_next_call<'tcx>(
125+
cx: &LateContext<'_>,
126+
expr: &Expr<'tcx>,
127+
) -> Option<&'tcx Expr<'tcx>> {
128+
// This won't work for `Iterator::next(iter)`, is this an issue?
129+
if let hir::ExprKind::MethodCall(_, recv, _, _) = expr.kind
130+
&& cx.typeck_results().type_dependent_def_id(expr.hir_id) == cx.tcx.lang_items().next_fn()
131+
{
132+
Some(recv)
133+
} else {
134+
return None
135+
}
136+
}
137+
138+
fn suggest_question_mark<'tcx>(
139+
cx: &LateContext<'tcx>,
140+
adt: ty::AdtDef<'tcx>,
141+
substs: &List<ty::GenericArg<'tcx>>,
142+
span: Span,
143+
) -> bool {
144+
let Some(body_id) = cx.enclosing_body else { return false };
145+
let Some(into_iterator_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) else { return false };
146+
147+
if !cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
148+
return false;
149+
}
150+
151+
// Check that the function/closure/constant we are in has a `Result` type.
152+
// Otherwise suggesting using `?` may not be a good idea.
153+
{
154+
let ty = cx.typeck_results().expr_ty(&cx.tcx.hir().body(body_id).value);
155+
let ty::Adt(ret_adt, ..) = ty.kind() else { return false };
156+
if !cx.tcx.is_diagnostic_item(sym::Result, ret_adt.did()) {
157+
return false;
158+
}
159+
}
160+
161+
let ty = substs.type_at(0);
162+
let infcx = cx.tcx.infer_ctxt().build();
163+
let mut fulfill_cx = <dyn TraitEngine<'_>>::new(infcx.tcx);
164+
165+
let cause = ObligationCause::new(
166+
span,
167+
body_id.hir_id,
168+
rustc_infer::traits::ObligationCauseCode::MiscObligation,
169+
);
170+
fulfill_cx.register_bound(
171+
&infcx,
172+
ty::ParamEnv::empty(),
173+
// Erase any region vids from the type, which may not be resolved
174+
infcx.tcx.erase_regions(ty),
175+
into_iterator_did,
176+
cause,
177+
);
178+
179+
// Select all, including ambiguous predicates
180+
let errors = fulfill_cx.select_all_or_error(&infcx);
181+
182+
errors.is_empty()
183+
}

compiler/rustc_lint/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ mod early;
5252
mod enum_intrinsics_non_enums;
5353
mod errors;
5454
mod expect;
55+
mod for_loops_over_fallibles;
5556
pub mod hidden_unicode_codepoints;
5657
mod internal;
5758
mod late;
@@ -86,6 +87,7 @@ use rustc_span::Span;
8687
use array_into_iter::ArrayIntoIter;
8788
use builtin::*;
8889
use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums;
90+
use for_loops_over_fallibles::*;
8991
use hidden_unicode_codepoints::*;
9092
use internal::*;
9193
use let_underscore::*;
@@ -188,6 +190,7 @@ macro_rules! late_lint_mod_passes {
188190
$macro!(
189191
$args,
190192
[
193+
ForLoopsOverFallibles: ForLoopsOverFallibles,
191194
HardwiredLints: HardwiredLints,
192195
ImproperCTypesDeclarations: ImproperCTypesDeclarations,
193196
ImproperCTypesDefinitions: ImproperCTypesDefinitions,

library/core/tests/option.rs

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ fn test_get_resource() {
5757
}
5858

5959
#[test]
60+
#[cfg_attr(not(bootstrap), allow(for_loops_over_fallibles))]
6061
fn test_option_dance() {
6162
let x = Some(());
6263
let mut y = Some(5);

src/test/ui/drop/dropck_legal_cycles.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -1017,7 +1017,7 @@ impl<'a> Children<'a> for HM<'a> {
10171017
where C: Context + PrePost<Self>, Self: Sized
10181018
{
10191019
if let Some(ref hm) = self.contents.get() {
1020-
for (k, v) in hm.iter().nth(index / 2) {
1020+
if let Some((k, v)) = hm.iter().nth(index / 2) {
10211021
[k, v][index % 2].descend_into_self(context);
10221022
}
10231023
}
@@ -1032,7 +1032,7 @@ impl<'a> Children<'a> for VD<'a> {
10321032
where C: Context + PrePost<Self>, Self: Sized
10331033
{
10341034
if let Some(ref vd) = self.contents.get() {
1035-
for r in vd.iter().nth(index) {
1035+
if let Some(r) = vd.iter().nth(index) {
10361036
r.descend_into_self(context);
10371037
}
10381038
}
@@ -1047,7 +1047,7 @@ impl<'a> Children<'a> for VM<'a> {
10471047
where C: Context + PrePost<VM<'a>>
10481048
{
10491049
if let Some(ref vd) = self.contents.get() {
1050-
for (_idx, r) in vd.iter().nth(index) {
1050+
if let Some((_idx, r)) = vd.iter().nth(index) {
10511051
r.descend_into_self(context);
10521052
}
10531053
}
@@ -1062,7 +1062,7 @@ impl<'a> Children<'a> for LL<'a> {
10621062
where C: Context + PrePost<LL<'a>>
10631063
{
10641064
if let Some(ref ll) = self.contents.get() {
1065-
for r in ll.iter().nth(index) {
1065+
if let Some(r) = ll.iter().nth(index) {
10661066
r.descend_into_self(context);
10671067
}
10681068
}
@@ -1077,7 +1077,7 @@ impl<'a> Children<'a> for BH<'a> {
10771077
where C: Context + PrePost<BH<'a>>
10781078
{
10791079
if let Some(ref bh) = self.contents.get() {
1080-
for r in bh.iter().nth(index) {
1080+
if let Some(r) = bh.iter().nth(index) {
10811081
r.descend_into_self(context);
10821082
}
10831083
}
@@ -1092,7 +1092,7 @@ impl<'a> Children<'a> for BTM<'a> {
10921092
where C: Context + PrePost<BTM<'a>>
10931093
{
10941094
if let Some(ref bh) = self.contents.get() {
1095-
for (k, v) in bh.iter().nth(index / 2) {
1095+
if let Some((k, v)) = bh.iter().nth(index / 2) {
10961096
[k, v][index % 2].descend_into_self(context);
10971097
}
10981098
}
@@ -1107,7 +1107,7 @@ impl<'a> Children<'a> for BTS<'a> {
11071107
where C: Context + PrePost<BTS<'a>>
11081108
{
11091109
if let Some(ref bh) = self.contents.get() {
1110-
for r in bh.iter().nth(index) {
1110+
if let Some(r) = bh.iter().nth(index) {
11111111
r.descend_into_self(context);
11121112
}
11131113
}

src/test/ui/issues/issue-30371.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// run-pass
22
#![allow(unreachable_code)]
3+
#![allow(for_loops_over_fallibles)]
34
#![deny(unused_variables)]
45

56
fn main() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// check-pass
2+
3+
fn main() {
4+
// Common
5+
for _ in Some(1) {}
6+
//~^ WARN for loop over an `Option`. This is more readably written as an `if let` statement
7+
//~| HELP to check pattern in a loop use `while let`
8+
//~| HELP consider using `if let` to clear intent
9+
for _ in Ok::<_, ()>(1) {}
10+
//~^ WARN for loop over a `Result`. This is more readably written as an `if let` statement
11+
//~| HELP to check pattern in a loop use `while let`
12+
//~| HELP consider using `if let` to clear intent
13+
14+
// `Iterator::next` specific
15+
for _ in [0; 0].iter().next() {}
16+
//~^ WARN for loop over an `Option`. This is more readably written as an `if let` statement
17+
//~| HELP to iterate over `[0; 0].iter()` remove the call to `next`
18+
//~| HELP consider using `if let` to clear intent
19+
20+
// `Result<impl Iterator, _>`, but function doesn't return `Result`
21+
for _ in Ok::<_, ()>([0; 0].iter()) {}
22+
//~^ WARN for loop over a `Result`. This is more readably written as an `if let` statement
23+
//~| HELP to check pattern in a loop use `while let`
24+
//~| HELP consider using `if let` to clear intent
25+
}
26+
27+
fn _returns_result() -> Result<(), ()> {
28+
// `Result<impl Iterator, _>`
29+
for _ in Ok::<_, ()>([0; 0].iter()) {}
30+
//~^ WARN for loop over a `Result`. This is more readably written as an `if let` statement
31+
//~| HELP to check pattern in a loop use `while let`
32+
//~| HELP consider unwrapping the `Result` with `?` to iterate over its contents
33+
//~| HELP consider using `if let` to clear intent
34+
35+
// `Result<impl IntoIterator>`
36+
for _ in Ok::<_, ()>([0; 0]) {}
37+
//~^ WARN for loop over a `Result`. This is more readably written as an `if let` statement
38+
//~| HELP to check pattern in a loop use `while let`
39+
//~| HELP consider unwrapping the `Result` with `?` to iterate over its contents
40+
//~| HELP consider using `if let` to clear intent
41+
42+
Ok(())
43+
}

0 commit comments

Comments
 (0)