Skip to content

Commit 17a27e8

Browse files
committed
Suggest cloning captured binding in move closure
``` error[E0507]: cannot move out of `bar`, a captured variable in an `FnMut` closure --> $DIR/borrowck-move-by-capture.rs:9:29 | LL | let bar: Box<_> = Box::new(3); | --- captured outer variable LL | let _g = to_fn_mut(|| { | -- captured by this `FnMut` closure LL | let _h = to_fn_once(move || -> isize { *bar }); | ^^^^^^^^^^^^^^^^ ---- | | | | | variable moved due to use in closure | | move occurs because `bar` has type `Box<isize>`, which does not implement the `Copy` trait | `bar` is moved here | help: clone the value before moving it into the closure | LL ~ let value = bar.clone(); LL ~ let _h = to_fn_once(move || -> isize { value }); | ```
1 parent 0136766 commit 17a27e8

File tree

8 files changed

+267
-22
lines changed

8 files changed

+267
-22
lines changed

compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
12411241
}
12421242
}
12431243

1244-
fn implements_clone(&self, ty: Ty<'tcx>) -> bool {
1244+
pub(crate) fn implements_clone(&self, ty: Ty<'tcx>) -> bool {
12451245
let Some(clone_trait_def) = self.infcx.tcx.lang_items().clone_trait() else { return false };
12461246
self.infcx
12471247
.type_implements_trait(clone_trait_def, [ty], self.param_env)

compiler/rustc_borrowck/src/diagnostics/move_errors.rs

+135-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
#![allow(rustc::untranslatable_diagnostic)]
33

44
use rustc_errors::{Applicability, Diag};
5+
use rustc_hir::intravisit::Visitor;
6+
use rustc_hir::{CaptureBy, ExprKind, HirId, Node};
57
use rustc_middle::mir::*;
68
use rustc_middle::ty::{self, Ty};
79
use rustc_mir_dataflow::move_paths::{LookupResult, MovePathIndex};
810
use rustc_span::{BytePos, ExpnKind, MacroKind, Span};
11+
use rustc_trait_selection::traits::error_reporting::FindExprBySpan;
912

1013
use crate::diagnostics::CapturedMessageOpt;
1114
use crate::diagnostics::{DescribePlaceOpt, UseSpans};
@@ -303,17 +306,133 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
303306
self.cannot_move_out_of(span, &description)
304307
}
305308

309+
fn suggest_clone_of_captured_var_in_move_closure(
310+
&self,
311+
err: &mut Diag<'_>,
312+
upvar_hir_id: HirId,
313+
upvar_name: &str,
314+
use_spans: Option<UseSpans<'tcx>>,
315+
) {
316+
let tcx = self.infcx.tcx;
317+
let typeck_results = tcx.typeck(self.mir_def_id());
318+
let Some(use_spans) = use_spans else { return };
319+
// We only care about the case where a closure captured a binding.
320+
let UseSpans::ClosureUse { args_span, .. } = use_spans else { return };
321+
let Some(body_id) = tcx.hir_node(self.mir_hir_id()).body_id() else { return };
322+
// Fetch the type of the expression corresponding to the closure-captured binding.
323+
let Some(captured_ty) = typeck_results.node_type_opt(upvar_hir_id) else { return };
324+
if !self.implements_clone(captured_ty) {
325+
// We only suggest cloning the captured binding if the type can actually be cloned.
326+
return;
327+
};
328+
// Find the closure that captured the binding.
329+
let mut expr_finder = FindExprBySpan::new(args_span, tcx);
330+
expr_finder.include_closures = true;
331+
expr_finder.visit_expr(tcx.hir().body(body_id).value);
332+
let Some(closure_expr) = expr_finder.result else { return };
333+
let ExprKind::Closure(closure) = closure_expr.kind else { return };
334+
// We'll only suggest cloning the binding if it's a `move` closure.
335+
let CaptureBy::Value { .. } = closure.capture_clause else { return };
336+
// Find the expression within the closure where the binding is consumed.
337+
let mut suggested = false;
338+
let use_span = use_spans.var_or_use();
339+
let mut expr_finder = FindExprBySpan::new(use_span, tcx);
340+
expr_finder.include_closures = true;
341+
expr_finder.visit_expr(tcx.hir().body(body_id).value);
342+
let Some(use_expr) = expr_finder.result else { return };
343+
let parent = tcx.parent_hir_node(use_expr.hir_id);
344+
if let Node::Expr(expr) = parent
345+
&& let ExprKind::Assign(lhs, ..) = expr.kind
346+
&& lhs.hir_id == use_expr.hir_id
347+
{
348+
// Cloning the value being assigned makes no sense:
349+
//
350+
// error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure
351+
// --> $DIR/option-content-move2.rs:11:9
352+
// |
353+
// LL | let mut var = None;
354+
// | ------- captured outer variable
355+
// LL | func(|| {
356+
// | -- captured by this `FnMut` closure
357+
// LL | // Shouldn't suggest `move ||.as_ref()` here
358+
// LL | move || {
359+
// | ^^^^^^^ `var` is moved here
360+
// LL |
361+
// LL | var = Some(NotCopyable);
362+
// | ---
363+
// | |
364+
// | variable moved due to use in closure
365+
// | move occurs because `var` has type `Option<NotCopyable>`, which does not implement the `Copy` trait
366+
// |
367+
return;
368+
}
369+
370+
// Search for an appropriate place for the structured `.clone()` suggestion to be applied.
371+
// If we encounter a statement before the borrow error, we insert a statement there.
372+
for (_, node) in tcx.hir().parent_iter(closure_expr.hir_id) {
373+
if let Node::Stmt(stmt) = node {
374+
let padding = tcx
375+
.sess
376+
.source_map()
377+
.indentation_before(stmt.span)
378+
.unwrap_or_else(|| " ".to_string());
379+
err.multipart_suggestion_verbose(
380+
"clone the value before moving it into the closure",
381+
vec![
382+
(
383+
stmt.span.shrink_to_lo(),
384+
format!("let value = {upvar_name}.clone();\n{padding}"),
385+
),
386+
(use_span, "value".to_string()),
387+
],
388+
Applicability::MachineApplicable,
389+
);
390+
suggested = true;
391+
break;
392+
} else if let Node::Expr(expr) = node
393+
&& let ExprKind::Closure(_) = expr.kind
394+
{
395+
// We want to suggest cloning only on the first closure, not
396+
// subsequent ones (like `ui/suggestions/option-content-move2.rs`).
397+
break;
398+
}
399+
}
400+
if !suggested {
401+
// If we couldn't find a statement for us to insert a new `.clone()` statement before,
402+
// we have a bare expression, so we suggest the creation of a new block inline to go
403+
// from `move || val` to `{ let value = val.clone(); move || value }`.
404+
let padding = tcx
405+
.sess
406+
.source_map()
407+
.indentation_before(closure_expr.span)
408+
.unwrap_or_else(|| " ".to_string());
409+
err.multipart_suggestion_verbose(
410+
"clone the value before moving it into the closure",
411+
vec![
412+
(
413+
closure_expr.span.shrink_to_lo(),
414+
format!("{{\n{padding}let value = {upvar_name}.clone();\n{padding}"),
415+
),
416+
(use_spans.var_or_use(), "value".to_string()),
417+
(closure_expr.span.shrink_to_hi(), format!("\n{padding}}}")),
418+
],
419+
Applicability::MachineApplicable,
420+
);
421+
}
422+
}
423+
306424
fn report_cannot_move_from_borrowed_content(
307425
&mut self,
308426
move_place: Place<'tcx>,
309427
deref_target_place: Place<'tcx>,
310428
span: Span,
311429
use_spans: Option<UseSpans<'tcx>>,
312430
) -> Diag<'tcx> {
431+
let tcx = self.infcx.tcx;
313432
// Inspect the type of the content behind the
314433
// borrow to provide feedback about why this
315434
// was a move rather than a copy.
316-
let ty = deref_target_place.ty(self.body, self.infcx.tcx).ty;
435+
let ty = deref_target_place.ty(self.body, tcx).ty;
317436
let upvar_field = self
318437
.prefixes(move_place.as_ref(), PrefixSet::All)
319438
.find_map(|p| self.is_upvar_field_projection(p));
@@ -363,8 +482,8 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
363482

364483
let upvar = &self.upvars[upvar_field.unwrap().index()];
365484
let upvar_hir_id = upvar.get_root_variable();
366-
let upvar_name = upvar.to_string(self.infcx.tcx);
367-
let upvar_span = self.infcx.tcx.hir().span(upvar_hir_id);
485+
let upvar_name = upvar.to_string(tcx);
486+
let upvar_span = tcx.hir().span(upvar_hir_id);
368487

369488
let place_name = self.describe_any_place(move_place.as_ref());
370489

@@ -380,12 +499,21 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
380499
closure_kind_ty, closure_kind, place_description,
381500
);
382501

