Skip to content

Commit 24c4bad

Browse files
committed
Detect when move of !Copy value occurs within loop and should likely not be cloned
When encountering a move error on a value within a loop of any kind, identify if the moved value belongs to a call expression that should not be cloned and avoid the semantically incorrect suggestion. Also try to suggest moving the call expression outside of the loop instead. ``` error[E0382]: use of moved value: `vec` --> $DIR/recreating-value-in-loop-condition.rs:6:33 | LL | let vec = vec!["one", "two", "three"]; | --- move occurs because `vec` has type `Vec<&str>`, which does not implement the `Copy` trait LL | while let Some(item) = iter(vec).next() { | ----------------------------^^^-------- | | | | | value moved here, in previous iteration of loop | inside of this loop | note: consider changing this parameter type in function `iter` to borrow instead if owning the value isn't necessary --> $DIR/recreating-value-in-loop-condition.rs:1:17 | LL | fn iter<T>(vec: Vec<T>) -> impl Iterator<Item = T> { | ---- ^^^^^^ this parameter takes ownership of the value | | | in this function help: consider moving the expression out of the loop so it is only moved once | LL ~ let mut value = iter(vec); LL ~ while let Some(item) = value.next() { | ``` We use the presence of a `break` in the loop that would be affected by the moved value as a heuristic for "shouldn't be cloned". Fix rust-lang#121466.
1 parent 933a05b commit 24c4bad

13 files changed

+441
-42
lines changed

compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,8 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
443443
err.span_note(
444444
span,
445445
format!(
446-
"consider changing this parameter type in {descr} `{ident}` to \
447-
borrow instead if owning the value isn't necessary",
446+
"consider changing this parameter type in {descr} `{ident}` to borrow \
447+
instead if owning the value isn't necessary",
448448
),
449449
);
450450
}
@@ -738,6 +738,163 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
738738
true
739739
}
740740

