Skip to content

Commit c3fb102

Browse files
authored
Consider side effects when rewriting iterator behaviors (#14490)
Closes #9191 Closes #14444 Closes #8055 Adds a new helper to partly check for side effects by recursively checking if the iterator type contains closures with mutable captures. changelog: [`double_ended_iterator_last`] fix FP when iter has side effects changelog: [`needless_collect`] fix lint not consider side effects
2 parents aeb6ac9 + a50e043 commit c3fb102

13 files changed

+292
-97
lines changed

clippy_lints/src/methods/double_ended_iterator_last.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::ty::implements_trait;
2+
use clippy_utils::ty::{has_non_owning_mutable_access, implements_trait};
33
use clippy_utils::{is_mutable, is_trait_method, path_to_local};
44
use rustc_errors::Applicability;
55
use rustc_hir::{Expr, Node, PatKind};
@@ -27,10 +27,15 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, self_expr: &'_ Exp
2727
&& let Some(last_def) = cx.tcx.provided_trait_methods(item).find(|m| m.name().as_str() == "last")
2828
// if the resolved method is the same as the provided definition
2929
&& fn_def.def_id() == last_def.def_id
30+
&& let self_ty = cx.typeck_results().expr_ty(self_expr)
31+
&& !has_non_owning_mutable_access(cx, self_ty)
3032
{
3133
let mut sugg = vec![(call_span, String::from("next_back()"))];
3234
let mut dont_apply = false;
35+
3336
// if `self_expr` is a reference, it is mutable because it is used for `.last()`
37+
// TODO: Change this to lint only when the referred iterator is not used later. If it is used later,
38+
// changing to `next_back()` may change its behavior.
3439
if !(is_mutable(cx, self_expr) || self_type.is_ref()) {
3540
if let Some(hir_id) = path_to_local(self_expr)
3641
&& let Node::Pat(pat) = cx.tcx.hir_node(hir_id)

clippy_lints/src/methods/needless_collect.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use super::NEEDLESS_COLLECT;
44
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then};
55
use clippy_utils::source::{snippet, snippet_with_applicability};
66
use clippy_utils::sugg::Sugg;
7-
use clippy_utils::ty::{get_type_diagnostic_name, make_normalized_projection, make_projection};
7+
use clippy_utils::ty::{
8+
get_type_diagnostic_name, has_non_owning_mutable_access, make_normalized_projection, make_projection,
9+
};
810
use clippy_utils::{
911
CaptureKind, can_move_expr_to_closure, fn_def_id, get_enclosing_block, higher, is_trait_method, path_to_local,
1012
path_to_local_id,
@@ -23,13 +25,19 @@ use rustc_span::{Span, sym};
2325

2426
const NEEDLESS_COLLECT_MSG: &str = "avoid using `collect()` when not needed";
2527

28+
#[expect(clippy::too_many_lines)]
2629
pub(super) fn check<'tcx>(
2730
cx: &LateContext<'tcx>,
2831
name_span: Span,
2932
collect_expr: &'tcx Expr<'_>,
3033
iter_expr: &'tcx Expr<'tcx>,
3134
call_span: Span,
3235
) {
36+
let iter_ty = cx.typeck_results().expr_ty(iter_expr);
37+
if has_non_owning_mutable_access(cx, iter_ty) {
38+
return; // don't lint if the iterator has side effects
39+
}
40+
3341
match cx.tcx.parent_hir_node(collect_expr.hir_id) {
3442
Node::Expr(parent) => {
3543
check_collect_into_intoiterator(cx, parent, collect_expr, call_span, iter_expr);

clippy_utils/src/ty/mod.rs

+46
Original file line numberDiff line numberDiff line change
@@ -1376,3 +1376,49 @@ pub fn option_arg_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'t
13761376
_ => None,
13771377
}
13781378
}
1379+
1380+
/// Check if a Ty<'_> of `Iterator` contains any mutable access to non-owning types by checking if
1381+
/// it contains fields of mutable references or pointers, or references/pointers to non-`Freeze`
1382+
/// types, or `PhantomData` types containing any of the previous. This can be used to check whether
1383+
/// skipping iterating over an iterator will change its behavior.
1384+
pub fn has_non_owning_mutable_access<'tcx>(cx: &LateContext<'tcx>, iter_ty: Ty<'tcx>) -> bool {
1385+
fn normalize_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Ty<'tcx> {
1386+
cx.tcx.try_normalize_erasing_regions(cx.typing_env(), ty).unwrap_or(ty)
1387+
}
1388+
1389+
/// Check if `ty` contains mutable references or equivalent, which includes:
1390+
/// - A mutable reference/pointer.
1391+
/// - A reference/pointer to a non-`Freeze` type.
1392+
/// - A `PhantomData` type containing any of the previous.
1393+
fn has_non_owning_mutable_access_inner<'tcx>(
1394+
cx: &LateContext<'tcx>,
1395+
phantoms: &mut FxHashSet<Ty<'tcx>>,
1396+
ty: Ty<'tcx>,
1397+
) -> bool {
1398+
match ty.kind() {
1399+
ty::Adt(adt_def, args) if adt_def.is_phantom_data() => {
1400+
phantoms.insert(ty)
1401+
&& args
1402+
.types()
1403+
.any(|arg_ty| has_non_owning_mutable_access_inner(cx, phantoms, arg_ty))
1404+
},
1405+
ty::Adt(adt_def, args) => adt_def.all_fields().any(|field| {
1406+
has_non_owning_mutable_access_inner(cx, phantoms, normalize_ty(cx, field.ty(cx.tcx, args)))
1407+
}),
1408+
ty::Array(elem_ty, _) | ty::Slice(elem_ty) => has_non_owning_mutable_access_inner(cx, phantoms, *elem_ty),
1409+
ty::RawPtr(pointee_ty, mutability) | ty::Ref(_, pointee_ty, mutability) => {
1410+
mutability.is_mut() || !pointee_ty.is_freeze(cx.tcx, cx.typing_env())
1411+
},
1412+
ty::Closure(_, closure_args) => {
1413+
matches!(closure_args.types().next_back(), Some(captures) if has_non_owning_mutable_access_inner(cx, phantoms, captures))
1414+
},
1415+
ty::Tuple(tuple_args) => tuple_args
1416+
.iter()
1417+
.any(|arg_ty| has_non_owning_mutable_access_inner(cx, phantoms, arg_ty)),
1418+
_ => false,
1419+
}
1420+
}
1421+
1422+
let mut phantoms = FxHashSet::default();
1423+
has_non_owning_mutable_access_inner(cx, &mut phantoms, iter_ty)
1424+
}