383-
self.cannot_move_out_of(span, &place_description)
502+
let closure_span = tcx.def_span(def_id);
503+
let mut err = self
504+
.cannot_move_out_of(span, &place_description)
384505
.with_span_label(upvar_span, "captured outer variable")
385506
.with_span_label(
386-
self.infcx.tcx.def_span(def_id),
507+
closure_span,
387508
format!("captured by this `{closure_kind}` closure"),
388-
)
509+
);
510+
self.suggest_clone_of_captured_var_in_move_closure(
511+
&mut err,
512+
upvar_hir_id,
513+
&upvar_name,
514+
use_spans,
515+
);
516+
err
389517
}
390518
_ => {
391519
let source = self.borrowed_content_source(deref_base);
@@ -415,7 +543,7 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> {
415543
),
416544
(_, _, _) => self.cannot_move_out_of(
417545
span,
418-
&source.describe_for_unnamed_place(self.infcx.tcx),
546+
&source.describe_for_unnamed_place(tcx),
419547
),
420548
}
421549
}

compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ pub struct FindExprBySpan<'hir> {
5050
pub span: Span,
5151
pub result: Option<&'hir hir::Expr<'hir>>,
5252
pub ty_result: Option<&'hir hir::Ty<'hir>>,
53+
pub include_closures: bool,
5354
pub tcx: TyCtxt<'hir>,
5455
}
5556

