Skip to content

Commit 824aa1f

Browse files
authored
New by_ref_peekable_peek lint (#17042)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/rust-clippy/pull/17042)* changelog: [`by_ref_peekable_peek`]: new lint Closes #17020
2 parents 5f29bd0 + 2ae1735 commit 824aa1f

9 files changed

Lines changed: 445 additions & 0 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6670,6 +6670,7 @@ Released 2018-09-13
66706670
[`boxed_local`]: https://rust-lang.github.io/rust-clippy/master/index.html#boxed_local
66716671
[`branches_sharing_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#branches_sharing_code
66726672
[`builtin_type_shadow`]: https://rust-lang.github.io/rust-clippy/master/index.html#builtin_type_shadow
6673+
[`by_ref_peekable_peek`]: https://rust-lang.github.io/rust-clippy/master/index.html#by_ref_peekable_peek
66736674
[`byte_char_slices`]: https://rust-lang.github.io/rust-clippy/master/index.html#byte_char_slices
66746675
[`bytes_count_to_len`]: https://rust-lang.github.io/rust-clippy/master/index.html#bytes_count_to_len
66756676
[`bytes_nth`]: https://rust-lang.github.io/rust-clippy/master/index.html#bytes_nth

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
361361
crate::mem_replace::MEM_REPLACE_WITH_DEFAULT_INFO,
362362
crate::mem_replace::MEM_REPLACE_WITH_UNINIT_INFO,
363363
crate::methods::BIND_INSTEAD_OF_MAP_INFO,
364+
crate::methods::BY_REF_PEEKABLE_PEEK_INFO,
364365
crate::methods::BYTES_COUNT_TO_LEN_INFO,
365366
crate::methods::BYTES_NTH_INFO,
366367
crate::methods::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS_INFO,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
use crate::clippy_utils::res::MaybeTypeckRes;
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::res::{MaybeDef, MaybeResPath as _};
4+
use clippy_utils::sugg::Sugg;
5+
use clippy_utils::sym;
6+
use clippy_utils::ty::implements_trait;
7+
use rustc_ast::BindingMode;
8+
use rustc_errors::Applicability;
9+
use rustc_hir::{Expr, ExprKind, LetStmt, Node, PatKind};
10+
use rustc_lint::LateContext;
11+
use rustc_middle::ty;
12+
13+
use super::BY_REF_PEEKABLE_PEEK;
14+
15+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>) {
16+
if let ExprKind::MethodCall(maybe_peekable, peekable_recv, [], _) = recv.kind
17+
&& maybe_peekable.ident.name == sym::peekable
18+
&& !peekable_recv.span.from_expansion()
19+
&& let ExprKind::MethodCall(maybe_by_ref, by_ref_recv, [], _) = peekable_recv.kind
20+
&& maybe_by_ref.ident.name == sym::by_ref
21+
&& !by_ref_recv.span.from_expansion()
22+
&& [peekable_recv, recv]
23+
.into_iter()
24+
.all(|e| cx.ty_based_def(e).opt_parent(cx).is_diag_item(cx, sym::Iterator))
25+
{
26+
span_lint_and_then(
27+
cx,
28+
BY_REF_PEEKABLE_PEEK,
29+
expr.span,
30+
"calling `.by_ref().peekable().peek()` will advance the underlying iterator and consume its first output",
31+
|diag| {
32+
let span = by_ref_recv.span.shrink_to_hi().with_hi(expr.span.hi());
33+
if let ty::Ref(_, iter_ty, _) = cx.typeck_results().expr_ty_adjusted(by_ref_recv).kind()
34+
&& let Some(clone_trait) = cx.tcx.lang_items().clone_trait()
35+
&& implements_trait(cx, *iter_ty, clone_trait, &[])
36+
{
37+
diag.span_suggestion_verbose(
38+
span,
39+
"to peek the first item without advancing the underlying iterator, use",
40+
".clone().next().as_ref()",
41+
Applicability::MaybeIncorrect,
42+
);
43+
}
44+
diag.span_suggestion_verbose(
45+
span,
46+
"to advance the underlying iterator, use",
47+
".next().as_ref()",
48+
Applicability::MaybeIncorrect,
49+
);
50+
// If the iterator is a local variable, initialized through a simple binding with an inferred
51+
// initialization expression, suggest making the initialization expression peekable.
52+
if let Some(iter_local_id) = by_ref_recv.res_local_id()
53+
&& let Node::LetStmt(LetStmt {
54+
pat: let_pat,
55+
ty: None,
56+
init: Some(init_expr),
57+
els: None,
58+
span: let_stmt_span,
59+
..
60+
}) = cx.tcx.parent_hir_node(iter_local_id)
61+
&& let PatKind::Binding(BindingMode::MUT, _, _, None) = let_pat.kind
62+
&& !let_stmt_span.from_expansion()
63+
// Changing the type of the iterator may prevent the code from compiling
64+
&& let mut app = Applicability::MaybeIncorrect
65+
&& let sugg =
66+
Sugg::hir_with_context(cx, init_expr, let_stmt_span.ctxt(), "_", &mut app).maybe_paren()
67+
{
68+
diag.multipart_suggestion(
69+
"to make the iterator peekable, use",
70+
vec![
71+
(init_expr.span.source_callsite(), format!("{sugg}.peekable()")),
72+
(recv.span.with_lo(by_ref_recv.span.hi()), String::new()),
73+
],
74+
app,
75+
);
76+
} else {
77+
diag.help(
78+
"you might want to transform the iterator itself using `.peekable()` without using `.by_ref()`",
79+
);
80+
}
81+
},
82+
);
83+
}
84+
}

clippy_lints/src/methods/mod.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod bind_instead_of_map;
2+
mod by_ref_peekable_peek;
23
mod bytecount;
34
mod bytes_count_to_len;
45
mod bytes_nth;
@@ -201,6 +202,39 @@ declare_clippy_lint! {
201202
"using `Option.and_then(|x| Some(y))`, which is more succinctly expressed as `map(|x| y)`"
202203
}
203204

205+
declare_clippy_lint! {
206+
/// ### What it does
207+
/// Checks for usages of `Iterator::by_ref().peekable().peek()`.
208+
///
209+
/// ### Why is this bad?
210+
/// While it might look like this will allow peeking on the first
211+
/// element of an iterator without consuming it and without consuming
212+
/// the iterator itself, it will in practice consume the first element.
213+
///
214+
/// The implementation of `Peekable::peek()` produces the first element
215+
/// of the underlying iterator, and stores it internally so that it can
216+
/// be later produced. As a consequence, it advances the underlying
217+
/// iterator, whose `.next()` method will now produce its second element.
218+
///
219+
/// ### Example
220+
/// ```no_run
221+
/// let mut iter = [1, 2, 3].into_iter();
222+
/// let x = iter.by_ref().peekable().peek(); // 1
223+
/// let y = iter.by_ref().peekable().peek(); // 2
224+
/// ```
225+
/// If this does what you intended, use the following instead, which is
226+
/// shorter and clearer:
227+
/// ```no_run
228+
/// let mut iter = [1, 2, 3].into_iter();
229+
/// let x = iter.next().as_ref(); // 1
230+
/// let y = iter.next().as_ref(); // 2
231+
/// ```
232+
#[clippy::version = "1.98.0"]
233+
pub BY_REF_PEEKABLE_PEEK,
234+
suspicious,
235+
"Using `.by_ref().peekable().peek()` on an iterator"
236+
}
237+
204238
declare_clippy_lint! {
205239
/// ### What it does
206240
/// It checks for `str::bytes().count()` and suggests replacing it with
@@ -4835,6 +4869,7 @@ impl_lint_pass!(Methods => [
48354869
BIND_INSTEAD_OF_MAP,
48364870
BYTES_COUNT_TO_LEN,
48374871
BYTES_NTH,
4872+
BY_REF_PEEKABLE_PEEK,
48384873
CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS,
48394874
CHARS_LAST_CMP,
48404875
CHARS_NEXT_CMP,
@@ -5604,6 +5639,9 @@ impl Methods {
56045639
unnecessary_lazy_eval::check(cx, expr, recv, arg, "or");
56055640
}
56065641
},
5642+
(sym::peek, []) => {
5643+
by_ref_peekable_peek::check(cx, expr, recv);
5644+
},
56075645
(sym::push, [arg]) => {
56085646
path_buf_push_overwrite::check(cx, expr, arg);
56095647
},
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![warn(clippy::by_ref_peekable_peek)]
2+
3+
struct S;
4+
5+
impl S {
6+
fn by_ref(&mut self) -> impl Iterator<Item = i32> {
7+
std::iter::empty()
8+
}
9+
}
10+
11+
macro_rules! mac {
12+
($x:expr) => {
13+
$x.by_ref().peekable().peek()
14+
};
15+
}
16+
17+
fn with_non_clone_parameter(i: &mut impl Iterator<Item = i32>) {
18+
// This won't suggest `.clone().next().as_ref()` as `i` is not `Clone`
19+
let _: Option<&i32> = i.next().as_ref();
20+
//~^ by_ref_peekable_peek
21+
}
22+
23+
fn with_cloneable_local_iterator(a: Vec<i32>) {
24+
let mut i = a.into_iter();
25+
let _: Option<&i32> = i.clone().next().as_ref();
26+
//~^ by_ref_peekable_peek
27+
}
28+
29+
fn with_cloneable_local_iterator_from_macro() {
30+
macro_rules! mac {
31+
() => {
32+
[1, 2, 3].into_iter()
33+
};
34+
}
35+
let mut i = mac!();
36+
let _: Option<&i32> = i.clone().next().as_ref();
37+
//~^ by_ref_peekable_peek
38+
}
39+
40+
fn main() {
41+
let mut iter = [1, 2, 3].into_iter();
42+
let _: Option<&i32> = iter.clone().next().as_ref();
43+
//~^ by_ref_peekable_peek
44+
#[expect(clippy::needless_borrow)]
45+
#[allow(clippy::unnecessary_mut_passed)] // For the `.clone().next().as_ref()` suggestion
46+
let _: Option<&i32> = (&mut iter).clone().next().as_ref();
47+
//~^ by_ref_peekable_peek
48+
49+
// Do not lint if `by_ref()` is not the one on `Iterator`
50+
let _: Option<&i32> = S.by_ref().peekable().peek();
51+
52+
// Do not lint if coming from a macro, as we cannot ensure
53+
// that all uses of `.by_ref()` will be `Iterator::by_ref()`.
54+
let _: Option<&i32> = mac!(iter);
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![warn(clippy::by_ref_peekable_peek)]
2+
3+
struct S;
4+
5+
impl S {
6+
fn by_ref(&mut self) -> impl Iterator<Item = i32> {
7+
std::iter::empty()
8+
}
9+
}
10+
11+
macro_rules! mac {
12+
($x:expr) => {
13+
$x.by_ref().peekable().peek()
14+
};
15+
}
16+
17+
fn with_non_clone_parameter(i: &mut impl Iterator<Item = i32>) {
18+
// This won't suggest `.clone().next().as_ref()` as `i` is not `Clone`
19+
let _: Option<&i32> = i.next().as_ref();
20+
//~^ by_ref_peekable_peek
21+
}
22+
23+
fn with_cloneable_local_iterator(a: Vec<i32>) {
24+
let mut i = a.into_iter();
25+
let _: Option<&i32> = i.next().as_ref();
26+
//~^ by_ref_peekable_peek
27+
}
28+
29+
fn with_cloneable_local_iterator_from_macro() {
30+
macro_rules! mac {
31+
() => {
32+
[1, 2, 3].into_iter()
33+
};
34+
}
35+
let mut i = mac!();
36+
let _: Option<&i32> = i.next().as_ref();
37+
//~^ by_ref_peekable_peek
38+
}
39+
40+
fn main() {
41+
let mut iter = [1, 2, 3].into_iter();
42+
let _: Option<&i32> = iter.next().as_ref();
43+
//~^ by_ref_peekable_peek
44+
#[expect(clippy::needless_borrow)]
45+
#[allow(clippy::unnecessary_mut_passed)] // For the `.clone().next().as_ref()` suggestion
46+
let _: Option<&i32> = (&mut iter).next().as_ref();
47+
//~^ by_ref_peekable_peek
48+
49+
// Do not lint if `by_ref()` is not the one on `Iterator`
50+
let _: Option<&i32> = S.by_ref().peekable().peek();
51+
52+
// Do not lint if coming from a macro, as we cannot ensure
53+
// that all uses of `.by_ref()` will be `Iterator::by_ref()`.
54+
let _: Option<&i32> = mac!(iter);
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![warn(clippy::by_ref_peekable_peek)]
2+
3+
struct S;
4+
5+
impl S {
6+
fn by_ref(&mut self) -> impl Iterator<Item = i32> {
7+
std::iter::empty()
8+
}
9+
}
10+
11+
macro_rules! mac {
12+
($x:expr) => {
13+
$x.by_ref().peekable().peek()
14+
};
15+
}
16+
17+
fn with_non_clone_parameter(i: &mut impl Iterator<Item = i32>) {
18+
// This won't suggest `.clone().next().as_ref()` as `i` is not `Clone`
19+
let _: Option<&i32> = i.next().as_ref();
20+
//~^ by_ref_peekable_peek
21+
}
22+
23+
fn with_cloneable_local_iterator(a: Vec<i32>) {
24+
let mut i = a.into_iter().peekable();
25+
let _: Option<&i32> = i.peek();
26+
//~^ by_ref_peekable_peek
27+
}
28+
29+
fn with_cloneable_local_iterator_from_macro() {
30+
macro_rules! mac {
31+
() => {
32+
[1, 2, 3].into_iter()
33+
};
34+
}
35+
let mut i = mac!().peekable();
36+
let _: Option<&i32> = i.peek();
37+
//~^ by_ref_peekable_peek
38+
}
39+
40+
fn main() {
41+
let mut iter = [1, 2, 3].into_iter().peekable();
42+
let _: Option<&i32> = iter.peek();
43+
//~^ by_ref_peekable_peek
44+
#[expect(clippy::needless_borrow)]
45+
#[allow(clippy::unnecessary_mut_passed)] // For the `.clone().next().as_ref()` suggestion
46+
let _: Option<&i32> = (&mut iter).clone().next().as_ref();
47+
//~^ by_ref_peekable_peek
48+
49+
// Do not lint if `by_ref()` is not the one on `Iterator`
50+
let _: Option<&i32> = S.by_ref().peekable().peek();
51+
52+
// Do not lint if coming from a macro, as we cannot ensure
53+
// that all uses of `.by_ref()` will be `Iterator::by_ref()`.
54+
let _: Option<&i32> = mac!(iter);
55+
}

tests/ui/by_ref_peekable_peek.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![warn(clippy::by_ref_peekable_peek)]
2+
3+
struct S;
4+
5+
impl S {
6+
fn by_ref(&mut self) -> impl Iterator<Item = i32> {
7+
std::iter::empty()
8+
}
9+
}
10+
11+
macro_rules! mac {
12+
($x:expr) => {
13+
$x.by_ref().peekable().peek()
14+
};
15+
}
16+
17+
fn with_non_clone_parameter(i: &mut impl Iterator<Item = i32>) {
18+
// This won't suggest `.clone().next().as_ref()` as `i` is not `Clone`
19+
let _: Option<&i32> = i.by_ref().peekable().peek();
20+
//~^ by_ref_peekable_peek
21+
}
22+
23+
fn with_cloneable_local_iterator(a: Vec<i32>) {
24+
let mut i = a.into_iter();
25+
let _: Option<&i32> = i.by_ref().peekable().peek();
26+
//~^ by_ref_peekable_peek
27+
}
28+
29+
fn with_cloneable_local_iterator_from_macro() {
30+
macro_rules! mac {
31+
() => {
32+
[1, 2, 3].into_iter()
33+
};
34+
}
35+
let mut i = mac!();
36+
let _: Option<&i32> = i.by_ref().peekable().peek();
37+
//~^ by_ref_peekable_peek
38+
}
39+
40+
fn main() {
41+
let mut iter = [1, 2, 3].into_iter();
42+
let _: Option<&i32> = iter.by_ref().peekable().peek();
43+
//~^ by_ref_peekable_peek
44+
#[expect(clippy::needless_borrow)]
45+
#[allow(clippy::unnecessary_mut_passed)] // For the `.clone().next().as_ref()` suggestion
46+
let _: Option<&i32> = (&mut iter).by_ref().peekable().peek();
47+
//~^ by_ref_peekable_peek
48+
49+
// Do not lint if `by_ref()` is not the one on `Iterator`
50+
let _: Option<&i32> = S.by_ref().peekable().peek();
51+
52+
// Do not lint if coming from a macro, as we cannot ensure
53+
// that all uses of `.by_ref()` will be `Iterator::by_ref()`.
54+
let _: Option<&i32> = mac!(iter);
55+
}

0 commit comments

Comments
 (0)