| use super::SAME_ITEM_PUSH; |
| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::msrvs::Msrv; |
| use clippy_utils::res::{MaybeDef, MaybeResPath}; |
| use clippy_utils::source::snippet_with_context; |
| use clippy_utils::ty::implements_trait; |
| use clippy_utils::{msrvs, std_or_core, sym}; |
| use rustc_data_structures::fx::FxHashSet; |
| use rustc_errors::Applicability; |
| use rustc_hir::def::{DefKind, Res}; |
| use rustc_hir::intravisit::{Visitor, walk_expr}; |
| use rustc_hir::{BindingMode, Block, Expr, ExprKind, HirId, Mutability, Node, Pat, PatKind, Stmt, StmtKind}; |
| use rustc_lint::LateContext; |
| use rustc_span::SyntaxContext; |
| |
| /// Detects for loop pushing the same item into a Vec |
| pub(super) fn check<'tcx>( |
| cx: &LateContext<'tcx>, |
| pat: &'tcx Pat<'_>, |
| _: &'tcx Expr<'_>, |
| body: &'tcx Expr<'_>, |
| _: &'tcx Expr<'_>, |
| msrv: Msrv, |
| ) { |
| fn emit_lint(cx: &LateContext<'_>, vec: &Expr<'_>, pushed_item: &Expr<'_>, ctxt: SyntaxContext, msrv: Msrv) { |
| let mut app = Applicability::Unspecified; |
| let vec_str = snippet_with_context(cx, vec.span, ctxt, "", &mut app).0; |
| let item_str = snippet_with_context(cx, pushed_item.span, ctxt, "", &mut app).0; |
| |
| let secondary_help = if msrv.meets(cx, msrvs::REPEAT_N) |
| && let Some(std_or_core) = std_or_core(cx) |
| { |
| format!("or `{vec_str}.extend({std_or_core}::iter::repeat_n({item_str}, SIZE))`") |
| } else { |
| format!("or `{vec_str}.resize(NEW_SIZE, {item_str})`") |
| }; |
| |
| span_lint_and_then( |
| cx, |
| SAME_ITEM_PUSH, |
| vec.span, |
| "it looks like the same item is being pushed into this `Vec`", |
| |diag| { |
| diag.help(format!("consider using `vec![{item_str};SIZE]`")) |
| .help(secondary_help); |
| }, |
| ); |
| } |
| |
| if !matches!(pat.kind, PatKind::Wild) { |
| return; |
| } |
| |
| // Determine whether it is safe to lint the body |
| let mut same_item_push_visitor = SameItemPushVisitor::new(cx); |
| walk_expr(&mut same_item_push_visitor, body); |
| if same_item_push_visitor.should_lint() |
| && let Some((vec, pushed_item, ctxt)) = same_item_push_visitor.vec_push |
| && let vec_ty = cx.typeck_results().expr_ty(vec) |
| && let ty = vec_ty.walk().nth(1).unwrap().expect_ty() |
| && cx |
| .tcx |
| .lang_items() |
| .clone_trait() |
| .is_some_and(|id| implements_trait(cx, ty, id, &[])) |
| { |
| // Make sure that the push does not involve possibly mutating values |
| match pushed_item.kind { |
| ExprKind::Path(ref qpath) => { |
| match cx.qpath_res(qpath, pushed_item.hir_id) { |
| // immutable bindings that are initialized with literal or constant |
| Res::Local(hir_id) => { |
| let node = cx.tcx.hir_node(hir_id); |
| if let Node::Pat(pat) = node |
| && let PatKind::Binding(bind_ann, ..) = pat.kind |
| && !matches!(bind_ann, BindingMode(_, Mutability::Mut)) |
| && let Node::LetStmt(parent_let_expr) = cx.tcx.parent_hir_node(hir_id) |
| && let Some(init) = parent_let_expr.init |
| { |
| match init.kind { |
| // immutable bindings that are initialized with literal |
| ExprKind::Lit(..) => emit_lint(cx, vec, pushed_item, ctxt, msrv), |
| // immutable bindings that are initialized with constant |
| ExprKind::Path(ref path) => { |
| if let Res::Def(DefKind::Const, ..) = cx.qpath_res(path, init.hir_id) { |
| emit_lint(cx, vec, pushed_item, ctxt, msrv); |
| } |
| }, |
| _ => {}, |
| } |
| } |
| }, |
| // constant |
| Res::Def(DefKind::Const, ..) => emit_lint(cx, vec, pushed_item, ctxt, msrv), |
| _ => {}, |
| } |
| }, |
| ExprKind::Lit(..) => emit_lint(cx, vec, pushed_item, ctxt, msrv), |
| _ => {}, |
| } |
| } |
| } |
| |
| // Scans the body of the for loop and determines whether lint should be given |
| struct SameItemPushVisitor<'a, 'tcx> { |
| non_deterministic_expr: bool, |
| multiple_pushes: bool, |
| // this field holds the last vec push operation visited, which should be the only push seen |
| vec_push: Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>, SyntaxContext)>, |
| cx: &'a LateContext<'tcx>, |
| used_locals: FxHashSet<HirId>, |
| } |
| |
| impl<'a, 'tcx> SameItemPushVisitor<'a, 'tcx> { |
| fn new(cx: &'a LateContext<'tcx>) -> Self { |
| Self { |
| non_deterministic_expr: false, |
| multiple_pushes: false, |
| vec_push: None, |
| cx, |
| used_locals: FxHashSet::default(), |
| } |
| } |
| |
| fn should_lint(&self) -> bool { |
| if !self.non_deterministic_expr |
| && !self.multiple_pushes |
| && let Some((vec, _, _)) = self.vec_push |
| && let Some(hir_id) = vec.res_local_id() |
| { |
| !self.used_locals.contains(&hir_id) |
| } else { |
| false |
| } |
| } |
| } |
| |
| impl<'tcx> Visitor<'tcx> for SameItemPushVisitor<'_, 'tcx> { |
| fn visit_expr(&mut self, expr: &'tcx Expr<'_>) { |
| match &expr.kind { |
| // Non-determinism may occur ... don't give a lint |
| ExprKind::Loop(..) | ExprKind::Match(..) | ExprKind::If(..) => self.non_deterministic_expr = true, |
| ExprKind::Block(block, _) => self.visit_block(block), |
| _ => { |
| if let Some(hir_id) = expr.res_local_id() { |
| self.used_locals.insert(hir_id); |
| } |
| walk_expr(self, expr); |
| }, |
| } |
| } |
| |
| fn visit_block(&mut self, b: &'tcx Block<'_>) { |
| for stmt in b.stmts { |
| self.visit_stmt(stmt); |
| } |
| } |
| |
| fn visit_stmt(&mut self, s: &'tcx Stmt<'_>) { |
| let vec_push_option = get_vec_push(self.cx, s); |
| if vec_push_option.is_none() { |
| // Current statement is not a push so visit inside |
| match &s.kind { |
| StmtKind::Expr(expr) | StmtKind::Semi(expr) => self.visit_expr(expr), |
| _ => {}, |
| } |
| } |
| // Current statement is a push ...check whether another |
| // push had been previously done |
| else if self.vec_push.is_none() { |
| self.vec_push = vec_push_option; |
| } else { |
| // There are multiple pushes ... don't lint |
| self.multiple_pushes = true; |
| } |
| } |
| } |
| |
| // Given some statement, determine if that statement is a push on a Vec. If it is, return |
| // the Vec being pushed into and the item being pushed |
| fn get_vec_push<'tcx>( |
| cx: &LateContext<'tcx>, |
| stmt: &'tcx Stmt<'_>, |
| ) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>, SyntaxContext)> { |
| if let StmtKind::Semi(semi_stmt) = &stmt.kind |
| // Extract method being called and figure out the parameters for the method call |
| && let ExprKind::MethodCall(path, self_expr, [pushed_item], _) = &semi_stmt.kind |
| // Check that the method being called is push() on a Vec |
| && path.ident.name == sym::push |
| && cx.typeck_results().expr_ty(self_expr).is_diag_item(cx, sym::Vec) |
| { |
| return Some((self_expr, pushed_item, semi_stmt.span.ctxt())); |
| } |
| None |
| } |