Skip to content

Commit d36794f

Browse files
authored
Add iterator reduction coverage to never_loop (#16222)
Fixes #16061. Extend `never_loop` to also lint iterator reduction methods (e.g. `for_each`, `try_for_each`, `fold`, `try_fold`, `reduce`, `all`, `any`) when the closure’s body diverges (return type `!`). Add a UI test `never_loop_iterator_reduction` to cover these cases. Testing: - TESTNAME=never_loop_iterator_reduction cargo uitest changelog: [`never_loop`]: lint diverging iterator reduction closures like `for_each` and `fold`
2 parents 80fce9b + c59da4f commit d36794f

File tree

6 files changed

+120
-17
lines changed

6 files changed

+120
-17
lines changed

clippy_lints/src/loops/mod.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod while_let_on_iterator;
2626

2727
use clippy_config::Conf;
2828
use clippy_utils::msrvs::Msrv;
29+
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
2930
use clippy_utils::{higher, sym};
3031
use rustc_ast::Label;
3132
use rustc_hir::{Expr, ExprKind, LoopSource, Pat};
@@ -881,13 +882,44 @@ impl<'tcx> LateLintPass<'tcx> for Loops {
881882
manual_while_let_some::check(cx, condition, body, span);
882883
}
883884

884-
if let ExprKind::MethodCall(path, recv, [arg], _) = expr.kind
885-
&& matches!(
886-
path.ident.name,
887-
sym::all | sym::any | sym::filter_map | sym::find_map | sym::flat_map | sym::for_each | sym::map
888-
)
889-
{
890-
unused_enumerate_index::check_method(cx, expr, recv, arg);
885+
if let ExprKind::MethodCall(path, recv, args, _) = expr.kind {
886+
let name = path.ident.name;
887+
888+
let is_iterator_method = || {
889+
cx.ty_based_def(expr)
890+
.assoc_fn_parent(cx)
891+
.is_diag_item(cx, sym::Iterator)
892+
};
893+
894+
// is_iterator_method is a bit expensive, so we call it last in each match arm
895+
match (name, args) {
896+
(sym::for_each | sym::all | sym::any, [arg]) => {
897+
if let ExprKind::Closure(closure) = arg.kind
898+
&& is_iterator_method()
899+
{
900+
unused_enumerate_index::check_method(cx, recv, arg, closure);
901+
never_loop::check_iterator_reduction(cx, expr, recv, closure);
902+
}
903+
},
904+
905+
(sym::filter_map | sym::find_map | sym::flat_map | sym::map, [arg]) => {
906+
if let ExprKind::Closure(closure) = arg.kind
907+
&& is_iterator_method()
908+
{
909+
unused_enumerate_index::check_method(cx, recv, arg, closure);
910+
}
911+
},
912+
913+
(sym::try_for_each | sym::reduce, [arg]) | (sym::fold | sym::try_fold, [_, arg]) => {
914+
if let ExprKind::Closure(closure) = arg.kind
915+
&& is_iterator_method()
916+
{
917+
never_loop::check_iterator_reduction(cx, expr, recv, closure);
918+
}
919+
},
920+
921+
_ => {},
922+
}
891923
}
892924
}
893925
}

clippy_lints/src/loops/never_loop.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ use super::utils::make_iterator_snippet;
33
use clippy_utils::diagnostics::span_lint_and_then;
44
use clippy_utils::higher::ForLoop;
55
use clippy_utils::macros::root_macro_call_first_node;
6-
use clippy_utils::source::snippet;
6+
use clippy_utils::source::{snippet, snippet_with_context};
7+
use clippy_utils::sym;
78
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
89
use rustc_errors::Applicability;
910
use rustc_hir::{
10-
Block, Destination, Expr, ExprKind, HirId, InlineAsm, InlineAsmOperand, Node, Pat, Stmt, StmtKind, StructTailExpr,
11+
Block, Closure, Destination, Expr, ExprKind, HirId, InlineAsm, InlineAsmOperand, Node, Pat, Stmt, StmtKind,
12+
StructTailExpr,
1113
};
1214
use rustc_lint::LateContext;
13-
use rustc_span::{BytePos, Span, sym};
15+
use rustc_span::{BytePos, Span};
1416
use std::iter::once;
1517
use std::ops::ControlFlow;
1618

@@ -72,6 +74,31 @@ pub(super) fn check<'tcx>(
7274
}
7375
}
7476

