1 use clippy_utils::diagnostics::span_lint;
2 use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item};
3 use clippy_utils::visitors::for_each_expr_with_closures;
4 use clippy_utils::{get_enclosing_block, get_parent_node, path_to_local_id};
5 use core::ops::ControlFlow;
6 use rustc_hir::{Block, ExprKind, HirId, LangItem, Local, Node, PatKind};
7 use rustc_lint::{LateContext, LateLintPass};
8 use rustc_session::{declare_lint_pass, declare_tool_lint};
9 use rustc_span::symbol::sym;
10 use rustc_span::Symbol;
11
12 declare_clippy_lint! {
13 /// ### What it does
14 /// Checks for collections that are never queried.
15 ///
16 /// ### Why is this bad?
17 /// Putting effort into constructing a collection but then never querying it might indicate that
18 /// the author forgot to do whatever they intended to do with the collection. Example: Clone
19 /// a vector, sort it for iteration, but then mistakenly iterate the original vector
20 /// instead.
21 ///
22 /// ### Example
23 /// ```rust
24 /// # let samples = vec![3, 1, 2];
25 /// let mut sorted_samples = samples.clone();
26 /// sorted_samples.sort();
27 /// for sample in &samples { // Oops, meant to use `sorted_samples`.
28 /// println!("{sample}");
29 /// }
30 /// ```
31 /// Use instead:
32 /// ```rust
33 /// # let samples = vec![3, 1, 2];
34 /// let mut sorted_samples = samples.clone();
35 /// sorted_samples.sort();
36 /// for sample in &sorted_samples {
37 /// println!("{sample}");
38 /// }
39 /// ```
40 #[clippy::version = "1.70.0"]
41 pub COLLECTION_IS_NEVER_READ,
42 nursery,
43 "a collection is never queried"
44 }
45 declare_lint_pass!(CollectionIsNeverRead => [COLLECTION_IS_NEVER_READ]);
46
47 // Add `String` here when it is added to diagnostic items
48 static COLLECTIONS: [Symbol; 9] = [
49 sym::BTreeMap,
50 sym::BTreeSet,
51 sym::BinaryHeap,
52 sym::HashMap,
53 sym::HashSet,
54 sym::LinkedList,
55 sym::Option,
56 sym::Vec,
57 sym::VecDeque,
58 ];
59
60 impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead {
check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>)61 fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) {
62 // Look for local variables whose type is a container. Search surrounding bock for read access.
63 if match_acceptable_type(cx, local, &COLLECTIONS)
64 && let PatKind::Binding(_, local_id, _, _) = local.pat.kind
65 && let Some(enclosing_block) = get_enclosing_block(cx, local.hir_id)
66 && has_no_read_access(cx, local_id, enclosing_block)
67 {
68 span_lint(cx, COLLECTION_IS_NEVER_READ, local.span, "collection is never read");
69 }
70 }
71 }
72
match_acceptable_type(cx: &LateContext<'_>, local: &Local<'_>, collections: &[rustc_span::Symbol]) -> bool73 fn match_acceptable_type(cx: &LateContext<'_>, local: &Local<'_>, collections: &[rustc_span::Symbol]) -> bool {
74 let ty = cx.typeck_results().pat_ty(local.pat);
75 collections.iter().any(|&sym| is_type_diagnostic_item(cx, ty, sym))
76 // String type is a lang item but not a diagnostic item for now so we need a separate check
77 || is_type_lang_item(cx, ty, LangItem::String)
78 }
79
has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Block<'tcx>) -> bool80 fn has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Block<'tcx>) -> bool {
81 let mut has_access = false;
82 let mut has_read_access = false;
83
84 // Inspect all expressions and sub-expressions in the block.
85 for_each_expr_with_closures(cx, block, |expr| {
86 // Ignore expressions that are not simply `id`.
87 if !path_to_local_id(expr, id) {
88 return ControlFlow::Continue(());
89 }
90
91 // `id` is being accessed. Investigate if it's a read access.
92 has_access = true;
93
94 // `id` appearing in the left-hand side of an assignment is not a read access:
95 //
96 // id = ...; // Not reading `id`.
97 if let Some(Node::Expr(parent)) = get_parent_node(cx.tcx, expr.hir_id)
98 && let ExprKind::Assign(lhs, ..) = parent.kind
99 && path_to_local_id(lhs, id)
100 {
101 return ControlFlow::Continue(());
102 }
103
104 // Look for method call with receiver `id`. It might be a non-read access:
105 //
106 // id.foo(args)
107 //
108 // Only assuming this for "official" methods defined on the type. For methods defined in extension
109 // traits (identified as local, based on the orphan rule), pessimistically assume that they might
110 // have side effects, so consider them a read.
111 if let Some(Node::Expr(parent)) = get_parent_node(cx.tcx, expr.hir_id)
112 && let ExprKind::MethodCall(_, receiver, _, _) = parent.kind
113 && path_to_local_id(receiver, id)
114 && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id)
115 && !method_def_id.is_local()
116 {
117 // The method call is a statement, so the return value is not used. That's not a read access:
118 //
119 // id.foo(args);
120 if let Some(Node::Stmt(..)) = get_parent_node(cx.tcx, parent.hir_id) {
121 return ControlFlow::Continue(());
122 }
123
124 // The method call is not a statement, so its return value is used somehow but its type is the
125 // unit type, so this is not a real read access. Examples:
126 //
127 // let y = x.clear();
128 // println!("{:?}", x.clear());
129 if cx.typeck_results().expr_ty(parent).is_unit() {
130 return ControlFlow::Continue(());
131 }
132 }
133
134 // Any other access to `id` is a read access. Stop searching.
135 has_read_access = true;
136 ControlFlow::Break(())
137 });
138
139 // Ignore collections that have no access at all. Other lints should catch them.
140 has_access && !has_read_access
141 }
142