| use super::SAME_ITEM_PUSH; |
| use clippy_utils::diagnostics::span_lint_and_help; |
| use clippy_utils::path_to_local; |
| use clippy_utils::source::snippet_with_context; |
| use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; |
| use rustc_data_structures::fx::FxHashSet; |
| use rustc_errors::Applicability; |
| use rustc_hir::def::{DefKind, Res}; |
| use rustc_hir::intravisit::{walk_expr, Visitor}; |
| use rustc_hir::{BindingAnnotation, Block, Expr, ExprKind, HirId, Mutability, Node, Pat, PatKind, Stmt, StmtKind}; |
| use rustc_lint::LateContext; |
| use rustc_span::symbol::sym; |
| 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<'_>, |
| ) { |
| fn emit_lint(cx: &LateContext<'_>, vec: &Expr<'_>, pushed_item: &Expr<'_>, ctxt: SyntaxContext) { |
| 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; |
| |
| span_lint_and_help( |
| cx, |
| SAME_ITEM_PUSH, |
| vec.span, |
| "it looks like the same item is being pushed into this Vec", |
| None, |
| &format!("consider using vec![{item_str};SIZE] or {vec_str}.resize(NEW_SIZE, {item_str})"), |
| ); |
| } |
| |
| 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() |
| .map_or(false, |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, BindingAnnotation(_, Mutability::Mut)) |
| && let parent_node = cx.tcx.hir().parent_id(hir_id) |
| && let Some(Node::Local(parent_let_expr)) = cx.tcx.opt_hir_node(parent_node) |
| && 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), |
| // 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); |
| } |
| }, |
| _ => {}, |
| } |
| } |
| }, |
| // constant |
| Res::Def(DefKind::Const, ..) => emit_lint(cx, vec, pushed_item, ctxt), |
| _ => {}, |
| } |
| }, |
| ExprKind::Lit(..) => emit_lint(cx, vec, pushed_item, ctxt), |
| _ => {}, |
| } |
| } |
| } |
| |
| // 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) = path_to_local(vec) |
| { |
| !self.used_locals.contains(&hir_id) |
| } else { |
| false |
| } |
| } |
| } |
| |
| impl<'a, 'tcx> Visitor<'tcx> for SameItemPushVisitor<'a, '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) = path_to_local(expr) { |
| 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), |
| _ => {}, |
| } |
| } else { |
| // Current statement is a push ...check whether another |
| // push had been previously done |
| 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 |
| && let ExprKind::MethodCall(path, self_expr, args, _) = &semi_stmt.kind |
| // Figure out the parameters for the method call |
| && let Some(pushed_item) = args.first() |
| // Check that the method being called is push() on a Vec |
| && is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(self_expr), sym::Vec) |
| && path.ident.name.as_str() == "push" |
| { |
| return Some((self_expr, pushed_item, semi_stmt.span.ctxt())); |
| } |
| None |
| } |