Skip to content

Commit e4c0d8c

Browse files
New lint: iter_missing_exact_size
Fixes #17393
1 parent 9006d6c commit e4c0d8c

6 files changed

Lines changed: 342 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6929,6 +6929,7 @@ Released 2018-09-13
69296929
[`iter_filter_is_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_filter_is_ok
69306930
[`iter_filter_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_filter_is_some
69316931
[`iter_kv_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map
6932+
[`iter_missing_exact_size`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_missing_exact_size
69326933
[`iter_next_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_loop
69336934
[`iter_next_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_slice
69346935
[`iter_not_returning_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_not_returning_iterator

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
242242
crate::item_name_repetitions::STRUCT_FIELD_NAMES_INFO,
243243
crate::items_after_statements::ITEMS_AFTER_STATEMENTS_INFO,
244244
crate::items_after_test_module::ITEMS_AFTER_TEST_MODULE_INFO,
245+
crate::iter_missing_exact_size::ITER_MISSING_EXACT_SIZE_INFO,
245246
crate::iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR_INFO,
246247
crate::iter_over_hash_type::ITER_OVER_HASH_TYPE_INFO,
247248
crate::iter_without_into_iter::INTO_ITER_WITHOUT_ITER_INFO,
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::paths::{PathNS, lookup_path_str};
3+
use clippy_utils::ty::{get_field_by_name, implements_trait, ty_from_hir_ty};
4+
use rustc_errors::MultiSpan;
5+
use rustc_hir::{Body, ExprKind, Impl, ImplItemKind, Item, ItemKind, OwnerId, OwnerNode};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::impl_lint_pass;
8+
use rustc_span::def_id::DefId;
9+
use rustc_span::sym;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
///
14+
/// Suggest that iterators be marked as `ExactSizeIterator` when they wrap
15+
/// around another iterator that *does* implement `ExactSizeIterator`.
16+
///
17+
/// ### Why is this bad?
18+
///
19+
/// When the size of an iterator is based on some other iterator that is
20+
/// known to have an exact size, the wrapping iterator also has an exact
21+
/// size and should be marked as such.
22+
///
23+
/// ### Example
24+
/// ```no_run
25+
/// struct StringRepeater {
26+
/// original: String,
27+
/// range: std::ops::Range<usize>,
28+
/// }
29+
///
30+
/// impl Iterator for StringRepeater {
31+
/// type Item = String;
32+
/// fn next(&mut self) -> Option<Self::Item> {
33+
/// self.range.next().map(|i| self.original.repeat(i) )
34+
/// }
35+
/// fn size_hint(&self) -> (usize, Option<usize>) {
36+
/// self.range.size_hint()
37+
/// }
38+
/// }
39+
///
40+
/// let repeater = StringRepeater { original: "Foo".to_string(), range: 1..5 };
41+
/// for value in repeater {
42+
/// println!("{value}");
43+
/// }
44+
///
45+
/// ```
46+
/// Use instead:
47+
/// ```no_run
48+
/// ```no_run
49+
/// struct StringRepeater {
50+
/// original: String,
51+
/// range: std::ops::Range<usize>,
52+
/// }
53+
///
54+
/// impl Iterator for StringRepeater {
55+
/// type Item = String;
56+
/// fn next(&mut self) -> Option<Self::Item> {
57+
/// self.range.next().map(|i| self.original.repeat(i) )
58+
/// }
59+
/// fn size_hint(&self) -> (usize, Option<usize>) {
60+
/// self.range.size_hint()
61+
/// }
62+
/// }
63+
///
64+
/// impl ExactSizeIterator for StringRepeater {}
65+
///
66+
/// let repeater = StringRepeater { original: "Foo".to_string(), range: 1..5 };
67+
/// for value in repeater {
68+
/// println!("{value}");
69+
/// }
70+
/// ```
71+
#[clippy::version = "1.99.0"]
72+
pub ITER_MISSING_EXACT_SIZE,
73+
nursery,
74+
"iterator delegates to an ExactSizeIterator for its size hint but does not itself implement ExactSizeIterator"
75+
}
76+
77+
impl_lint_pass!(IterMissingExactSize => [ITER_MISSING_EXACT_SIZE]);
78+
79+
pub struct IterMissingExactSize {
80+
exact_size_lookup: Option<Vec<DefId>>,
81+
}
82+
impl IterMissingExactSize {
83+
pub fn new() -> Self {
84+
IterMissingExactSize {
85+
exact_size_lookup: None,
86+
}
87+
}
88+
89+
fn get_exact_size_lookup_defs(&mut self, cx: &LateContext<'_>) -> &[DefId] {
90+
// ExactSizeIterator doesn't have a diagnostic item, or even a symobl
91+
// that we can use in paths.rs to add a static type path. Lookup up the
92+
// paths for the DefId the first time needed
93+
if self.exact_size_lookup.is_none() {
94+
self.exact_size_lookup = Some(lookup_path_str(cx.tcx, PathNS::Type, "core::iter::ExactSizeIterator"));
95+
}
96+
self.exact_size_lookup.as_ref().unwrap()
97+
}
98+
}
99+
100+
/// Given an `OwnerId` for an item that is `impl Iterator for {type}`, try to
101+
/// locate the `size_hint()` function being defined in that block.
102+
fn size_hint_body<'tcx>(cx: &LateContext<'tcx>, owner_id: OwnerId) -> Option<&'tcx Body<'tcx>> {
103+
let size_hint_fn = cx
104+
.tcx
105+
.associated_items(owner_id)
106+
.in_definition_order()
107+
.find(|assoc_item| {
108+
assoc_item.expect_trait_impl().is_ok()
109+
&& assoc_item.is_method()
110+
&& assoc_item.name().as_str() == "size_hint"
111+
})?;
112+
113+
let node = cx.tcx.expect_hir_owner_node(size_hint_fn.def_id.expect_local());
114+
let OwnerNode::ImplItem(impl_item) = node else {
115+
return None;
116+
};
117+
let ImplItemKind::Fn(_, body_id) = impl_item.kind else {
118+
return None;
119+
};
120+
Some(cx.tcx.hir_body(body_id))
121+
}
122+
123+
impl<'tcx> LateLintPass<'tcx> for IterMissingExactSize {
124+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
125+
// Check for this item being an implementation of the iterator trait:
126+
// 1) is it implementing a trait?
127+
let ItemKind::Impl(Impl {
128+
of_trait: Some(of_trait),
129+
self_ty: current_type,
130+
..
131+
}) = item.kind
132+
else {
133+
return;
134+
};
135+
// 2) can we find the trait definition id?
136+
let Some(trait_id) = of_trait.trait_ref.trait_def_id() else {
137+
return;
138+
};
139+
// 3) is it the iterator trait?
140+
if !cx.tcx.is_diagnostic_item(sym::Iterator, trait_id) {
141+
return;
142+
}
143+
144+
// We know that this item is an `impl Iterator for _` block; find the
145+
// size_hint() function (if present)
146+
let Some(size_hint_body) = size_hint_body(cx, item.owner_id) else {
147+
return;
148+
};
149+
// We found the function body for size_hint()!
150+
if let ExprKind::Block(block, None) = size_hint_body.value.kind
151+
&& block.stmts.is_empty()
152+
&& let Some(trailing_expr) = block.expr
153+
&& let ExprKind::MethodCall(method_name, receiver, args, _) = trailing_expr.kind
154+
&& method_name.ident.as_str() == "size_hint"
155+
&& let ExprKind::Field(_object, field_name) = receiver.kind
156+
&& args.is_empty()
157+
{
158+
// The function body is just `self.{field}.size_hint()`, check
159+
// for the type of the field
160+
let current_middle_ty = ty_from_hir_ty(cx, current_type);
161+
let field = get_field_by_name(cx.tcx, current_middle_ty, field_name.name);
162+
let Some(field) = field else {
163+
return;
164+
};
165+
// Does that type implement ExactSizeIterator ?
166+
let exact_traits = self.get_exact_size_lookup_defs(cx);
167+
let mut found = false;
168+
for exact_trait_def in exact_traits {
169+
if implements_trait(cx, field, *exact_trait_def, &[]) {
170+
found = true;
171+
break;
172+
}
173+
}
174+
if !found {
175+
return;
176+
}
177+
// Delegates size hint to a field that implements ExactSizeIterator
178+
// so this iterator should do so too
179+
for exact_trait_def in exact_traits {
180+
if implements_trait(cx, current_middle_ty, *exact_trait_def, &[]) {
181+
// This iterator type already implements ExactSizeIterator
182+
return;
183+
}
184+
}
185+
let mut lint_span = MultiSpan::from_span(item.span);
186+
lint_span.push_span_label(
187+
trailing_expr.span,
188+
"This size_hint() implementation delegates to the size_hint() of an ExactSizeIterator",
189+
);
190+
span_lint_and_help(
191+
cx,
192+
ITER_MISSING_EXACT_SIZE,
193+
lint_span,
194+
"iterator can implement ExactSizeIterator",
195+
None,
196+
"since size_hint() delegates to an ExactSizeIterator, the overall iterator is likely to have an exact size",
197+
);
198+
}
199+
}
200+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ mod int_plus_one;
177177
mod item_name_repetitions;
178178
mod items_after_statements;
179179
mod items_after_test_module;
180+
mod iter_missing_exact_size;
180181
mod iter_not_returning_iterator;
181182
mod iter_over_hash_type;
182183
mod iter_without_into_iter;
@@ -859,6 +860,7 @@ rustc_lint::late_lint_methods!(
859860
WithCapacityZero: with_capacity_zero::WithCapacityZero = with_capacity_zero::WithCapacityZero,
860861
RefPatterns: ref_patterns::RefPatterns = ref_patterns::RefPatterns,
861862
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
863+
IterMissingExactSize: iter_missing_exact_size::IterMissingExactSize = iter_missing_exact_size::IterMissingExactSize::new(),
862864
// add late passes here, used by `cargo dev new_lint`
863865
]]
864866
);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#![warn(clippy::iter_missing_exact_size)]
2+
3+
use std::ops::Range;
4+
5+
// Struct field
6+
struct StringRepeater1 {
7+
original: String,
8+
range: Range<usize>,
9+
}
10+
11+
impl Iterator for StringRepeater1 {
12+
//~^ iter_missing_exact_size
13+
type Item = String;
14+
fn next(&mut self) -> Option<Self::Item> {
15+
self.range.next().map(|i| self.original.repeat(i))
16+
}
17+
fn size_hint(&self) -> (usize, Option<usize>) {
18+
self.range.size_hint()
19+
}
20+
}
21+
22+
// Tuple-like struct
23+
struct StringRepeater2(String, Range<usize>);
24+
25+
impl Iterator for StringRepeater2 {
26+
//~^ iter_missing_exact_size
27+
type Item = String;
28+
fn next(&mut self) -> Option<Self::Item> {
29+
self.1.next().map(|i| self.0.repeat(i))
30+
}
31+
fn size_hint(&self) -> (usize, Option<usize>) {
32+
self.1.size_hint()
33+
}
34+
}
35+
36+
// Already marked as an ExactSizeIterator
37+
struct StringRepeater3(String, Range<usize>);
38+
39+
impl Iterator for StringRepeater3 {
40+
type Item = String;
41+
fn next(&mut self) -> Option<Self::Item> {
42+
self.1.next().map(|i| self.0.repeat(i))
43+
}
44+
fn size_hint(&self) -> (usize, Option<usize>) {
45+
self.1.size_hint()
46+
}
47+
}
48+
impl ExactSizeIterator for StringRepeater3 {}
49+
50+
// Delegates but to a non-ExactSizeIterator iterator
51+
struct MyCollection {
52+
elements: Vec<u8>,
53+
}
54+
impl MyCollection {
55+
fn size_hint(&self) -> (usize, Option<usize>) {
56+
(self.elements.len(), Some(self.elements.len()))
57+
}
58+
}
59+
60+
struct MyCollectionIter {
61+
inner: MyCollection,
62+
}
63+
64+
impl Iterator for MyCollectionIter {
65+
type Item = u8;
66+
67+
fn next(&mut self) -> Option<Self::Item> {
68+
self.inner.elements.pop()
69+
}
70+
fn size_hint(&self) -> (usize, Option<usize>) {
71+
self.inner.size_hint()
72+
}
73+
}
74+
75+
fn main() {
76+
let repeater = StringRepeater1 {
77+
original: "Foo".to_string(),
78+
range: 1..5,
79+
};
80+
for value in repeater {
81+
println!("{value}");
82+
}
83+
84+
let repeater = StringRepeater2("Bar".to_string(), 1..5);
85+
for value in repeater {
86+
println!("{value}");
87+
}
88+
89+
let repeater = StringRepeater3("Bar".to_string(), 1..5);
90+
for value in repeater {
91+
println!("{value}");
92+
}
93+
94+
let collection = MyCollectionIter {
95+
inner: MyCollection {
96+
elements: vec![3, 2, 1],
97+
},
98+
};
99+
for value in collection {
100+
println!("{value}");
101+
}
102+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
error: iterator can implement ExactSizeIterator
2+
--> tests/ui/iter_missing_exact_size.rs:11:1
3+
|
4+
LL | / impl Iterator for StringRepeater1 {
5+
LL | |
6+
LL | | type Item = String;
7+
LL | | fn next(&mut self) -> Option<Self::Item> {
8+
... |
9+
LL | | self.range.size_hint()
10+
| | ---------------------- This size_hint() implementation delegates to the size_hint() of an ExactSizeIterator
11+
LL | | }
12+
LL | | }
13+
| |_^
14+
|
15+
= help: since size_hint() delegates to an ExactSizeIterator, the overall iterator is likely to have an exact size
16+
= note: `-D clippy::iter-missing-exact-size` implied by `-D warnings`
17+
= help: to override `-D warnings` add `#[allow(clippy::iter_missing_exact_size)]`
18+
19+
error: iterator can implement ExactSizeIterator
20+
--> tests/ui/iter_missing_exact_size.rs:25:1
21+
|
22+
LL | / impl Iterator for StringRepeater2 {
23+
LL | |
24+
LL | | type Item = String;
25+
LL | | fn next(&mut self) -> Option<Self::Item> {
26+
... |
27+
LL | | self.1.size_hint()
28+
| | ------------------ This size_hint() implementation delegates to the size_hint() of an ExactSizeIterator
29+
LL | | }
30+
LL | | }
31+
| |_^
32+
|
33+
= help: since size_hint() delegates to an ExactSizeIterator, the overall iterator is likely to have an exact size
34+
35+
error: aborting due to 2 previous errors
36+

0 commit comments

Comments
 (0)