Skip to content

Commit b92c43d

Browse files
committed
Implement a lint for .clone().into_iter()
1 parent 35a7095 commit b92c43d

23 files changed

+369
-116
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6058,6 +6058,7 @@ Released 2018-09-13
60586058
[`unnecessary_box_returns`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_returns
60596059
[`unnecessary_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast
60606060
[`unnecessary_clippy_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_clippy_cfg
6061+
[`unnecessary_collection_clone`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_collection_clone
60616062
[`unnecessary_fallible_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_fallible_conversions
60626063
[`unnecessary_filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_filter_map
60636064
[`unnecessary_find_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_find_map

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
470470
crate::methods::TYPE_ID_ON_BOX_INFO,
471471
crate::methods::UNINIT_ASSUMED_INIT_INFO,
472472
crate::methods::UNIT_HASH_INFO,
473+
crate::methods::UNNECESSARY_COLLECTION_CLONE_INFO,
473474
crate::methods::UNNECESSARY_FALLIBLE_CONVERSIONS_INFO,
474475
crate::methods::UNNECESSARY_FILTER_MAP_INFO,
475476
crate::methods::UNNECESSARY_FIND_MAP_INFO,

clippy_lints/src/methods/mod.rs

+35
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ mod suspicious_to_owned;
111111
mod type_id_on_box;
112112
mod uninit_assumed_init;
113113
mod unit_hash;
114+
mod unnecessary_collection_clone;
114115
mod unnecessary_fallible_conversions;
115116
mod unnecessary_filter_map;
116117
mod unnecessary_first_then_check;
@@ -4218,6 +4219,35 @@ declare_clippy_lint! {
42184219
"combine `.map(_)` followed by `.all(identity)`/`.any(identity)` into a single call"
42194220
}
42204221

4222+
declare_clippy_lint! {
4223+
///
4224+
/// Detects when an entire collection is being cloned eagerly, instead of each item lazily.
4225+
///
4226+
/// ### Why is this bad?
4227+
///
4228+
/// Cloning a collection requires allocating space for all elements and cloning each element into this new space,
4229+
/// whereas using `Iterator::cloned` does not allocate any more space and only requires cloning each element as they are consumed.
4230+
///
4231+
/// ### Example
4232+
/// ```no_run
4233+
/// fn process_string(val: String) -> String { val }
4234+
/// fn process_strings(strings: &Vec<String>) -> Vec<String> {
4235+
/// strings.clone().into_iter().filter(|s| s.len() < 10).map(process_string).collect()
4236+
/// }
4237+
/// ```
4238+
/// Use instead:
4239+
/// ```no_run
4240+
/// fn process_string(val: String) -> String { val }
4241+
/// fn process_strings(strings: &Vec<String>) -> Vec<String> {
4242+
/// strings.iter().cloned().filter(|s| s.len() < 10).map(process_string).collect()
4243+
/// }
4244+
/// ```
4245+
#[clippy::version = "1.84.0"]
4246+
pub UNNECESSARY_COLLECTION_CLONE,
4247+
perf,
4248+
"calling `.clone().into_iter()` instead of `.iter().cloned()`"
4249+
}
4250+
42214251
pub struct Methods {
42224252
avoid_breaking_exported_api: bool,
42234253
msrv: Msrv,
@@ -4381,6 +4411,7 @@ impl_lint_pass!(Methods => [
43814411
UNNECESSARY_MIN_OR_MAX,
43824412
NEEDLESS_AS_BYTES,
43834413
MAP_ALL_ANY_IDENTITY,
4414+
UNNECESSARY_COLLECTION_CLONE,
43844415
]);
43854416

43864417
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4843,6 +4874,10 @@ impl Methods {
48434874
("is_none", []) => check_is_some_is_none(cx, expr, recv, call_span, false),
48444875
("is_some", []) => check_is_some_is_none(cx, expr, recv, call_span, true),
48454876
("iter" | "iter_mut" | "into_iter", []) => {
4877+
if name == "into_iter" {
4878+
unnecessary_collection_clone::check(cx, expr, recv);
4879+
}
4880+
48464881
iter_on_single_or_empty_collections::check(cx, expr, name, recv);
48474882
},
48484883
("join", [join_arg]) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::is_path_mutable;
3+
use clippy_utils::source::snippet_with_applicability;
4+
use clippy_utils::ty::{deref_chain, get_inherent_method, implements_trait, make_normalized_projection};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::Expr;
7+
use rustc_lint::LateContext;
8+
use rustc_middle::ty::{self, Ty};
9+
use rustc_span::sym;
10+
11+
use super::{UNNECESSARY_COLLECTION_CLONE, method_call};
12+
13+
// FIXME: This does not check if the iter method is actually compatible with the replacement, but
14+
// you have to be actively evil to have an `IntoIterator` impl that returns one type and an `iter`
15+
// method that returns something other than references of that type.... and it is a massive
16+
// complicated hassle to check this
17+
fn has_iter_method<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
18+
deref_chain(cx, ty).any(|ty| match ty.kind() {
19+
ty::Adt(adt_def, _) => get_inherent_method(cx, adt_def.did(), sym::iter).is_some(),
20+
ty::Slice(_) => true,
21+
_ => false,
22+
})
23+
}
24+
25+
/// Check for `x.clone().into_iter()` to suggest `x.iter().cloned()`.
26+
// ^^^^^^^^^ is recv
27+
// ^^^^^^^^^^^^^^^^^^^^^ is expr
28+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>) {
29+
let typeck_results = cx.typeck_results();
30+
let diagnostic_items = cx.tcx.all_diagnostic_items(());
31+
32+
// If the call before `into_iter` is `.clone()`
33+
if let Some(("clone", collection_expr, [], _, _)) = method_call(recv)
34+
// and the binding being cloned is not mutable
35+
&& let Some(false) = is_path_mutable(cx, collection_expr)
36+
// and the result of `into_iter` is an Iterator
37+
&& let Some(&iterator_def_id) = diagnostic_items.name_to_id.get(&sym::Iterator)
38+
&& let expr_ty = typeck_results.expr_ty(expr)
39+
&& implements_trait(cx, expr_ty, iterator_def_id, &[])
40+
// with an item that implements clone
41+
&& let Some(&clone_def_id) = diagnostic_items.name_to_id.get(&sym::Clone)
42+
&& let Some(item_ty) = make_normalized_projection(cx.tcx, cx.param_env, iterator_def_id, sym::Item, [expr_ty])
43+
&& implements_trait(cx, item_ty, clone_def_id, &[])
44+
// and the type has an `iter` method
45+
&& has_iter_method(cx, typeck_results.expr_ty(collection_expr))
46+
{
47+
let mut applicability = Applicability::MachineApplicable;
48+
let collection_expr_snippet = snippet_with_applicability(cx, collection_expr.span, "...", &mut applicability);
49+
span_lint_and_sugg(
50+
cx,
51+
UNNECESSARY_COLLECTION_CLONE,
52+
expr.span,
53+
"using clone on collection to own iterated items",
54+
"replace with",
55+
format!("{collection_expr_snippet}.iter().cloned()"),
56+
applicability,
57+
);
58+
}
59+
}

clippy_lints/src/methods/unnecessary_iter_cloned.rs

+8-18
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ use clippy_utils::higher::ForLoop;
44
use clippy_utils::source::SpanRangeExt;
55
use clippy_utils::ty::{get_iterator_item_ty, implements_trait};
66
use clippy_utils::visitors::for_each_expr_without_closures;
7-
use clippy_utils::{can_mut_borrow_both, fn_def_id, get_parent_expr, path_to_local};
7+
use clippy_utils::{can_mut_borrow_both, fn_def_id, get_parent_expr, is_path_mutable};
88
use core::ops::ControlFlow;
99
use rustc_errors::Applicability;
1010
use rustc_hir::def_id::DefId;
11-
use rustc_hir::{BindingMode, Expr, ExprKind, Node, PatKind};
11+
use rustc_hir::{Expr, ExprKind};
1212
use rustc_lint::LateContext;
1313
use rustc_span::{Symbol, sym};
1414

@@ -42,22 +42,7 @@ pub fn check_for_loop_iter(
4242
&& !clone_or_copy_needed
4343
&& let Some(receiver_snippet) = receiver.span.get_source_text(cx)
4444
{
45-
// Issue 12098
46-
// https://github.com/rust-lang/rust-clippy/issues/12098
47-
// if the assignee have `mut borrow` conflict with the iteratee
48-
// the lint should not execute, former didn't consider the mut case
49-
5045
// check whether `expr` is mutable
51-
fn is_mutable(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
52-
if let Some(hir_id) = path_to_local(expr)
53-
&& let Node::Pat(pat) = cx.tcx.hir_node(hir_id)
54-
{
55-
matches!(pat.kind, PatKind::Binding(BindingMode::MUT, ..))
56-
} else {
57-
true
58-
}
59-
}
60-
6146
fn is_caller_or_fields_change(cx: &LateContext<'_>, body: &Expr<'_>, caller: &Expr<'_>) -> bool {
6247
let mut change = false;
6348
if let ExprKind::Block(block, ..) = body.kind {
@@ -82,7 +67,12 @@ pub fn check_for_loop_iter(
8267
while let ExprKind::MethodCall(_, caller, _, _) = child.kind {
8368
child = caller;
8469
}
85-
if is_mutable(cx, child) && is_caller_or_fields_change(cx, body, child) {
70+
71+
// Issue 12098
72+
// https://github.com/rust-lang/rust-clippy/issues/12098
73+
// if the assignee have `mut borrow` conflict with the iteratee
74+
// the lint should not execute, former didn't consider the mut case
75+
if is_path_mutable(cx, child).unwrap_or(true) && is_caller_or_fields_change(cx, body, child) {
8676
// skip lint
8777
return true;
8878
}

clippy_lints/src/unnecessary_struct_initialization.rs

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::source::snippet;
33
use clippy_utils::ty::is_copy;
4-
use clippy_utils::{get_parent_expr, path_to_local};
5-
use rustc_hir::{BindingMode, Expr, ExprField, ExprKind, Node, PatKind, Path, QPath, UnOp};
4+
use clippy_utils::{get_parent_expr, is_path_mutable, path_to_local};
5+
use rustc_hir::{Expr, ExprField, ExprKind, Path, QPath, UnOp};
66
use rustc_lint::{LateContext, LateLintPass};
77
use rustc_session::declare_lint_pass;
88

@@ -155,16 +155,6 @@ fn same_path_in_all_fields<'tcx>(
155155
}
156156
}
157157

158-
fn is_mutable(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
159-
if let Some(hir_id) = path_to_local(expr)
160-
&& let Node::Pat(pat) = cx.tcx.hir_node(hir_id)
161-
{
162-
matches!(pat.kind, PatKind::Binding(BindingMode::MUT, ..))
163-
} else {
164-
true
165-
}
166-
}
167-
168158
fn check_references(cx: &LateContext<'_>, expr_a: &Expr<'_>, expr_b: &Expr<'_>) -> bool {
169159
if let Some(parent) = get_parent_expr(cx, expr_a)
170160
&& let parent_ty = cx.typeck_results().expr_ty_adjusted(parent)
@@ -176,7 +166,7 @@ fn check_references(cx: &LateContext<'_>, expr_a: &Expr<'_>, expr_b: &Expr<'_>)
176166
return false;
177167
}
178168

179-
if parent_ty.is_mutable_ptr() && !is_mutable(cx, expr_b) {
169+
if parent_ty.is_mutable_ptr() && !is_path_mutable(cx, expr_b).unwrap_or(true) {
180170
// The original can be used in a mutable reference context only if it is mutable.
181171
return false;
182172
}

clippy_utils/src/lib.rs

+13
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,19 @@ pub fn is_trait_item(cx: &LateContext<'_>, expr: &Expr<'_>, diag_item: Symbol) -
412412
}
413413
}
414414

415+
/// Checks if `expr` is a path to a mutable binding.
416+
pub fn is_path_mutable(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<bool> {
417+
if let Some(hir_id) = path_to_local(expr)
418+
&& let Node::Pat(pat) = cx.tcx.hir_node(hir_id)
419+
{
420+
Some(matches!(pat.kind, PatKind::Binding(BindingMode::MUT, ..)))
421+
} else if let ExprKind::Field(recv, _) = expr.kind {
422+
is_path_mutable(cx, recv)
423+
} else {
424+
None
425+
}
426+
}
427+
415428
pub fn last_path_segment<'tcx>(path: &QPath<'tcx>) -> &'tcx PathSegment<'tcx> {
416429
match *path {
417430
QPath::Resolved(_, path) => path.segments.last().expect("A path must have at least one segment"),

clippy_utils/src/ty.rs

+16-9
Original file line numberDiff line numberDiff line change
@@ -1323,22 +1323,29 @@ pub fn deref_chain<'cx, 'tcx>(cx: &'cx LateContext<'tcx>, ty: Ty<'tcx>) -> impl
13231323

13241324
/// Checks if a Ty<'_> has some inherent method Symbol.
13251325
///
1326-
/// This does not look for impls in the type's `Deref::Target` type.
1327-
/// If you need this, you should wrap this call in `clippy_utils::ty::deref_chain().any(...)`.
1326+
/// This is a helper for [`get_inherent_method`].
13281327
pub fn get_adt_inherent_method<'a>(cx: &'a LateContext<'_>, ty: Ty<'_>, method_name: Symbol) -> Option<&'a AssocItem> {
13291328
if let Some(ty_did) = ty.ty_adt_def().map(AdtDef::did) {
1330-
cx.tcx.inherent_impls(ty_did).iter().find_map(|&did| {
1331-
cx.tcx
1332-
.associated_items(did)
1333-
.filter_by_name_unhygienic(method_name)
1334-
.next()
1335-
.filter(|item| item.kind == AssocKind::Fn)
1336-
})
1329+
get_inherent_method(cx, ty_did, method_name)
13371330
} else {
13381331
None
13391332
}
13401333
}
13411334

1335+
/// Checks if the [`DefId`] of a Ty has some inherent method Symbol.
1336+
///
1337+
/// This does not look for impls in the type's `Deref::Target` type.
1338+
/// If you need this, you should wrap this call in `clippy_utils::ty::deref_chain().any(...)`.
1339+
pub fn get_inherent_method<'a>(cx: &'a LateContext<'_>, ty_did: DefId, method_name: Symbol) -> Option<&'a AssocItem> {
1340+
cx.tcx.inherent_impls(ty_did).iter().find_map(|&did| {
1341+
cx.tcx
1342+
.associated_items(did)
1343+
.filter_by_name_unhygienic(method_name)
1344+
.next()
1345+
.filter(|item| item.kind == AssocKind::Fn)
1346+
})
1347+
}
1348+
13421349
/// Get's the type of a field by name.
13431350
pub fn get_field_by_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, name: Symbol) -> Option<Ty<'tcx>> {
13441351
match *ty.kind() {

tests/ui/filter_map_bool_then.fixed

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
clippy::map_identity,
55
clippy::unnecessary_lazy_evaluations,
66
clippy::unnecessary_filter_map,
7+
clippy::unnecessary_collection_clone,
78
unused
89
)]
910
#![warn(clippy::filter_map_bool_then)]

tests/ui/filter_map_bool_then.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
clippy::map_identity,
55
clippy::unnecessary_lazy_evaluations,
66
clippy::unnecessary_filter_map,
7+
clippy::unnecessary_collection_clone,
78
unused
89
)]
910
#![warn(clippy::filter_map_bool_then)]

tests/ui/filter_map_bool_then.stderr

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: usage of `bool::then` in `filter_map`
2-
--> tests/ui/filter_map_bool_then.rs:19:22
2+
--> tests/ui/filter_map_bool_then.rs:20:22
33
|
44
LL | v.clone().iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
@@ -8,55 +8,55 @@ LL | v.clone().iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
88
= help: to override `-D warnings` add `#[allow(clippy::filter_map_bool_then)]`
99

1010
error: usage of `bool::then` in `filter_map`
11-
--> tests/ui/filter_map_bool_then.rs:20:27
11+
--> tests/ui/filter_map_bool_then.rs:21:27
1212
|
1313
LL | v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
1414
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
1515

1616
error: usage of `bool::then` in `filter_map`
17-
--> tests/ui/filter_map_bool_then.rs:23:10
17+
--> tests/ui/filter_map_bool_then.rs:24:10
1818
|
1919
LL | .filter_map(|i| -> Option<_> { (i % 2 == 0).then(|| i + 1) });
2020
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
2121

2222
error: usage of `bool::then` in `filter_map`
23-
--> tests/ui/filter_map_bool_then.rs:27:10
23+
--> tests/ui/filter_map_bool_then.rs:28:10
2424
|
2525
LL | .filter_map(|i| (i % 2 == 0).then(|| i + 1));
2626
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
2727

2828
error: usage of `bool::then` in `filter_map`
29-
--> tests/ui/filter_map_bool_then.rs:31:10
29+
--> tests/ui/filter_map_bool_then.rs:32:10
3030
|
3131
LL | .filter_map(|i| (i.clone() % 2 == 0).then(|| i + 1));
3232
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i.clone() % 2 == 0)).map(|i| i + 1)`
3333

3434
error: usage of `bool::then` in `filter_map`
35-
--> tests/ui/filter_map_bool_then.rs:37:22
35+
--> tests/ui/filter_map_bool_then.rs:38:22
3636
|
3737
LL | v.clone().iter().filter_map(|i| (i == &NonCopy).then(|| i));
3838
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i == &NonCopy)).map(|i| i)`
3939

4040
error: usage of `bool::then` in `filter_map`
41-
--> tests/ui/filter_map_bool_then.rs:61:50
41+
--> tests/ui/filter_map_bool_then.rs:62:50
4242
|
4343
LL | let _: Vec<usize> = bools.iter().enumerate().filter_map(|(i, b)| b.then(|| i)).collect();
4444
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&(i, b)| *b).map(|(i, b)| i)`
4545

4646
error: usage of `bool::then` in `filter_map`
47-
--> tests/ui/filter_map_bool_then.rs:65:50
47+
--> tests/ui/filter_map_bool_then.rs:66:50
4848
|
4949
LL | let _: Vec<usize> = bools.iter().enumerate().filter_map(|(i, b)| b.then(|| i)).collect();
5050
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&(i, b)| ***b).map(|(i, b)| i)`
5151

5252
error: usage of `bool::then` in `filter_map`
53-
--> tests/ui/filter_map_bool_then.rs:69:50
53+
--> tests/ui/filter_map_bool_then.rs:70:50
5454
|
5555
LL | let _: Vec<usize> = bools.iter().enumerate().filter_map(|(i, b)| b.then(|| i)).collect();
5656
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&(i, b)| **b).map(|(i, b)| i)`
5757

5858
error: usage of `bool::then` in `filter_map`
59-
--> tests/ui/filter_map_bool_then.rs:80:50
59+
--> tests/ui/filter_map_bool_then.rs:81:50
6060
|
6161
LL | let _: Vec<usize> = bools.iter().enumerate().filter_map(|(i, b)| b.then(|| i)).collect();
6262
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&(i, b)| ****b).map(|(i, b)| i)`

tests/ui/iter_filter_is_ok.fixed

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
clippy::map_identity,
44
clippy::result_filter_map,
55
clippy::needless_borrow,
6-
clippy::redundant_closure
6+
clippy::redundant_closure,
7+
clippy::unnecessary_collection_clone
78
)]
89

910
fn main() {

tests/ui/iter_filter_is_ok.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
clippy::map_identity,
44
clippy::result_filter_map,
55
clippy::needless_borrow,
6-
clippy::redundant_closure
6+
clippy::redundant_closure,
7+
clippy::unnecessary_collection_clone
78
)]
89

910
fn main() {

0 commit comments

Comments
 (0)