Skip to content

Commit aede4dc

Browse files
add new lint: for_unbounded_range (#16257)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/rust-clippy/pull/16257)* This adds a Clippy lint for the possible confusing behaviour pointed out in rust-lang/rust#150084 and rust-lang/rust#111514, and discussed in rust-lang/libs-team#304. While no consensus have been reached in the API Change Proposal linked above I thought that it would still make sense to add a Clippy lint for the time being. Open questions: - Which group should it be in? - Should it give warnings for `AsciiChar`, `Ipv4Addr` and `Ipv6Addr`? (Currently these will always panic when running over the max value) changelog: new lint: [`for_unbounded_range`]
2 parents 87aaa5a + 4858a73 commit aede4dc

12 files changed

Lines changed: 206 additions & 20 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6840,6 +6840,7 @@ Released 2018-09-13
68406840
[`for_loop_over_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loop_over_option
68416841
[`for_loop_over_result`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loop_over_result
68426842
[`for_loops_over_fallibles`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_loops_over_fallibles
6843+
[`for_unbounded_range`]: https://rust-lang.github.io/rust-clippy/master/index.html#for_unbounded_range
68436844
[`forget_copy`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_copy
68446845
[`forget_non_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_non_drop
68456846
[`forget_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#forget_ref

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
276276
crate::loops::EXPLICIT_INTO_ITER_LOOP_INFO,
277277
crate::loops::EXPLICIT_ITER_LOOP_INFO,
278278
crate::loops::FOR_KV_MAP_INFO,
279+
crate::loops::FOR_UNBOUNDED_RANGE_INFO,
279280
crate::loops::INFINITE_LOOP_INFO,
280281
crate::loops::ITER_NEXT_LOOP_INFO,
281282
crate::loops::MANUAL_FIND_INFO,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use super::FOR_UNBOUNDED_RANGE;
2+
use super::infinite_loop::LoopVisitor;
3+
use clippy_utils::diagnostics::span_lint_and_then;
4+
use clippy_utils::higher;
5+
use rustc_ast::Label;
6+
use rustc_hir::Expr;
7+
use rustc_hir::intravisit::Visitor as _;
8+
use rustc_lint::LateContext;
9+
use rustc_span::Span;
10+
11+
pub fn check<'tcx>(
12+
cx: &LateContext<'tcx>,
13+
label: Option<Label>,
14+
arg: &'tcx Expr<'tcx>,
15+
span: Span,
16+
body: &'tcx Expr<'tcx>,
17+
) {
18+
if let Some(range) = higher::Range::hir(cx, arg)
19+
&& let Some(range_start) = range.start
20+
&& let None = range.end
21+
&& let ty = cx.typeck_results().expr_ty_adjusted(range_start)
22+
&& (ty.is_integral() || ty.is_char())
23+
{
24+
let mut loop_visitor = LoopVisitor::new(cx, label);
25+
26+
loop_visitor.visit_expr(body);
27+
28+
if loop_visitor.is_finite {
29+
// The loop is likely finite.
30+
return;
31+
}
32+
33+
span_lint_and_then(cx, FOR_UNBOUNDED_RANGE, span, "for loop on unbounded range", |diag| {
34+
diag.span_suggestion_verbose(
35+
arg.span.shrink_to_hi(),
36+
"for loops over unbounded ranges will wrap around and may panic, consider adding an inclusive high bound",
37+
format!("={ty}::MAX"),
38+
rustc_errors::Applicability::MaybeIncorrect,
39+
);
40+
});
41+
}
42+
}

clippy_lints/src/loops/infinite_loop.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,7 @@ pub(super) fn check<'tcx>(
3939
return;
4040
}
4141

42-
let mut loop_visitor = LoopVisitor {
43-
cx,
44-
label,
45-
inner_labels: label.into_iter().collect(),
46-
loop_depth: 0,
47-
is_finite: false,
48-
};
42+
let mut loop_visitor = LoopVisitor::new(cx, label);
4943
loop_visitor.visit_block(loop_block);
5044

5145
let is_finite_loop = loop_visitor.is_finite;
@@ -146,12 +140,24 @@ fn get_parent_fn_ret_ty<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> Option
146140
None
147141
}
148142

149-
struct LoopVisitor<'hir, 'tcx> {
150-
cx: &'hir LateContext<'tcx>,
151-
label: Option<Label>,
152-
inner_labels: Vec<Label>,
153-
loop_depth: usize,
154-
is_finite: bool,
143+
pub(super) struct LoopVisitor<'hir, 'tcx> {
144+
pub cx: &'hir LateContext<'tcx>,
145+
pub label: Option<Label>,
146+
pub inner_labels: Vec<Label>,
147+
pub loop_depth: usize,
148+
pub is_finite: bool,
149+
}
150+
151+
impl<'hir, 'tcx> LoopVisitor<'hir, 'tcx> {
152+
pub fn new(cx: &'hir LateContext<'tcx>, label: Option<Label>) -> Self {
153+
LoopVisitor {
154+
cx,
155+
label,
156+
inner_labels: label.into_iter().collect(),
157+
loop_depth: 0,
158+
is_finite: false,
159+
}
160+
}
155161
}
156162

157163
impl<'hir> Visitor<'hir> for LoopVisitor<'hir, '_> {

clippy_lints/src/loops/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod explicit_counter_loop;
44
mod explicit_into_iter_loop;
55
mod explicit_iter_loop;
66
mod for_kv_map;
7+
mod for_unbounded_range;
78
mod infinite_loop;
89
mod iter_next_loop;
910
mod manual_find;
@@ -236,6 +237,35 @@ declare_clippy_lint! {
236237
"looping on a map using `iter` when `keys` or `values` would do"
237238
}
238239

240+
declare_clippy_lint! {
241+
/// ### What it does
242+
/// Checks for unbounded for loops over char or integers.
243+
///
244+
/// ### Why is this bad?
245+
/// Using a unbounded range over char and integers will unexpectedly not handle overflows so it will lead to panics
246+
/// or infinite loops.
247+
///
248+
/// Instead there should be a max value set, usually the `MAX` constant for a given type such as `'\0'..char::MAX`
249+
/// or `250..u8::MAX`.
250+
///
251+
/// ### Example
252+
/// ```no_run
253+
/// for i in 250u8.. {
254+
/// println!("{i}");
255+
/// }
256+
/// ```
257+
/// Use instead:
258+
/// ```no_run
259+
/// for i in 250u8..=u8::MAX {
260+
/// println!("{i}");
261+
/// }
262+
/// ```
263+
#[clippy::version = "1.98.0"]
264+
pub FOR_UNBOUNDED_RANGE,
265+
suspicious,
266+
"using a for loop over unbounded range such as `0..`"
267+
}
268+
239269
declare_clippy_lint! {
240270
/// ### What it does
241271
/// Checks for infinite loops in a function where the return type is not `!`
@@ -792,6 +822,7 @@ impl_lint_pass!(Loops => [
792822
EXPLICIT_INTO_ITER_LOOP,
793823
EXPLICIT_ITER_LOOP,
794824
FOR_KV_MAP,
825+
FOR_UNBOUNDED_RANGE,
795826
INFINITE_LOOP,
796827
ITER_NEXT_LOOP,
797828
MANUAL_FIND,
@@ -951,6 +982,7 @@ impl Loops {
951982
manual_find::check(cx, pat, arg, body, span, expr);
952983
unused_enumerate_index::check(cx, arg, pat, None, body);
953984
char_indices_as_byte_indices::check(cx, pat, arg, body);
985+
for_unbounded_range::check(cx, label, arg, span, body);
954986
}
955987

956988
fn check_for_loop_arg(&self, cx: &LateContext<'_>, _: &Pat<'_>, arg: &Expr<'_>) {

tests/ui/for_unbounded_range.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![warn(clippy::for_unbounded_range)]
2+
3+
fn do_something<T: Copy>(_t: T) {}
4+
5+
fn main() {
6+
for i in 0_u8..=u8::MAX {
7+
do_something(i);
8+
}
9+
10+
for i in 0_u8.. {
11+
if i > 2 {
12+
break;
13+
}
14+
do_something(i);
15+
}
16+
17+
'outer: for i in 0_u8..10 {
18+
for i in 0_u8.. {
19+
if i > 2 {
20+
break 'outer;
21+
}
22+
do_something(i);
23+
}
24+
}
25+
26+
for i in 0_u8..=u8::MAX {
27+
//~^ for_unbounded_range
28+
do_something(i);
29+
}
30+
31+
for i in '\0'..=char::MAX {
32+
//~^ for_unbounded_range
33+
do_something(i);
34+
}
35+
}

tests/ui/for_unbounded_range.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![warn(clippy::for_unbounded_range)]
2+
3+
fn do_something<T: Copy>(_t: T) {}
4+
5+
fn main() {
6+
for i in 0_u8..=u8::MAX {
7+
do_something(i);
8+
}
9+
10+
for i in 0_u8.. {
11+
if i > 2 {
12+
break;
13+
}
14+
do_something(i);
15+
}
16+
17+
'outer: for i in 0_u8..10 {
18+
for i in 0_u8.. {
19+
if i > 2 {
20+
break 'outer;
21+
}
22+
do_something(i);
23+
}
24+
}
25+
26+
for i in 0_u8.. {
27+
//~^ for_unbounded_range
28+
do_something(i);
29+
}
30+
31+
for i in '\0'.. {
32+
//~^ for_unbounded_range
33+
do_something(i);
34+
}
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
error: for loop on unbounded range
2+
--> tests/ui/for_unbounded_range.rs:26:5
3+
|
4+
LL | / for i in 0_u8.. {
5+
LL | |
6+
LL | | do_something(i);
7+
LL | | }
8+
| |_____^
9+
|
10+
= note: `-D clippy::for-unbounded-range` implied by `-D warnings`
11+
= help: to override `-D warnings` add `#[allow(clippy::for_unbounded_range)]`
12+
help: for loops over unbounded ranges will wrap around and may panic, consider adding an inclusive high bound
13+
|
14+
LL | for i in 0_u8..=u8::MAX {
15+
| ++++++++
16+
17+
error: for loop on unbounded range
18+
--> tests/ui/for_unbounded_range.rs:31:5
19+
|
20+
LL | / for i in '\0'.. {
21+
LL | |
22+
LL | | do_something(i);
23+
LL | | }
24+
| |_____^
25+
|
26+
help: for loops over unbounded ranges will wrap around and may panic, consider adding an inclusive high bound
27+
|
28+
LL | for i in '\0'..=char::MAX {
29+
| ++++++++++
30+
31+
error: aborting due to 2 previous errors
32+

tests/ui/manual_memcpy/without_loop_counters.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub fn manual_copy(src: &[i32], dst: &mut [i32], dst2: &mut [i32]) {
9393
dst[..0].copy_from_slice(&src[..0]);
9494

9595
// `RangeTo` `for` loop - don't trigger lint
96+
#[expect(clippy::for_unbounded_range)]
9697
for i in 0.. {
9798
dst[i] = src[i];
9899
}

tests/ui/manual_memcpy/without_loop_counters.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ pub fn manual_copy(src: &[i32], dst: &mut [i32], dst2: &mut [i32]) {
141141
}
142142

143143
// `RangeTo` `for` loop - don't trigger lint
144+
#[expect(clippy::for_unbounded_range)]
144145
for i in 0.. {
145146
dst[i] = src[i];
146147
}

0 commit comments

Comments
 (0)