Skip to content

Commit ca1028b

Browse files
New lint: iter_missing_exact_size
1 parent 038a273 commit ca1028b

7 files changed

Lines changed: 461 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7039,6 +7039,7 @@ Released 2018-09-13
70397039
[`iter_filter_is_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_filter_is_ok
70407040
[`iter_filter_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_filter_is_some
70417041
[`iter_kv_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map
7042+
[`iter_missing_exact_size`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_missing_exact_size
70427043
[`iter_next_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_loop
70437044
[`iter_next_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_next_slice
70447045
[`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
@@ -244,6 +244,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
244244
crate::item_name_repetitions::STRUCT_FIELD_NAMES_INFO,
245245
crate::items_after_statements::ITEMS_AFTER_STATEMENTS_INFO,
246246
crate::items_after_test_module::ITEMS_AFTER_TEST_MODULE_INFO,
247+
crate::iter_missing_exact_size::ITER_MISSING_EXACT_SIZE_INFO,
247248
crate::iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR_INFO,
248249
crate::iter_over_hash_type::ITER_OVER_HASH_TYPE_INFO,
249250
crate::iter_without_into_iter::INTO_ITER_WITHOUT_ITER_INFO,
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::paths::{PathNS, lookup_path_str};
3+
use clippy_utils::sym;
4+
use clippy_utils::ty::{get_field_by_name, implements_trait, ty_from_hir_ty};
5+
use rustc_hir::{Body, Expr, ExprKind, Impl, ImplItemKind, Item, ItemKind, OwnerId, OwnerNode, QPath, StmtKind};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::impl_lint_pass;
8+
use rustc_span::def_id::DefId;
9+
use rustc_span::symbol::kw;
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+
///
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+
pedantic,
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() && assoc_item.is_method() && assoc_item.name() == sym::size_hint
109+
})?;
110+
111+
let node = cx.tcx.expect_hir_owner_node(size_hint_fn.def_id.expect_local());
112+
let OwnerNode::ImplItem(impl_item) = node else {
113+
return None;
114+
};
115+
let ImplItemKind::Fn(_, body_id) = impl_item.kind else {
116+
return None;
117+
};
118+
Some(cx.tcx.hir_body(body_id))
119+
}
120+
121+
/// Given a `Body` for the `size_hint()` function, try to get the return
122+
/// expression, either
123+
/// - a trailing expression when the block has no statements
124+
/// - the singular `return` statement in a block with exactly one statement (the return) (and
125+
/// optionally a dead-code trailing expression that we can ignore)
126+
fn size_hint_return<'tcx>(body: &'tcx Body<'tcx>) -> Option<&'tcx Expr<'tcx>> {
127+
let ExprKind::Block(block, None) = body.value.kind else {
128+
// Function body isn't a block?
129+
return None;
130+
};
131+
// Block without statements - either it has a trailing expression (and we
132+
// return that) or it doesn't (and we return none)
133+
if block.stmts.is_empty() {
134+
return block.expr;
135+
}
136+
if let [only_statement] = block.stmts
137+
&& let StmtKind::Semi(statement_semi) = only_statement.kind
138+
&& let ExprKind::Ret(returned_value) = statement_semi.kind
139+
{
140+
returned_value
141+
} else {
142+
None
143+
}
144+
}
145+
146+
impl<'tcx> LateLintPass<'tcx> for IterMissingExactSize {
147+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
148+
// Check for this item being an implementation of the iterator trait:
149+
// 1) is it implementing a trait?
150+
let ItemKind::Impl(Impl {
151+
of_trait: Some(of_trait),
152+
self_ty: current_type,
153+
..
154+
}) = item.kind
155+
else {
156+
return;
157+
};
158+
// 2) can we find the trait definition id?
159+
let Some(trait_id) = of_trait.trait_ref.trait_def_id() else {
160+
return;
161+
};
162+
// 3) is it the iterator trait?
163+
if !cx.tcx.is_diagnostic_item(sym::Iterator, trait_id) {
164+
return;
165+
}
166+
167+
// We know that this item is an `impl Iterator for _` block; find the
168+
// size_hint() function (if present)
169+
let Some(size_hint_body) = size_hint_body(cx, item.owner_id) else {
170+
return;
171+
};
172+
// We found the function body for size_hint()!
173+
let Some(size_hint_return) = size_hint_return(size_hint_body) else {
174+
return;
175+
};
176+
if let ExprKind::MethodCall(method_name, receiver, args, _) = size_hint_return.kind
177+
&& method_name.ident.name == sym::size_hint
178+
&& let ExprKind::Field(object, field_name) = receiver.kind
179+
&& let ExprKind::Path(QPath::Resolved(_, object_path)) = object.kind
180+
&& let [path_segment] = object_path.segments
181+
&& path_segment.ident.name == kw::SelfLower
182+
&& args.is_empty()
183+
{
184+
// The function body is just `self.{field}.size_hint()`, check
185+
// for the type of the field
186+
let current_middle_ty = ty_from_hir_ty(cx, current_type);
187+
let field = get_field_by_name(cx.tcx, current_middle_ty, field_name.name);
188+
let Some(field) = field else {
189+
return;
190+
};
191+
// Does that type implement ExactSizeIterator ?
192+
let exact_traits = self.get_exact_size_lookup_defs(cx);
193+
let mut found = false;
194+
for exact_trait_def in exact_traits {
195+
if implements_trait(cx, field, *exact_trait_def, &[]) {
196+
found = true;
197+
break;
198+
}
199+
}
200+
if !found {
201+
return;
202+
}
203+
// Delegates size hint to a field that implements ExactSizeIterator
204+
// so this iterator should do so too
205+
for exact_trait_def in exact_traits {
206+
if implements_trait(cx, current_middle_ty, *exact_trait_def, &[]) {
207+
// This iterator type already implements ExactSizeIterator
208+
return;
209+
}
210+
}
211+
span_lint_and_help(
212+
cx,
213+
ITER_MISSING_EXACT_SIZE,
214+
item.span,
215+
"iterator can implement `ExactSizeIterator`",
216+
Some(size_hint_return.span),
217+
"this `size_hint()` implementation delegates to to the `size_hint()` of an `ExactSizeIterator`, so the overall iterator is likely to have an exact size",
218+
);
219+
}
220+
}
221+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ mod int_plus_one;
179179
mod item_name_repetitions;
180180
mod items_after_statements;
181181
mod items_after_test_module;
182+
mod iter_missing_exact_size;
182183
mod iter_not_returning_iterator;
183184
mod iter_over_hash_type;
184185
mod iter_without_into_iter;
@@ -868,6 +869,7 @@ rustc_lint::late_lint_methods!(
868869
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
869870
RestWhenDestructuringStruct: rest_when_destructuring_struct::RestWhenDestructuringStruct = rest_when_destructuring_struct::RestWhenDestructuringStruct,
870871
BlockScrutinee: block_scrutinee::BlockScrutinee = block_scrutinee::BlockScrutinee,
872+
IterMissingExactSize: iter_missing_exact_size::IterMissingExactSize = iter_missing_exact_size::IterMissingExactSize::new(),
871873
// add late passes here, used by `cargo dev new_lint`
872874
]]
873875
);

clippy_utils/src/sym.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ generate! {
545545
set_readonly,
546546
signum,
547547
single_component_path_imports,
548+
size_hint,
548549
skip,
549550
skip_while,
550551
slice_from_ref,

0 commit comments

Comments
 (0)