]>
Commit | Line | Data |
---|---|---|
f20569fa | 1 | use super::NEEDLESS_COLLECT; |
17df50a5 | 2 | use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; |
f2b60f7d | 3 | use clippy_utils::higher; |
17df50a5 | 4 | use clippy_utils::source::{snippet, snippet_with_applicability}; |
cdc7bbd5 | 5 | use clippy_utils::sugg::Sugg; |
487cf647 FG |
6 | use clippy_utils::ty::{is_type_diagnostic_item, make_normalized_projection, make_projection}; |
7 | use clippy_utils::{ | |
8 | can_move_expr_to_closure, get_enclosing_block, get_parent_node, is_trait_method, path_to_local, path_to_local_id, | |
9 | CaptureKind, | |
10 | }; | |
a2a8927a | 11 | use rustc_data_structures::fx::FxHashMap; |
04454e1e | 12 | use rustc_errors::{Applicability, MultiSpan}; |
5099ac24 | 13 | use rustc_hir::intravisit::{walk_block, walk_expr, Visitor}; |
487cf647 FG |
14 | use rustc_hir::{ |
15 | BindingAnnotation, Block, Expr, ExprKind, HirId, HirIdSet, Local, Mutability, Node, PatKind, Stmt, StmtKind, | |
16 | }; | |
f20569fa | 17 | use rustc_lint::LateContext; |
5099ac24 | 18 | use rustc_middle::hir::nested_filter; |
487cf647 FG |
19 | use rustc_middle::ty::{self, AssocKind, EarlyBinder, GenericArg, GenericArgKind, Ty}; |
20 | use rustc_span::symbol::Ident; | |
21 | use rustc_span::{sym, Span, Symbol}; | |
f20569fa XL |
22 | |
23 | const NEEDLESS_COLLECT_MSG: &str = "avoid using `collect()` when not needed"; | |
24 | ||
487cf647 FG |
25 | pub(super) fn check<'tcx>( |
26 | cx: &LateContext<'tcx>, | |
27 | name_span: Span, | |
28 | collect_expr: &'tcx Expr<'_>, | |
29 | iter_expr: &'tcx Expr<'tcx>, | |
30 | call_span: Span, | |
31 | ) { | |
32 | if let Some(parent) = get_parent_node(cx.tcx, collect_expr.hir_id) { | |
33 | match parent { | |
34 | Node::Expr(parent) => { | |
35 | if let ExprKind::MethodCall(name, _, args @ ([] | [_]), _) = parent.kind { | |
36 | let mut app = Applicability::MachineApplicable; | |
37 | let name = name.ident.as_str(); | |
38 | let collect_ty = cx.typeck_results().expr_ty(collect_expr); | |
39 | ||
40 | let sugg: String = match name { | |
41 | "len" => { | |
42 | if let Some(adt) = collect_ty.ty_adt_def() | |
43 | && matches!( | |
44 | cx.tcx.get_diagnostic_name(adt.did()), | |
45 | Some(sym::Vec | sym::VecDeque | sym::LinkedList | sym::BinaryHeap) | |
46 | ) | |
47 | { | |
48 | "count()".into() | |
49 | } else { | |
50 | return; | |
51 | } | |
52 | }, | |
53 | "is_empty" | |
54 | if is_is_empty_sig(cx, parent.hir_id) | |
55 | && iterates_same_ty(cx, cx.typeck_results().expr_ty(iter_expr), collect_ty) => | |
56 | { | |
57 | "next().is_none()".into() | |
58 | }, | |
59 | "contains" => { | |
60 | if is_contains_sig(cx, parent.hir_id, iter_expr) | |
61 | && let Some(arg) = args.first() | |
62 | { | |
63 | let (span, prefix) = if let ExprKind::AddrOf(_, _, arg) = arg.kind { | |
64 | (arg.span, "") | |
65 | } else { | |
66 | (arg.span, "*") | |
67 | }; | |
68 | let snip = snippet_with_applicability(cx, span, "??", &mut app); | |
69 | format!("any(|x| x == {prefix}{snip})") | |
70 | } else { | |
71 | return; | |
72 | } | |
73 | }, | |
74 | _ => return, | |
75 | }; | |
f20569fa | 76 | |
487cf647 FG |
77 | span_lint_and_sugg( |
78 | cx, | |
79 | NEEDLESS_COLLECT, | |
80 | call_span.with_hi(parent.span.hi()), | |
81 | NEEDLESS_COLLECT_MSG, | |
82 | "replace with", | |
83 | sugg, | |
84 | app, | |
85 | ); | |
86 | } | |
87 | }, | |
88 | Node::Local(l) => { | |
89 | if let PatKind::Binding(BindingAnnotation::NONE | BindingAnnotation::MUT, id, _, None) | |
90 | = l.pat.kind | |
91 | && let ty = cx.typeck_results().expr_ty(collect_expr) | |
92 | && [sym::Vec, sym::VecDeque, sym::BinaryHeap, sym::LinkedList].into_iter() | |
93 | .any(|item| is_type_diagnostic_item(cx, ty, item)) | |
94 | && let iter_ty = cx.typeck_results().expr_ty(iter_expr) | |
95 | && let Some(block) = get_enclosing_block(cx, l.hir_id) | |
96 | && let Some(iter_calls) = detect_iter_and_into_iters(block, id, cx, get_captured_ids(cx, iter_ty)) | |
97 | && let [iter_call] = &*iter_calls | |
98 | { | |
f20569fa XL |
99 | let mut used_count_visitor = UsedCountVisitor { |
100 | cx, | |
136023e0 | 101 | id, |
f20569fa XL |
102 | count: 0, |
103 | }; | |
104 | walk_block(&mut used_count_visitor, block); | |
105 | if used_count_visitor.count > 1 { | |
106 | return; | |
107 | } | |
108 | ||
109 | // Suggest replacing iter_call with iter_replacement, and removing stmt | |
487cf647 | 110 | let mut span = MultiSpan::from_span(name_span); |
04454e1e | 111 | span.push_span_label(iter_call.span, "the iterator could be used here instead"); |
17df50a5 | 112 | span_lint_hir_and_then( |
f20569fa XL |
113 | cx, |
114 | super::NEEDLESS_COLLECT, | |
487cf647 | 115 | collect_expr.hir_id, |
cdc7bbd5 | 116 | span, |
f20569fa XL |
117 | NEEDLESS_COLLECT_MSG, |
118 | |diag| { | |
487cf647 | 119 | let iter_replacement = format!("{}{}", Sugg::hir(cx, iter_expr, ".."), iter_call.get_iter_method(cx)); |
f20569fa XL |
120 | diag.multipart_suggestion( |
121 | iter_call.get_suggestion_text(), | |
122 | vec![ | |
487cf647 | 123 | (l.span, String::new()), |
f20569fa XL |
124 | (iter_call.span, iter_replacement) |
125 | ], | |
c295e0f8 | 126 | Applicability::MaybeIncorrect, |
cdc7bbd5 | 127 | ); |
f20569fa XL |
128 | }, |
129 | ); | |
130 | } | |
487cf647 FG |
131 | }, |
132 | _ => (), | |
f20569fa XL |
133 | } |
134 | } | |
135 | } | |
136 | ||
487cf647 FG |
137 | /// Checks if the given method call matches the expected signature of `([&[mut]] self) -> bool` |
138 | fn is_is_empty_sig(cx: &LateContext<'_>, call_id: HirId) -> bool { | |
139 | cx.typeck_results().type_dependent_def_id(call_id).map_or(false, |id| { | |
140 | let sig = cx.tcx.fn_sig(id).skip_binder(); | |
141 | sig.inputs().len() == 1 && sig.output().is_bool() | |
142 | }) | |
143 | } | |
144 | ||
145 | /// Checks if `<iter_ty as Iterator>::Item` is the same as `<collect_ty as IntoIter>::Item` | |
146 | fn iterates_same_ty<'tcx>(cx: &LateContext<'tcx>, iter_ty: Ty<'tcx>, collect_ty: Ty<'tcx>) -> bool { | |
147 | let item = Symbol::intern("Item"); | |
148 | if let Some(iter_trait) = cx.tcx.get_diagnostic_item(sym::Iterator) | |
149 | && let Some(into_iter_trait) = cx.tcx.get_diagnostic_item(sym::IntoIterator) | |
150 | && let Some(iter_item_ty) = make_normalized_projection(cx.tcx, cx.param_env, iter_trait, item, [iter_ty]) | |
151 | && let Some(into_iter_item_proj) = make_projection(cx.tcx, into_iter_trait, item, [collect_ty]) | |
152 | && let Ok(into_iter_item_ty) = cx.tcx.try_normalize_erasing_regions( | |
153 | cx.param_env, | |
f25598a0 | 154 | cx.tcx.mk_projection(into_iter_item_proj.def_id, into_iter_item_proj.substs) |
487cf647 FG |
155 | ) |
156 | { | |
157 | iter_item_ty == into_iter_item_ty | |
158 | } else { | |
159 | false | |
160 | } | |
161 | } | |
162 | ||
163 | /// Checks if the given method call matches the expected signature of | |
164 | /// `([&[mut]] self, &<iter_ty as Iterator>::Item) -> bool` | |
165 | fn is_contains_sig(cx: &LateContext<'_>, call_id: HirId, iter_expr: &Expr<'_>) -> bool { | |
166 | let typeck = cx.typeck_results(); | |
167 | if let Some(id) = typeck.type_dependent_def_id(call_id) | |
168 | && let sig = cx.tcx.fn_sig(id) | |
169 | && sig.skip_binder().output().is_bool() | |
170 | && let [_, search_ty] = *sig.skip_binder().inputs() | |
171 | && let ty::Ref(_, search_ty, Mutability::Not) = *cx.tcx.erase_late_bound_regions(sig.rebind(search_ty)).kind() | |
172 | && let Some(iter_trait) = cx.tcx.get_diagnostic_item(sym::Iterator) | |
173 | && let Some(iter_item) = cx.tcx | |
174 | .associated_items(iter_trait) | |
175 | .find_by_name_and_kind(cx.tcx, Ident::with_dummy_span(Symbol::intern("Item")), AssocKind::Type, iter_trait) | |
176 | && let substs = cx.tcx.mk_substs([GenericArg::from(typeck.expr_ty_adjusted(iter_expr))].into_iter()) | |
177 | && let proj_ty = cx.tcx.mk_projection(iter_item.def_id, substs) | |
178 | && let Ok(item_ty) = cx.tcx.try_normalize_erasing_regions(cx.param_env, proj_ty) | |
179 | { | |
180 | item_ty == EarlyBinder(search_ty).subst(cx.tcx, cx.typeck_results().node_substs(call_id)) | |
181 | } else { | |
182 | false | |
183 | } | |
184 | } | |
185 | ||
f20569fa XL |
186 | struct IterFunction { |
187 | func: IterFunctionKind, | |
188 | span: Span, | |
189 | } | |
190 | impl IterFunction { | |
191 | fn get_iter_method(&self, cx: &LateContext<'_>) -> String { | |
192 | match &self.func { | |
193 | IterFunctionKind::IntoIter => String::new(), | |
194 | IterFunctionKind::Len => String::from(".count()"), | |
195 | IterFunctionKind::IsEmpty => String::from(".next().is_none()"), | |
196 | IterFunctionKind::Contains(span) => { | |
197 | let s = snippet(cx, *span, ".."); | |
198 | if let Some(stripped) = s.strip_prefix('&') { | |
2b03887a | 199 | format!(".any(|x| x == {stripped})") |
f20569fa | 200 | } else { |
2b03887a | 201 | format!(".any(|x| x == *{s})") |
f20569fa XL |
202 | } |
203 | }, | |
204 | } | |
205 | } | |
206 | fn get_suggestion_text(&self) -> &'static str { | |
207 | match &self.func { | |
208 | IterFunctionKind::IntoIter => { | |
209 | "use the original Iterator instead of collecting it and then producing a new one" | |
210 | }, | |
211 | IterFunctionKind::Len => { | |
212 | "take the original Iterator's count instead of collecting it and finding the length" | |
213 | }, | |
214 | IterFunctionKind::IsEmpty => { | |
215 | "check if the original Iterator has anything instead of collecting it and seeing if it's empty" | |
216 | }, | |
217 | IterFunctionKind::Contains(_) => { | |
218 | "check if the original Iterator contains an element instead of collecting then checking" | |
219 | }, | |
220 | } | |
221 | } | |
222 | } | |
223 | enum IterFunctionKind { | |
224 | IntoIter, | |
225 | Len, | |
226 | IsEmpty, | |
227 | Contains(Span), | |
228 | } | |
229 | ||
a2a8927a XL |
230 | struct IterFunctionVisitor<'a, 'tcx> { |
231 | illegal_mutable_capture_ids: HirIdSet, | |
232 | current_mutably_captured_ids: HirIdSet, | |
233 | cx: &'a LateContext<'tcx>, | |
234 | uses: Vec<Option<IterFunction>>, | |
235 | hir_id_uses_map: FxHashMap<HirId, usize>, | |
236 | current_statement_hir_id: Option<HirId>, | |
f20569fa | 237 | seen_other: bool, |
136023e0 | 238 | target: HirId, |
f20569fa | 239 | } |
a2a8927a XL |
240 | impl<'tcx> Visitor<'tcx> for IterFunctionVisitor<'_, 'tcx> { |
241 | fn visit_block(&mut self, block: &'tcx Block<'tcx>) { | |
242 | for (expr, hir_id) in block.stmts.iter().filter_map(get_expr_and_hir_id_from_stmt) { | |
f2b60f7d FG |
243 | if check_loop_kind(expr).is_some() { |
244 | continue; | |
245 | } | |
a2a8927a XL |
246 | self.visit_block_expr(expr, hir_id); |
247 | } | |
248 | if let Some(expr) = block.expr { | |
f2b60f7d FG |
249 | if let Some(loop_kind) = check_loop_kind(expr) { |
250 | if let LoopKind::Conditional(block_expr) = loop_kind { | |
251 | self.visit_block_expr(block_expr, None); | |
252 | } | |
253 | } else { | |
254 | self.visit_block_expr(expr, None); | |
255 | } | |
a2a8927a XL |
256 | } |
257 | } | |
258 | ||
f20569fa XL |
259 | fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { |
260 | // Check function calls on our collection | |
f2b60f7d | 261 | if let ExprKind::MethodCall(method_name, recv, [args @ ..], _) = &expr.kind { |
a2a8927a XL |
262 | if method_name.ident.name == sym!(collect) && is_trait_method(self.cx, expr, sym::Iterator) { |
263 | self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(recv)); | |
264 | self.visit_expr(recv); | |
265 | return; | |
266 | } | |
267 | ||
136023e0 | 268 | if path_to_local_id(recv, self.target) { |
a2a8927a XL |
269 | if self |
270 | .illegal_mutable_capture_ids | |
271 | .intersection(&self.current_mutably_captured_ids) | |
272 | .next() | |
273 | .is_none() | |
274 | { | |
275 | if let Some(hir_id) = self.current_statement_hir_id { | |
276 | self.hir_id_uses_map.insert(hir_id, self.uses.len()); | |
277 | } | |
278 | match method_name.ident.name.as_str() { | |
279 | "into_iter" => self.uses.push(Some(IterFunction { | |
280 | func: IterFunctionKind::IntoIter, | |
281 | span: expr.span, | |
282 | })), | |
283 | "len" => self.uses.push(Some(IterFunction { | |
284 | func: IterFunctionKind::Len, | |
285 | span: expr.span, | |
286 | })), | |
287 | "is_empty" => self.uses.push(Some(IterFunction { | |
288 | func: IterFunctionKind::IsEmpty, | |
289 | span: expr.span, | |
290 | })), | |
291 | "contains" => self.uses.push(Some(IterFunction { | |
292 | func: IterFunctionKind::Contains(args[0].span), | |
293 | span: expr.span, | |
294 | })), | |
295 | _ => { | |
296 | self.seen_other = true; | |
297 | if let Some(hir_id) = self.current_statement_hir_id { | |
298 | self.hir_id_uses_map.remove(&hir_id); | |
299 | } | |
300 | }, | |
301 | } | |
f20569fa | 302 | } |
136023e0 | 303 | return; |
f20569fa | 304 | } |
a2a8927a XL |
305 | |
306 | if let Some(hir_id) = path_to_local(recv) { | |
307 | if let Some(index) = self.hir_id_uses_map.remove(&hir_id) { | |
308 | if self | |
309 | .illegal_mutable_capture_ids | |
310 | .intersection(&self.current_mutably_captured_ids) | |
311 | .next() | |
312 | .is_none() | |
313 | { | |
314 | if let Some(hir_id) = self.current_statement_hir_id { | |
315 | self.hir_id_uses_map.insert(hir_id, index); | |
316 | } | |
317 | } else { | |
318 | self.uses[index] = None; | |
319 | } | |
320 | } | |
321 | } | |
f20569fa XL |
322 | } |
323 | // Check if the collection is used for anything else | |
136023e0 XL |
324 | if path_to_local_id(expr, self.target) { |
325 | self.seen_other = true; | |
326 | } else { | |
327 | walk_expr(self, expr); | |
f20569fa XL |
328 | } |
329 | } | |
f20569fa XL |
330 | } |
331 | ||
f2b60f7d FG |
332 | enum LoopKind<'tcx> { |
333 | Conditional(&'tcx Expr<'tcx>), | |
334 | Loop, | |
335 | } | |
336 | ||
337 | fn check_loop_kind<'tcx>(expr: &Expr<'tcx>) -> Option<LoopKind<'tcx>> { | |
338 | if let Some(higher::WhileLet { let_expr, .. }) = higher::WhileLet::hir(expr) { | |
339 | return Some(LoopKind::Conditional(let_expr)); | |
340 | } | |
341 | if let Some(higher::While { condition, .. }) = higher::While::hir(expr) { | |
342 | return Some(LoopKind::Conditional(condition)); | |
343 | } | |
344 | if let Some(higher::ForLoop { arg, .. }) = higher::ForLoop::hir(expr) { | |
345 | return Some(LoopKind::Conditional(arg)); | |
346 | } | |
347 | if let ExprKind::Loop { .. } = expr.kind { | |
348 | return Some(LoopKind::Loop); | |
349 | } | |
350 | ||
351 | None | |
352 | } | |
353 | ||
a2a8927a XL |
354 | impl<'tcx> IterFunctionVisitor<'_, 'tcx> { |
355 | fn visit_block_expr(&mut self, expr: &'tcx Expr<'tcx>, hir_id: Option<HirId>) { | |
356 | self.current_statement_hir_id = hir_id; | |
357 | self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(expr)); | |
358 | self.visit_expr(expr); | |
359 | } | |
360 | } | |
361 | ||
362 | fn get_expr_and_hir_id_from_stmt<'v>(stmt: &'v Stmt<'v>) -> Option<(&'v Expr<'v>, Option<HirId>)> { | |
363 | match stmt.kind { | |
364 | StmtKind::Expr(expr) | StmtKind::Semi(expr) => Some((expr, None)), | |
365 | StmtKind::Item(..) => None, | |
366 | StmtKind::Local(Local { init, pat, .. }) => { | |
367 | if let PatKind::Binding(_, hir_id, ..) = pat.kind { | |
368 | init.map(|init_expr| (init_expr, Some(hir_id))) | |
369 | } else { | |
370 | init.map(|init_expr| (init_expr, None)) | |
371 | } | |
372 | }, | |
373 | } | |
374 | } | |
375 | ||
f20569fa XL |
376 | struct UsedCountVisitor<'a, 'tcx> { |
377 | cx: &'a LateContext<'tcx>, | |
378 | id: HirId, | |
379 | count: usize, | |
380 | } | |
381 | ||
382 | impl<'a, 'tcx> Visitor<'tcx> for UsedCountVisitor<'a, 'tcx> { | |
5099ac24 | 383 | type NestedFilter = nested_filter::OnlyBodies; |
f20569fa XL |
384 | |
385 | fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { | |
386 | if path_to_local_id(expr, self.id) { | |
387 | self.count += 1; | |
388 | } else { | |
389 | walk_expr(self, expr); | |
390 | } | |
391 | } | |
392 | ||
5099ac24 FG |
393 | fn nested_visit_map(&mut self) -> Self::Map { |
394 | self.cx.tcx.hir() | |
f20569fa XL |
395 | } |
396 | } | |
397 | ||
398 | /// Detect the occurrences of calls to `iter` or `into_iter` for the | |
399 | /// given identifier | |
a2a8927a XL |
400 | fn detect_iter_and_into_iters<'tcx: 'a, 'a>( |
401 | block: &'tcx Block<'tcx>, | |
402 | id: HirId, | |
403 | cx: &'a LateContext<'tcx>, | |
404 | captured_ids: HirIdSet, | |
405 | ) -> Option<Vec<IterFunction>> { | |
f20569fa XL |
406 | let mut visitor = IterFunctionVisitor { |
407 | uses: Vec::new(), | |
136023e0 | 408 | target: id, |
f20569fa | 409 | seen_other: false, |
a2a8927a XL |
410 | cx, |
411 | current_mutably_captured_ids: HirIdSet::default(), | |
412 | illegal_mutable_capture_ids: captured_ids, | |
413 | hir_id_uses_map: FxHashMap::default(), | |
414 | current_statement_hir_id: None, | |
f20569fa XL |
415 | }; |
416 | visitor.visit_block(block); | |
a2a8927a XL |
417 | if visitor.seen_other { |
418 | None | |
419 | } else { | |
420 | Some(visitor.uses.into_iter().flatten().collect()) | |
421 | } | |
422 | } | |
423 | ||
5099ac24 FG |
424 | fn get_captured_ids(cx: &LateContext<'_>, ty: Ty<'_>) -> HirIdSet { |
425 | fn get_captured_ids_recursive(cx: &LateContext<'_>, ty: Ty<'_>, set: &mut HirIdSet) { | |
a2a8927a XL |
426 | match ty.kind() { |
427 | ty::Adt(_, generics) => { | |
428 | for generic in *generics { | |
429 | if let GenericArgKind::Type(ty) = generic.unpack() { | |
430 | get_captured_ids_recursive(cx, ty, set); | |
431 | } | |
432 | } | |
433 | }, | |
434 | ty::Closure(def_id, _) => { | |
435 | let closure_hir_node = cx.tcx.hir().get_if_local(*def_id).unwrap(); | |
436 | if let Node::Expr(closure_expr) = closure_hir_node { | |
437 | can_move_expr_to_closure(cx, closure_expr) | |
438 | .unwrap() | |
439 | .into_iter() | |
440 | .for_each(|(hir_id, capture_kind)| { | |
441 | if matches!(capture_kind, CaptureKind::Ref(Mutability::Mut)) { | |
442 | set.insert(hir_id); | |
443 | } | |
444 | }); | |
445 | } | |
446 | }, | |
447 | _ => (), | |
448 | } | |
449 | } | |
450 | ||
451 | let mut set = HirIdSet::default(); | |
452 | ||
453 | get_captured_ids_recursive(cx, ty, &mut set); | |
454 | ||
455 | set | |
f20569fa | 456 | } |