@@ -34,8 +34,8 @@ use rustc_middle::ty::print::{
3434use rustc_middle:: ty:: { self , GenericArgKind , IsSuggestable , Ty , TyCtxt , TypeVisitableExt } ;
3535use rustc_span:: def_id:: DefIdSet ;
3636use rustc_span:: {
37- DUMMY_SP , ErrorGuaranteed , ExpnKind , FileName , Ident , MacroKind , Span , Symbol , edit_distance ,
38- kw, sym,
37+ DUMMY_SP , ErrorGuaranteed , ExpnKind , FileName , Ident , MacroKind , Pos , Span , Symbol ,
38+ edit_distance , kw, sym,
3939} ;
4040use rustc_trait_selection:: error_reporting:: traits:: DefIdOrName ;
4141use rustc_trait_selection:: infer:: InferCtxtExt ;
@@ -97,6 +97,119 @@ impl TraitBoundDuplicateTracker {
9797}
9898
9999impl < ' a , ' tcx > FnCtxt < ' a , ' tcx > {
100+ /// Returns the byte offset of the last `.` in `s` that lies outside any
101+ /// comment: line comments (`//`) or block comments (`/* ... */`, including nested).
102+ fn find_last_real_dot ( s : & str ) -> Option < usize > {
103+ enum State {
104+ Code ,
105+ LineComment ,
106+ BlockComment { depth : u32 } ,
107+ }
108+
109+ let mut state = State :: Code ;
110+ let mut last_dot = None ;
111+ let mut chars = s. char_indices ( ) . peekable ( ) ;
112+
113+ while let Some ( ( i, c) ) = chars. next ( ) {
114+ let peeked = chars. peek ( ) . map ( |& ( _, c) | c) ;
115+
116+ state = match state {
117+ State :: Code => match ( c, peeked) {
118+ ( '/' , Some ( '/' ) ) => {
119+ chars. next ( ) ;
120+ State :: LineComment
121+ }
122+ ( '/' , Some ( '*' ) ) => {
123+ chars. next ( ) ;
124+ State :: BlockComment { depth : 1 }
125+ }
126+ ( '.' , _) => {
127+ last_dot = Some ( i) ;
128+ State :: Code
129+ }
130+ _ => State :: Code ,
131+ } ,
132+ State :: LineComment => match c {
133+ '\n' => State :: Code ,
134+ _ => State :: LineComment ,
135+ } ,
136+ State :: BlockComment { depth } => match ( c, peeked) {
137+ ( '/' , Some ( '*' ) ) => {
138+ chars. next ( ) ;
139+ State :: BlockComment { depth : depth + 1 }
140+ }
141+ ( '*' , Some ( '/' ) ) => {
142+ chars. next ( ) ;
143+ if depth == 1 {
144+ State :: Code
145+ } else {
146+ State :: BlockComment { depth : depth - 1 }
147+ }
148+ }
149+ _ => State :: BlockComment { depth } ,
150+ } ,
151+ } ;
152+ }
153+
154+ last_dot
155+ }
156+
157+ /// Computes the exact span to delete and its applicability for removing a
158+ /// redundant `.iter()` call.
159+ /// Returns `None` if the suggestion cannot be safely derived from the source map.
160+ fn compute_iter_removal_suggestion (
161+ & self ,
162+ call_span : Span ,
163+ span : Span ,
164+ ) -> Option < ( Span , Applicability ) > {
165+ if span. lo ( ) < call_span. lo ( ) || span. hi ( ) > call_span. hi ( ) {
166+ return None ;
167+ }
168+
169+ let snippet = self . tcx . sess . source_map ( ) . span_to_snippet ( call_span) . ok ( ) ?;
170+ let method_offset = ( span. lo ( ) - call_span. lo ( ) ) . to_usize ( ) ;
171+ let dot_offset = Self :: find_last_real_dot ( & snippet[ ..method_offset] ) ?;
172+ let text_before_dot = & snippet[ ..dot_offset] ;
173+
174+ let contains_comment = |s : & str | s. contains ( "//" ) || s. contains ( "/*" ) || s. contains ( "*/" ) ;
175+
176+ // Inspect the text immediately preceding the dot, ignoring horizontal whitespace.
177+ let stripped_horizontal_ws = text_before_dot. trim_end_matches ( [ ' ' , '\t' ] ) ;
178+ let is_on_new_line = stripped_horizontal_ws. ends_with ( [ '\n' , '\r' ] ) ;
179+ let has_preceding_comment = contains_comment ( stripped_horizontal_ws) ;
180+
181+ // Determine where to start the replacement span based on surrounding formatting.
182+ let replacement_offset = match ( is_on_new_line, has_preceding_comment) {
183+ // The dot is on its own line with no preceding comment: collapse the whole indentation.
184+ ( true , false ) => stripped_horizontal_ws. trim_end ( ) . len ( ) ,
185+ // A comment sits between the expression and the dot on the same line:
186+ // preserve the comment by starting the replacement exactly at the dot.
187+ ( false , true ) => dot_offset,
188+ // `(false, false)`: standard inline call (e.g. `foo.iter()`).
189+ // `(true, true)`: comment on the previous line, followed by a newline and indented dot.
190+ // In both cases, only strip the trailing horizontal whitespace.
191+ _ => stripped_horizontal_ws. len ( ) ,
192+ } ;
193+
194+ let replacement_span = call_span. with_lo (
195+ call_span. lo ( )
196+ + rustc_span:: BytePos (
197+ u32:: try_from ( replacement_offset)
198+ . expect ( "replacement offset exceeded maximum span length" ) ,
199+ ) ,
200+ ) ;
201+
202+ // A comment in the section being removed risks deleting user documentation,
203+ // so we downgrade applicability to signal that human review is warranted.
204+ let applicability = if contains_comment ( & snippet[ replacement_offset..] ) {
205+ Applicability :: MaybeIncorrect
206+ } else {
207+ Applicability :: MachineApplicable
208+ } ;
209+
210+ Some ( ( replacement_span, applicability) )
211+ }
212+
100213 fn is_slice_ty ( & self , ty : Ty < ' tcx > , span : Span ) -> bool {
101214 self . autoderef ( span, ty)
102215 . silence_errors ( )
@@ -758,6 +871,50 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
758871 }
759872 }
760873
874+ /// Suggests removing a redundant `.iter()` call if the receiver already
875+ /// implements the `Iterator` trait.
876+ /// Returns `true` if a suggestion or note was emitted, signaling that
877+ /// further diagnostic fallback should be suppressed.
878+ fn suggest_removing_iter_call (
879+ & self ,
880+ err : & mut Diag < ' _ > ,
881+ rcvr_expr : & hir:: Expr < ' _ > ,
882+ rcvr_ty : Ty < ' tcx > ,
883+ span : Span ,
884+ sugg_span : Span ,
885+ ) -> bool {
886+ // Ensure the receiver and the suggestion are in the same macro context.
887+ if !rcvr_expr. span . eq_ctxt ( sugg_span) {
888+ return false ;
889+ }
890+
891+ // Guard against constructing an invalid span across macro edges.
892+ if rcvr_expr. span . hi ( ) > sugg_span. hi ( ) {
893+ return false ;
894+ }
895+
896+ // The span encompassing the entire `.iter()` call.
897+ let call_span = span. with_lo ( rcvr_expr. span . hi ( ) ) . with_hi ( sugg_span. hi ( ) ) ;
898+ if call_span. from_expansion ( ) {
899+ return false ;
900+ }
901+
902+ let ty_str = self . tcx . short_string ( rcvr_ty, err. long_ty_path ( ) ) ;
903+ err. note ( format ! ( "`{ty_str}` already implements `Iterator`" ) ) ;
904+ match self . compute_iter_removal_suggestion ( call_span, span) {
905+ Some ( ( replacement_span, applicability) ) => {
906+ err. span_suggestion_verbose (
907+ replacement_span,
908+ format ! ( "consider removing the `.iter()` call" ) ,
909+ "" ,
910+ applicability,
911+ ) ;
912+ true
913+ }
914+ None => false ,
915+ }
916+ }
917+
761918 fn suggest_static_method_candidates (
762919 & self ,
763920 err : & mut Diag < ' _ > ,
@@ -828,6 +985,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
828985 source : SelfSource < ' tcx > ,
829986 unsatisfied_predicates : & UnsatisfiedPredicates < ' tcx > ,
830987 static_candidates : & [ CandidateSource ] ,
988+ sugg_span : Span ,
831989 ) -> Result < ( bool , bool , bool , bool , SortedMap < Span , Vec < String > > ) , ( ) > {
832990 let mut restrict_type_params = false ;
833991 let mut suggested_derive = false ;
@@ -836,6 +994,21 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
836994 let mut bound_spans: SortedMap < Span , Vec < String > > = Default :: default ( ) ;
837995 let tcx = self . tcx ;
838996
997+ if item_ident. name == sym:: iter
998+ && let SelfSource :: MethodCall ( rcvr_expr) = source
999+ && let Some ( iterator_trait) = self . tcx . get_diagnostic_item ( sym:: Iterator )
1000+ && self . predicate_must_hold_modulo_regions ( & Obligation :: new (
1001+ self . tcx ,
1002+ self . misc ( span) ,
1003+ self . param_env ,
1004+ ty:: TraitRef :: new ( self . tcx , iterator_trait, [ rcvr_ty] ) ,
1005+ ) )
1006+ {
1007+ if self . suggest_removing_iter_call ( err, rcvr_expr, rcvr_ty, span, sugg_span) {
1008+ return Err ( ( ) ) ;
1009+ }
1010+ }
1011+
8391012 if item_ident. name == sym:: count && self . is_slice_ty ( rcvr_ty, span) {
8401013 let msg = "consider using `len` instead" ;
8411014 if let SelfSource :: MethodCall ( _expr) = source {
@@ -1268,6 +1441,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
12681441 source,
12691442 unsatisfied_predicates,
12701443 & static_candidates,
1444+ sugg_span,
12711445 )
12721446 else {
12731447 return err. emit ( ) ;
0 commit comments