Skip to content

Commit 14f5d78

Browse files
committed
Point at continue and break that might be in the wrong place
Sometimes move errors are because of a misplaced `continue`, but we didn't surface that anywhere. Now when there are more than one set of nested loops we show them out and point at the `continue` and `break` expressions within that might need to go elsewhere. ``` error[E0382]: use of moved value: `foo` --> $DIR/nested-loop-moved-value-wrong-continue.rs:46:18 | LL | for foo in foos { | --- | | | this reinitialization might get skipped | move occurs because `foo` has type `String`, which does not implement the `Copy` trait ... LL | for bar in &bars { | ---------------- inside of this loop ... LL | baz.push(foo); | --- value moved here, in previous iteration of loop ... LL | qux.push(foo); | ^^^ value used here after move | note: verify that your loop breaking logic is correct --> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17 | LL | for foo in foos { | --------------- ... LL | for bar in &bars { | ---------------- ... LL | continue; | ^^^^^^^^ this `continue` advances the loop at line 33 help: consider moving the expression out of the loop so it is only moved once | LL ~ let mut value = baz.push(foo); LL ~ for bar in &bars { LL | ... LL | if foo == *bar { LL ~ value; | help: consider cloning the value if the performance cost is acceptable | LL | baz.push(foo.clone()); | ++++++++ ``` Fix #92531.
1 parent 24c4bad commit 14f5d78

9 files changed

+225
-55
lines changed

Diff for: compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs

