Skip to content

Commit 41fa24c

Browse files
committed
Auto merge of rust-lang#10415 - schubart:collection_is_never_read, r=llogiq
Add `collection_is_never_read` Fixes rust-lang#9267 `@flip1995` and `@llogiq,` I talked with you about this one at Rust Nation in London last week. :-) This is my first contribution to Clippy, so lots of feedback would be greatly appreciated. - \[ ] Followed [lint naming conventions][lint_naming] - \[x] Added passing UI tests (including committed `.stderr` file) - \[x] `cargo test` passes locally - \[x] Executed `cargo dev update_lints` - \[x] Added lint documentation - \[x] Run `cargo dev fmt` `dogfood` found one true positive (see rust-lang#9509) and no false positives. `lintcheck` found no (true or false) positives, even when running on an extended set of crates. --- changelog: new lint [`collection_is_never_read`] [rust-lang#10415](rust-lang/rust-clippy#10415) <!-- changelog_checked -->
2 parents 9035958 + 4ee6553 commit 41fa24c

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4452,6 +4452,7 @@ Released 2018-09-13
44524452
[`collapsible_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
44534453
[`collapsible_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_match
44544454
[`collapsible_str_replace`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_str_replace
4455+
[`collection_is_never_read`]: https://rust-lang.github.io/rust-clippy/master/index.html#collection_is_never_read
44554456
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
44564457
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
44574458
[`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::ty::is_type_diagnostic_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, 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.69.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+
static COLLECTIONS: [Symbol; 10] = [
48+
sym::BTreeMap,
49+
sym::BTreeSet,
50+
sym::BinaryHeap,
51+
sym::HashMap,
52+
sym::HashSet,
53+
sym::LinkedList,
54+
sym::Option,
55+
sym::String,
56+
sym::Vec,
57+
sym::VecDeque,
58+
];
59+
60+
impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead {
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+
let ty = cx.typeck_results().pat_ty(local.pat);
64+
if COLLECTIONS.iter().any(|&sym| is_type_diagnostic_item(cx, ty, sym))
65+
&& let PatKind::Binding(_, local_id, _, _) = local.pat.kind
66+
&& let Some(enclosing_block) = get_enclosing_block(cx, local.hir_id)
67+
&& has_no_read_access(cx, local_id, enclosing_block)
68+
{
69+
span_lint(cx, COLLECTION_IS_NEVER_READ, local.span, "collection is never read");
70+
}
71+
}
72+
}
73+
74+
fn has_no_read_access<'tcx>(cx: &LateContext<'tcx>, id: HirId, block: &'tcx Block<'tcx>) -> bool {
75+
let mut has_access = false;
76+
let mut has_read_access = false;
77+
78+
// Inspect all expressions and sub-expressions in the block.
79+
for_each_expr_with_closures(cx, block, |expr| {
80+
// Ignore expressions that are not simply `id`.
81+
if !path_to_local_id(expr, id) {
82+
return ControlFlow::Continue(());
83+
}
84+
85+
// `id` is being accessed. Investigate if it's a read access.
86+
has_access = true;
87+
88+
// `id` appearing in the left-hand side of an assignment is not a read access:
89+
//
90+
// id = ...; // Not reading `id`.
91+
if let Some(Node::Expr(parent)) = get_parent_node(cx.tcx, expr.hir_id)
92+
&& let ExprKind::Assign(lhs, ..) = parent.kind
93+
&& path_to_local_id(lhs, id)
94+
{
95+
return ControlFlow::Continue(());
96+
}
97+
98+
// Method call on `id` in a statement ignores any return value, so it's not a read access:
99+
//
100+
// id.foo(...); // Not reading `id`.
101+
//
102+
// Only assuming this for "official" methods defined on the type. For methods defined in extension
103+
// traits (identified as local, based on the orphan rule), pessimistically assume that they might
104+
// have side effects, so consider them a read.
105+
if let Some(Node::Expr(parent)) = get_parent_node(cx.tcx, expr.hir_id)
106+
&& let ExprKind::MethodCall(_, receiver, _, _) = parent.kind
107+
&& path_to_local_id(receiver, id)
108+
&& let Some(Node::Stmt(..)) = get_parent_node(cx.tcx, parent.hir_id)
109+
&& let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id)
110+
&& !method_def_id.is_local()
111+
{
112+
return ControlFlow::Continue(());
113+
}
114+
115+
// Any other access to `id` is a read access. Stop searching.
116+
has_read_access = true;
117+
ControlFlow::Break(())
118+
});
119+
120+
// Ignore collections that have no access at all. Other lints should catch them.
121+
has_access && !has_read_access
122+
}

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
9292
crate::cognitive_complexity::COGNITIVE_COMPLEXITY_INFO,
9393
crate::collapsible_if::COLLAPSIBLE_ELSE_IF_INFO,
9494
crate::collapsible_if::COLLAPSIBLE_IF_INFO,
95+
crate::collection_is_never_read::COLLECTION_IS_NEVER_READ_INFO,
9596
crate::comparison_chain::COMPARISON_CHAIN_INFO,
9697
crate::copies::BRANCHES_SHARING_CODE_INFO,
9798
crate::copies::IFS_SAME_COND_INFO,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ mod casts;
8787
mod checked_conversions;
8888
mod cognitive_complexity;
8989
mod collapsible_if;
90+
mod collection_is_never_read;
9091
mod comparison_chain;
9192
mod copies;
9293
mod copy_iterator;
@@ -924,6 +925,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
924925
))
925926
});
926927
store.register_late_pass(|_| Box::new(no_mangle_with_rust_abi::NoMangleWithRustAbi));
928+
store.register_late_pass(|_| Box::new(collection_is_never_read::CollectionIsNeverRead));
927929
// add lints here, do not remove this comment, it's used in `new_lint`
928930
}
929931

tests/ui/collection_is_never_read.rs

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#![allow(unused)]
2+
#![warn(clippy::collection_is_never_read)]
3+
4+
use std::collections::{HashMap, HashSet};
5+
6+
fn main() {}
7+
8+
fn not_a_collection() {
9+
// TODO: Expand `collection_is_never_read` beyond collections?
10+
let mut x = 10; // Ok
11+
x += 1;
12+
}
13+
14+
fn no_access_at_all() {
15+
// Other lints should catch this.
16+
let x = vec![1, 2, 3]; // Ok
17+
}
18+
19+
fn write_without_read() {
20+
// The main use case for `collection_is_never_read`.
21+
let mut x = HashMap::new(); // WARNING
22+
x.insert(1, 2);
23+
}
24+
25+
fn read_without_write() {
26+
let mut x = vec![1, 2, 3]; // Ok
27+
let _ = x.len();
28+
}
29+
30+
fn write_and_read() {
31+
let mut x = vec![1, 2, 3]; // Ok
32+
x.push(4);
33+
let _ = x.len();
34+
}
35+
36+
fn write_after_read() {
37+
// TODO: Warn here, but this requires more extensive data flow analysis.
38+
let mut x = vec![1, 2, 3]; // Ok
39+
let _ = x.len();
40+
x.push(4); // Pointless
41+
}
42+
43+
fn write_before_reassign() {
44+
// TODO: Warn here, but this requires more extensive data flow analysis.
45+
let mut x = HashMap::new(); // Ok
46+
x.insert(1, 2); // Pointless
47+
x = HashMap::new();
48+
let _ = x.len();
49+
}
50+
51+
fn read_in_closure() {
52+
let mut x = HashMap::new(); // Ok
53+
x.insert(1, 2);
54+
let _ = || {
55+
let _ = x.len();
56+
};
57+
}
58+
59+
fn write_in_closure() {
60+
let mut x = vec![1, 2, 3]; // WARNING
61+
let _ = || {
62+
x.push(4);
63+
};
64+
}
65+
66+
fn read_in_format() {
67+
let mut x = HashMap::new(); // Ok
68+
x.insert(1, 2);
69+
format!("{x:?}");
70+
}
71+
72+
fn shadowing_1() {
73+
let x = HashMap::<usize, usize>::new(); // Ok
74+
let _ = x.len();
75+
let mut x = HashMap::new(); // WARNING
76+
x.insert(1, 2);
77+
}
78+
79+
fn shadowing_2() {
80+
let mut x = HashMap::new(); // WARNING
81+
x.insert(1, 2);
82+
let x = HashMap::<usize, usize>::new(); // Ok
83+
let _ = x.len();
84+
}
85+
86+
#[allow(clippy::let_unit_value)]
87+
fn fake_read() {
88+
let mut x = vec![1, 2, 3]; // Ok
89+
x.reverse();
90+
// `collection_is_never_read` gets fooled, but other lints should catch this.
91+
let _: () = x.clear();
92+
}
93+
94+
fn assignment() {
95+
let mut x = vec![1, 2, 3]; // WARNING
96+
let y = vec![4, 5, 6]; // Ok
97+
x = y;
98+
}
99+
100+
#[allow(clippy::self_assignment)]
101+
fn self_assignment() {
102+
let mut x = vec![1, 2, 3]; // WARNING
103+
x = x;
104+
}
105+
106+
fn method_argument_but_not_target() {
107+
struct MyStruct;
108+
impl MyStruct {
109+
fn my_method(&self, _argument: &[usize]) {}
110+
}
111+
let my_struct = MyStruct;
112+
113+
let mut x = vec![1, 2, 3]; // Ok
114+
x.reverse();
115+
my_struct.my_method(&x);
116+
}
117+
118+
fn insert_is_not_a_read() {
119+
let mut x = HashSet::new(); // WARNING
120+
x.insert(5);
121+
}
122+
123+
fn insert_is_a_read() {
124+
let mut x = HashSet::new(); // Ok
125+
if x.insert(5) {
126+
println!("5 was inserted");
127+
}
128+
}
129+
130+
fn not_read_if_return_value_not_used() {
131+
// `is_empty` does not modify the set, so it's a query. But since the return value is not used, the
132+
// lint does not consider it a read here.
133+
let x = vec![1, 2, 3]; // WARNING
134+
x.is_empty();
135+
}
136+
137+
fn extension_traits() {
138+
trait VecExt<T> {
139+
fn method_with_side_effect(&self);
140+
fn method_without_side_effect(&self);
141+
}
142+
143+
impl<T> VecExt<T> for Vec<T> {
144+
fn method_with_side_effect(&self) {
145+
println!("my length: {}", self.len());
146+
}
147+
fn method_without_side_effect(&self) {}
148+
}
149+
150+
let x = vec![1, 2, 3]; // Ok
151+
x.method_with_side_effect();
152+
153+
let y = vec![1, 2, 3]; // Ok (false negative)
154+
y.method_without_side_effect();
155+
}
156+
157+
fn function_argument() {
158+
#[allow(clippy::ptr_arg)]
159+
fn foo<T>(v: &Vec<T>) -> usize {
160+
v.len()
161+
}
162+
163+
let x = vec![1, 2, 3]; // Ok
164+
foo(&x);
165+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
error: collection is never read
2+
--> $DIR/collection_is_never_read.rs:21:5
3+
|
4+
LL | let mut x = HashMap::new(); // WARNING
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::collection-is-never-read` implied by `-D warnings`
8+
9+
error: collection is never read
10+
--> $DIR/collection_is_never_read.rs:60:5
11+
|
12+
LL | let mut x = vec![1, 2, 3]; // WARNING
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
15+
error: collection is never read
16+
--> $DIR/collection_is_never_read.rs:75:5
17+
|
18+
LL | let mut x = HashMap::new(); // WARNING
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
21+
error: collection is never read
22+
--> $DIR/collection_is_never_read.rs:80:5
23+
|
24+
LL | let mut x = HashMap::new(); // WARNING
25+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
27+
error: collection is never read
28+
--> $DIR/collection_is_never_read.rs:95:5
29+
|
30+
LL | let mut x = vec![1, 2, 3]; // WARNING
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
33+
error: collection is never read
34+
--> $DIR/collection_is_never_read.rs:102:5
35+
|
36+
LL | let mut x = vec![1, 2, 3]; // WARNING
37+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
38+
39+
error: collection is never read
40+
--> $DIR/collection_is_never_read.rs:119:5
41+
|
42+
LL | let mut x = HashSet::new(); // WARNING
43+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
44+
45+
error: collection is never read
46+
--> $DIR/collection_is_never_read.rs:133:5
47+
|
48+
LL | let x = vec![1, 2, 3]; // WARNING
49+
| ^^^^^^^^^^^^^^^^^^^^^^
50+
51+
error: aborting due to 8 previous errors
52+

0 commit comments

Comments
 (0)