use super::NEEDLESS_COLLECT; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then}; use clippy_utils::source::{snippet, snippet_with_applicability}; use clippy_utils::sugg::Sugg; use clippy_utils::ty::{get_type_diagnostic_name, make_normalized_projection, make_projection}; use clippy_utils::{ CaptureKind, can_move_expr_to_closure, fn_def_id, get_enclosing_block, higher, is_trait_method, path_to_local, path_to_local_id, }; use rustc_data_structures::fx::FxHashMap; use rustc_errors::{Applicability, MultiSpan}; use rustc_hir::intravisit::{Visitor, walk_block, walk_expr}; use rustc_hir::{ BindingMode, Block, Expr, ExprKind, HirId, HirIdSet, LetStmt, Mutability, Node, PatKind, Stmt, StmtKind, }; use rustc_lint::LateContext; use rustc_middle::hir::nested_filter; use rustc_middle::ty::{self, AssocKind, ClauseKind, EarlyBinder, GenericArg, GenericArgKind, Ty}; use rustc_span::symbol::Ident; use rustc_span::{Span, sym}; const NEEDLESS_COLLECT_MSG: &str = "avoid using `collect()` when not needed"; pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, name_span: Span, collect_expr: &'tcx Expr<'_>, iter_expr: &'tcx Expr<'tcx>, call_span: Span, ) { match cx.tcx.parent_hir_node(collect_expr.hir_id) { Node::Expr(parent) => { check_collect_into_intoiterator(cx, parent, collect_expr, call_span, iter_expr); if let ExprKind::MethodCall(name, _, args @ ([] | [_]), _) = parent.kind { let mut app = Applicability::MachineApplicable; let name = name.ident.as_str(); let collect_ty = cx.typeck_results().expr_ty(collect_expr); let sugg: String = match name { "len" => { if let Some(adt) = collect_ty.ty_adt_def() && matches!( cx.tcx.get_diagnostic_name(adt.did()), Some(sym::Vec | sym::VecDeque | sym::LinkedList | sym::BinaryHeap) ) { "count()".into() } else { return; } }, "is_empty" if is_is_empty_sig(cx, parent.hir_id) && iterates_same_ty(cx, cx.typeck_results().expr_ty(iter_expr), collect_ty) => { "next().is_none()".into() }, "contains" => { if is_contains_sig(cx, parent.hir_id, iter_expr) && let Some(arg) = args.first() { let (span, prefix) = if let ExprKind::AddrOf(_, _, arg) = arg.kind { (arg.span, "") } else { (arg.span, "*") }; let snip = snippet_with_applicability(cx, span, "??", &mut app); format!("any(|x| x == {prefix}{snip})") } else { return; } }, _ => return, }; span_lint_and_sugg( cx, NEEDLESS_COLLECT, call_span.with_hi(parent.span.hi()), NEEDLESS_COLLECT_MSG, "replace with", sugg, app, ); } }, Node::LetStmt(l) => { if let PatKind::Binding(BindingMode::NONE | BindingMode::MUT, id, _, None) = l.pat.kind && let ty = cx.typeck_results().expr_ty(collect_expr) && matches!( get_type_diagnostic_name(cx, ty), Some(sym::Vec | sym::VecDeque | sym::BinaryHeap | sym::LinkedList) ) && let iter_ty = cx.typeck_results().expr_ty(iter_expr) && let Some(block) = get_enclosing_block(cx, l.hir_id) && let Some(iter_calls) = detect_iter_and_into_iters(block, id, cx, get_captured_ids(cx, iter_ty)) && let [iter_call] = &*iter_calls { let mut used_count_visitor = UsedCountVisitor { cx, id, count: 0 }; walk_block(&mut used_count_visitor, block); if used_count_visitor.count > 1 { return; } // Suggest replacing iter_call with iter_replacement, and removing stmt let mut span = MultiSpan::from_span(name_span); span.push_span_label(iter_call.span, "the iterator could be used here instead"); span_lint_hir_and_then( cx, NEEDLESS_COLLECT, collect_expr.hir_id, span, NEEDLESS_COLLECT_MSG, |diag| { let iter_replacement = format!("{}{}", Sugg::hir(cx, iter_expr, ".."), iter_call.get_iter_method(cx)); diag.multipart_suggestion( iter_call.get_suggestion_text(), vec![(l.span, String::new()), (iter_call.span, iter_replacement)], Applicability::MaybeIncorrect, ); }, ); } }, _ => (), } } /// checks for collecting into a (generic) method or function argument /// taking an `IntoIterator` fn check_collect_into_intoiterator<'tcx>( cx: &LateContext<'tcx>, parent: &'tcx Expr<'tcx>, collect_expr: &'tcx Expr<'tcx>, call_span: Span, iter_expr: &'tcx Expr<'tcx>, ) { if let Some(id) = fn_def_id(cx, parent) { let args = match parent.kind { ExprKind::Call(_, args) | ExprKind::MethodCall(_, _, args, _) => args, _ => &[], }; // find the argument index of the `collect_expr` in the // function / method call if let Some(arg_idx) = args.iter().position(|e| e.hir_id == collect_expr.hir_id).map(|i| { if matches!(parent.kind, ExprKind::MethodCall(_, _, _, _)) { i + 1 } else { i } }) { // extract the input types of the function/method call // that contains `collect_expr` let inputs = cx .tcx .liberate_late_bound_regions(id, cx.tcx.fn_sig(id).instantiate_identity()) .inputs(); // map IntoIterator generic bounds to their signature // types and check whether the argument type is an // `IntoIterator` if cx .tcx .param_env(id) .caller_bounds() .into_iter() .filter_map(|p| { if let ClauseKind::Trait(t) = p.kind().skip_binder() && cx.tcx.is_diagnostic_item(sym::IntoIterator, t.trait_ref.def_id) { Some(t.self_ty()) } else { None } }) .any(|ty| ty == inputs[arg_idx]) { span_lint_and_sugg( cx, NEEDLESS_COLLECT, call_span.with_lo(iter_expr.span.hi()), NEEDLESS_COLLECT_MSG, "remove this call", String::new(), Applicability::MachineApplicable, ); } } } } /// Checks if the given method call matches the expected signature of `([&[mut]] self) -> bool` fn is_is_empty_sig(cx: &LateContext<'_>, call_id: HirId) -> bool { cx.typeck_results().type_dependent_def_id(call_id).is_some_and(|id| { let sig = cx.tcx.fn_sig(id).instantiate_identity().skip_binder(); sig.inputs().len() == 1 && sig.output().is_bool() }) } /// Checks if `::Item` is the same as `::Item` fn iterates_same_ty<'tcx>(cx: &LateContext<'tcx>, iter_ty: Ty<'tcx>, collect_ty: Ty<'tcx>) -> bool { if let Some(iter_trait) = cx.tcx.get_diagnostic_item(sym::Iterator) && let Some(into_iter_trait) = cx.tcx.get_diagnostic_item(sym::IntoIterator) && let Some(iter_item_ty) = make_normalized_projection(cx.tcx, cx.typing_env(), iter_trait, sym::Item, [iter_ty]) && let Some(into_iter_item_proj) = make_projection(cx.tcx, into_iter_trait, sym::Item, [collect_ty]) && let Ok(into_iter_item_ty) = cx.tcx.try_normalize_erasing_regions( cx.typing_env(), Ty::new_projection_from_args(cx.tcx, into_iter_item_proj.def_id, into_iter_item_proj.args), ) { iter_item_ty == into_iter_item_ty } else { false } } /// Checks if the given method call matches the expected signature of /// `([&[mut]] self, &::Item) -> bool` fn is_contains_sig(cx: &LateContext<'_>, call_id: HirId, iter_expr: &Expr<'_>) -> bool { let typeck = cx.typeck_results(); if let Some(id) = typeck.type_dependent_def_id(call_id) && let sig = cx.tcx.fn_sig(id).instantiate_identity() && sig.skip_binder().output().is_bool() && let [_, search_ty] = *sig.skip_binder().inputs() && let ty::Ref(_, search_ty, Mutability::Not) = *cx .tcx .instantiate_bound_regions_with_erased(sig.rebind(search_ty)) .kind() && let Some(iter_trait) = cx.tcx.get_diagnostic_item(sym::Iterator) && let Some(iter_item) = cx.tcx.associated_items(iter_trait).find_by_name_and_kind( cx.tcx, Ident::with_dummy_span(sym::Item), AssocKind::Type, iter_trait, ) && let args = cx.tcx.mk_args(&[GenericArg::from(typeck.expr_ty_adjusted(iter_expr))]) && let proj_ty = Ty::new_projection_from_args(cx.tcx, iter_item.def_id, args) && let Ok(item_ty) = cx.tcx.try_normalize_erasing_regions(cx.typing_env(), proj_ty) { item_ty == EarlyBinder::bind(search_ty).instantiate(cx.tcx, cx.typeck_results().node_args(call_id)) } else { false } } struct IterFunction { func: IterFunctionKind, span: Span, } impl IterFunction { fn get_iter_method(&self, cx: &LateContext<'_>) -> String { match &self.func { IterFunctionKind::IntoIter => String::new(), IterFunctionKind::Len => String::from(".count()"), IterFunctionKind::IsEmpty => String::from(".next().is_none()"), IterFunctionKind::Contains(span) => { let s = snippet(cx, *span, ".."); if let Some(stripped) = s.strip_prefix('&') { format!(".any(|x| x == {stripped})") } else { format!(".any(|x| x == *{s})") } }, } } fn get_suggestion_text(&self) -> &'static str { match &self.func { IterFunctionKind::IntoIter => { "use the original Iterator instead of collecting it and then producing a new one" }, IterFunctionKind::Len => { "take the original Iterator's count instead of collecting it and finding the length" }, IterFunctionKind::IsEmpty => { "check if the original Iterator has anything instead of collecting it and seeing if it's empty" }, IterFunctionKind::Contains(_) => { "check if the original Iterator contains an element instead of collecting then checking" }, } } } enum IterFunctionKind { IntoIter, Len, IsEmpty, Contains(Span), } struct IterFunctionVisitor<'a, 'tcx> { illegal_mutable_capture_ids: HirIdSet, current_mutably_captured_ids: HirIdSet, cx: &'a LateContext<'tcx>, uses: Vec>, hir_id_uses_map: FxHashMap, current_statement_hir_id: Option, seen_other: bool, target: HirId, } impl<'tcx> Visitor<'tcx> for IterFunctionVisitor<'_, 'tcx> { fn visit_block(&mut self, block: &'tcx Block<'tcx>) { for (expr, hir_id) in block.stmts.iter().filter_map(get_expr_and_hir_id_from_stmt) { if check_loop_kind(expr).is_some() { continue; } self.visit_block_expr(expr, hir_id); } if let Some(expr) = block.expr { if let Some(loop_kind) = check_loop_kind(expr) { if let LoopKind::Conditional(block_expr) = loop_kind { self.visit_block_expr(block_expr, None); } } else { self.visit_block_expr(expr, None); } } } fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { // Check function calls on our collection if let ExprKind::MethodCall(method_name, recv, args, _) = &expr.kind { if args.is_empty() && method_name.ident.name.as_str() == "collect" && is_trait_method(self.cx, expr, sym::Iterator) { self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(recv)); self.visit_expr(recv); return; } if path_to_local_id(recv, self.target) { if self .illegal_mutable_capture_ids .intersection(&self.current_mutably_captured_ids) .next() .is_none() { if let Some(hir_id) = self.current_statement_hir_id { self.hir_id_uses_map.insert(hir_id, self.uses.len()); } match method_name.ident.name.as_str() { "into_iter" => self.uses.push(Some(IterFunction { func: IterFunctionKind::IntoIter, span: expr.span, })), "len" => self.uses.push(Some(IterFunction { func: IterFunctionKind::Len, span: expr.span, })), "is_empty" => self.uses.push(Some(IterFunction { func: IterFunctionKind::IsEmpty, span: expr.span, })), "contains" => self.uses.push(Some(IterFunction { func: IterFunctionKind::Contains(args[0].span), span: expr.span, })), _ => { self.seen_other = true; if let Some(hir_id) = self.current_statement_hir_id { self.hir_id_uses_map.remove(&hir_id); } }, } } return; } if let Some(hir_id) = path_to_local(recv) { if let Some(index) = self.hir_id_uses_map.remove(&hir_id) { if self .illegal_mutable_capture_ids .intersection(&self.current_mutably_captured_ids) .next() .is_none() { if let Some(hir_id) = self.current_statement_hir_id { self.hir_id_uses_map.insert(hir_id, index); } } else { self.uses[index] = None; } } } } // Check if the collection is used for anything else if path_to_local_id(expr, self.target) { self.seen_other = true; } else { walk_expr(self, expr); } } } enum LoopKind<'tcx> { Conditional(&'tcx Expr<'tcx>), Loop, } fn check_loop_kind<'tcx>(expr: &Expr<'tcx>) -> Option> { if let Some(higher::WhileLet { let_expr, .. }) = higher::WhileLet::hir(expr) { return Some(LoopKind::Conditional(let_expr)); } if let Some(higher::While { condition, .. }) = higher::While::hir(expr) { return Some(LoopKind::Conditional(condition)); } if let Some(higher::ForLoop { arg, .. }) = higher::ForLoop::hir(expr) { return Some(LoopKind::Conditional(arg)); } if let ExprKind::Loop { .. } = expr.kind { return Some(LoopKind::Loop); } None } impl<'tcx> IterFunctionVisitor<'_, 'tcx> { fn visit_block_expr(&mut self, expr: &'tcx Expr<'tcx>, hir_id: Option) { self.current_statement_hir_id = hir_id; self.current_mutably_captured_ids = get_captured_ids(self.cx, self.cx.typeck_results().expr_ty(expr)); self.visit_expr(expr); } } fn get_expr_and_hir_id_from_stmt<'v>(stmt: &'v Stmt<'v>) -> Option<(&'v Expr<'v>, Option)> { match stmt.kind { StmtKind::Expr(expr) | StmtKind::Semi(expr) => Some((expr, None)), StmtKind::Item(..) => None, StmtKind::Let(LetStmt { init, pat, .. }) => { if let PatKind::Binding(_, hir_id, ..) = pat.kind { init.map(|init_expr| (init_expr, Some(hir_id))) } else { init.map(|init_expr| (init_expr, None)) } }, } } struct UsedCountVisitor<'a, 'tcx> { cx: &'a LateContext<'tcx>, id: HirId, count: usize, } impl<'tcx> Visitor<'tcx> for UsedCountVisitor<'_, 'tcx> { type NestedFilter = nested_filter::OnlyBodies; fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { if path_to_local_id(expr, self.id) { self.count += 1; } else { walk_expr(self, expr); } } fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { self.cx.tcx } } /// Detect the occurrences of calls to `iter` or `into_iter` for the /// given identifier fn detect_iter_and_into_iters<'tcx: 'a, 'a>( block: &'tcx Block<'tcx>, id: HirId, cx: &'a LateContext<'tcx>, captured_ids: HirIdSet, ) -> Option> { let mut visitor = IterFunctionVisitor { illegal_mutable_capture_ids: captured_ids, current_mutably_captured_ids: HirIdSet::default(), cx, uses: Vec::new(), hir_id_uses_map: FxHashMap::default(), current_statement_hir_id: None, seen_other: false, target: id, }; visitor.visit_block(block); if visitor.seen_other { None } else { Some(visitor.uses.into_iter().flatten().collect()) } } fn get_captured_ids(cx: &LateContext<'_>, ty: Ty<'_>) -> HirIdSet { fn get_captured_ids_recursive(cx: &LateContext<'_>, ty: Ty<'_>, set: &mut HirIdSet) { match ty.kind() { ty::Adt(_, generics) => { for generic in *generics { if let GenericArgKind::Type(ty) = generic.unpack() { get_captured_ids_recursive(cx, ty, set); } } }, ty::Closure(def_id, _) => { let closure_hir_node = cx.tcx.hir_get_if_local(*def_id).unwrap(); if let Node::Expr(closure_expr) = closure_hir_node { can_move_expr_to_closure(cx, closure_expr) .unwrap() .into_iter() .for_each(|(hir_id, capture_kind)| { if matches!(capture_kind, CaptureKind::Ref(Mutability::Mut)) { set.insert(hir_id); } }); } }, _ => (), } } let mut set = HirIdSet::default(); get_captured_ids_recursive(cx, ty, &mut set); set }