@@ -9,7 +9,7 @@ use rustc_data_structures::fx::FxIndexSet;
99use rustc_errors:: { codes:: * , struct_span_code_err, Applicability , Diag , MultiSpan } ;
1010use rustc_hir as hir;
1111use 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 } ;
1313use rustc_hir:: { CoroutineDesugaring , PatField } ;
1414use rustc_hir:: { CoroutineKind , CoroutineSource , LangItem } ;
1515use rustc_middle:: hir:: nested_filter:: OnlyBodies ;
@@ -447,8 +447,8 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
447447 err. span_note (
448448 span,
449449 format ! (
450- "consider changing this parameter type in {descr} `{ident}` to \
451- borrow instead if owning the value isn't necessary",
450+ "consider changing this parameter type in {descr} `{ident}` to borrow \
451+ instead if owning the value isn't necessary",
452452 ) ,
453453 ) ;
454454 }
@@ -463,7 +463,9 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
463463 } else if let UseSpans :: FnSelfUse { kind : CallKind :: Normal { .. } , .. } = move_spans
464464 {
465465 // We already suggest cloning for these cases in `explain_captures`.
466- } else {
466+ } else if self . suggest_hoisting_call_outside_loop ( err, expr) {
467+ // The place where the the type moves would be misleading to suggest clone.
468+ // #121466
467469 self . suggest_cloning ( err, ty, expr, move_span) ;
468470 }
469471 }
@@ -747,6 +749,234 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
747749 true
748750 }
749751
752+ /// In a move error that occurs on a call wihtin a loop, we try to identify cases where cloning
753+ /// the value would lead to a logic error. We infer these cases by seeing if the moved value is
754+ /// part of the logic to break the loop, either through an explicit `break` or if the expression
755+ /// is part of a `while let`.
756+ fn suggest_hoisting_call_outside_loop ( & self , err : & mut Diag < ' _ > , expr : & hir:: Expr < ' _ > ) -> bool {
757+ let tcx = self . infcx . tcx ;
758+ let mut can_suggest_clone = true ;
759+
760+ // If the moved value is a locally declared binding, we'll look upwards on the expression
761+ // tree until the scope where it is defined, and no further, as suggesting to move the
762+ // expression beyond that point would be illogical.
763+ let local_hir_id = if let hir:: ExprKind :: Path ( hir:: QPath :: Resolved (
764+ _,
765+ hir:: Path { res : hir:: def:: Res :: Local ( local_hir_id) , .. } ,
766+ ) ) = expr. kind
767+ {
768+ Some ( local_hir_id)
769+ } else {
770+ // This case would be if the moved value comes from an argument binding, we'll just
771+ // look within the entire item, that's fine.
772+ None
773+ } ;
774+
775+ /// This will allow us to look for a specific `HirId`, in our case `local_hir_id` where the
776+ /// binding was declared, within any other expression. We'll use it to search for the
777+ /// binding declaration within every scope we inspect.
778+ struct Finder {
779+ hir_id : hir:: HirId ,
780+ found : bool ,
781+ }
782+ impl < ' hir > Visitor < ' hir > for Finder {
783+ fn visit_pat ( & mut self , pat : & ' hir hir:: Pat < ' hir > ) {
784+ if pat. hir_id == self . hir_id {
785+ self . found = true ;
786+ }
787+ hir:: intravisit:: walk_pat ( self , pat) ;
788+ }
789+ fn visit_expr ( & mut self , ex : & ' hir hir:: Expr < ' hir > ) {
790+ if ex. hir_id == self . hir_id {
791+ self . found = true ;
792+ }
793+ hir:: intravisit:: walk_expr ( self , ex) ;
794+ }
795+ }
796+ // The immediate HIR parent of the moved expression. We'll look for it to be a call.
797+ let mut parent = None ;
798+ // The top-most loop where the moved expression could be moved to a new binding.
799+ let mut outer_most_loop: Option < & hir:: Expr < ' _ > > = None ;
800+ for ( _, node) in tcx. hir ( ) . parent_iter ( expr. hir_id ) {
801+ let e = match node {
802+ hir:: Node :: Expr ( e) => e,
803+ hir:: Node :: Local ( hir:: Local { els : Some ( els) , .. } ) => {
804+ let mut finder = BreakFinder { found_breaks : vec ! [ ] , found_continues : vec ! [ ] } ;
805+ finder. visit_block ( els) ;
806+ if !finder. found_breaks . is_empty ( ) {
807+ // Don't suggest clone as it could be will likely end in an infinite
808+ // loop.
809+ // let Some(_) = foo(non_copy.clone()) else { break; }
810+ // --- ^^^^^^^^ -----
811+ can_suggest_clone = false ;
812+ }
813+ continue ;
814+ }
815+ _ => continue ,
816+ } ;
817+ if let Some ( & hir_id) = local_hir_id {
818+ let mut finder = Finder { hir_id, found : false } ;
819+ finder. visit_expr ( e) ;
820+ if finder. found {
821+ // The current scope includes the declaration of the binding we're accessing, we
822+ // can't look up any further for loops.
823+ break ;
824+ }
825+ }
826+ if parent. is_none ( ) {
827+ parent = Some ( e) ;
828+ }
829+ match e. kind {
830+ hir:: ExprKind :: Let ( _) => {
831+ match tcx. parent_hir_node ( e. hir_id ) {
832+ hir:: Node :: Expr ( hir:: Expr {
833+ kind : hir:: ExprKind :: If ( cond, ..) , ..
834+ } ) => {
835+ let mut finder = Finder { hir_id : expr. hir_id , found : false } ;
836+ finder. visit_expr ( cond) ;
837+ if finder. found {
838+ // The expression where the move error happened is in a `while let`
839+ // condition Don't suggest clone as it will likely end in an
840+ // infinite loop.
841+ // while let Some(_) = foo(non_copy.clone()) { }
842+ // --------- ^^^^^^^^
843+ can_suggest_clone = false ;
844+ }
845+ }
846+ _ => { }
847+ }
848+ }
849+ hir:: ExprKind :: Loop ( ..) => {
850+ outer_most_loop = Some ( e) ;
851+ }
852+ _ => { }
853+ }
854+ }
855+ let loop_count: usize = tcx
856+ . hir ( )
857+ . parent_iter ( expr. hir_id )
858+ . map ( |( _, node) | match node {
859+ hir:: Node :: Expr ( hir:: Expr { kind : hir:: ExprKind :: Loop ( ..) , .. } ) => 1 ,
860+ _ => 0 ,
861+ } )
862+ . sum ( ) ;
863+
864+ let sm = tcx. sess . source_map ( ) ;
865+ if let Some ( in_loop) = outer_most_loop {
866+ let mut finder = BreakFinder { found_breaks : vec ! [ ] , found_continues : vec ! [ ] } ;
867+ finder. visit_expr ( in_loop) ;
868+ // All of the spans for `break` and `continue` expressions.
869+ let spans = finder
870+ . found_breaks
871+ . iter ( )
872+ . chain ( finder. found_continues . iter ( ) )
873+ . map ( |( _, span) | * span)
874+ . filter ( |span| {
875+ !matches ! (
876+ span. desugaring_kind( ) ,
877+ Some ( DesugaringKind :: ForLoop | DesugaringKind :: WhileLoop )
878+ )
879+ } )
880+ . collect :: < Vec < Span > > ( ) ;
881+ // All of the spans for the loops above the expression with the move error.
882+ let loop_spans: Vec < _ > = tcx
883+ . hir ( )
884+ . parent_iter ( expr. hir_id )
885+ . filter_map ( |( _, node) | match node {
886+ hir:: Node :: Expr ( hir:: Expr { span, kind : hir:: ExprKind :: Loop ( ..) , .. } ) => {
887+ Some ( * span)
888+ }
889+ _ => None ,
890+ } )
891+ . collect ( ) ;
892+ // It is possible that a user written `break` or `continue` is in the wrong place. We
893+ // point them out at the user for them to make a determination. (#92531)
894+ if !spans. is_empty ( ) && loop_count > 1 {
895+ // Getting fancy: if the spans of the loops *do not* overlap, we only use the line
896+ // number when referring to them. If there *are* overlaps (multiple loops on the
897+ // same line) then we use the more verbose span output (`file.rs:col:ll`).
898+ let mut lines: Vec < _ > =
899+ loop_spans. iter ( ) . map ( |sp| sm. lookup_char_pos ( sp. lo ( ) ) . line ) . collect ( ) ;
900+ lines. sort ( ) ;
901+ lines. dedup ( ) ;
902+ let fmt_span = |span : Span | {
903+ if lines. len ( ) == loop_spans. len ( ) {
904+ format ! ( "line {}" , sm. lookup_char_pos( span. lo( ) ) . line)
905+ } else {
906+ sm. span_to_diagnostic_string ( span)
907+ }
908+ } ;
909+ let mut spans: MultiSpan = spans. clone ( ) . into ( ) ;
910+ // Point at all the `continue`s and explicit `break`s in the relevant loops.
911+ for ( desc, elements) in [
912+ ( "`break` exits" , & finder. found_breaks ) ,
913+ ( "`continue` advances" , & finder. found_continues ) ,
914+ ] {
915+ for ( destination, sp) in elements {
916+ if let Ok ( hir_id) = destination. target_id
917+ && let hir:: Node :: Expr ( expr) = tcx. hir ( ) . hir_node ( hir_id)
918+ && !matches ! (
919+ sp. desugaring_kind( ) ,
920+ Some ( DesugaringKind :: ForLoop | DesugaringKind :: WhileLoop )
921+ )
922+ {
923+ spans. push_span_label (
924+ * sp,
925+ format ! ( "this {desc} the loop at {}" , fmt_span( expr. span) ) ,
926+ ) ;
927+ }
928+ }
929+ }
930+ // Point at all the loops that are between this move and the parent item.
931+ for span in loop_spans {
932+ spans. push_span_label ( sm. guess_head_span ( span) , "" ) ;
933+ }
934+
935+ // note: verify that your loop breaking logic is correct
936+ // --> $DIR/nested-loop-moved-value-wrong-continue.rs:41:17
937+ // |
938+ // 28 | for foo in foos {
939+ // | ---------------
940+ // ...
941+ // 33 | for bar in &bars {
942+ // | ----------------
943+ // ...
944+ // 41 | continue;
945+ // | ^^^^^^^^ this `continue` advances the loop at line 33
946+ err. span_note ( spans, "verify that your loop breaking logic is correct" ) ;
947+ }
948+ if let Some ( parent) = parent
949+ && let hir:: ExprKind :: MethodCall ( ..) | hir:: ExprKind :: Call ( ..) = parent. kind
950+ {
951+ // FIXME: We could check that the call's *parent* takes `&mut val` to make the
952+ // suggestion more targeted to the `mk_iter(val).next()` case. Maybe do that only to
953+ // check for wheter to suggest `let value` or `let mut value`.
954+
955+ let span = in_loop. span ;
956+ if !finder. found_breaks . is_empty ( )
957+ && let Ok ( value) = sm. span_to_snippet ( parent. span )
958+ {
959+ // We know with high certainty that this move would affect the early return of a
960+ // loop, so we suggest moving the expression with the move out of the loop.
961+ let indent = if let Some ( indent) = sm. indentation_before ( span) {
962+ format ! ( "\n {indent}" )
963+ } else {
964+ " " . to_string ( )
965+ } ;
966+ err. multipart_suggestion (
967+ "consider moving the expression out of the loop so it is only moved once" ,
968+ vec ! [
969+ ( parent. span, "value" . to_string( ) ) ,
970+ ( span. shrink_to_lo( ) , format!( "let mut value = {value};{indent}" ) ) ,
971+ ] ,
972+ Applicability :: MaybeIncorrect ,
973+ ) ;
974+ }
975+ }
976+ }
977+ can_suggest_clone
978+ }
979+
750980 fn suggest_cloning ( & self , err : & mut Diag < ' _ > , ty : Ty < ' tcx > , expr : & hir:: Expr < ' _ > , span : Span ) {
751981 let tcx = self . infcx . tcx ;
752982 // Try to find predicates on *generic params* that would allow copying `ty`
@@ -3688,6 +3918,28 @@ impl<'a, 'v> Visitor<'v> for ReferencedStatementsVisitor<'a> {
36883918 }
36893919}
36903920
3921+ /// Look for `break` expressions within any arbitrary expressions. We'll do this to infer
3922+ /// whether this is a case where the moved value would affect the exit of a loop, making it
3923+ /// unsuitable for a `.clone()` suggestion.
3924+ struct BreakFinder {
3925+ found_breaks : Vec < ( hir:: Destination , Span ) > ,
3926+ found_continues : Vec < ( hir:: Destination , Span ) > ,
3927+ }
3928+ impl < ' hir > Visitor < ' hir > for BreakFinder {
3929+ fn visit_expr ( & mut self , ex : & ' hir hir:: Expr < ' hir > ) {
3930+ match ex. kind {
3931+ hir:: ExprKind :: Break ( destination, _) => {
3932+ self . found_breaks . push ( ( destination, ex. span ) ) ;
3933+ }
3934+ hir:: ExprKind :: Continue ( destination) => {
3935+ self . found_continues . push ( ( destination, ex. span ) ) ;
3936+ }
3937+ _ => { }
3938+ }
3939+ hir:: intravisit:: walk_expr ( self , ex) ;
3940+ }
3941+ }
3942+
36913943/// Given a set of spans representing statements initializing the relevant binding, visit all the
36923944/// function expressions looking for branching code paths that *do not* initialize the binding.
36933945struct ConditionVisitor < ' b > {
0 commit comments