77+
pub(super) fn check_iterator_reduction<'tcx>(
78+
cx: &LateContext<'tcx>,
79+
expr: &'tcx Expr<'tcx>,
80+
recv: &'tcx Expr<'tcx>,
81+
closure: &'tcx Closure<'tcx>,
82+
) {
83+
let closure_body = cx.tcx.hir_body(closure.body).value;
84+
let body_ty = cx.typeck_results().expr_ty(closure_body);
85+
if body_ty.is_never() {
86+
span_lint_and_then(
87+
cx,
88+
NEVER_LOOP,
89+
expr.span,
90+
"this iterator reduction never loops (closure always diverges)",
91+
|diag| {
92+
let mut app = Applicability::HasPlaceholders;
93+
let recv_snip = snippet_with_context(cx, recv.span, expr.span.ctxt(), "<iter>", &mut app).0;
94+
diag.note("if you only need one element, `if let Some(x) = iter.next()` is clearer");
95+
let sugg = format!("if let Some(x) = {recv_snip}.next() {{ ... }}");
96+
diag.span_suggestion_verbose(expr.span, "consider this pattern", sugg, app);
97+
},
98+
);
99+
}
100+
}
101+
75102
fn contains_any_break_or_continue(block: &Block<'_>) -> bool {
76103
for_each_expr_without_closures(block, |e| match e.kind {
77104
ExprKind::Break(..) | ExprKind::Continue(..) => ControlFlow::Break(()),

clippy_lints/src/loops/unused_enumerate_index.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use super::UNUSED_ENUMERATE_INDEX;
22
use clippy_utils::diagnostics::span_lint_hir_and_then;
3-
use clippy_utils::res::{MaybeDef, MaybeTypeckRes};
3+
use clippy_utils::res::MaybeDef;
44
use clippy_utils::source::{SpanRangeExt, walk_span_to_context};
55
use clippy_utils::{expr_or_init, pat_is_wild};
66
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind, Pat, PatKind, TyKind};
7+
use rustc_hir::{Closure, Expr, ExprKind, Pat, PatKind, TyKind};
88
use rustc_lint::LateContext;
99
use rustc_span::{Span, SyntaxContext, sym};
1010

@@ -60,14 +60,12 @@ pub(super) fn check<'tcx>(
6060

6161
pub(super) fn check_method<'tcx>(
6262
cx: &LateContext<'tcx>,
63-
e: &'tcx Expr<'tcx>,
6463
recv: &'tcx Expr<'tcx>,
6564
arg: &'tcx Expr<'tcx>,
65+
closure: &'tcx Closure<'tcx>,
6666
) {
67-
if let ExprKind::Closure(closure) = arg.kind
68-
&& let body = cx.tcx.hir_body(closure.body)
69-
&& let [param] = body.params
70-
&& cx.ty_based_def(e).opt_parent(cx).is_diag_item(cx, sym::Iterator)
67+
let body = cx.tcx.hir_body(closure.body);
68+
if let [param] = body.params
7169
&& let [input] = closure.fn_decl.inputs
7270
&& !arg.span.from_expansion()
7371
&& !input.span.from_expansion()

clippy_utils/src/sym.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ generate! {
275275
read_to_string,
276276
read_unaligned,
277277
read_volatile,
278+
reduce,
278279
redundant_imports,
279280
redundant_pub_crate,
280281
regex,
@@ -364,6 +365,7 @@ generate! {
364365
trim_start,
365366
trim_start_matches,
366367
truncate,
368+
try_fold,
367369
try_for_each,
368370
unreachable_pub,
369371
unsafe_removed_from_name,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//@no-rustfix
2+
#![warn(clippy::never_loop)]
3+
4+
fn main() {
5+
// diverging closure: should trigger
6+
[0, 1].into_iter().for_each(|x| {
7+
//~^ never_loop
8+
9+
let _ = x;
10+
panic!("boom");
11+
});
12+
13+
// benign closure: should NOT trigger
14+
[0, 1].into_iter().for_each(|x| {
15+
let _ = x + 1;
16+
});
17+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
error: this iterator reduction never loops (closure always diverges)
2+
--> tests/ui/never_loop_iterator_reduction.rs:6:5
3+
|
4+
LL | / [0, 1].into_iter().for_each(|x| {
5+
LL | |
6+
LL | |
7+
LL | | let _ = x;
8+
LL | | panic!("boom");
9+
LL | | });
10+
| |______^
11+
|
12+
= note: if you only need one element, `if let Some(x) = iter.next()` is clearer
13+
= note: `-D clippy::never-loop` implied by `-D warnings`
14+
= help: to override `-D warnings` add `#[allow(clippy::never_loop)]`
15+
help: consider this pattern
16+
|
17+
LL - [0, 1].into_iter().for_each(|x| {
18+
LL -
19+
LL -
20+
LL - let _ = x;
21+
LL - panic!("boom");
22+
LL - });
23+
LL + if let Some(x) = [0, 1].into_iter().next() { ... };
24+
|
25+
26+
error: aborting due to 1 previous error
27+

0 commit comments

Comments
 (0)