Skip to content

Commit 6b03e95

Browse files
hir_typeck: suggest removing redundant .iter() calls
This diagnostic intercepts cases where `.iter()` is called on a type that already implements `Iterator` Instead of confusing trait fallback suggestions, it explicitly guides the user to remove the `.iter()` call. It implements precise span computation to support `cargo fix` while carefully preserving all inline comments and horizontal whitespace during the AST string replacement.
1 parent 0c68443 commit 6b03e95

File tree

7 files changed

+590
-2
lines changed

7 files changed

+590
-2
lines changed

compiler/rustc_hir_typeck/src/method/suggest.rs

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ use rustc_middle::ty::print::{
3434
use rustc_middle::ty::{self, GenericArgKind, IsSuggestable, Ty, TyCtxt, TypeVisitableExt};
3535
use rustc_span::def_id::DefIdSet;
3636
use 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
};
4040
use rustc_trait_selection::error_reporting::traits::DefIdOrName;
4141
use rustc_trait_selection::infer::InferCtxtExt;
@@ -97,6 +97,119 @@ impl TraitBoundDuplicateTracker {
9797
}
9898

9999
impl<'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();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@ run-rustfix
2+
#![allow(dead_code, unused_imports)]
3+
// Regression test for short-circuiting redundant iter() diagnostics
4+
// to prevent competing suggestions.
5+
6+
mod out_of_scope {
7+
pub trait MyIterExt {
8+
fn iter(&self) {}
9+
}
10+
impl<T: Iterator> MyIterExt for T {}
11+
}
12+
13+
fn out_of_scope_trait(iter: impl Iterator<Item = i32>) {
14+
// Out-of-scope trait that actually has `.iter()`.
15+
let _ = iter;
16+
//~^ ERROR no method named `iter` found
17+
}
18+
19+
fn main() {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@ run-rustfix
2+
#![allow(dead_code, unused_imports)]
3+
// Regression test for short-circuiting redundant iter() diagnostics
4+
// to prevent competing suggestions.
5+
6+
mod out_of_scope {
7+
pub trait MyIterExt {
8+
fn iter(&self) {}
9+
}
10+
impl<T: Iterator> MyIterExt for T {}
11+
}
12+
13+
fn out_of_scope_trait(iter: impl Iterator<Item = i32>) {
14+
// Out-of-scope trait that actually has `.iter()`.
15+
let _ = iter.iter();
16+
//~^ ERROR no method named `iter` found
17+
}
18+
19+
fn main() {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
error[E0599]: no method named `iter` found for type parameter `impl Iterator<Item = i32>` in the current scope
2+
--> $DIR/iter-on-already-iterator-competing-suggestions.rs:15:18
3+
|
4+
LL | let _ = iter.iter();
5+
| ^^^^
6+
|
7+
= note: `impl Iterator<Item = i32>` already implements `Iterator`
8+
help: consider removing the `.iter()` call
9+
|
10+
LL - let _ = iter.iter();
11+
LL + let _ = iter;
12+
|
13+
14+
error: aborting due to 1 previous error
15+
16+
For more information about this error, try `rustc --explain E0599`.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//@ run-rustfix
2+
//! Exhaustive tests for the targeted diagnostic that suggests removing redundant `.iter()` calls.
3+
//! This verifies that the span surgery handles comments and whitespace correctly
4+
//! during `cargo fix`.
5+
#![allow(unused)]
6+
7+
#[rustfmt::skip]
8+
fn main() {
9+
let receiver = vec![1, 2, 3].into_iter();
10+
11+
// Simple inline call.
12+
let _ = receiver;
13+
//~^ ERROR no method named `iter`
14+
15+
let receiver = vec![1, 2, 3].into_iter();
16+
// Call with extra horizontal whitespace between receiver and dot.
17+
let _ = receiver;
18+
//~^ ERROR no method named `iter`
19+
20+
let receiver = vec![1, 2, 3].into_iter();
21+
// Call with a newline and indentation before the dot.
22+
let _ = receiver;
23+
//~^ ERROR no method named `iter`
24+
25+
let receiver = vec![1, 2, 3].into_iter();
26+
// Call with a block comment between the receiver and the dot.
27+
let _ = receiver /* info */ ;
28+
//~^ ERROR no method named `iter`
29+
30+
let receiver = vec![1, 2, 3].into_iter();
31+
// Call with a line comment on the preceding line.
32+
let _ = receiver // info
33+
;
34+
//~^ ERROR no method named `iter`
35+
36+
let receiver = vec![1, 2, 3].into_iter();
37+
// Downgrade applicability if a comment is present within the deleted span.
38+
// Because it's `MaybeIncorrect`, `rustfix` will only show the suggestion
39+
// but not apply it.
40+
let _ = receiver /* wait, keep this! */;
41+
//~^ ERROR no method named `iter`
42+
43+
let receiver = vec![1, 2, 3].into_iter();
44+
// A comment containing a dot shouldn't trick the span parser.
45+
let _ = receiver;
46+
//~^ ERROR no method named `iter`
47+
48+
let receiver = vec![1, 2, 3].into_iter();
49+
// A comment containing a dot shouldn't trick the span parser (dot afterwards).
50+
let _ = receiver /* . */ ;
51+
//~^ ERROR no method named `iter`
52+
53+
let receiver = vec![1, 2, 3].into_iter();
54+
// Chained method call after `.iter()`: only `.iter()` is removed,
55+
let _ = receiver.map(|x| x);
56+
//~^ ERROR no method named `iter`
57+
58+
// Multi-segment path receiver.
59+
let _ = std::iter::empty::<i32>();
60+
//~^ ERROR no method named `iter`
61+
}
62+
63+
// Concrete user-defined struct implementing Iterator directly.
64+
struct MyIter;
65+
66+
impl Iterator for MyIter {
67+
type Item = i32;
68+
fn next(&mut self) -> Option<Self::Item> {
69+
None
70+
}
71+
}
72+
73+
fn concrete_iterator() {
74+
let it = MyIter;
75+
let _ = it;
76+
//~^ ERROR no method named `iter`
77+
}
78+
79+
// `Box<dyn Iterator>` -- verify the trait check fires through trait objects.
80+
fn dyn_iterator(it: Box<dyn Iterator<Item = i32>>) {
81+
let _ = it;
82+
//~^ ERROR no method named `iter`
83+
}
84+
85+
// Generic context with an explicit bound.
86+
fn generic_iterator<I: Iterator<Item = i32>>(it: I) {
87+
let _ = it;
88+
//~^ ERROR no method named `iter`
89+
}

0 commit comments

Comments
 (0)