|
| 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 | +} |
0 commit comments