diff --git a/CHANGELOG.md b/CHANGELOG.md index 188ba02b0bfde..70aff7f60e059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5150,6 +5150,7 @@ Released 2018-09-13 [`inefficient_to_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#inefficient_to_string [`infallible_destructuring_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#infallible_destructuring_match [`infinite_iter`]: https://rust-lang.github.io/rust-clippy/master/index.html#infinite_iter +[`infinite_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#infinite_loop [`inherent_to_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#inherent_to_string [`inherent_to_string_shadow_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#inherent_to_string_shadow_display [`init_numbered_fields`]: https://rust-lang.github.io/rust-clippy/master/index.html#init_numbered_fields diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index d3105ed9d1859..2ce938fbe92ac 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -266,6 +266,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO, crate::loops::EXPLICIT_ITER_LOOP_INFO, crate::loops::FOR_KV_MAP_INFO, + crate::loops::INFINITE_LOOP_INFO, crate::loops::ITER_NEXT_LOOP_INFO, crate::loops::MANUAL_FIND_INFO, crate::loops::MANUAL_FLATTEN_INFO, diff --git a/clippy_lints/src/loops/infinite_loop.rs b/clippy_lints/src/loops/infinite_loop.rs new file mode 100644 index 0000000000000..9b88dd76e5cc5 --- /dev/null +++ b/clippy_lints/src/loops/infinite_loop.rs @@ -0,0 +1,125 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::{fn_def_id, is_lint_allowed}; +use hir::intravisit::{walk_expr, Visitor}; +use hir::{Expr, ExprKind, FnRetTy, FnSig, Node}; +use rustc_ast::Label; +use rustc_errors::Applicability; +use rustc_hir as hir; +use rustc_lint::LateContext; + +use super::INFINITE_LOOP; + +pub(super) fn check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'_>, + loop_block: &'tcx hir::Block<'_>, + label: Option<Label>, +) { + if is_lint_allowed(cx, INFINITE_LOOP, expr.hir_id) { + return; + } + + // Skip check if this loop is not in a function/method/closure. (In some weird case) + let Some(parent_fn_ret) = get_parent_fn_ret_ty(cx, expr) else { + return; + }; + // Or, its parent function is already returning `Never` + if matches!( + parent_fn_ret, + FnRetTy::Return(hir::Ty { + kind: hir::TyKind::Never, + .. + }) + ) { + return; + } + + let mut loop_visitor = LoopVisitor { + cx, + label, + is_finite: false, + loop_depth: 0, + }; + loop_visitor.visit_block(loop_block); + + let is_finite_loop = loop_visitor.is_finite; + + if !is_finite_loop { + span_lint_and_then(cx, INFINITE_LOOP, expr.span, "infinite loop detected", |diag| { + if let FnRetTy::DefaultReturn(ret_span) = parent_fn_ret { + diag.span_suggestion( + ret_span, + "if this is intentional, consider specifing `!` as function return", + " -> !", + Applicability::MaybeIncorrect, + ); + } else { + diag.help("if this is not intended, try adding a `break` or `return` condition in the loop"); + } + }); + } +} + +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 { + Node::Item(hir::Item { + kind: hir::ItemKind::Fn(FnSig { decl, .. }, _, _), + .. + }) + | Node::TraitItem(hir::TraitItem { + kind: hir::TraitItemKind::Fn(FnSig { decl, .. }, _), + .. + }) + | Node::ImplItem(hir::ImplItem { + kind: hir::ImplItemKind::Fn(FnSig { decl, .. }, _), + .. + }) + | Node::Expr(Expr { + kind: ExprKind::Closure(hir::Closure { fn_decl: decl, .. }), + .. + }) => return Some(decl.output), + _ => (), + } + } + None +} + +struct LoopVisitor<'hir, 'tcx> { + cx: &'hir LateContext<'tcx>, + label: Option<Label>, + loop_depth: usize, + is_finite: bool, +} + +impl<'hir> Visitor<'hir> for LoopVisitor<'hir, '_> { + fn visit_expr(&mut self, ex: &'hir Expr<'_>) { + match &ex.kind { + ExprKind::Break(hir::Destination { label, .. }, ..) => { + // Assuming breaks the loop when `loop_depth` is 0, + // as it could only means this `break` breaks current loop or any of its upper loop. + // Or, the depth is not zero but the label is matched. + if self.loop_depth == 0 || (label.is_some() && *label == self.label) { + self.is_finite = true; + } + }, + ExprKind::Ret(..) => self.is_finite = true, + ExprKind::Loop(..) => { + self.loop_depth += 1; + walk_expr(self, ex); + self.loop_depth = self.loop_depth.saturating_sub(1); + }, + _ => { + // Calls to a function that never return + if let Some(did) = fn_def_id(self.cx, ex) { + let fn_ret_ty = self.cx.tcx.fn_sig(did).skip_binder().output().skip_binder(); + if fn_ret_ty.is_never() { + self.is_finite = true; + return; + } + } + walk_expr(self, ex); + }, + } + } +} diff --git a/clippy_lints/src/loops/mod.rs b/clippy_lints/src/loops/mod.rs index 892336878c7b3..3c9bde86bb6f8 100644 --- a/clippy_lints/src/loops/mod.rs +++ b/clippy_lints/src/loops/mod.rs @@ -3,6 +3,7 @@ mod explicit_counter_loop; mod explicit_into_iter_loop; mod explicit_iter_loop; mod for_kv_map; +mod infinite_loop; mod iter_next_loop; mod manual_find; mod manual_flatten; @@ -635,6 +636,48 @@ declare_clippy_lint! { "checking for emptiness of a `Vec` in the loop condition and popping an element in the body" } +declare_clippy_lint! { + /// ### What it does + /// Checks for infinite loops in a function where the return type is not `!` + /// and lint accordingly. + /// + /// ### Why is this bad? + /// A loop should be gently exited somewhere, or at least mark its parent function as + /// never return (`!`). + /// + /// ### Example + /// ```no_run,ignore + /// fn run_forever() { + /// loop { + /// // do something + /// } + /// } + /// ``` + /// If infinite loops are as intended: + /// ```no_run,ignore + /// fn run_forever() -> ! { + /// loop { + /// // do something + /// } + /// } + /// ``` + /// Otherwise add a `break` or `return` condition: + /// ```no_run,ignore + /// fn run_forever() { + /// loop { + /// // do something + /// if condition { + /// break; + /// } + /// } + /// } + /// ``` + #[clippy::version = "1.75.0"] + pub INFINITE_LOOP, + restriction, + "possibly unintended infinite loop" +} + pub struct Loops { msrv: Msrv, enforce_iter_loop_reborrow: bool, @@ -669,6 +712,7 @@ impl_lint_pass!(Loops => [ MANUAL_FIND, MANUAL_WHILE_LET_SOME, UNUSED_ENUMERATE_INDEX, + INFINITE_LOOP, ]); impl<'tcx> LateLintPass<'tcx> for Loops { @@ -707,10 +751,11 @@ impl<'tcx> LateLintPass<'tcx> for Loops { // check for `loop { if let {} else break }` that could be `while let` // (also matches an explicit "match" instead of "if let") // (even if the "match" or "if let" is used for declaration) - if let ExprKind::Loop(block, _, LoopSource::Loop, _) = expr.kind { + if let ExprKind::Loop(block, label, LoopSource::Loop, _) = expr.kind { // also check for empty `loop {}` statements, skipping those in #[panic_handler] empty_loop::check(cx, expr, block); while_let_loop::check(cx, expr, block); + infinite_loop::check(cx, expr, block, label); } while_let_on_iterator::check(cx, expr); diff --git a/tests/ui/infinite_loops.rs b/tests/ui/infinite_loops.rs new file mode 100644 index 0000000000000..646f1eca56d01 --- /dev/null +++ b/tests/ui/infinite_loops.rs @@ -0,0 +1,366 @@ +//@no-rustfix +#![allow(clippy::never_loop)] +#![warn(clippy::infinite_loop)] + +fn do_something() {} + +fn no_break() { + loop { + //~^ ERROR: infinite loop detected + do_something(); + } +} + +fn all_inf() { + loop { + //~^ ERROR: infinite loop detected + loop { + //~^ ERROR: infinite loop detected + loop { + //~^ ERROR: infinite loop detected + do_something(); + } + } + do_something(); + } +} + +fn no_break_return_some_ty() -> Option<u8> { + loop { + do_something(); + return None; + } + loop { + //~^ ERROR: infinite loop detected + do_something(); + } +} + +fn no_break_never_ret() -> ! { + loop { + do_something(); + } +} + +fn no_break_never_ret_noise() { + loop { + fn inner_fn() -> ! { + std::process::exit(0); + } + do_something(); + } +} + +fn has_direct_break_1() { + loop { + do_something(); + break; + } +} + +fn has_direct_break_2() { + 'outer: loop { + do_something(); + break 'outer; + } +} + +fn has_indirect_break_1(cond: bool) { + 'outer: loop { + loop { + if cond { + break 'outer; + } + } + } +} + +fn has_indirect_break_2(stop_num: i32) { + 'outer: loop { + for x in 0..5 { + if x == stop_num { + break 'outer; + } + } + } +} + +fn break_inner_but_not_outer_1(cond: bool) { + loop { + //~^ ERROR: infinite loop detected + loop { + if cond { + break; + } + } + } +} + +fn break_inner_but_not_outer_2(cond: bool) { + loop { + //~^ ERROR: infinite loop detected + 'inner: loop { + loop { + if cond { + break 'inner; + } + } + } + } +} + +fn break_outer_but_not_inner() { + loop { + loop { + //~^ ERROR: infinite loop detected + do_something(); + } + break; + } +} + +fn can_break_both_inner_and_outer(cond: bool) { + 'outer: loop { + loop { + if cond { + break 'outer; + } else { + break; + } + } + } +} + +fn break_wrong_loop(cond: bool) { + // 'inner has statement to break 'outer loop, but it was breaked early by a labeled child loop + 'outer: loop { + loop { + //~^ ERROR: infinite loop detected + 'inner: loop { + loop { + loop { + break 'inner; + } + break 'outer; + } + } + } + } +} + +fn has_direct_return(cond: bool) { + loop { + if cond { + return; + } + } +} + +fn ret_in_inner(cond: bool) { + loop { + loop { + if cond { + return; + } + } + } +} + +enum Foo { + A, + B, + C, +} + +fn match_like() { + let opt: Option<u8> = Some(1); + loop { + //~^ ERROR: infinite loop detected + match opt { + Some(v) => { + println!("{v}"); + }, + None => { + do_something(); + }, + } + } + + loop { + match opt { + Some(v) => { + println!("{v}"); + }, + None => { + do_something(); + break; + }, + } + } + + let result: Result<u8, u16> = Ok(1); + loop { + let _val = match result { + Ok(1) => 1 + 1, + Ok(v) => v / 2, + Err(_) => return, + }; + } + + loop { + let Ok(_val) = result else { return }; + } + + loop { + let Ok(_val) = result.map(|v| 10) else { break }; + } + + loop { + //~^ ERROR: infinite loop detected + let _x = matches!(result, Ok(v) if v != 0).then_some(0); + } + + loop { + //~^ ERROR: infinite loop detected + // This `return` does not return the function, so it doesn't count + let _x = matches!(result, Ok(v) if v != 0).then(|| { + if true { + return; + } + do_something(); + }); + } + + let mut val = 0; + let mut fooc = Foo::C; + + loop { + val = match fooc { + Foo::A => 0, + Foo::B => { + fooc = Foo::C; + 1 + }, + Foo::C => break, + }; + } + + loop { + val = match fooc { + Foo::A => 0, + Foo::B => 1, + Foo::C => { + break; + }, + }; + } +} + +macro_rules! set_or_ret { + ($opt:expr, $a:expr) => {{ + match $opt { + Some(val) => $a = val, + None => return, + } + }}; +} + +fn ret_in_macro(opt: Option<u8>) { + let opt: Option<u8> = Some(1); + let mut a: u8 = 0; + loop { + set_or_ret!(opt, a); + } + + let res: Result<bool, u8> = Ok(true); + loop { + match res { + Ok(true) => set_or_ret!(opt, a), + _ => do_something(), + } + } +} + +fn panic_like_macros_1() { + loop { + do_something(); + panic!(); + } +} + +fn panic_like_macros_2() { + let mut x = 0; + + loop { + do_something(); + if true { + todo!(); + } + } + loop { + do_something(); + x += 1; + assert_eq!(x, 0); + } + loop { + do_something(); + assert!(x % 2 == 0); + } + loop { + do_something(); + match Some(1) { + Some(n) => println!("{n}"), + None => unreachable!("It won't happen"), + } + } +} + +fn exit_directly(cond: bool) { + loop { + if cond { + std::process::exit(0); + } + } +} + +trait MyTrait { + fn problematic_trait_method() { + loop { + //~^ ERROR: infinite loop detected + do_something(); + } + } + fn could_be_problematic(); +} + +impl MyTrait for String { + fn could_be_problematic() { + loop { + //~^ ERROR: infinite loop detected + do_something(); + } + } +} + +fn inf_loop_in_closure() { + let _loop_forever = || { + loop { + //~^ ERROR: infinite loop detected + do_something(); + } + }; + + let _somehow_ok = || -> ! { + loop { + do_something(); + } + }; +} + +fn inf_loop_in_res() -> Result<(), i32> { + Ok(loop { + do_something() + }) +} + +fn main() {} diff --git a/tests/ui/infinite_loops.stderr b/tests/ui/infinite_loops.stderr new file mode 100644 index 0000000000000..f58b3cebbc34d --- /dev/null +++ b/tests/ui/infinite_loops.stderr @@ -0,0 +1,259 @@ +error: infinite loop detected + --> $DIR/infinite_loops.rs:8:5 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_____^ + | + = note: `-D clippy::infinite-loop` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::infinite_loop)]` +help: if this is intentional, consider specifing `!` as function return + | +LL | fn no_break() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:15:5 + | +LL | / loop { +LL | | +LL | | loop { +LL | | +... | +LL | | do_something(); +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn all_inf() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:17:9 + | +LL | / loop { +LL | | +LL | | loop { +LL | | +LL | | do_something(); +LL | | } +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn all_inf() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:19:13 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_____________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn all_inf() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:33:5 + | +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 + --> $DIR/infinite_loops.rs:46:5 + | +LL | / loop { +LL | | fn inner_fn() -> ! { +LL | | std::process::exit(0); +LL | | } +LL | | do_something(); +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn no_break_never_ret_noise() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:89:5 + | +LL | / loop { +LL | | +LL | | loop { +LL | | if cond { +... | +LL | | } +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn break_inner_but_not_outer_1(cond: bool) -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:100:5 + | +LL | / loop { +LL | | +LL | | 'inner: loop { +LL | | loop { +... | +LL | | } +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn break_inner_but_not_outer_2(cond: bool) -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:114:9 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn break_outer_but_not_inner() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:137:9 + | +LL | / loop { +LL | | +LL | | 'inner: loop { +LL | | loop { +... | +LL | | } +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn break_wrong_loop(cond: bool) -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:177:5 + | +LL | / loop { +LL | | +LL | | match opt { +LL | | Some(v) => { +... | +LL | | } +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn match_like() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:218:5 + | +LL | / loop { +LL | | +LL | | let _x = matches!(result, Ok(v) if v != 0).then_some(0); +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn match_like() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:223:5 + | +LL | / loop { +LL | | +LL | | // This `return` does not return the function, so it doesn't count +LL | | let _x = matches!(result, Ok(v) if v != 0).then(|| { +... | +LL | | }); +LL | | } + | |_____^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn match_like() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:328:9 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn problematic_trait_method() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:338:9 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | fn could_be_problematic() -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:347:9 + | +LL | / loop { +LL | | +LL | | do_something(); +LL | | } + | |_________^ + | +help: if this is intentional, consider specifing `!` as function return + | +LL | let _loop_forever = || -> ! { + | ++++ + +error: infinite loop detected + --> $DIR/infinite_loops.rs:361:8 + | +LL | Ok(loop { + | ________^ +LL | | do_something() +LL | | }) + | |_____^ + | + = help: if this is not intended, try adding a `break` or `return` condition in the loop + +error: aborting due to 17 previous errors +