| use clippy_utils::diagnostics::span_lint; |
| use clippy_utils::ty::{get_type_diagnostic_name, is_type_lang_item}; |
| use clippy_utils::visitors::{Visitable, for_each_expr}; |
| use clippy_utils::{get_enclosing_block, path_to_local_id}; |
| use core::ops::ControlFlow; |
| use rustc_hir::{Body, ExprKind, HirId, LangItem, LetStmt, Node, PatKind}; |
| use rustc_lint::{LateContext, LateLintPass}; |
| use rustc_session::declare_lint_pass; |
| use rustc_span::symbol::sym; |
| |
| declare_clippy_lint! { |
| /// ### What it does |
| /// Checks for collections that are never queried. |
| /// |
| /// ### Why is this bad? |
| /// Putting effort into constructing a collection but then never querying it might indicate that |
| /// the author forgot to do whatever they intended to do with the collection. Example: Clone |
| /// a vector, sort it for iteration, but then mistakenly iterate the original vector |
| /// instead. |
| /// |
| /// ### Example |
| /// ```no_run |
| /// # let samples = vec![3, 1, 2]; |
| /// let mut sorted_samples = samples.clone(); |
| /// sorted_samples.sort(); |
| /// for sample in &samples { // Oops, meant to use `sorted_samples`. |
| /// println!("{sample}"); |
| /// } |
| /// ``` |
| /// Use instead: |
| /// ```no_run |
| /// # let samples = vec![3, 1, 2]; |
| /// let mut sorted_samples = samples.clone(); |
| /// sorted_samples.sort(); |
| /// for sample in &sorted_samples { |
| /// println!("{sample}"); |
| /// } |
| /// ``` |
| #[clippy::version = "1.70.0"] |
| pub COLLECTION_IS_NEVER_READ, |
| nursery, |
| "a collection is never queried" |
| } |
| declare_lint_pass!(CollectionIsNeverRead => [COLLECTION_IS_NEVER_READ]); |
| |
| impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead { |
| fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx LetStmt<'tcx>) { |
| // Look for local variables whose type is a container. Search surrounding block for read access. |
| if let PatKind::Binding(_, local_id, _, _) = local.pat.kind |
| && match_acceptable_type(cx, local) |
| && let Some(enclosing_block) = get_enclosing_block(cx, local.hir_id) |
| && has_no_read_access(cx, local_id, enclosing_block) |
| { |
| span_lint(cx, COLLECTION_IS_NEVER_READ, local.span, "collection is never read"); |
| } |
| } |
| } |
| |
| fn match_acceptable_type(cx: &LateContext<'_>, local: &LetStmt<'_>) -> bool { |
| let ty = cx.typeck_results().pat_ty(local.pat); |
| matches!( |
| get_type_diagnostic_name(cx, ty), |
| Some( |
| sym::BTreeMap |
| | sym::BTreeSet |
| | sym::BinaryHeap |
| | sym::HashMap |
| | sym::HashSet |
| | sym::LinkedList |
| | sym::Option |
| | sym::Vec |
| | sym::VecDeque |
| ) |
| ) || is_type_lang_item(cx, ty, LangItem::String) |
| } |
| |
| fn has_no_read_access<'tcx, T: Visitable<'tcx>>(cx: &LateContext<'tcx>, id: HirId, block: T) -> bool { |
| let mut has_access = false; |
| let mut has_read_access = false; |
| |
| // Inspect all expressions and sub-expressions in the block. |
| for_each_expr(cx, block, |expr| { |
| // Ignore expressions that are not simply `id`. |
| if !path_to_local_id(expr, id) { |
| return ControlFlow::Continue(()); |
| } |
| |
| // `id` is being accessed. Investigate if it's a read access. |
| has_access = true; |
| |
| // `id` appearing in the left-hand side of an assignment is not a read access: |
| // |
| // id = ...; // Not reading `id`. |
| if let Node::Expr(parent) = cx.tcx.parent_hir_node(expr.hir_id) |
| && let ExprKind::Assign(lhs, ..) = parent.kind |
| && path_to_local_id(lhs, id) |
| { |
| return ControlFlow::Continue(()); |
| } |
| |
| // Look for method call with receiver `id`. It might be a non-read access: |
| // |
| // id.foo(args) |
| // |
| // Only assuming this for "official" methods defined on the type. For methods defined in extension |
| // traits (identified as local, based on the orphan rule), pessimistically assume that they might |
| // have side effects, so consider them a read. |
| if let Node::Expr(parent) = cx.tcx.parent_hir_node(expr.hir_id) |
| && let ExprKind::MethodCall(_, receiver, args, _) = parent.kind |
| && path_to_local_id(receiver, id) |
| && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id) |
| && !method_def_id.is_local() |
| { |
| // If this "official" method takes closures, |
| // it has read access if one of the closures has read access. |
| // |
| // items.retain(|item| send_item(item).is_ok()); |
| let is_read_in_closure_arg = args.iter().any(|arg| { |
| if let ExprKind::Closure(closure) = arg.kind |
| // To keep things simple, we only check the first param to see if its read. |
| && let Body { params: [param, ..], value } = cx.tcx.hir_body(closure.body) |
| { |
| !has_no_read_access(cx, param.hir_id, *value) |
| } else { |
| false |
| } |
| }); |
| if is_read_in_closure_arg { |
| has_read_access = true; |
| return ControlFlow::Break(()); |
| } |
| |
| // The method call is a statement, so the return value is not used. That's not a read access: |
| // |
| // id.foo(args); |
| if let Node::Stmt(..) = cx.tcx.parent_hir_node(parent.hir_id) { |
| return ControlFlow::Continue(()); |
| } |
| |
| // The method call is not a statement, so its return value is used somehow but its type is the |
| // unit type, so this is not a real read access. Examples: |
| // |
| // let y = x.clear(); |
| // println!("{:?}", x.clear()); |
| if cx.typeck_results().expr_ty(parent).is_unit() { |
| return ControlFlow::Continue(()); |
| } |
| } |
| |
| // Any other access to `id` is a read access. Stop searching. |
| has_read_access = true; |
| ControlFlow::Break(()) |
| }); |
| |
| // Ignore collections that have no access at all. Other lints should catch them. |
| has_access && !has_read_access |
| } |