Skip to content

Commit 2fbc9a4

Browse files
authored
Rollup merge of rust-lang#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 rust-lang#99272 [`clippy::for_loops_over_fallibles`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles
2 parents 54e79af + 252c65e commit 2fbc9a4

13 files changed

+365
-28
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_loop_over_fallibles))]
250+
for elem in $list {
251+
$visitor.$method(elem $(, $($extra_args,)* )?)
252+
}
255253
}
256254
}
257255
}

compiler/rustc_borrowck/src/type_check/input_output.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ impl<'a, 'tcx> TypeChecker<'a, 'tcx> {
225225

226226
debug!("{:?} normalized to {:?}", t, norm_ty);
227227

228-
for data in constraints {
228+
if let Some(data) = constraints {
229229
ConstraintConversion::new(
230230
self.infcx,
231231
&self.borrowck_context.universal_regions,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use crate::{LateContext, LateLintPass, LintContext};
2+
3+
use hir::{Expr, Pat};
4+
use rustc_errors::Applicability;
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_loop_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_LOOP_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!(ForLoopOverFallibles => [FOR_LOOP_OVER_FALLIBLES]);
43+
44+
impl<'tcx> LateLintPass<'tcx> for ForLoopOverFallibles {
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 = format!(
59+
"for loop over {article} `{ty}`. This is more readably written as an `if let` statement",
60+
);
61+
62+
cx.struct_span_lint(FOR_LOOP_OVER_FALLIBLES, arg.span, |diag| {
63+
let mut warn = diag.build(msg);
64+
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+
warn.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+
warn.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+
warn.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+
warn.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+
warn.emit()
106+
})
107+
}
108+
}
109+
110+
fn extract_for_loop<'tcx>(expr: &Expr<'tcx>) -> Option<(&'tcx Pat<'tcx>, &'tcx Expr<'tcx>)> {
111+
if let hir::ExprKind::DropTemps(e) = expr.kind
112+
&& let hir::ExprKind::Match(iterexpr, [arm], hir::MatchSource::ForLoopDesugar) = e.kind
113+
&& let hir::ExprKind::Call(_, [arg]) = iterexpr.kind
114+
&& let hir::ExprKind::Loop(block, ..) = arm.body.kind
115+
&& let [stmt] = block.stmts
116+
&& let hir::StmtKind::Expr(e) = stmt.kind
117+
&& let hir::ExprKind::Match(_, [_, some_arm], _) = e.kind
118+
&& let hir::PatKind::Struct(_, [field], _) = some_arm.pat.kind
119+
{
120+
Some((field.pat, arg))
121+
} else {
122+
None
123+
}
124+
}
125+
126+
fn extract_iterator_next_call<'tcx>(
127+
cx: &LateContext<'_>,
128+
expr: &Expr<'tcx>,
129+
) -> Option<&'tcx Expr<'tcx>> {
130+
// This won't work for `Iterator::next(iter)`, is this an issue?
131+
if let hir::ExprKind::MethodCall(_, [recv], _) = expr.kind
132+
&& cx.typeck_results().type_dependent_def_id(expr.hir_id) == cx.tcx.lang_items().next_fn()
133+
{
134+
Some(recv)
135+
} else {
136+
return None
137+
}
138+
}
139+
140+
fn suggest_question_mark<'tcx>(
141+
cx: &LateContext<'tcx>,
142+
adt: ty::AdtDef<'tcx>,
143+
substs: &List<ty::GenericArg<'tcx>>,
144+
span: Span,
145+
) -> bool {
146+
let Some(body_id) = cx.enclosing_body else { return false };
147+
let Some(into_iterator_did) = cx.tcx.get_diagnostic_item(sym::IntoIterator) else { return false };
148+
149+
if !cx.tcx.is_diagnostic_item(sym::Result, adt.did()) {
150+
return false;
151+
}
152+
153+
// Check that the function/closure/constant we are in has a `Result` type.
154+
// Otherwise suggesting using `?` may not be a good idea.
155+
{
156+
let ty = cx.typeck_results().expr_ty(&cx.tcx.hir().body(body_id).value);
157+
let ty::Adt(ret_adt, ..) = ty.kind() else { return false };
158+
if !cx.tcx.is_diagnostic_item(sym::Result, ret_adt.did()) {
159+
return false;
160+
}
161+
}
162+
163+
let ty = substs.type_at(0);
164+
let is_iterator = cx.tcx.infer_ctxt().enter(|infcx| {
165+
let mut fulfill_cx = <dyn TraitEngine<'_>>::new(infcx.tcx);
166+
167+
let cause = ObligationCause::new(
168+
span,
169+
body_id.hir_id,
170+
rustc_infer::traits::ObligationCauseCode::MiscObligation,
171+
);
172+
fulfill_cx.register_bound(
173+
&infcx,
174+
ty::ParamEnv::empty(),
175+
// Erase any region vids from the type, which may not be resolved
176+
infcx.tcx.erase_regions(ty),
177+
into_iterator_did,
178+
cause,
179+
);
180+
181+
// Select all, including ambiguous predicates
182+
let errors = fulfill_cx.select_all_or_error(&infcx);
183+
184+
errors.is_empty()
185+
});
186+
187+
is_iterator
188+
}

compiler/rustc_lint/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ mod early;
4949
mod enum_intrinsics_non_enums;
5050
mod errors;
5151
mod expect;
52+
mod for_loop_over_fallibles;
5253
pub mod hidden_unicode_codepoints;
5354
mod internal;
5455
mod late;
@@ -81,6 +82,7 @@ use rustc_span::Span;
8182
use array_into_iter::ArrayIntoIter;
8283
use builtin::*;
8384
use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums;
85+
use for_loop_over_fallibles::*;
8486
use hidden_unicode_codepoints::*;
8587
use internal::*;
8688
use methods::*;
@@ -179,6 +181,7 @@ macro_rules! late_lint_mod_passes {
179181
$macro!(
180182
$args,
181183
[
184+
ForLoopOverFallibles: ForLoopOverFallibles,
182185
HardwiredLints: HardwiredLints,
183186
ImproperCTypesDeclarations: ImproperCTypesDeclarations,
184187
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_loop_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_loop_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)