5657
impl<'hir> FindExprBySpan<'hir> {
5758
pub fn new(span: Span, tcx: TyCtxt<'hir>) -> Self {
58-
Self { span, result: None, ty_result: None, tcx }
59+
Self { span, result: None, ty_result: None, tcx, include_closures: false }
5960
}
6061
}
6162

@@ -70,9 +71,17 @@ impl<'v> Visitor<'v> for FindExprBySpan<'v> {
7071
if self.span == ex.span {
7172
self.result = Some(ex);
7273
} else {
74+
if let hir::ExprKind::Closure(..) = ex.kind
75+
&& self.include_closures
76+
&& let closure_header_sp = self.span.with_hi(ex.span.hi())
77+
&& closure_header_sp == ex.span
78+
{
79+
self.result = Some(ex);
80+
}
7381
hir::intravisit::walk_expr(self, ex);
7482
}
7583
}
84+
7685
fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) {
7786
if self.span == ty.span {
7887
self.ty_result = Some(ty);

tests/ui/borrowck/borrowck-move-by-capture.stderr

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ LL | let _h = to_fn_once(move || -> isize { *bar });
1111
| | variable moved due to use in closure
1212
| | move occurs because `bar` has type `Box<isize>`, which does not implement the `Copy` trait
1313
| `bar` is moved here
14+
|
15+
help: clone the value before moving it into the closure
16+
|
17+
LL ~ let value = bar.clone();
18+
LL ~ let _h = to_fn_once(move || -> isize { value });
19+
|
1420

1521
error: aborting due to 1 previous error
1622

tests/ui/suggestions/option-content-move2.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
struct NotCopyable;
2+
#[derive(Clone)]
3+
struct NotCopyableButCloneable;
24

35
fn func<F: FnMut() -> H, H: FnMut()>(_: F) {}
46

5-
fn parse() {
7+
fn foo() {
68
let mut var = None;
79
func(|| {
810
// Shouldn't suggest `move ||.as_ref()` here
@@ -12,5 +14,15 @@ fn parse() {
1214
}
1315
});
1416
}
17+
fn bar() {
18+
let mut var = None;
19+
func(|| {
20+
// Shouldn't suggest `move ||.as_ref()` nor to `clone()` here
21+
move || {
22+
//~^ ERROR: cannot move out of `var`
23+
var = Some(NotCopyableButCloneable);
24+
}
25+
});
26+
}
1527

1628
fn main() {}

tests/ui/suggestions/option-content-move2.stderr

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure
2-
--> $DIR/option-content-move2.rs:9:9
2+
--> $DIR/option-content-move2.rs:11:9
33
|
44
LL | let mut var = None;
55
| ------- captured outer variable
@@ -15,6 +15,23 @@ LL | var = Some(NotCopyable);
1515
| variable moved due to use in closure
1616
| move occurs because `var` has type `Option<NotCopyable>`, which does not implement the `Copy` trait
1717

18-
error: aborting due to 1 previous error
18+
error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure
19+
--> $DIR/option-content-move2.rs:21:9
20+
|
21+
LL | let mut var = None;
22+
| ------- captured outer variable
23+
LL | func(|| {
24+
| -- captured by this `FnMut` closure
25+
LL | // Shouldn't suggest `move ||.as_ref()` nor to `clone()` here
26+
LL | move || {
27+
| ^^^^^^^ `var` is moved here
28+
LL |
29+
LL | var = Some(NotCopyableButCloneable);
30+
| ---
31+
| |
32+
| variable moved due to use in closure
33+
| move occurs because `var` has type `Option<NotCopyableButCloneable>`, which does not implement the `Copy` trait
34+
35+
error: aborting due to 2 previous errors
1936

2037
For more information about this error, try `rustc --explain E0507`.

tests/ui/suggestions/option-content-move3.rs

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
#[derive(Debug, Clone)]
1+
#[derive(Debug)]
22
struct NotCopyable;
3+
#[derive(Debug, Clone)]
4+
struct NotCopyableButCloneable;
35

46
fn func<F: FnMut() -> H, H: FnMut()>(_: F) {}
57

6-
fn parse() {
7-
let mut var = NotCopyable;
8+
fn foo() {
9+
let var = NotCopyable;
10+
func(|| {
11+
// Shouldn't suggest `move ||.as_ref()` here
12+
move || { //~ ERROR cannot move out of `var`
13+
let x = var; //~ ERROR cannot move out of `var`
14+
println!("{x:?}");
15+
}
16+
});
17+
}
18+
19+
fn bar() {
20+
let var = NotCopyableButCloneable;
821
func(|| {
922
// Shouldn't suggest `move ||.as_ref()` here
1023
move || { //~ ERROR cannot move out of `var`

0 commit comments

Comments
 (0)