@@ -9,7 +9,7 @@ use rustc_data_structures::fx::FxIndexSet;
9
9
use rustc_errors:: { codes:: * , struct_span_code_err, Applicability , Diag , MultiSpan } ;
10
10
use rustc_hir as hir;
11
11
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 } ;
13
13
use rustc_hir:: { CoroutineDesugaring , PatField } ;
14
14
use rustc_hir:: { CoroutineKind , CoroutineSource , LangItem } ;
15
15
use rustc_middle:: hir:: nested_filter:: OnlyBodies ;
@@ -447,8 +447,8 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
447
447
err. span_note (
448
448
span,
449
449
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",
452
452
) ,
453
453
) ;
454
454
}
@@ -463,7 +463,9 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
463
463
} else if let UseSpans :: FnSelfUse { kind : CallKind :: Normal { .. } , .. } = move_spans
464
464
{
465
465
// 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
467
469
self . suggest_cloning ( err, ty, expr, move_span) ;
468
470
}
469
471
}
@@ -747,6 +749,234 @@ impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
747
749
true
748
750
}
749
751
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
+
750
980
fn suggest_cloning ( & self , err : & mut Diag < ' _ > , ty : Ty < ' tcx > , expr : & hir:: Expr < ' _ > , span : Span ) {
751
981
let tcx = self . infcx . tcx ;
752
982
// Try to find predicates on *generic params* that would allow copying `ty`
@@ -3688,6 +3918,28 @@ impl<'a, 'v> Visitor<'v> for ReferencedStatementsVisitor<'a> {
3688
3918
}
3689
3919
}
3690
3920
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
+
3691
3943
/// Given a set of spans representing statements initializing the relevant binding, visit all the
3692
3944
/// function expressions looking for branching code paths that *do not* initialize the binding.
3693
3945
struct ConditionVisitor < ' b > {
0 commit comments