[[infinite_loop] fix infinite loop false positive (#15157)

changelog: [infinite_loop]: Improve handling of infinite loops in async
blocks

Fix rust-lang/rust-clippy#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.
This commit is contained in:
dswij 2025-08-09 15:08:54 +00:00 committed by GitHub
commit 386372a0e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 6 deletions

View file

@ -1,10 +1,11 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::{fn_def_id, is_from_proc_macro, is_lint_allowed};
use hir::intravisit::{Visitor, walk_expr};
use hir::{Expr, ExprKind, FnRetTy, FnSig, Node, TyKind};
use rustc_ast::Label;
use rustc_errors::Applicability;
use rustc_hir as hir;
use rustc_hir::{
self as hir, Closure, ClosureKind, CoroutineDesugaring, CoroutineKind, Expr, ExprKind, FnRetTy, FnSig, Node, TyKind,
};
use rustc_lint::{LateContext, LintContext};
use rustc_span::sym;
@ -29,6 +30,10 @@ pub(super) fn check<'tcx>(
return;
}
if is_inside_unawaited_async_block(cx, expr) {
return;
}
if expr.span.in_external_macro(cx.sess().source_map()) || is_from_proc_macro(cx, expr) {
return;
}
@ -60,6 +65,39 @@ pub(super) fn check<'tcx>(
}
}
/// Check if the given expression is inside an async block that is not being awaited.
/// This helps avoid false positives when async blocks are spawned or assigned to variables.
fn is_inside_unawaited_async_block(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let current_hir_id = expr.hir_id;
for (_, parent_node) in cx.tcx.hir_parent_iter(current_hir_id) {
if let Node::Expr(Expr {
kind:
ExprKind::Closure(Closure {
kind: ClosureKind::Coroutine(CoroutineKind::Desugared(CoroutineDesugaring::Async, _)),
..
}),
..
}) = parent_node
{
return !is_async_block_awaited(cx, expr);
}
}
false
}
fn is_async_block_awaited(cx: &LateContext<'_>, async_expr: &Expr<'_>) -> bool {
for (_, parent_node) in cx.tcx.hir_parent_iter(async_expr.hir_id) {
if let Node::Expr(Expr {
kind: ExprKind::Match(_, _, hir::MatchSource::AwaitDesugar),
..
}) = parent_node
{
return true;
}
}
false
}
fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option<FnRetTy<'tcx>> {
for (_, parent_node) in cx.tcx.hir_parent_iter(expr.hir_id) {
match parent_node {
@ -67,8 +105,8 @@ fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option
// This is because we still need to backtrack one parent node to get the `OpaqueDef` ty.
Node::Expr(Expr {
kind:
ExprKind::Closure(hir::Closure {
kind: hir::ClosureKind::Coroutine(_),
ExprKind::Closure(Closure {
kind: ClosureKind::Coroutine(_),
..
}),
..
@ -90,7 +128,7 @@ fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option
..
})
| Node::Expr(Expr {
kind: ExprKind::Closure(hir::Closure { fn_decl: decl, .. }),
kind: ExprKind::Closure(Closure { fn_decl: decl, .. }),
..
}) => return Some(decl.output),
_ => (),

View file

@ -450,4 +450,75 @@ mod issue_12338 {
}
}
#[allow(clippy::let_underscore_future, clippy::empty_loop)]
mod issue_14000 {
use super::do_something;
async fn foo() {
let _ = async move {
loop {
//~^ infinite_loop
do_something();
}
}
.await;
let _ = async move {
loop {
//~^ infinite_loop
continue;
}
}
.await;
}
fn bar() {
let _ = async move {
loop {
do_something();
}
};
let _ = async move {
loop {
continue;
}
};
}
}
#[allow(clippy::let_underscore_future)]
mod tokio_spawn_test {
use super::do_something;
fn install_ticker() {
// This should NOT trigger the lint because the async block is spawned, not awaited
std::thread::spawn(move || {
async move {
loop {
// This loop should not trigger infinite_loop lint
do_something();
}
}
});
}
fn spawn_async_block() {
// This should NOT trigger the lint because the async block is not awaited
let _handle = async move {
loop {
do_something();
}
};
}
fn await_async_block() {
// This SHOULD trigger the lint because the async block is awaited
let _ = async move {
loop {
do_something();
}
};
}
}
fn main() {}

View file

@ -311,5 +311,27 @@ help: if this is intentional, consider specifying `!` as function return
LL | fn continue_outer() -> ! {
| ++++
error: aborting due to 21 previous errors
error: infinite loop detected
--> tests/ui/infinite_loops.rs:459:13
|
LL | / loop {
LL | |
LL | | do_something();
LL | | }
| |_____________^
|
= help: if this is not intended, try adding a `break` or `return` condition in the loop
error: infinite loop detected
--> tests/ui/infinite_loops.rs:466:13
|
LL | / loop {
LL | |
LL | | continue;
LL | | }
| |_____________^
|
= help: if this is not intended, try adding a `break` or `return` condition in the loop
error: aborting due to 23 previous errors