741+
/// In a move error that occurs on a call wihtin a loop, we try to identify cases where cloning
742+
/// the value would lead to a logic error. We infer these cases by seeing if the moved value is
743+
/// part of the logic to break the loop, either through an explicit `break` or if the expression
744+
/// is part of a `while let`.
745+
fn suggest_hoisting_call_outside_loop(
746+
&self,
747+
err: &mut DiagnosticBuilder<'_>,
748+
expr: &hir::Expr<'_>,
749+
) -> bool {
750+
let tcx = self.infcx.tcx;
751+
let mut can_suggest_clone = true;
752+
753+
// If the moved value is a locally declared binding, we'll look upwards on the expression
754+
// tree until the scope where it is defined, and no further, as suggesting to move the
755+
// expression beyond that point would be illogical.
756+
let local_hir_id = if let hir::ExprKind::Path(hir::QPath::Resolved(
757+
_,
758+
hir::Path { res: hir::def::Res::Local(local_hir_id), .. },
759+
)) = expr.kind
760+
{
761+
Some(local_hir_id)
762+
} else {
763+
// This case would be if the moved value comes from an argument binding, we'll just
764+
// look within the entire item, that's fine.
765+
None
766+
};
767+
768+
/// This will allow us to look for a specific `HirId`, in our case `local_hir_id` where the
769+
/// binding was declared, within any other expression. We'll use it to search for the
770+
/// binding declaration within every scope we inspect.
771+
struct Finder {
772+
hir_id: hir::HirId,
773+
found: bool,
774+
}
775+
impl<'hir> Visitor<'hir> for Finder {
776+
fn visit_pat(&mut self, pat: &'hir hir::Pat<'hir>) {
777+
if pat.hir_id == self.hir_id {
778+
self.found = true;
779+
}
780+
hir::intravisit::walk_pat(self, pat);
781+
}
782+
fn visit_expr(&mut self, ex: &'hir hir::Expr<'hir>) {
783+
if ex.hir_id == self.hir_id {
784+
self.found = true;
785+
}
786+
hir::intravisit::walk_expr(self, ex);
787+
}
788+
}
789+
// The immediate HIR parent of the moved expression. We'll look for it to be a call.
790+
let mut parent = None;
791+
// The top-most loop where the moved expression could be moved to a new binding.
792+
let mut outer_most_loop: Option<&hir::Expr<'_>> = None;
793+
for (_, node) in tcx.hir().parent_iter(expr.hir_id) {
794+
let e = match node {
795+
hir::Node::Expr(e) => e,
796+
hir::Node::Local(hir::Local { els: Some(els), .. }) => {
797+
let mut finder = BreakFinder { found_break: false };
798+
finder.visit_block(els);
799+
if finder.found_break {
800+
// Don't suggest clone as it could be will likely end in an infinite
801+
// loop.
802+
// let Some(_) = foo(non_copy.clone()) else { break; }
803+
// --- ^^^^^^^^ -----
804+
can_suggest_clone = false;
805+
}
806+
continue;
807+
}
808+
_ => continue,
809+
};
810+
if let Some(&hir_id) = local_hir_id {
811+
let mut finder = Finder { hir_id, found: false };
812+
finder.visit_expr(e);
813+
if finder.found {
814+
// The current scope includes the declaration of the binding we're accessing, we
815+
// can't look up any further for loops.
816+
break;
817+
}
818+
}
819+
if parent.is_none() {
820+
parent = Some(e);
821+
}
822+
match e.kind {
823+
hir::ExprKind::Let(_) => {
824+
match tcx.parent_hir_node(e.hir_id) {
825+
hir::Node::Expr(hir::Expr {
826+
kind: hir::ExprKind::If(cond, ..), ..
827+
}) => {
828+
let mut finder = Finder { hir_id: expr.hir_id, found: false };
829+
finder.visit_expr(cond);
830+
if finder.found {
831+
// The expression where the move error happened is in a `while let`
832+
// condition Don't suggest clone as it will likely end in an
833+
// infinite loop.
834+
// while let Some(_) = foo(non_copy.clone()) { }
835+
// --------- ^^^^^^^^
836+
can_suggest_clone = false;
837+
}
838+
}
839+
_ => {}
840+
}
841+
}
842+
hir::ExprKind::Loop(..) => {
843+
outer_most_loop = Some(e);
844+
}
845+
_ => {}
846+
}
847+
}
848+
849+
/// Look for `break` expressions within any arbitrary expressions. We'll do this to infer
850+
/// whether this is a case where the moved value would affect the exit of a loop, making it
851+
/// unsuitable for a `.clone()` suggestion.
852+
struct BreakFinder {
853+
found_break: bool,
854+
}
855+
impl<'hir> Visitor<'hir> for BreakFinder {
856+
fn visit_expr(&mut self, ex: &'hir hir::Expr<'hir>) {
857+
if let hir::ExprKind::Break(..) = ex.kind {
858+
self.found_break = true;
859+
}
860+
hir::intravisit::walk_expr(self, ex);
861+
}
862+
}
863+
if let Some(in_loop) = outer_most_loop
864+
&& let Some(parent) = parent
865+
&& let hir::ExprKind::MethodCall(..) | hir::ExprKind::Call(..) = parent.kind
866+
{
867+
// FIXME: We could check that the call's *parent* takes `&mut val` to make the
868+
// suggestion more targeted to the `mk_iter(val).next()` case. Maybe do that only to
869+
// check for wheter to suggest `let value` or `let mut value`.
870+
871+
let span = in_loop.span;
872+
let mut finder = BreakFinder { found_break: false };
873+
finder.visit_expr(in_loop);
874+
let sm = tcx.sess.source_map();
875+
if (finder.found_break || true)
876+
&& let Ok(value) = sm.span_to_snippet(parent.span)
877+
{
878+
// We know with high certainty that this move would affect the early return of a
879+
// loop, so we suggest moving the expression with the move out of the loop.
880+
let indent = if let Some(indent) = sm.indentation_before(span) {
881+
format!("\n{indent}")
882+
} else {
883+
" ".to_string()
884+
};
885+
err.multipart_suggestion(
886+
"consider moving the expression out of the loop so it is only moved once",
887+
vec![
888+
(parent.span, "value".to_string()),
889+
(span.shrink_to_lo(), format!("let mut value = {value};{indent}")),
890+
],
891+
Applicability::MaybeIncorrect,
892+
);
893+
}
894+
}
895+
can_suggest_clone
896+
}
897+
741898
fn suggest_cloning(
742899
&self,
743900
err: &mut DiagnosticBuilder<'_>,
@@ -746,6 +903,11 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
746903
span: Span,
747904
) {
748905
let tcx = self.infcx.tcx;
906+
if !self.suggest_hoisting_call_outside_loop(err, expr) {
907+
// The place where the the type moves would be misleading to suggest clone. (#121466)
908+
return;
909+
}
910+
749911
// Try to find predicates on *generic params* that would allow copying `ty`
750912
let suggestion =
751913
if let Some(symbol) = tcx.hir().maybe_get_struct_pattern_shorthand_field(expr) {

compiler/rustc_hir/src/hir.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2049,7 +2049,7 @@ impl LoopSource {
20492049
}
20502050
}
20512051

2052-
#[derive(Copy, Clone, Debug, HashStable_Generic)]
2052+
#[derive(Copy, Clone, Debug, PartialEq, HashStable_Generic)]
20532053
pub enum LoopIdError {
20542054
OutsideLoopScope,
20552055
UnlabeledCfInWhileCondition,

tests/ui/borrowck/mut-borrow-in-loop-2.fixed

Lines changed: 0 additions & 35 deletions
This file was deleted.

tests/ui/borrowck/mut-borrow-in-loop-2.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
//@ run-rustfix
21
#![allow(dead_code)]
32

43
struct Events<R>(R);

tests/ui/borrowck/mut-borrow-in-loop-2.stderr

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error[E0382]: use of moved value: `value`
2-
--> $DIR/mut-borrow-in-loop-2.rs:31:23
2+
--> $DIR/mut-borrow-in-loop-2.rs:30:23
33
|
44
LL | fn this_does_not<'a, R>(value: &'a mut Events<R>) {
55
| ----- move occurs because `value` has type `&mut Events<R>`, which does not implement the `Copy` trait
@@ -9,12 +9,18 @@ LL | Other::handle(value);
99
| ^^^^^ value moved here, in previous iteration of loop
1010
|
1111
note: consider changing this parameter type in function `handle` to borrow instead if owning the value isn't necessary
12-
--> $DIR/mut-borrow-in-loop-2.rs:9:22
12+
--> $DIR/mut-borrow-in-loop-2.rs:8:22
1313
|
1414
LL | fn handle(value: T) -> Self;
1515
| ------ ^ this parameter takes ownership of the value
1616
| |
1717
| in this function
18+
help: consider moving the expression out of the loop so it is only moved once
19+
|
20+
LL ~ let mut value = Other::handle(value);
21+
LL ~ for _ in 0..3 {
22+
LL ~ value;
23+
|
1824
help: consider creating a fresh reborrow of `value` here
1925
|
2026
LL | Other::handle(&mut *value);

tests/ui/liveness/liveness-move-call-arg-2.stderr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ LL | fn take(_x: Box<isize>) {}
1616
| ---- ^^^^^^^^^^ this parameter takes ownership of the value
1717
| |
1818
| in this function
19+
help: consider moving the expression out of the loop so it is only moved once
20+
|
21+
LL ~ let mut value = take(x);
22+
LL ~ loop {
23+
LL ~ value;
24+
|
1925
help: consider cloning the value if the performance cost is acceptable
2026
|
2127
LL | take(x.clone());

tests/ui/liveness/liveness-move-call-arg.stderr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ LL | fn take(_x: Box<isize>) {}
1616
| ---- ^^^^^^^^^^ this parameter takes ownership of the value
1717
| |
1818
| in this function
19+
help: consider moving the expression out of the loop so it is only moved once
20+
|
21+
LL ~ let mut value = take(x);
22+
LL ~ loop {
23+
LL ~ value;
24+
|
1925
help: consider cloning the value if the performance cost is acceptable
2026
|
2127
LL | take(x.clone());

tests/ui/moves/borrow-closures-instead-of-move.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
fn takes_fn(f: impl Fn()) {
2-
loop {
2+
loop { //~ HELP consider moving the expression out of the loop so it is only computed once
33
takes_fnonce(f);
44
//~^ ERROR use of moved value
55
//~| HELP consider borrowing

tests/ui/moves/borrow-closures-instead-of-move.stderr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ LL | fn takes_fnonce(_: impl FnOnce()) {}
1515
| ------------ ^^^^^^^^^^^^^ this parameter takes ownership of the value
1616
| |
1717
| in this function
18+
help: consider moving the expression out of the loop so it is only moved once
19+
|
20+
LL ~ let mut value = takes_fnonce(f);
21+
LL ~ loop {
22+
LL ~ value;
23+
|
1824
help: consider borrowing `f`
1925
|
2026
LL | takes_fnonce(&f);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
fn main() {
2+
let foos = vec![String::new()];
3+
let bars = vec![""];
4+
let mut baz = vec![];
5+
let mut qux = vec![];
6+
for foo in foos {
7+
//~^ NOTE this reinitialization might get skipped
8+
//~| NOTE move occurs because `foo` has type `String`
9+
for bar in &bars {
10+
//~^ NOTE inside of this loop
11+
//~| HELP consider moving the expression out of the loop
12+
//~| NOTE in this expansion of desugaring of `for` loop
13+
if foo == *bar {
14+
baz.push(foo);
15+
//~^ NOTE value moved here
16+
//~| HELP consider cloning the value
17+
continue;
18+
}
19+
}
20+
qux.push(foo);
21+
//~^ ERROR use of moved value
22+
//~| NOTE value used here
23+
}
24+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
error[E0382]: use of moved value: `foo`
2+
--> $DIR/nested-loop-moved-value-wrong-continue.rs:20:18
3+
|
4+
LL | for foo in foos {
5+
| ---
6+
| |
7+
| this reinitialization might get skipped
8+
| move occurs because `foo` has type `String`, which does not implement the `Copy` trait
9+
...
10+
LL | for bar in &bars {
11+
| ---------------- inside of this loop
12+
...
13+
LL | baz.push(foo);
14+
| --- value moved here, in previous iteration of loop
15+
...
16+
LL | qux.push(foo);
17+
| ^^^ value used here after move
18+
|
19+
help: consider moving the expression out of the loop so it is only moved once
20+
|
21+
LL ~ let mut value = baz.push(foo);
22+
LL ~ for bar in &bars {
23+
LL |
24+
...
25+
LL | if foo == *bar {
26+
LL ~ value;
27+
|
28+
help: consider cloning the value if the performance cost is acceptable
29+
|
30+
LL | baz.push(foo.clone());
31+
| ++++++++
32+
33+
error: aborting due to 1 previous error
34+
35+
For more information about this error, try `rustc --explain E0382`.

0 commit comments

Comments
 (0)