tests/ui/crashes/ice-11230.fixed

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn main() {
1212
// needless_collect
1313
trait Helper<'a>: Iterator<Item = fn()> {}
1414

15+
// Should not be linted because we have no idea whether the iterator has side effects
1516
fn x(w: &mut dyn for<'a> Helper<'a>) {
16-
w.next().is_none();
17-
//~^ needless_collect
17+
w.collect::<Vec<_>>().is_empty();
1818
}

tests/ui/crashes/ice-11230.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ fn main() {
1212
// needless_collect
1313
trait Helper<'a>: Iterator<Item = fn()> {}
1414

15+
// Should not be linted because we have no idea whether the iterator has side effects
1516
fn x(w: &mut dyn for<'a> Helper<'a>) {
1617
w.collect::<Vec<_>>().is_empty();
17-
//~^ needless_collect
1818
}

tests/ui/crashes/ice-11230.stderr

+1-10
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,5 @@ LL | for v in A.iter() {}
77
= note: `-D clippy::explicit-iter-loop` implied by `-D warnings`
88
= help: to override `-D warnings` add `#[allow(clippy::explicit_iter_loop)]`
99

10-
error: avoid using `collect()` when not needed
11-
--> tests/ui/crashes/ice-11230.rs:16:7
12-
|
13-
LL | w.collect::<Vec<_>>().is_empty();
14-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `next().is_none()`
15-
|
16-
= note: `-D clippy::needless-collect` implied by `-D warnings`
17-
= help: to override `-D warnings` add `#[allow(clippy::needless_collect)]`
18-
19-
error: aborting due to 2 previous errors
10+
error: aborting due to 1 previous error
2011

tests/ui/double_ended_iterator_last.fixed

+25-7
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,35 @@ fn main() {
5252
let _ = CustomLast.last();
5353
}
5454

55+
// Should not be linted because applying the lint would move the original iterator. This can only be
56+
// linted if the iterator is used thereafter.
5557
fn issue_14139() {
5658
let mut index = [true, true, false, false, false, true].iter();
57-
let mut subindex = index.by_ref().take(3);
58-
let _ = subindex.next_back(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
59+
let subindex = index.by_ref().take(3);
60+
let _ = subindex.last();
61+
let _ = index.next();
5962

6063
let mut index = [true, true, false, false, false, true].iter();
6164
let mut subindex = index.by_ref().take(3);
62-
let _ = subindex.next_back(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
65+
let _ = subindex.last();
66+
let _ = index.next();
6367

6468
let mut index = [true, true, false, false, false, true].iter();
6569
let mut subindex = index.by_ref().take(3);
6670
let subindex = &mut subindex;
67-
let _ = subindex.next_back(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
71+
let _ = subindex.last();
72+
let _ = index.next();
6873

6974
let mut index = [true, true, false, false, false, true].iter();
7075
let mut subindex = index.by_ref().take(3);
7176
let subindex = &mut subindex;
72-
let _ = subindex.next_back(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
77+
let _ = subindex.last();
78+
let _ = index.next();
7379

7480
let mut index = [true, true, false, false, false, true].iter();
75-
let (mut subindex, _) = (index.by_ref().take(3), 42);
76-
let _ = subindex.next_back(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
81+
let (subindex, _) = (index.by_ref().take(3), 42);
82+
let _ = subindex.last();
83+
let _ = index.next();
7784
}
7885

7986
fn drop_order() {
@@ -90,3 +97,14 @@ fn drop_order() {
9097
//~^ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
9198
println!("Done");
9299
}
100+
101+
fn issue_14444() {
102+
let mut squares = vec![];
103+
let last_square = [1, 2, 3]
104+
.into_iter()
105+
.map(|x| {
106+
squares.push(x * x);
107+
Some(x * x)
108+
})
109+
.last();
110+
}

tests/ui/double_ended_iterator_last.rs

+23-5
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,35 @@ fn main() {
5252
let _ = CustomLast.last();
5353
}
5454

55+
// Should not be linted because applying the lint would move the original iterator. This can only be
56+
// linted if the iterator is used thereafter.
5557
fn issue_14139() {
5658
let mut index = [true, true, false, false, false, true].iter();
5759
let subindex = index.by_ref().take(3);
58-
let _ = subindex.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
60+
let _ = subindex.last();
61+
let _ = index.next();
5962

6063
let mut index = [true, true, false, false, false, true].iter();
6164
let mut subindex = index.by_ref().take(3);
62-
let _ = subindex.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
65+
let _ = subindex.last();
66+
let _ = index.next();
6367

6468
let mut index = [true, true, false, false, false, true].iter();
6569
let mut subindex = index.by_ref().take(3);
6670
let subindex = &mut subindex;
67-
let _ = subindex.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
71+
let _ = subindex.last();
72+
let _ = index.next();
6873

6974
let mut index = [true, true, false, false, false, true].iter();
7075
let mut subindex = index.by_ref().take(3);
7176
let subindex = &mut subindex;
72-
let _ = subindex.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
77+
let _ = subindex.last();
78+
let _ = index.next();
7379

7480
let mut index = [true, true, false, false, false, true].iter();
7581
let (subindex, _) = (index.by_ref().take(3), 42);
76-
let _ = subindex.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
82+
let _ = subindex.last();
83+
let _ = index.next();
7784
}
7885

7986
fn drop_order() {
@@ -90,3 +97,14 @@ fn drop_order() {
9097
//~^ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
9198
println!("Done");
9299
}
100+
101+
fn issue_14444() {
102+
let mut squares = vec![];
103+
let last_square = [1, 2, 3]
104+
.into_iter()
105+
.map(|x| {
106+
squares.push(x * x);
107+
Some(x * x)
108+
})
109+
.last();
110+
}

tests/ui/double_ended_iterator_last.stderr

+2-50
Original file line numberDiff line numberDiff line change
@@ -18,55 +18,7 @@ LL | let _ = DeIterator.last();
1818
| help: try: `next_back()`
1919

2020
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
21-
--> tests/ui/double_ended_iterator_last.rs:58:13
22-
|
23-
LL | let _ = subindex.last();
24-
| ^^^^^^^^^^^^^^^
25-
|
26-
help: try
27-
|
28-
LL ~ let mut subindex = index.by_ref().take(3);
29-
LL ~ let _ = subindex.next_back();
30-
|
31-
32-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
33-
--> tests/ui/double_ended_iterator_last.rs:62:13
34-
|
35-
LL | let _ = subindex.last();
36-
| ^^^^^^^^^------
37-
| |
38-
| help: try: `next_back()`
39-
40-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
41-
--> tests/ui/double_ended_iterator_last.rs:67:13
42-
|
43-
LL | let _ = subindex.last();
44-
| ^^^^^^^^^------
45-
| |
46-
| help: try: `next_back()`
47-
48-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
49-
--> tests/ui/double_ended_iterator_last.rs:72:13
50-
|
51-
LL | let _ = subindex.last();
52-
| ^^^^^^^^^------
53-
| |
54-
| help: try: `next_back()`
55-
56-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
57-
--> tests/ui/double_ended_iterator_last.rs:76:13
58-
|
59-
LL | let _ = subindex.last();
60-
| ^^^^^^^^^^^^^^^
61-
|
62-
help: try
63-
|
64-
LL ~ let (mut subindex, _) = (index.by_ref().take(3), 42);
65-
LL ~ let _ = subindex.next_back();
66-
|
67-
68-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
69-
--> tests/ui/double_ended_iterator_last.rs:89:36
21+
--> tests/ui/double_ended_iterator_last.rs:96:36
7022
|
7123
LL | println!("Last element is {}", v.last().unwrap().0);
7224
| ^^^^^^^^
@@ -78,5 +30,5 @@ LL ~ let mut v = v.into_iter();
7830
LL ~ println!("Last element is {}", v.next_back().unwrap().0);
7931
|
8032

81-
error: aborting due to 8 previous errors
33+
error: aborting due to 3 previous errors
8234

tests/ui/double_ended_iterator_last_unfixable.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
//@no-rustfix
22
#![warn(clippy::double_ended_iterator_last)]
33

4+
// Should not be linted because applying the lint would move the original iterator. This can only be
5+
// linted if the iterator is used thereafter.
46
fn main() {
57
let mut index = [true, true, false, false, false, true].iter();
68
let subindex = (index.by_ref().take(3), 42);
7-
let _ = subindex.0.last(); //~ ERROR: called `Iterator::last` on a `DoubleEndedIterator`
9+
let _ = subindex.0.last();
10+
let _ = index.next();
811
}
912

1013
fn drop_order() {
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,5 @@
11
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
2-
--> tests/ui/double_ended_iterator_last_unfixable.rs:7:13
3-
|
4-
LL | let _ = subindex.0.last();
5-
| ^^^^^^^^^^^------
6-
| |
7-
| help: try: `next_back()`
8-
|
9-
note: this must be made mutable to use `.next_back()`
10-
--> tests/ui/double_ended_iterator_last_unfixable.rs:7:13
11-
|
12-
LL | let _ = subindex.0.last();
13-
| ^^^^^^^^^^
14-
= note: `-D clippy::double-ended-iterator-last` implied by `-D warnings`
15-
= help: to override `-D warnings` add `#[allow(clippy::double_ended_iterator_last)]`
16-
17-
error: called `Iterator::last` on a `DoubleEndedIterator`; this will needlessly iterate the entire iterator
18-
--> tests/ui/double_ended_iterator_last_unfixable.rs:20:36
2+
--> tests/ui/double_ended_iterator_last_unfixable.rs:23:36
193
|
204
LL | println!("Last element is {}", v.0.last().unwrap().0);
215
| ^^^^------
@@ -24,10 +8,12 @@ LL | println!("Last element is {}", v.0.last().unwrap().0);
248
|
259
= note: this change will alter drop order which may be undesirable
2610
note: this must be made mutable to use `.next_back()`
27-
--> tests/ui/double_ended_iterator_last_unfixable.rs:20:36
11+
--> tests/ui/double_ended_iterator_last_unfixable.rs:23:36
2812
|
2913
LL | println!("Last element is {}", v.0.last().unwrap().0);
3014
| ^^^
15+
= note: `-D clippy::double-ended-iterator-last` implied by `-D warnings`
16+
= help: to override `-D warnings` add `#[allow(clippy::double_ended_iterator_last)]`
3117

32-
error: aborting due to 2 previous errors
18+
error: aborting due to 1 previous error
3319

0 commit comments

Comments
 (0)