Skip to content

Commit 3da21b0

Browse files
committed
Auto merge of #11396 - y21:issue11345, r=Jarcho
new lint: `iter_out_of_bounds` Closes #11345 The original idea in the linked issue seemed to be just about arrays afaict, but I extended this to catch some other iterator sources such as `iter::once` or `iter::empty`. I'm not entirely sure if this name makes a lot of sense now that it's not just about arrays anymore (specifically, not sure if you can call `.take(1)` on an `iter::Empty` to be "out of bounds"?). changelog: [`iter_out_of_bounds`]: new lint
2 parents b97eaab + 33cc140 commit 3da21b0

13 files changed

+341
-12
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5031,6 +5031,7 @@ Released 2018-09-13
50315031
[`iter_nth_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_nth_zero
50325032
[`iter_on_empty_collections`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_empty_collections
50335033
[`iter_on_single_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_single_items
5034+
[`iter_out_of_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_out_of_bounds
50345035
[`iter_overeager_cloned`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_overeager_cloned
50355036
[`iter_skip_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_next
50365037
[`iter_skip_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_zero

clippy_lints/src/declared_lints.rs

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

clippy_lints/src/methods/mod.rs

+30-2
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.74.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.
@@ -4146,6 +4172,7 @@ impl Methods {
41464172
},
41474173
("skip", [arg]) => {
41484174
iter_skip_zero::check(cx, expr, arg);
4175+
iter_out_of_bounds::check_skip(cx, expr, recv, arg);
41494176

41504177
if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
41514178
iter_overeager_cloned::check(cx, expr, recv, recv2,
@@ -4173,7 +4200,8 @@ impl Methods {
41734200
}
41744201
},
41754202
("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg),
4176-
("take", [_arg]) => {
4203+
("take", [arg]) => {
4204+
iter_out_of_bounds::check_take(cx, expr, recv, arg);
41774205
if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
41784206
iter_overeager_cloned::check(cx, expr, recv, recv2,
41794207
iter_overeager_cloned::Op::LaterCloned, false);

clippy_utils/src/paths.rs

+2
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"];
@@ -163,3 +164,4 @@ pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
163164
pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
164165
#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
165166
pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
167+
pub const ARRAY_INTO_ITER: [&str; 4] = ["core", "array", "iter", "IntoIter"];

tests/ui/iter_out_of_bounds.rs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//@no-rustfix
2+
3+
#![deny(clippy::iter_out_of_bounds)]
4+
#![allow(clippy::useless_vec)]
5+
6+
fn opaque_empty_iter() -> impl Iterator<Item = ()> {
7+
std::iter::empty()
8+
}
9+
10+
fn main() {
11+
for _ in [1, 2, 3].iter().skip(4) {
12+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
13+
unreachable!();
14+
}
15+
for (i, _) in [1, 2, 3].iter().take(4).enumerate() {
16+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
17+
assert!(i <= 2);
18+
}
19+
20+
#[allow(clippy::needless_borrow)]
21+
for _ in (&&&&&&[1, 2, 3]).iter().take(4) {}
22+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
23+
24+
for _ in [1, 2, 3].iter().skip(4) {}
25+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
26+
27+
for _ in [1; 3].iter().skip(4) {}
28+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
29+
30+
// Should not lint
31+
for _ in opaque_empty_iter().skip(1) {}
32+
33+
for _ in vec![1, 2, 3].iter().skip(4) {}
34+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
35+
36+
for _ in vec![1; 3].iter().skip(4) {}
37+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
38+
39+
let x = [1, 2, 3];
40+
for _ in x.iter().skip(4) {}
41+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
42+
43+
let n = 4;
44+
for _ in x.iter().skip(n) {}
45+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
46+
47+
let empty = std::iter::empty::<i8>;
48+
49+
for _ in empty().skip(1) {}
50+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
51+
52+
for _ in empty().take(1) {}
53+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
54+
55+
for _ in std::iter::once(1).skip(2) {}
56+
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
57+
58+
for _ in std::iter::once(1).take(2) {}
59+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
60+
61+
for x in [].iter().take(1) {
62+
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
63+
let _: &i32 = x;
64+
}
65+
66+
// ok, not out of bounds
67+
for _ in [1].iter().take(1) {}
68+
for _ in [1, 2, 3].iter().take(2) {}
69+
for _ in [1, 2, 3].iter().skip(2) {}
70+
}

tests/ui/iter_out_of_bounds.stderr

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
error: this `.skip()` call skips more items than the iterator will produce
2+
--> $DIR/iter_out_of_bounds.rs:11: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:3: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:15: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:21: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:24: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:27:14
40+
|
41+
LL | for _ in [1; 3].iter().skip(4) {}
42+
| ^^^^^^^^^^^^^^^^^^^^^
43+
|
44+
= note: this operation is useless and will create an empty iterator
45+
46+
error: this `.skip()` call skips more items than the iterator will produce
47+
--> $DIR/iter_out_of_bounds.rs:33:14
48+
|
49+
LL | for _ in vec![1, 2, 3].iter().skip(4) {}
50+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
51+
|
52+
= note: this operation is useless and will create an empty iterator
53+
54+
error: this `.skip()` call skips more items than the iterator will produce
55+
--> $DIR/iter_out_of_bounds.rs:36:14
56+
|
57+
LL | for _ in vec![1; 3].iter().skip(4) {}
58+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
59+
|
60+
= note: this operation is useless and will create an empty iterator
61+
62+
error: this `.skip()` call skips more items than the iterator will produce
63+
--> $DIR/iter_out_of_bounds.rs:40:14
64+
|
65+
LL | for _ in x.iter().skip(4) {}
66+
| ^^^^^^^^^^^^^^^^
67+
|
68+
= note: this operation is useless and will create an empty iterator
69+
70+
error: this `.skip()` call skips more items than the iterator will produce
71+
--> $DIR/iter_out_of_bounds.rs:44:14
72+
|
73+
LL | for _ in x.iter().skip(n) {}
74+
| ^^^^^^^^^^^^^^^^
75+
|
76+
= note: this operation is useless and will create an empty iterator
77+
78+
error: this `.skip()` call skips more items than the iterator will produce
79+
--> $DIR/iter_out_of_bounds.rs:49:14
80+
|
81+
LL | for _ in empty().skip(1) {}
82+
| ^^^^^^^^^^^^^^^
83+
|
84+
= note: this operation is useless and will create an empty iterator
85+
86+
error: this `.take()` call takes more items than the iterator will produce
87+
--> $DIR/iter_out_of_bounds.rs:52:14
88+
|
89+
LL | for _ in empty().take(1) {}
90+
| ^^^^^^^^^^^^^^^
91+
|
92+
= note: this operation is useless and the returned iterator will simply yield the same items
93+
94+
error: this `.skip()` call skips more items than the iterator will produce
95+
--> $DIR/iter_out_of_bounds.rs:55:14
96+
|
97+
LL | for _ in std::iter::once(1).skip(2) {}
98+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
99+
|
100+
= note: this operation is useless and will create an empty iterator
101+
102+
error: this `.take()` call takes more items than the iterator will produce
103+
--> $DIR/iter_out_of_bounds.rs:58:14
104+
|
105+
LL | for _ in std::iter::once(1).take(2) {}
106+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
107+
|
108+
= note: this operation is useless and the returned iterator will simply yield the same items
109+
110+
error: this `.take()` call takes more items than the iterator will produce
111+
--> $DIR/iter_out_of_bounds.rs:61:14
112+
|
113+
LL | for x in [].iter().take(1) {
114+
| ^^^^^^^^^^^^^^^^^
115+
|
116+
= note: this operation is useless and the returned iterator will simply yield the same items
117+
118+
error: aborting due to 14 previous errors
119+

0 commit comments

Comments
 (0)