From ad9a5a5f9f5dcd440eff22f29c11b4129d2ee338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20K=C3=BCber?= Date: Fri, 19 Apr 2024 04:26:42 +0000 Subject: [PATCH] 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`, 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 }); | ``` --- .../src/diagnostics/conflict_errors.rs | 2 +- .../src/diagnostics/move_errors.rs | 142 +++++++++++++++++- .../src/traits/error_reporting/mod.rs | 11 +- .../borrowck/borrowck-move-by-capture.stderr | 6 + tests/ui/suggestions/option-content-move2.rs | 14 +- .../suggestions/option-content-move2.stderr | 21 ++- tests/ui/suggestions/option-content-move3.rs | 19 ++- .../suggestions/option-content-move3.stderr | 74 ++++++++- 8 files changed, 267 insertions(+), 22 deletions(-) diff --git a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs index 8215dc68c08..93ea8fb6979 100644 --- a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs +++ b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs @@ -1241,7 +1241,7 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> { } } - fn implements_clone(&self, ty: Ty<'tcx>) -> bool { + pub(crate) fn implements_clone(&self, ty: Ty<'tcx>) -> bool { let Some(clone_trait_def) = self.infcx.tcx.lang_items().clone_trait() else { return false }; self.infcx .type_implements_trait(clone_trait_def, [ty], self.param_env) diff --git a/compiler/rustc_borrowck/src/diagnostics/move_errors.rs b/compiler/rustc_borrowck/src/diagnostics/move_errors.rs index 1bbf36355cb..288b846daf5 100644 --- a/compiler/rustc_borrowck/src/diagnostics/move_errors.rs +++ b/compiler/rustc_borrowck/src/diagnostics/move_errors.rs @@ -2,10 +2,13 @@ #![allow(rustc::untranslatable_diagnostic)] use rustc_errors::{Applicability, Diag}; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{CaptureBy, ExprKind, HirId, Node}; use rustc_middle::mir::*; use rustc_middle::ty::{self, Ty}; use rustc_mir_dataflow::move_paths::{LookupResult, MovePathIndex}; use rustc_span::{BytePos, ExpnKind, MacroKind, Span}; +use rustc_trait_selection::traits::error_reporting::FindExprBySpan; use crate::diagnostics::CapturedMessageOpt; use crate::diagnostics::{DescribePlaceOpt, UseSpans}; @@ -303,6 +306,121 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> { self.cannot_move_out_of(span, &description) } + fn suggest_clone_of_captured_var_in_move_closure( + &self, + err: &mut Diag<'_>, + upvar_hir_id: HirId, + upvar_name: &str, + use_spans: Option>, + ) { + let tcx = self.infcx.tcx; + let typeck_results = tcx.typeck(self.mir_def_id()); + let Some(use_spans) = use_spans else { return }; + // We only care about the case where a closure captured a binding. + let UseSpans::ClosureUse { args_span, .. } = use_spans else { return }; + let Some(body_id) = tcx.hir_node(self.mir_hir_id()).body_id() else { return }; + // Fetch the type of the expression corresponding to the closure-captured binding. + let Some(captured_ty) = typeck_results.node_type_opt(upvar_hir_id) else { return }; + if !self.implements_clone(captured_ty) { + // We only suggest cloning the captured binding if the type can actually be cloned. + return; + }; + // Find the closure that captured the binding. + let mut expr_finder = FindExprBySpan::new(args_span, tcx); + expr_finder.include_closures = true; + expr_finder.visit_expr(tcx.hir().body(body_id).value); + let Some(closure_expr) = expr_finder.result else { return }; + let ExprKind::Closure(closure) = closure_expr.kind else { return }; + // We'll only suggest cloning the binding if it's a `move` closure. + let CaptureBy::Value { .. } = closure.capture_clause else { return }; + // Find the expression within the closure where the binding is consumed. + let mut suggested = false; + let use_span = use_spans.var_or_use(); + let mut expr_finder = FindExprBySpan::new(use_span, tcx); + expr_finder.include_closures = true; + expr_finder.visit_expr(tcx.hir().body(body_id).value); + let Some(use_expr) = expr_finder.result else { return }; + let parent = tcx.parent_hir_node(use_expr.hir_id); + if let Node::Expr(expr) = parent + && let ExprKind::Assign(lhs, ..) = expr.kind + && lhs.hir_id == use_expr.hir_id + { + // Cloning the value being assigned makes no sense: + // + // error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure + // --> $DIR/option-content-move2.rs:11:9 + // | + // LL | let mut var = None; + // | ------- captured outer variable + // LL | func(|| { + // | -- captured by this `FnMut` closure + // LL | // Shouldn't suggest `move ||.as_ref()` here + // LL | move || { + // | ^^^^^^^ `var` is moved here + // LL | + // LL | var = Some(NotCopyable); + // | --- + // | | + // | variable moved due to use in closure + // | move occurs because `var` has type `Option`, which does not implement the `Copy` trait + // | + return; + } + + // Search for an appropriate place for the structured `.clone()` suggestion to be applied. + // If we encounter a statement before the borrow error, we insert a statement there. + for (_, node) in tcx.hir().parent_iter(closure_expr.hir_id) { + if let Node::Stmt(stmt) = node { + let padding = tcx + .sess + .source_map() + .indentation_before(stmt.span) + .unwrap_or_else(|| " ".to_string()); + err.multipart_suggestion_verbose( + "clone the value before moving it into the closure", + vec![ + ( + stmt.span.shrink_to_lo(), + format!("let value = {upvar_name}.clone();\n{padding}"), + ), + (use_span, "value".to_string()), + ], + Applicability::MachineApplicable, + ); + suggested = true; + break; + } else if let Node::Expr(expr) = node + && let ExprKind::Closure(_) = expr.kind + { + // We want to suggest cloning only on the first closure, not + // subsequent ones (like `ui/suggestions/option-content-move2.rs`). + break; + } + } + if !suggested { + // If we couldn't find a statement for us to insert a new `.clone()` statement before, + // we have a bare expression, so we suggest the creation of a new block inline to go + // from `move || val` to `{ let value = val.clone(); move || value }`. + let padding = tcx + .sess + .source_map() + .indentation_before(closure_expr.span) + .unwrap_or_else(|| " ".to_string()); + err.multipart_suggestion_verbose( + "clone the value before moving it into the closure", + vec![ + ( + closure_expr.span.shrink_to_lo(), + format!("{{\n{padding}let value = {upvar_name}.clone();\n{padding}"), + ), + (use_spans.var_or_use(), "value".to_string()), + (closure_expr.span.shrink_to_hi(), format!("\n{padding}}}")), + ], + Applicability::MachineApplicable, + ); + } + } + fn report_cannot_move_from_borrowed_content( &mut self, move_place: Place<'tcx>, @@ -310,10 +428,11 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> { span: Span, use_spans: Option>, ) -> Diag<'tcx> { + let tcx = self.infcx.tcx; // Inspect the type of the content behind the // borrow to provide feedback about why this // was a move rather than a copy. - let ty = deref_target_place.ty(self.body, self.infcx.tcx).ty; + let ty = deref_target_place.ty(self.body, tcx).ty; let upvar_field = self .prefixes(move_place.as_ref(), PrefixSet::All) .find_map(|p| self.is_upvar_field_projection(p)); @@ -363,8 +482,8 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> { let upvar = &self.upvars[upvar_field.unwrap().index()]; let upvar_hir_id = upvar.get_root_variable(); - let upvar_name = upvar.to_string(self.infcx.tcx); - let upvar_span = self.infcx.tcx.hir().span(upvar_hir_id); + let upvar_name = upvar.to_string(tcx); + let upvar_span = tcx.hir().span(upvar_hir_id); let place_name = self.describe_any_place(move_place.as_ref()); @@ -380,12 +499,21 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> { closure_kind_ty, closure_kind, place_description, ); - self.cannot_move_out_of(span, &place_description) + let closure_span = tcx.def_span(def_id); + let mut err = self + .cannot_move_out_of(span, &place_description) .with_span_label(upvar_span, "captured outer variable") .with_span_label( - self.infcx.tcx.def_span(def_id), + closure_span, format!("captured by this `{closure_kind}` closure"), - ) + ); + self.suggest_clone_of_captured_var_in_move_closure( + &mut err, + upvar_hir_id, + &upvar_name, + use_spans, + ); + err } _ => { let source = self.borrowed_content_source(deref_base); @@ -415,7 +543,7 @@ impl<'a, 'tcx> MirBorrowckCtxt<'a, 'tcx> { ), (_, _, _) => self.cannot_move_out_of( span, - &source.describe_for_unnamed_place(self.infcx.tcx), + &source.describe_for_unnamed_place(tcx), ), } } diff --git a/compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs b/compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs index 4731f11ad32..e50edfdc656 100644 --- a/compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs +++ b/compiler/rustc_trait_selection/src/traits/error_reporting/mod.rs @@ -50,12 +50,13 @@ pub struct FindExprBySpan<'hir> { pub span: Span, pub result: Option<&'hir hir::Expr<'hir>>, pub ty_result: Option<&'hir hir::Ty<'hir>>, + pub include_closures: bool, pub tcx: TyCtxt<'hir>, } impl<'hir> FindExprBySpan<'hir> { pub fn new(span: Span, tcx: TyCtxt<'hir>) -> Self { - Self { span, result: None, ty_result: None, tcx } + Self { span, result: None, ty_result: None, tcx, include_closures: false } } } @@ -70,9 +71,17 @@ impl<'v> Visitor<'v> for FindExprBySpan<'v> { if self.span == ex.span { self.result = Some(ex); } else { + if let hir::ExprKind::Closure(..) = ex.kind + && self.include_closures + && let closure_header_sp = self.span.with_hi(ex.span.hi()) + && closure_header_sp == ex.span + { + self.result = Some(ex); + } hir::intravisit::walk_expr(self, ex); } } + fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) { if self.span == ty.span { self.ty_result = Some(ty); diff --git a/tests/ui/borrowck/borrowck-move-by-capture.stderr b/tests/ui/borrowck/borrowck-move-by-capture.stderr index 01647011207..9915acfe065 100644 --- a/tests/ui/borrowck/borrowck-move-by-capture.stderr +++ b/tests/ui/borrowck/borrowck-move-by-capture.stderr @@ -11,6 +11,12 @@ LL | let _h = to_fn_once(move || -> isize { *bar }); | | variable moved due to use in closure | | move occurs because `bar` has type `Box`, 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 }); + | error: aborting due to 1 previous error diff --git a/tests/ui/suggestions/option-content-move2.rs b/tests/ui/suggestions/option-content-move2.rs index 88e8a5b7aee..b0104d9bafb 100644 --- a/tests/ui/suggestions/option-content-move2.rs +++ b/tests/ui/suggestions/option-content-move2.rs @@ -1,8 +1,10 @@ struct NotCopyable; +#[derive(Clone)] +struct NotCopyableButCloneable; fn func H, H: FnMut()>(_: F) {} -fn parse() { +fn foo() { let mut var = None; func(|| { // Shouldn't suggest `move ||.as_ref()` here @@ -12,5 +14,15 @@ fn parse() { } }); } +fn bar() { + let mut var = None; + func(|| { + // Shouldn't suggest `move ||.as_ref()` nor to `clone()` here + move || { + //~^ ERROR: cannot move out of `var` + var = Some(NotCopyableButCloneable); + } + }); +} fn main() {} diff --git a/tests/ui/suggestions/option-content-move2.stderr b/tests/ui/suggestions/option-content-move2.stderr index 0297c031ecc..be97cba17b9 100644 --- a/tests/ui/suggestions/option-content-move2.stderr +++ b/tests/ui/suggestions/option-content-move2.stderr @@ -1,5 +1,5 @@ error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure - --> $DIR/option-content-move2.rs:9:9 + --> $DIR/option-content-move2.rs:11:9 | LL | let mut var = None; | ------- captured outer variable @@ -15,6 +15,23 @@ LL | var = Some(NotCopyable); | variable moved due to use in closure | move occurs because `var` has type `Option`, which does not implement the `Copy` trait -error: aborting due to 1 previous error +error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure + --> $DIR/option-content-move2.rs:21:9 + | +LL | let mut var = None; + | ------- captured outer variable +LL | func(|| { + | -- captured by this `FnMut` closure +LL | // Shouldn't suggest `move ||.as_ref()` nor to `clone()` here +LL | move || { + | ^^^^^^^ `var` is moved here +LL | +LL | var = Some(NotCopyableButCloneable); + | --- + | | + | variable moved due to use in closure + | move occurs because `var` has type `Option`, which does not implement the `Copy` trait + +error: aborting due to 2 previous errors For more information about this error, try `rustc --explain E0507`. diff --git a/tests/ui/suggestions/option-content-move3.rs b/tests/ui/suggestions/option-content-move3.rs index afd8fd0c3a2..a245309d97f 100644 --- a/tests/ui/suggestions/option-content-move3.rs +++ b/tests/ui/suggestions/option-content-move3.rs @@ -1,10 +1,23 @@ -#[derive(Debug, Clone)] +#[derive(Debug)] struct NotCopyable; +#[derive(Debug, Clone)] +struct NotCopyableButCloneable; fn func H, H: FnMut()>(_: F) {} -fn parse() { - let mut var = NotCopyable; +fn foo() { + let var = NotCopyable; + func(|| { + // Shouldn't suggest `move ||.as_ref()` here + move || { //~ ERROR cannot move out of `var` + let x = var; //~ ERROR cannot move out of `var` + println!("{x:?}"); + } + }); +} + +fn bar() { + let var = NotCopyableButCloneable; func(|| { // Shouldn't suggest `move ||.as_ref()` here move || { //~ ERROR cannot move out of `var` diff --git a/tests/ui/suggestions/option-content-move3.stderr b/tests/ui/suggestions/option-content-move3.stderr index 2d28d9b5ea7..a20dcce1ee3 100644 --- a/tests/ui/suggestions/option-content-move3.stderr +++ b/tests/ui/suggestions/option-content-move3.stderr @@ -1,24 +1,32 @@ error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure - --> $DIR/option-content-move3.rs:11:21 + --> $DIR/option-content-move3.rs:13:21 | -LL | let mut var = NotCopyable; - | ------- captured outer variable +LL | let var = NotCopyable; + | --- captured outer variable ... LL | move || { | ------- captured by this `FnMut` closure LL | let x = var; | ^^^ move occurs because `var` has type `NotCopyable`, which does not implement the `Copy` trait | +note: if `NotCopyable` implemented `Clone`, you could clone the value + --> $DIR/option-content-move3.rs:2:1 + | +LL | struct NotCopyable; + | ^^^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type +... +LL | let x = var; + | --- you could clone this value help: consider borrowing here | LL | let x = &var; | + error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure - --> $DIR/option-content-move3.rs:10:9 + --> $DIR/option-content-move3.rs:12:9 | -LL | let mut var = NotCopyable; - | ------- captured outer variable +LL | let var = NotCopyable; + | --- captured outer variable LL | func(|| { | -- captured by this `FnMut` closure LL | // Shouldn't suggest `move ||.as_ref()` here @@ -29,7 +37,59 @@ LL | let x = var; | | | variable moved due to use in closure | move occurs because `var` has type `NotCopyable`, which does not implement the `Copy` trait + | +note: if `NotCopyable` implemented `Clone`, you could clone the value + --> $DIR/option-content-move3.rs:2:1 + | +LL | struct NotCopyable; + | ^^^^^^^^^^^^^^^^^^ consider implementing `Clone` for this type +... +LL | let x = var; + | --- you could clone this value -error: aborting due to 2 previous errors +error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure + --> $DIR/option-content-move3.rs:24:21 + | +LL | let var = NotCopyableButCloneable; + | --- captured outer variable +... +LL | move || { + | ------- captured by this `FnMut` closure +LL | let x = var; + | ^^^ move occurs because `var` has type `NotCopyableButCloneable`, which does not implement the `Copy` trait + | +help: consider borrowing here + | +LL | let x = &var; + | + + +error[E0507]: cannot move out of `var`, a captured variable in an `FnMut` closure + --> $DIR/option-content-move3.rs:23:9 + | +LL | let var = NotCopyableButCloneable; + | --- captured outer variable +LL | func(|| { + | -- captured by this `FnMut` closure +LL | // Shouldn't suggest `move ||.as_ref()` here +LL | move || { + | ^^^^^^^ `var` is moved here +LL | let x = var; + | --- + | | + | variable moved due to use in closure + | move occurs because `var` has type `NotCopyableButCloneable`, which does not implement the `Copy` trait + | +help: clone the value before moving it into the closure + | +LL ~ { +LL + let value = var.clone(); +LL ~ move || { +LL ~ let x = value; +LL | println!("{x:?}"); +LL ~ } +LL + } + | + +error: aborting due to 4 previous errors For more information about this error, try `rustc --explain E0507`.