Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new lint: iter_out_of_bounds #11396

Merged
merged 3 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5030,6 +5030,7 @@ Released 2018-09-13
[`iter_nth_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_nth_zero
[`iter_on_empty_collections`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_empty_collections
[`iter_on_single_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_on_single_items
[`iter_out_of_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_out_of_bounds
[`iter_overeager_cloned`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_overeager_cloned
[`iter_skip_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_next
[`iter_skip_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_skip_zero
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::methods::ITER_NTH_ZERO_INFO,
crate::methods::ITER_ON_EMPTY_COLLECTIONS_INFO,
crate::methods::ITER_ON_SINGLE_ITEMS_INFO,
crate::methods::ITER_OUT_OF_BOUNDS_INFO,
crate::methods::ITER_OVEREAGER_CLONED_INFO,
crate::methods::ITER_SKIP_NEXT_INFO,
crate::methods::ITER_SKIP_ZERO_INFO,
Expand Down
106 changes: 106 additions & 0 deletions clippy_lints/src/methods/iter_out_of_bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use clippy_utils::diagnostics::span_lint_and_note;
use clippy_utils::higher::VecArgs;
use clippy_utils::{expr_or_init, is_trait_method, match_def_path, paths};
use rustc_ast::LitKind;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self};
use rustc_span::sym;

use super::ITER_OUT_OF_BOUNDS;

fn expr_as_u128(cx: &LateContext<'_>, e: &Expr<'_>) -> Option<u128> {
if let ExprKind::Lit(lit) = expr_or_init(cx, e).kind
&& let LitKind::Int(n, _) = lit.node
{
Some(n)
} else {
None
}
}

/// Attempts to extract the length out of an iterator expression.
fn get_iterator_length<'tcx>(cx: &LateContext<'tcx>, iter: &'tcx Expr<'tcx>) -> Option<u128> {
let ty::Adt(adt, substs) = cx.typeck_results().expr_ty(iter).kind() else {
return None;
};
let did = adt.did();

if match_def_path(cx, did, &paths::ARRAY_INTO_ITER) {
// For array::IntoIter<T, const N: usize>, the length is the second generic
// parameter.
substs
.const_at(1)
.try_eval_target_usize(cx.tcx, cx.param_env)
.map(u128::from)
} else if match_def_path(cx, did, &paths::SLICE_ITER)
&& let ExprKind::MethodCall(_, recv, ..) = iter.kind
{
if let ty::Array(_, len) = cx.typeck_results().expr_ty(recv).peel_refs().kind() {
// For slice::Iter<'_, T>, the receiver might be an array literal: [1,2,3].iter().skip(..)
len.try_eval_target_usize(cx.tcx, cx.param_env).map(u128::from)
} else if let Some(args) = VecArgs::hir(cx, expr_or_init(cx, recv)) {
match args {
VecArgs::Vec(vec) => vec.len().try_into().ok(),
VecArgs::Repeat(_, len) => expr_as_u128(cx, len),
}
} else {
None
}
} else if match_def_path(cx, did, &paths::ITER_EMPTY) {
Some(0)
} else if match_def_path(cx, did, &paths::ITER_ONCE) {
Some(1)
} else {
None
}
}

fn check<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
recv: &'tcx Expr<'tcx>,
arg: &'tcx Expr<'tcx>,
message: &'static str,
note: &'static str,
) {
if is_trait_method(cx, expr, sym::Iterator)
&& let Some(len) = get_iterator_length(cx, recv)
&& let Some(skipped) = expr_as_u128(cx, arg)
&& skipped > len
{
span_lint_and_note(cx, ITER_OUT_OF_BOUNDS, expr.span, message, None, note);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really should have a MaybeIncorrect suggestion added to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the suggestion should be or if it makes sense to have one. At least for something like [1,2,3].iter().take(4) it could probably suggest removing the .take() call as that'll still yield the same items with or without it, but I don't know what the user could have meant with [1,2,3].iter().skip(4). Should it suggest and empty iterator? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was only considering take here. Given the suggestion isn't going to have a lot of value (decent chance of being the wrong fix), I guess it's fine to not have one.

}
}

pub(super) fn check_skip<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
recv: &'tcx Expr<'tcx>,
arg: &'tcx Expr<'tcx>,
) {
check(
cx,
expr,
recv,
arg,
"this `.skip()` call skips more items than the iterator will produce",
"this operation is useless and will create an empty iterator",
);
}

pub(super) fn check_take<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
recv: &'tcx Expr<'tcx>,
arg: &'tcx Expr<'tcx>,
) {
check(
cx,
expr,
recv,
arg,
"this `.take()` call takes more items than the iterator will produce",
"this operation is useless and the returned iterator will simply yield the same items",
);
}
32 changes: 30 additions & 2 deletions clippy_lints/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ mod iter_next_slice;
mod iter_nth;
mod iter_nth_zero;
mod iter_on_single_or_empty_collections;
mod iter_out_of_bounds;
mod iter_overeager_cloned;
mod iter_skip_next;
mod iter_skip_zero;
Expand Down Expand Up @@ -3538,6 +3539,30 @@ declare_clippy_lint! {
"acquiring a write lock when a read lock would work"
}

declare_clippy_lint! {
/// ### What it does
/// Looks for iterator combinator calls such as `.take(x)` or `.skip(x)`
/// where `x` is greater than the amount of items that an iterator will produce.
///
/// ### Why is this bad?
/// Taking or skipping more items than there are in an iterator either creates an iterator
/// with all items from the original iterator or an iterator with no items at all.
/// This is most likely not what the user intended to do.
///
/// ### Example
/// ```rust
/// for _ in [1, 2, 3].iter().take(4) {}
/// ```
/// Use instead:
/// ```rust
/// for _ in [1, 2, 3].iter() {}
/// ```
#[clippy::version = "1.74.0"]
pub ITER_OUT_OF_BOUNDS,
suspicious,
"calls to `.take()` or `.skip()` that are out of bounds"
}

pub struct Methods {
avoid_breaking_exported_api: bool,
msrv: Msrv,
Expand Down Expand Up @@ -3676,7 +3701,8 @@ impl_lint_pass!(Methods => [
STRING_LIT_CHARS_ANY,
ITER_SKIP_ZERO,
FILTER_MAP_BOOL_THEN,
READONLY_WRITE_LOCK
READONLY_WRITE_LOCK,
ITER_OUT_OF_BOUNDS,
]);

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

if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
iter_overeager_cloned::check(cx, expr, recv, recv2,
Expand Down Expand Up @@ -4163,7 +4190,8 @@ impl Methods {
}
},
("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg),
("take", [_arg]) => {
("take", [arg]) => {
iter_out_of_bounds::check_take(cx, expr, recv, arg);
if let Some(("cloned", recv2, [], _span2, _)) = method_call(recv) {
iter_overeager_cloned::check(cx, expr, recv, recv2,
iter_overeager_cloned::Op::LaterCloned, false);
Expand Down
2 changes: 2 additions & 0 deletions clippy_utils/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub const IDENT: [&str; 3] = ["rustc_span", "symbol", "Ident"];
pub const IDENT_AS_STR: [&str; 4] = ["rustc_span", "symbol", "Ident", "as_str"];
pub const INSERT_STR: [&str; 4] = ["alloc", "string", "String", "insert_str"];
pub const ITER_EMPTY: [&str; 5] = ["core", "iter", "sources", "empty", "Empty"];
pub const ITER_ONCE: [&str; 5] = ["core", "iter", "sources", "once", "Once"];
pub const ITERTOOLS_NEXT_TUPLE: [&str; 3] = ["itertools", "Itertools", "next_tuple"];
#[cfg(feature = "internal")]
pub const KW_MODULE: [&str; 3] = ["rustc_span", "symbol", "kw"];
Expand Down Expand Up @@ -166,3 +167,4 @@ pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
pub const ARRAY_INTO_ITER: [&str; 4] = ["core", "array", "iter", "IntoIter"];
70 changes: 70 additions & 0 deletions tests/ui/iter_out_of_bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//@no-rustfix

#![deny(clippy::iter_out_of_bounds)]
#![allow(clippy::useless_vec)]

fn opaque_empty_iter() -> impl Iterator<Item = ()> {
std::iter::empty()
}

fn main() {
for _ in [1, 2, 3].iter().skip(4) {
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce
unreachable!();
}
for (i, _) in [1, 2, 3].iter().take(4).enumerate() {
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
assert!(i <= 2);
}

#[allow(clippy::needless_borrow)]
for _ in (&&&&&&[1, 2, 3]).iter().take(4) {}
//~^ ERROR: this `.take()` call takes more items than the iterator will produce

for _ in [1, 2, 3].iter().skip(4) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

for _ in [1; 3].iter().skip(4) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

// Should not lint
for _ in opaque_empty_iter().skip(1) {}

for _ in vec![1, 2, 3].iter().skip(4) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

for _ in vec![1; 3].iter().skip(4) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

let x = [1, 2, 3];
for _ in x.iter().skip(4) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

let n = 4;
for _ in x.iter().skip(n) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

let empty = std::iter::empty::<i8>;

for _ in empty().skip(1) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

for _ in empty().take(1) {}
//~^ ERROR: this `.take()` call takes more items than the iterator will produce

for _ in std::iter::once(1).skip(2) {}
//~^ ERROR: this `.skip()` call skips more items than the iterator will produce

for _ in std::iter::once(1).take(2) {}
//~^ ERROR: this `.take()` call takes more items than the iterator will produce

for x in [].iter().take(1) {
//~^ ERROR: this `.take()` call takes more items than the iterator will produce
let _: &i32 = x;
}

// ok, not out of bounds
for _ in [1].iter().take(1) {}
for _ in [1, 2, 3].iter().take(2) {}
for _ in [1, 2, 3].iter().skip(2) {}
}
119 changes: 119 additions & 0 deletions tests/ui/iter_out_of_bounds.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:11:14
|
LL | for _ in [1, 2, 3].iter().skip(4) {
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator
note: the lint level is defined here
--> $DIR/iter_out_of_bounds.rs:3:9
|
LL | #![deny(clippy::iter_out_of_bounds)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^

error: this `.take()` call takes more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:15:19
|
LL | for (i, _) in [1, 2, 3].iter().take(4).enumerate() {
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and the returned iterator will simply yield the same items

error: this `.take()` call takes more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:21:14
|
LL | for _ in (&&&&&&[1, 2, 3]).iter().take(4) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and the returned iterator will simply yield the same items

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:24:14
|
LL | for _ in [1, 2, 3].iter().skip(4) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:27:14
|
LL | for _ in [1; 3].iter().skip(4) {}
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:33:14
|
LL | for _ in vec![1, 2, 3].iter().skip(4) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:36:14
|
LL | for _ in vec![1; 3].iter().skip(4) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:40:14
|
LL | for _ in x.iter().skip(4) {}
| ^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:44:14
|
LL | for _ in x.iter().skip(n) {}
| ^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:49:14
|
LL | for _ in empty().skip(1) {}
| ^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.take()` call takes more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:52:14
|
LL | for _ in empty().take(1) {}
| ^^^^^^^^^^^^^^^
|
= note: this operation is useless and the returned iterator will simply yield the same items

error: this `.skip()` call skips more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:55:14
|
LL | for _ in std::iter::once(1).skip(2) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and will create an empty iterator

error: this `.take()` call takes more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:58:14
|
LL | for _ in std::iter::once(1).take(2) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and the returned iterator will simply yield the same items

error: this `.take()` call takes more items than the iterator will produce
--> $DIR/iter_out_of_bounds.rs:61:14
|
LL | for x in [].iter().take(1) {
| ^^^^^^^^^^^^^^^^^
|
= note: this operation is useless and the returned iterator will simply yield the same items

error: aborting due to 14 previous errors