Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 86b6644

Browse files
committed
new lint: iter_out_of_bounds
1 parent 4932d05 commit 86b6644

File tree

9 files changed

+242
-4
lines changed

9 files changed

+242
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5030,6 +5030,7 @@ Released 2018-09-13
50305030
[`iter_nth_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_nth_zero
50315031
[`iter_on_empty_collections`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_empty_collections
50325032
[`iter_on_single_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_single_items
5033+
[`iter_out_of_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_out_of_bounds
50335034
[`iter_overeager_cloned`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_overeager_cloned
50345035
[`iter_skip_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_next
50355036
[`iter_skip_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_zero

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
364364
crate::methods::ITER_NTH_ZERO_INFO,
365365
crate::methods::ITER_ON_EMPTY_COLLECTIONS_INFO,
366366
crate::methods::ITER_ON_SINGLE_ITEMS_INFO,
367+
crate::methods::ITER_OUT_OF_BOUNDS_INFO,
367368
crate::methods::ITER_OVEREAGER_CLONED_INFO,
368369
crate::methods::ITER_SKIP_NEXT_INFO,
369370
crate::methods::ITER_SKIP_ZERO_INFO,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use clippy_utils::diagnostics::span_lint_and_note;
2+
use clippy_utils::{is_trait_method, match_def_path, paths};
3+
use rustc_ast::LitKind;
4+
use rustc_hir::{Expr, ExprKind};
5+
use rustc_lint::LateContext;
6+
use rustc_middle::ty::{self};
7+
use rustc_span::sym;
8+
9+
use super::ITER_OUT_OF_BOUNDS;
10+
11+
/// Attempts to extract the length out of an iterator expression.
12+
fn get_iterator_length<'tcx>(cx: &LateContext<'tcx>, iter: &'tcx Expr<'tcx>) -> Option<u128> {
13+
let iter_ty = cx.typeck_results().expr_ty(iter);
14+
15+
if let ty::Adt(adt, substs) = iter_ty.kind() {
16+
let did = adt.did();
17+
18+
if match_def_path(cx, did, &paths::ARRAY_INTO_ITER) {
19+
// For array::IntoIter<T, const N: usize>, the length is the second generic
20+
// parameter.
21+
substs
22+
.const_at(1)
23+
.try_eval_target_usize(cx.tcx, cx.param_env)
24+
.map(u128::from)
25+
} else if match_def_path(cx, did, &paths::SLICE_ITER)
26+
&& let ExprKind::MethodCall(_, recv, ..) = iter.kind
27+
&& let ExprKind::Array(array) = recv.peel_borrows().kind
28+
{
29+
// For slice::Iter<'_, T>, the receiver might be an array literal: [1,2,3].iter().skip(..)
30+
array.len().try_into().ok()
31+
} else if match_def_path(cx, did, &paths::ITER_EMPTY) {
32+
Some(0)
33+
} else if match_def_path(cx, did, &paths::ITER_ONCE) {
34+
Some(1)
35+
} else {
36+
None
37+
}
38+
} else {
39+
None
40+
}
41+
}
42+
43+
fn check<'tcx>(
44+
cx: &LateContext<'tcx>,
45+
expr: &'tcx Expr<'tcx>,
46+
recv: &'tcx Expr<'tcx>,
47+
arg: &'tcx Expr<'tcx>,
48+
message: &'static str,
49+
note: &'static str,
50+
) {
51+
if is_trait_method(cx, expr, sym::Iterator)
52+
&& let Some(len) = get_iterator_length(cx, recv)
53+
&& let ExprKind::Lit(lit) = arg.kind
54+
&& let LitKind::Int(skip, _) = lit.node
55+
&& skip > len
56+
{
57+
span_lint_and_note(cx, ITER_OUT_OF_BOUNDS, expr.span, message, None, note);
58+
}
59+
}
60+
61+
pub(super) fn check_skip<'tcx>(
62+
cx: &LateContext<'tcx>,
63+
expr: &'tcx Expr<'tcx>,
64+
recv: &'tcx Expr<'tcx>,
65+
arg: &'tcx Expr<'tcx>,
66+
) {
67+
check(
68+
cx,
69+
expr,
70+
recv,
71+
arg,
72+
"this `.skip()` call skips more items than the iterator will produce",
73+
"this operation is useless and will create an empty iterator",
74+
);
75+
}
76+
77+
pub(super) fn check_take<'tcx>(
78+
cx: &LateContext<'tcx>,
79+
expr: &'tcx Expr<'tcx>,
80+
recv: &'tcx Expr<'tcx>,
81+
arg: &'tcx Expr<'tcx>,
82+
) {
83+
check(
84+
cx,
85+
expr,
86+
recv,
87+
arg,
88+
"this `.take()` call takes more items than the iterator will produce",
89+
"this operation is useless and the returned iterator will simply yield the same items",
90+
);
91+
}

clippy_lints/src/methods/mod.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ mod iter_next_slice;
4343
mod iter_nth;
4444
mod iter_nth_zero;
4545
mod iter_on_single_or_empty_collections;
46+
mod iter_out_of_bounds;
4647
mod iter_overeager_cloned;
4748
mod iter_skip_next;
4849
mod iter_skip_zero;
@@ -3538,6 +3539,30 @@ declare_clippy_lint! {
35383539
"acquiring a write lock when a read lock would work"
35393540
}
35403541

3542+
declare_clippy_lint! {
3543+
/// ### What it does
3544+
/// Looks for iterator combinator calls such as `.take(x)` or `.skip(x)`
3545+
/// where `x` is greater than the amount of items that an iterator will produce.
3546+
///
3547+
/// ### Why is this bad?
3548+
/// Taking or skipping more items than there are in an iterator either creates an iterator
3549+
/// with all items from the original iterator or an iterator with no items at all.
3550+
/// This is most likely not what the user intended to do.
3551+
///
3552+
/// ### Example
3553+
/// ```rust
3554+
/// for _ in [1, 2, 3].iter().take(4) {}
3555+
/// ```
3556+
/// Use instead:
3557+
/// ```rust
3558+
/// for _ in [1, 2, 3].iter() {}
3559+
/// ```
3560+
#[clippy::version = "1.73.0"]
3561+
pub ITER_OUT_OF_BOUNDS,
3562+
suspicious,
3563+
"calls to `.take()` or `.skip()` that are out of bounds"
3564+
}
3565+
35413566
pub struct Methods {
35423567
avoid_breaking_exported_api: bool,
35433568
msrv: Msrv,
@@ -3676,7 +3701,8 @@ impl_lint_pass!(Methods => [
36763701
STRING_LIT_CHARS_ANY,
36773702
ITER_SKIP_ZERO,
36783703
FILTER_MAP_BOOL_THEN,
3679-
READONLY_WRITE_LOCK
3704+
READONLY_WRITE_LOCK,
3705+
ITER_OUT_OF_BOUNDS,
36803706
]);
36813707

36823708
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4136,6 +4162,7 @@ impl Methods {
41364162
},
41374163
("skip", [arg]) => {
41384164
iter_skip_zero::check(cx, expr, arg);
4165+
iter_out_of_bounds::check_skip(cx, expr, recv, arg);
41394166

41404167
if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
41414168
iter_overeager_cloned::check(cx, expr, recv, recv2,
@@ -4163,7 +4190,8 @@ impl Methods {
41634190
}
41644191
},
41654192
("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg),
4166-
("take", [_arg]) => {
4193+
("take", [arg]) => {
4194+
iter_out_of_bounds::check_take(cx, expr, recv, arg);
41674195
if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
41684196
iter_overeager_cloned::check(cx, expr, recv, recv2,
41694197
iter_overeager_cloned::Op::LaterCloned, false);

clippy_utils/src/paths.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub const IDENT: [&str; 3] = ["rustc_span", "symbol", "Ident"];
4949
pub const IDENT_AS_STR: [&str; 4] = ["rustc_span", "symbol", "Ident", "as_str"];
5050
pub const INSERT_STR: [&str; 4] = ["alloc", "string", "String", "insert_str"];
5151
pub const ITER_EMPTY: [&str; 5] = ["core", "iter", "sources", "empty", "Empty"];
52+
pub const ITER_ONCE: [&str; 5] = ["core", "iter", "sources", "once", "Once"];
5253
pub const ITERTOOLS_NEXT_TUPLE: [&str; 3] = ["itertools", "Itertools", "next_tuple"];
5354
#[cfg(feature = "internal")]
5455
pub const KW_MODULE: [&str; 3] = ["rustc_span", "symbol", "kw"];
@@ -166,3 +167,4 @@ pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
166167
pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
167168
#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
168169
pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
170+
pub const ARRAY_INTO_ITER: [&str; 4] = ["core", "array", "iter", "IntoIter"];

tests/ui/iter_out_of_bounds.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![deny(clippy::iter_out_of_bounds)]
2+
3+
fn opaque_empty_iter() -> impl Iterator<Item = ()> {
4+
std::iter::empty()
5+
}
6+
7+
fn main() {
8+
for _ in [1, 2, 3].iter().skip(4) {
9+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
10+
unreachable!();
11+
}
12+
for (i, _) in [1, 2, 3].iter().take(4).enumerate() {
13+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
14+
assert!(i <= 2);
15+
}
16+
17+
#[allow(clippy::needless_borrow)]
18+
for _ in (&&&&&&[1, 2, 3]).iter().take(4) {}
19+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
20+
21+
for _ in [1, 2, 3].iter().skip(4) {}
22+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
23+
24+
// Should not lint
25+
for _ in opaque_empty_iter().skip(1) {}
26+
27+
// Should not lint
28+
let empty: [i8; 0] = [];
29+
for _ in empty.iter().skip(1) {}
30+
31+
let empty = std::iter::empty::<i8>;
32+
33+
for _ in empty().skip(1) {}
34+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
35+
36+
for _ in empty().take(1) {}
37+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
38+
39+
for _ in std::iter::once(1).skip(2) {}
40+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
41+
42+
for _ in std::iter::once(1).take(2) {}
43+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
44+
}

tests/ui/iter_out_of_bounds.stderr

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
error: this `.skip()` call skips more items than the iterator will produce
2+
--> $DIR/iter_out_of_bounds.rs:8:14
3+
|
4+
LL | for _ in [1, 2, 3].iter().skip(4) {
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: this operation is useless and will create an empty iterator
8+
note: the lint level is defined here
9+
--> $DIR/iter_out_of_bounds.rs:1:9
10+
|
11+
LL | #![deny(clippy::iter_out_of_bounds)]
12+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
13+
14+
error: this `.take()` call takes more items than the iterator will produce
15+
--> $DIR/iter_out_of_bounds.rs:12:19
16+
|
17+
LL | for (i, _) in [1, 2, 3].iter().take(4).enumerate() {
18+
| ^^^^^^^^^^^^^^^^^^^^^^^^
19+
|
20+
= note: this operation is useless and the returned iterator will simply yield the same items
21+
22+
error: this `.take()` call takes more items than the iterator will produce
23+
--> $DIR/iter_out_of_bounds.rs:18:14
24+
|
25+
LL | for _ in (&&&&&&[1, 2, 3]).iter().take(4) {}
26+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27+
|
28+
= note: this operation is useless and the returned iterator will simply yield the same items
29+
30+
error: this `.skip()` call skips more items than the iterator will produce
31+
--> $DIR/iter_out_of_bounds.rs:21:14
32+
|
33+
LL | for _ in [1, 2, 3].iter().skip(4) {}
34+
| ^^^^^^^^^^^^^^^^^^^^^^^^
35+
|
36+
= note: this operation is useless and will create an empty iterator
37+
38+
error: this `.skip()` call skips more items than the iterator will produce
39+
--> $DIR/iter_out_of_bounds.rs:33:14
40+
|
41+
LL | for _ in empty().skip(1) {}
42+
| ^^^^^^^^^^^^^^^
43+
|
44+
= note: this operation is useless and will create an empty iterator
45+
46+
error: this `.take()` call takes more items than the iterator will produce
47+
--> $DIR/iter_out_of_bounds.rs:36:14
48+
|
49+
LL | for _ in empty().take(1) {}
50+
| ^^^^^^^^^^^^^^^
51+
|
52+
= note: this operation is useless and the returned iterator will simply yield the same items
53+
54+
error: this `.skip()` call skips more items than the iterator will produce
55+
--> $DIR/iter_out_of_bounds.rs:39:14
56+
|
57+
LL | for _ in std::iter::once(1).skip(2) {}
58+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
59+
|
60+
= note: this operation is useless and will create an empty iterator
61+
62+
error: this `.take()` call takes more items than the iterator will produce
63+
--> $DIR/iter_out_of_bounds.rs:42:14
64+
|
65+
LL | for _ in std::iter::once(1).take(2) {}
66+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
67+
|
68+
= note: this operation is useless and the returned iterator will simply yield the same items
69+
70+
error: aborting due to 8 previous errors
71+

tests/ui/iter_skip_zero.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//@aux-build:proc_macros.rs
2-
#![allow(clippy::useless_vec, unused)]
2+
#![allow(clippy::useless_vec, clippy::iter_out_of_bounds, unused)]
33
#![warn(clippy::iter_skip_zero)]
44

55
#[macro_use]

tests/ui/iter_skip_zero.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//@aux-build:proc_macros.rs
2-
#![allow(clippy::useless_vec, unused)]
2+
#![allow(clippy::useless_vec, clippy::iter_out_of_bounds, unused)]
33
#![warn(clippy::iter_skip_zero)]
44

55
#[macro_use]

0 commit comments

Comments
 (0)