+130-33
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use rustc_data_structures::fx::FxIndexSet;
99
use rustc_errors::{codes::*, struct_span_code_err, Applicability, DiagnosticBuilder, MultiSpan};
1010
use rustc_hir as hir;
1111
use rustc_hir::def::{DefKind, Res};
12-
use rustc_hir::intravisit::{walk_block, walk_expr, Visitor};
12+
use rustc_hir::intravisit::{walk_block, walk_expr, Map, Visitor};
1313
use rustc_hir::{CoroutineDesugaring, PatField};
1414
use rustc_hir::{CoroutineKind, CoroutineSource, LangItem};
1515
use rustc_infer::traits::ObligationCause;
@@ -794,9 +794,9 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
794794
let e = match node {
795795
hir::Node::Expr(e) => e,
796796
hir::Node::Local(hir::Local { els: Some(els), .. }) => {
797-
let mut finder = BreakFinder { found_break: false };
797+
let mut finder = BreakFinder { found_breaks: vec![], found_continues: vec![] };
798798
finder.visit_block(els);
799-
if finder.found_break {
799+
if !finder.found_breaks.is_empty() {
800800
// Don't suggest clone as it could be will likely end in an infinite
801801
// loop.
802802
// let Some(_) = foo(non_copy.clone()) else { break; }
@@ -845,51 +845,148 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
845845
_ => {}
846846
}
847847
}
848+
let loop_count: usize = tcx
849+
.hir()
850+
.parent_iter(expr.hir_id)
851+
.map(|(_, node)| match node {
852+
hir::Node::Expr(hir::Expr { kind: hir::ExprKind::Loop(..), .. }) => 1,
853+
_ => 0,
854+
})
855+
.sum();
848856

849857
/// Look for `break` expressions within any arbitrary expressions. We'll do this to infer
850858
/// whether this is a case where the moved value would affect the exit of a loop, making it
851859
/// unsuitable for a `.clone()` suggestion.
852860
struct BreakFinder {
853-
found_break: bool,
861+
found_breaks: Vec<(hir::Destination, Span)>,
862+
found_continues: Vec<(hir::Destination, Span)>,
854863
}
855864
impl<'hir> Visitor<'hir> for BreakFinder {
856865
fn visit_expr(&mut self, ex: &'hir hir::Expr<'hir>) {
857-
if let hir::ExprKind::Break(..) = ex.kind {
858-
self.found_break = true;
866+
match ex.kind {
867+
hir::ExprKind::Break(destination, _) => {
868+
self.found_breaks.push((destination, ex.span));
869+
}
870+
hir::ExprKind::Continue(destination) => {
871+
self.found_continues.push((destination, ex.span));
872+
}
873+
_ => {}
859874
}
860875
hir::intravisit::walk_expr(self, ex);
861876
}
862877
}
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`.
870878

871-
let span = in_loop.span;
872-
let mut finder = BreakFinder { found_break: false };
879+
let sm = tcx.sess.source_map();
880+
if let Some(in_loop) = outer_most_loop {
881+
let mut finder = BreakFinder { found_breaks: vec![], found_continues: vec![] };
873882
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()
883+
// All of the spans for `break` and `continue` expressions.
884+
let spans = finder
885+
.found_breaks
886+
.iter()
887+
.chain(finder.found_continues.iter())
888+
.map(|(_, span)| *span)
889+
.filter(|span| {
890+
!matches!(
891+
span.desugaring_kind(),
892+
Some(DesugaringKind::ForLoop | DesugaringKind::WhileLoop)
893+
)
894+
})
895+
.collect::<Vec<Span>>();
896+
// All of the spans for the loops above the expression with the move error.
897+
let loop_spans: Vec<_> = tcx
898+
.hir()
899+
.parent_iter(expr.hir_id)
900+
.filter_map(|(_, node)| match node {
901+
hir::Node::Expr(hir::Expr { span, kind: hir::ExprKind::Loop(..), .. }) => {
902+
Some(*span)
903+
}
904+
_ => None,
905+
})
906+
.collect();
907+
// It is possible that a user written `break` or `continue` is in the wrong place. We
908+
// point them out at the user for them to make a determination. (#92531)
909+
if !spans.is_empty() && loop_count > 1 {
910+
// Getting fancy: if the spans of the loops *do not* overlap, we only use the line
911+
// number when referring to them. If there *are* overlaps (multiple loops on the
912+
// same line) then we use the more verbose span output (`file.rs:col:ll`).
913+
let mut lines: Vec<_> =
914+
loop_spans.iter().map(|sp| sm.lookup_char_pos(sp.lo()).line).collect();
915+
lines.sort();
916+
lines.dedup();
917+
let fmt_span = |span: Span| {
918+
if lines.len() == loop_spans.len() {
919+
format!("line {}", sm.lookup_char_pos(span.lo()).line)
920+
} else {
921+
sm.span_to_diagnostic_string(span)
922+
}
884923
};
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-
);
924+
let mut spans: MultiSpan = spans.clone().into();
925+
// Point at all the `continue`s and explicit `break`s in the relevant loops.
926+
for (desc, elements) in [
927+
("`break` exits", &finder.found_breaks),
928+
("`continue` advances", &finder.found_continues),
929+
] {
930+
for (destination, sp) in elements {
931+
if let Ok(hir_id) = destination.target_id
932+
&& let hir::Node::Expr(expr) = tcx.hir().hir_node(hir_id)
933+
&& !matches!(
934+
sp.desugaring_kind(),
935+
Some(DesugaringKind::ForLoop | DesugaringKind::WhileLoop)
936+
)
937+
{
938+
spans.push_span_label(
939+
*sp,
940+
format!("this {desc} the loop at {}", fmt_span(expr.span)),
941+
);
942+
}
943+
}
944+
}
945+
// Point at all the loops that are between this move and the parent item.
946+
for span in loop_spans {
947+
spans.push_span_label(sm.guess_head_span(span), "");
948+
}
949+
950+
// note: verify that your loop breaking logic is correct
951+
// --> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17
952+
// |
953+
// 28 | for foo in foos {
954+
// | ---------------
955+
// ...
956+
// 33 | for bar in &bars {
957+
// | ----------------
958+
// ...
959+
// 41 | continue;
960+
// | ^^^^^^^^ this `continue` advances the loop at line 33
961+
err.span_note(spans, "verify that your loop breaking logic is correct");
962+
}
963+
if let Some(parent) = parent
964+
&& let hir::ExprKind::MethodCall(..) | hir::ExprKind::Call(..) = parent.kind
965+
{
966+
// FIXME: We could check that the call's *parent* takes `&mut val` to make the
967+
// suggestion more targeted to the `mk_iter(val).next()` case. Maybe do that only to
968+
// check for wheter to suggest `let value` or `let mut value`.
969+
970+
let span = in_loop.span;
971+
if !finder.found_breaks.is_empty()
972+
&& let Ok(value) = sm.span_to_snippet(parent.span)
973+
{
974+
// We know with high certainty that this move would affect the early return of a
975+
// loop, so we suggest moving the expression with the move out of the loop.
976+
let indent = if let Some(indent) = sm.indentation_before(span) {
977+
format!("\n{indent}")
978+
} else {
979+
" ".to_string()
980+
};
981+
err.multipart_suggestion(
982+
"consider moving the expression out of the loop so it is only moved once",
983+
vec![
984+
(parent.span, "value".to_string()),
985+
(span.shrink_to_lo(), format!("let mut value = {value};{indent}")),
986+
],
987+
Applicability::MaybeIncorrect,
988+
);
989+
}
893990
}
894991
}
895992
can_suggest_clone

Diff for: tests/ui/liveness/liveness-move-call-arg-2.stderr

-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@ 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-
|
2519
help: consider cloning the value if the performance cost is acceptable
2620
|
2721
LL | take(x.clone());

Diff for: tests/ui/liveness/liveness-move-call-arg.stderr

-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,6 @@ 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-
|
2519
help: consider cloning the value if the performance cost is acceptable
2620
|
2721
LL | take(x.clone());

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

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

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

-6
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ 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-
|
2418
help: consider borrowing `f`
2519
|
2620
LL | takes_fnonce(&f);

Diff for: tests/ui/moves/nested-loop-moved-value-wrong-continue.rs

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
fn foo() {
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 { for bar in &bars { if foo == *bar {
7+
//~^ NOTE this reinitialization might get skipped
8+
//~| NOTE move occurs because `foo` has type `String`
9+
//~| NOTE inside of this loop
10+
//~| HELP consider moving the expression out of the loop
11+
//~| NOTE in this expansion of desugaring of `for` loop
12+
baz.push(foo);
13+
//~^ NOTE value moved here
14+
//~| HELP consider cloning the value
15+
continue;
16+
//~^ NOTE verify that your loop breaking logic is correct
17+
//~| NOTE this `continue` advances the loop at $DIR/nested-loop-moved-value-wrong-continue.rs:6:23
18+
} }
19+
qux.push(foo);
20+
//~^ ERROR use of moved value
21+
//~| NOTE value used here
22+
}
23+
}
24+
125
fn main() {
226
let foos = vec![String::new()];
327
let bars = vec![""];
@@ -15,6 +39,8 @@ fn main() {
1539
//~^ NOTE value moved here
1640
//~| HELP consider cloning the value
1741
continue;
42+
//~^ NOTE verify that your loop breaking logic is correct
43+
//~| NOTE this `continue` advances the loop at line 33
1844
}
1945
}
2046
qux.push(foo);

Diff for: tests/ui/moves/nested-loop-moved-value-wrong-continue.stderr

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
error[E0382]: use of moved value: `foo`
2-
--> $DIR/nested-loop-moved-value-wrong-continue.rs:20:18
2+
--> $DIR/nested-loop-moved-value-wrong-continue.rs:19:14
3+
|
4+
LL | for foo in foos { for bar in &bars { if foo == *bar {
5+
| --- ---------------- inside of this loop
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 | baz.push(foo);
11+
| --- value moved here, in previous iteration of loop
12+
...
13+
LL | qux.push(foo);
14+
| ^^^ value used here after move
15+
|
16+
note: verify that your loop breaking logic is correct
17+
--> $DIR/nested-loop-moved-value-wrong-continue.rs:15:9
18+
|
19+
LL | for foo in foos { for bar in &bars { if foo == *bar {
20+
| --------------- ----------------
21+
...
22+
LL | continue;
23+
| ^^^^^^^^ this `continue` advances the loop at $DIR/nested-loop-moved-value-wrong-continue.rs:6:23: 18:8
24+
help: consider moving the expression out of the loop so it is only moved once
25+
|
26+
LL ~ for foo in foos { let mut value = baz.push(foo);
27+
LL ~ for bar in &bars { if foo == *bar {
28+
LL |
29+
...
30+
LL |
31+
LL ~ value;
32+
|
33+
help: consider cloning the value if the performance cost is acceptable
34+
|
35+
LL | baz.push(foo.clone());
36+
| ++++++++
37+
38+
error[E0382]: use of moved value: `foo`
39+
--> $DIR/nested-loop-moved-value-wrong-continue.rs:46:18
340
|
441
LL | for foo in foos {
542
| ---
@@ -16,6 +53,17 @@ LL | baz.push(foo);
1653
LL | qux.push(foo);
1754
| ^^^ value used here after move
1855
|
56+
note: verify that your loop breaking logic is correct
57+
--> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17
58+
|
59+
LL | for foo in foos {
60+
| ---------------
61+
...
62+
LL | for bar in &bars {
63+
| ----------------
64+
...
65+
LL | continue;
66+
| ^^^^^^^^ this `continue` advances the loop at line 33
1967
help: consider moving the expression out of the loop so it is only moved once
2068
|
2169
LL ~ let mut value = baz.push(foo);
@@ -30,6 +78,6 @@ help: consider cloning the value if the performance cost is acceptable
3078
LL | baz.push(foo.clone());
3179
| ++++++++
3280

33-
error: aborting due to 1 previous error
81+
error: aborting due to 2 previous errors
3482

3583
For more information about this error, try `rustc --explain E0382`.

Diff for: tests/ui/moves/recreating-value-in-loop-condition.rs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn qux() {
3131
loop {
3232
if let Some(item) = iter(vec).next() { //~ ERROR use of moved value
3333
println!("{:?}", item);
34+
break;
3435
}
3536
}
3637
}
@@ -42,6 +43,7 @@ fn zap() {
4243
loop {
4344
if let Some(item) = iter(vec).next() { //~ ERROR use of moved value
4445
println!("{:?}", item);
46+
break;
4547
}
4648
}
4749
}

Diff for: tests/ui/moves/recreating-value-in-loop-condition.stderr

+16-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ LL ~ if let Some(item) = value.next() {
9999
|
100100

101101
error[E0382]: use of moved value: `vec`
102-
--> $DIR/recreating-value-in-loop-condition.rs:43:46
102+
--> $DIR/recreating-value-in-loop-condition.rs:44:46
103103
|
104104
LL | let vec = vec!["one", "two", "three"];
105105
| --- move occurs because `vec` has type `Vec<&str>`, which does not implement the `Copy` trait
@@ -119,6 +119,21 @@ LL | fn iter<T>(vec: Vec<T>) -> impl Iterator<Item = T> {
119119
| ---- ^^^^^^ this parameter takes ownership of the value
120120
| |
121121
| in this function
122+
note: verify that your loop breaking logic is correct
123+
--> $DIR/recreating-value-in-loop-condition.rs:46:25
124+
|
125+
LL | loop {
126+
| ----
127+
LL | let vec = vec!["one", "two", "three"];
128+
LL | loop {
129+
| ----
130+
LL | loop {
131+
| ----
132+
LL | loop {
133+
| ----
134+
...
135+
LL | break;
136+
| ^^^^^ this `break` exits the loop at line 43
122137
help: consider moving the expression out of the loop so it is only moved once
123138
|
124139
LL ~ let mut value = iter(vec);

0 commit comments

Comments
 (0)