Skip to content

Commit 386372a

Browse files
authored
[[infinite_loop] fix infinite loop false positive (#15157)
changelog: [infinite_loop]: Improve handling of infinite loops in async blocks Fix #14000 This PR refines the [infinite_loop] lint to avoid false positives when infinite loops occur inside async blocks that are not awaited (such as those that are spawned or assigned to variables for later use). The lint will now only trigger when the async block containing the loop is directly awaited.
2 parents ea7ebaa + c945393 commit 386372a

File tree

3 files changed

+137
-6
lines changed

3 files changed

+137
-6
lines changed

clippy_lints/src/loops/infinite_loop.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::{fn_def_id, is_from_proc_macro, is_lint_allowed};
33
use hir::intravisit::{Visitor, walk_expr};
4-
use hir::{Expr, ExprKind, FnRetTy, FnSig, Node, TyKind};
54
use rustc_ast::Label;
65
use rustc_errors::Applicability;
7-
use rustc_hir as hir;
6+
use rustc_hir::{
7+
self as hir, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Expr, ExprKind, FnRetTy, FnSig, Node, TyKind,
8+
};
89
use rustc_lint::{LateContext, LintContext};
910
use rustc_span::sym;
1011

@@ -29,6 +30,10 @@ pub(super) fn check<'tcx>(
2930
return;
3031
}
3132

33+
if is_inside_unawaited_async_block(cx, expr) {
34+
return;
35+
}
36+
3237
if expr.span.in_external_macro(cx.sess().source_map()) || is_from_proc_macro(cx, expr) {
3338
return;
3439
}
@@ -60,15 +65,48 @@ pub(super) fn check<'tcx>(
6065
}
6166
}
6267

68+
/// Check if the given expression is inside an async block that is not being awaited.
69+
/// This helps avoid false positives when async blocks are spawned or assigned to variables.
70+
fn is_inside_unawaited_async_block(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
71+
let current_hir_id = expr.hir_id;
72+
for (_, parent_node) in cx.tcx.hir_parent_iter(current_hir_id) {
73+
if let Node::Expr(Expr {
74+
kind:
75+
ExprKind::Closure(Closure {
76+
kind: ClosureKind::Coroutine(CoroutineKind::Desugared(CoroutineDesugaring::Async, _)),
77+
..
78+
}),
79+
..
80+
}) = parent_node
81+
{
82+
return !is_async_block_awaited(cx, expr);
83+
}
84+
}
85+
false
86+
}
87+
88+
fn is_async_block_awaited(cx: &LateContext<'_>, async_expr: &Expr<'_>) -> bool {
89+
for (_, parent_node) in cx.tcx.hir_parent_iter(async_expr.hir_id) {
90+
if let Node::Expr(Expr {
91+
kind: ExprKind::Match(_, _, hir::MatchSource::AwaitDesugar),
92+
..
93+
}) = parent_node
94+
{
95+
return true;
96+
}
97+
}
98+
false
99+
}
100+
63101
fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option<FnRetTy<'tcx>> {
64102
for (_, parent_node) in cx.tcx.hir_parent_iter(expr.hir_id) {
65103
match parent_node {
66104
// Skip `Coroutine` closures, these are the body of `async fn`, not async closures.
67105
// This is because we still need to backtrack one parent node to get the `OpaqueDef` ty.
68106
Node::Expr(Expr {
69107
kind:
70-
ExprKind::Closure(hir::Closure {
71-
kind: hir::ClosureKind::Coroutine(_),
108+
ExprKind::Closure(Closure {
109+
kind: ClosureKind::Coroutine(_),
72110
..
73111
}),
74112
..
@@ -90,7 +128,7 @@ fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option
90128
..
91129
})
92130
| Node::Expr(Expr {
93-
kind: ExprKind::Closure(hir::Closure { fn_decl: decl, .. }),
131+
kind: ExprKind::Closure(Closure { fn_decl: decl, .. }),
94132
..
95133
}) => return Some(decl.output),
96134
_ => (),

tests/ui/infinite_loops.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,4 +450,75 @@ mod issue_12338 {
450450
}
451451
}
452452

453+
#[allow(clippy::let_underscore_future, clippy::empty_loop)]
454+
mod issue_14000 {
455+
use super::do_something;
456+
457+
async fn foo() {
458+
let _ = async move {
459+
loop {
460+
//~^ infinite_loop
461+
do_something();
462+
}
463+
}
464+
.await;
465+
let _ = async move {
466+
loop {
467+
//~^ infinite_loop
468+
continue;
469+
}
470+
}
471+
.await;
472+
}
473+
474+
fn bar() {
475+
let _ = async move {
476+
loop {
477+
do_something();
478+
}
479+
};
480+
481+
let _ = async move {
482+
loop {
483+
continue;
484+
}
485+
};
486+
}
487+
}
488+
489+
#[allow(clippy::let_underscore_future)]
490+
mod tokio_spawn_test {
491+
use super::do_something;
492+
493+
fn install_ticker() {
494+
// This should NOT trigger the lint because the async block is spawned, not awaited
495+
std::thread::spawn(move || {
496+
async move {
497+
loop {
498+
// This loop should not trigger infinite_loop lint
499+
do_something();
500+
}
501+
}
502+
});
503+
}
504+
505+
fn spawn_async_block() {
506+
// This should NOT trigger the lint because the async block is not awaited
507+
let _handle = async move {
508+
loop {
509+
do_something();
510+
}
511+
};
512+
}
513+
514+
fn await_async_block() {
515+
// This SHOULD trigger the lint because the async block is awaited
516+
let _ = async move {
517+
loop {
518+
do_something();
519+
}
520+
};
521+
}
522+
}
523+
453524
fn main() {}

tests/ui/infinite_loops.stderr

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,5 +311,27 @@ help: if this is intentional, consider specifying `!` as function return
311311
LL | fn continue_outer() -> ! {
312312
| ++++
313313

314-
error: aborting due to 21 previous errors
314+
error: infinite loop detected
315+
--> tests/ui/infinite_loops.rs:459:13
316+
|
317+
LL | / loop {
318+
LL | |
319+
LL | | do_something();
320+
LL | | }
321+
| |_____________^
322+
|
323+
= help: if this is not intended, try adding a `break` or `return` condition in the loop
324+
325+
error: infinite loop detected
326+
--> tests/ui/infinite_loops.rs:466:13
327+
|
328+
LL | / loop {
329+
LL | |
330+
LL | | continue;
331+
LL | | }
332+
| |_____________^
333+
|
334+
= help: if this is not intended, try adding a `break` or `return` condition in the loop
335+
336+
error: aborting due to 23 previous errors
315337

0 commit comments

Comments
 (0)