From 92205705aa821890bb592f867fb8bb3843ec289a Mon Sep 17 00:00:00 2001 From: Valdemar Erk Date: Thu, 18 Dec 2025 11:00:24 +0100 Subject: [PATCH] add new lint: `for_unbounded_range` --- CHANGELOG.md | 1 + clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/loops/for_unbounded_range.rs | 33 +++++++++++++++++++ clippy_lints/src/loops/mod.rs | 32 ++++++++++++++++++ tests/ui/for_unbounded_range.fixed | 19 +++++++++++ tests/ui/for_unbounded_range.rs | 19 +++++++++++ tests/ui/for_unbounded_range.stderr | 32 ++++++++++++++++++ 7 files changed, 137 insertions(+) create mode 100644 clippy_lints/src/loops/for_unbounded_range.rs create mode 100644 tests/ui/for_unbounded_range.fixed create mode 100644 tests/ui/for_unbounded_range.rs create mode 100644 tests/ui/for_unbounded_range.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f666caf306f..5cf15f9b7356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6472,6 +6472,7 @@ Released 2018-09-13 [`for_loop_over_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loop_over_option [`for_loop_over_result`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loop_over_result [`for_loops_over_fallibles`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles +[`for_unbounded_range`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_unbounded_range [`forget_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_copy [`forget_non_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_non_drop [`forget_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_ref diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 87d75234ebc0..8d42faf8a658 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -271,6 +271,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO, crate::loops::EXPLICIT_ITER_LOOP_INFO, crate::loops::FOR_KV_MAP_INFO, + crate::loops::FOR_UNBOUNDED_RANGE_INFO, crate::loops::INFINITE_LOOP_INFO, crate::loops::ITER_NEXT_LOOP_INFO, crate::loops::MANUAL_FIND_INFO, diff --git a/clippy_lints/src/loops/for_unbounded_range.rs b/clippy_lints/src/loops/for_unbounded_range.rs new file mode 100644 index 000000000000..418112cb5a80 --- /dev/null +++ b/clippy_lints/src/loops/for_unbounded_range.rs @@ -0,0 +1,33 @@ +use super::FOR_UNBOUNDED_RANGE; +use clippy_utils::diagnostics::span_lint_hir_and_then; +use clippy_utils::higher; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_span::Span; + +pub fn check<'tcx>(cx: &LateContext<'tcx>, arg: &'tcx Expr<'tcx>, span: Span) { + if let Some(range) = higher::Range::hir(cx, arg) + && let Some(range_start) = range.start + && let None = range.end + && let ty = cx.typeck_results().expr_ty_adjusted(range_start) + && (ty.is_integral() || ty.is_char()) + { + let until_max = format!("={ty}::MAX"); + + span_lint_hir_and_then( + cx, + FOR_UNBOUNDED_RANGE, + arg.hir_id, + span, + "for loop on unbounded range (`0..`)", + |diag| { + diag.span_suggestion_verbose( + arg.span.shrink_to_hi(), + "for loops over unbounded ranges will wrap around, consider using `start..=MAX` instead", + until_max, + rustc_errors::Applicability::MachineApplicable, + ); + }, + ); + } +} diff --git a/clippy_lints/src/loops/mod.rs b/clippy_lints/src/loops/mod.rs index ddc783069385..0002972ab3d4 100644 --- a/clippy_lints/src/loops/mod.rs +++ b/clippy_lints/src/loops/mod.rs @@ -4,6 +4,7 @@ mod explicit_counter_loop; mod explicit_into_iter_loop; mod explicit_iter_loop; mod for_kv_map; +mod for_unbounded_range; mod infinite_loop; mod iter_next_loop; mod manual_find; @@ -785,6 +786,35 @@ declare_clippy_lint! { "using the character position yielded by `.chars().enumerate()` in a context where a byte index is expected" } +declare_clippy_lint! { + /// ### What it does + /// Checks for unbounded for loops over char or integers. + /// + /// ### Why is this bad? + /// Using a unbounded range over char and integers will unexpectedly not handle overflows so it will lead to panics + /// or infinite loops. + /// + /// Instead there should be a max value set, usually the `MAX` constant for a given type such as `'\0'..char::MAX` + /// or `250..u8::MAX`. + /// + /// ### Example + /// ```no_run + /// for i in 250u8.. { + /// println!("{i}"); + /// } + /// ``` + /// Use instead: + /// ```no_run + /// for i in 250u8..=u8::MAX { + /// println!("{i}"); + /// } + /// ``` + #[clippy::version = "1.94.0"] + pub FOR_UNBOUNDED_RANGE, + nursery, + "using a for loop over unbounded range (`0..`)" +} + pub struct Loops { msrv: Msrv, enforce_iter_loop_reborrow: bool, @@ -823,6 +853,7 @@ impl_lint_pass!(Loops => [ INFINITE_LOOP, MANUAL_SLICE_FILL, CHAR_INDICES_AS_BYTE_INDICES, + FOR_UNBOUNDED_RANGE, ]); impl<'tcx> LateLintPass<'tcx> for Loops { @@ -951,6 +982,7 @@ impl Loops { manual_find::check(cx, pat, arg, body, span, expr); unused_enumerate_index::check(cx, arg, pat, None, body); char_indices_as_byte_indices::check(cx, pat, arg, body); + for_unbounded_range::check(cx, arg, span); } fn check_for_loop_arg(&self, cx: &LateContext<'_>, _: &Pat<'_>, arg: &Expr<'_>) { diff --git a/tests/ui/for_unbounded_range.fixed b/tests/ui/for_unbounded_range.fixed new file mode 100644 index 000000000000..acbc2fcf2a9e --- /dev/null +++ b/tests/ui/for_unbounded_range.fixed @@ -0,0 +1,19 @@ +#![warn(clippy::for_unbounded_range)] + +fn do_something(_t: T) {} + +fn main() { + for i in 0_u8..=u8::MAX { + do_something(i); + } + + for i in 0_u8..=u8::MAX { + //~^ for_unbounded_range + do_something(i); + } + + for i in '\0'..=char::MAX { + //~^ for_unbounded_range + do_something(i); + } +} diff --git a/tests/ui/for_unbounded_range.rs b/tests/ui/for_unbounded_range.rs new file mode 100644 index 000000000000..e38551b72eae --- /dev/null +++ b/tests/ui/for_unbounded_range.rs @@ -0,0 +1,19 @@ +#![warn(clippy::for_unbounded_range)] + +fn do_something(_t: T) {} + +fn main() { + for i in 0_u8..=u8::MAX { + do_something(i); + } + + for i in 0_u8.. { + //~^ for_unbounded_range + do_something(i); + } + + for i in '\0'.. { + //~^ for_unbounded_range + do_something(i); + } +} diff --git a/tests/ui/for_unbounded_range.stderr b/tests/ui/for_unbounded_range.stderr new file mode 100644 index 000000000000..5e45a61fedbd --- /dev/null +++ b/tests/ui/for_unbounded_range.stderr @@ -0,0 +1,32 @@ +error: for loop on unbounded range (`0..`) + --> tests/ui/for_unbounded_range.rs:10:5 + | +LL | / for i in 0_u8.. { +LL | | +LL | | do_something(i); +LL | | } + | |_____^ + | + = note: `-D clippy::for-unbounded-range` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::for_unbounded_range)]` +help: for loops over unbounded ranges will wrap around, consider using `start..=MAX` instead + | +LL | for i in 0_u8..=u8::MAX { + | ++++++++ + +error: for loop on unbounded range (`0..`) + --> tests/ui/for_unbounded_range.rs:15:5 + | +LL | / for i in '\0'.. { +LL | | +LL | | do_something(i); +LL | | } + | |_____^ + | +help: for loops over unbounded ranges will wrap around, consider using `start..=MAX` instead + | +LL | for i in '\0'..=char::MAX { + | ++++++++++ + +error: aborting due to 2 previous errors +