Skip to content

Commit 36201bd

Browse files
committed
Add iterator reduction coverage to never_loop
1 parent 9e3e964 commit 36201bd

File tree

5 files changed

+115
-2
lines changed

5 files changed

+115
-2
lines changed

clippy_lints/src/loops/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ mod while_let_on_iterator;
2626

2727
use clippy_config::Conf;
2828
use clippy_utils::msrvs::Msrv;
29-
use clippy_utils::{higher, sym};
29+
use clippy_utils::{higher, sym, ty};
3030
use rustc_ast::Label;
3131
use rustc_hir::{Expr, ExprKind, LoopSource, Pat};
3232
use rustc_lint::{LateContext, LateLintPass};
@@ -889,6 +889,22 @@ impl<'tcx> LateLintPass<'tcx> for Loops {
889889
{
890890
unused_enumerate_index::check_method(cx, expr, recv, arg);
891891
}
892+
893+
if let ExprKind::MethodCall(path, recv, args, _) = expr.kind
894+
&& matches!(
895+
path.ident.name,
896+
sym::for_each
897+
| sym::try_for_each
898+
| sym::fold
899+
| sym::try_fold
900+
| sym::reduce
901+
| sym::all
902+
| sym::any
903+
)
904+
&& ty::get_iterator_item_ty(cx, cx.typeck_results().expr_ty(recv)).is_some()
905+
{
906+
never_loop::check_iterator_reduction(cx, expr, recv, args);
907+
}
892908
}
893909
}
894910

clippy_lints/src/loops/never_loop.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ use clippy_utils::diagnostics::span_lint_and_then;
44
use clippy_utils::higher::ForLoop;
55
use clippy_utils::macros::root_macro_call_first_node;
66
use clippy_utils::source::snippet;
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::{
1011
Block, Destination, Expr, ExprKind, HirId, InlineAsm, InlineAsmOperand, Node, Pat, Stmt, StmtKind, StructTailExpr,
1112
};
1213
use rustc_lint::LateContext;
13-
use rustc_span::{BytePos, Span, sym};
14+
use rustc_span::{BytePos, Span};
1415
use std::iter::once;
1516
use std::ops::ControlFlow;
1617

@@ -72,6 +73,55 @@ pub(super) fn check<'tcx>(
7273
}
7374
}
7475

76+
pub(super) fn check_iterator_reduction<'tcx>(
77+
cx: &LateContext<'tcx>,
78+
expr: &'tcx Expr<'tcx>,
79+
recv: &'tcx Expr<'tcx>,
80+
args: &'tcx [Expr<'tcx>],
81+
) {
82+
// identify which argument is the closure based on the method kind
83+
let Some(method_name) = (match expr.kind {
84+
ExprKind::MethodCall(path, ..) => Some(path.ident.name),
85+
_ => None,
86+
}) else {
87+
return;
88+
};
89+
90+
let closure_arg = match method_name {
91+
sym::for_each | sym::try_for_each | sym::reduce | sym::all | sym::any => args.get(0),
92+
sym::fold | sym::try_fold => args.get(1),
93+
_ => None,
94+
};
95+
96+
let Some(closure_arg) = closure_arg else {
97+
return;
98+
};
99+
100+
// extract the closure body
101+
let closure_body = match closure_arg.kind {
102+
ExprKind::Closure(closure) => cx.tcx.hir_body(closure.body).value,
103+
_ => return,
104+
};
105+
106+
let body_ty = cx.typeck_results().expr_ty(closure_body);
107+
if body_ty.is_never() {
108+
span_lint_and_then(
109+
cx,
110+
NEVER_LOOP,
111+
expr.span,
112+
"this iterator reduction never loops (closure always diverges)",
113+
|diag| {
114+
let mut app = Applicability::Unspecified;
115+
let recv_snip = make_iterator_snippet(cx, recv, &mut app);
116+
diag.note("if you only need one element, `if let Some(x) = iter.next()` is clearer");
117+
let sugg = format!("if let Some(x) = {recv_snip}.next() {{ ... }}");
118+
diag.span_suggestion_verbose(expr.span, "consider this pattern", sugg, app);
119+
},
120+
);
121+
return;
122+
}
123+
}
124+
75125
fn contains_any_break_or_continue(block: &Block<'_>) -> bool {
76126
for_each_expr_without_closures(block, |e| match e.kind {
77127
ExprKind::Break(..) | ExprKind::Continue(..) => ControlFlow::Break(()),

clippy_utils/src/sym.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ generate! {
6666
Regex,
6767
RegexBuilder,
6868
RegexSet,
69+
reduce,
6970
Start,
7071
Symbol,
7172
SyntaxContext,
@@ -364,7 +365,9 @@ generate! {
364365
trim_start,
365366
trim_start_matches,
366367
truncate,
368+
try_fold,
367369
try_for_each,
370+
try_reduce,
368371
unreachable_pub,
369372
unsafe_removed_from_name,
370373
unused,
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)