Skip to content

Commit fb239f3

Browse files
committed
Add new lint hashset_insert_after_contains
1 parent e7efe43 commit fb239f3

6 files changed

+330
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5342,6 +5342,7 @@ Released 2018-09-13
53425342
[`get_first`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_first
53435343
[`get_last_with_len`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_last_with_len
53445344
[`get_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_unwrap
5345+
[`hashset_insert_after_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#hashset_insert_after_contains
53455346
[`host_endian_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#host_endian_bytes
53465347
[`identity_conversion`]: https://rust-lang.github.io/rust-clippy/master/index.html#identity_conversion
53475348
[`identity_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#identity_op

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
212212
crate::functions::TOO_MANY_ARGUMENTS_INFO,
213213
crate::functions::TOO_MANY_LINES_INFO,
214214
crate::future_not_send::FUTURE_NOT_SEND_INFO,
215+
crate::hashset_insert_after_contains::HASHSET_INSERT_AFTER_CONTAINS_INFO,
215216
crate::if_let_mutex::IF_LET_MUTEX_INFO,
216217
crate::if_not_else::IF_NOT_ELSE_INFO,
217218
crate::if_then_some_else_none::IF_THEN_SOME_ELSE_NONE_INFO,
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::ops::ControlFlow;
2+
3+
use clippy_utils::diagnostics::span_lint_and_then;
4+
use clippy_utils::ty::is_type_diagnostic_item;
5+
use clippy_utils::visitors::for_each_expr;
6+
use clippy_utils::{higher, peel_hir_expr_while, SpanlessEq};
7+
use rustc_hir::{Expr, ExprKind, UnOp};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::declare_lint_pass;
10+
use rustc_span::{sym, Span};
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Checks for usage of `contains` to see if a value is not
15+
/// present on `HashSet` followed by a `insert`.
16+
///
17+
/// ### Why is this bad?
18+
/// Using just `insert` and checking the returned `bool` is more efficient.
19+
///
20+
/// ### Example
21+
/// ```rust
22+
/// use std::collections::HashSet;
23+
/// let mut set = HashSet::new();
24+
/// let value = 5;
25+
/// if !set.contains(&value) {
26+
/// set.insert(value);
27+
/// println!("inserted {value:?}");
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// use std::collections::HashSet;
33+
/// let mut set = HashSet::new();
34+
/// let value = 5;
35+
/// if set.insert(&value) {
36+
/// println!("inserted {value:?}");
37+
/// }
38+
/// ```
39+
#[clippy::version = "1.80.0"]
40+
pub HASHSET_INSERT_AFTER_CONTAINS,
41+
nursery,
42+
"unnecessary call to `HashSet::contains` followed by `HashSet::insert`"
43+
}
44+
45+
declare_lint_pass!(HashsetInsertAfterContains => [HASHSET_INSERT_AFTER_CONTAINS]);
46+
47+
impl<'tcx> LateLintPass<'tcx> for HashsetInsertAfterContains {
48+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
49+
if !expr.span.from_expansion()
50+
&& let Some(higher::If {
51+
cond: cond_expr,
52+
then: then_expr,
53+
..
54+
}) = higher::If::hir(expr)
55+
&& let Some(contains_expr) = try_parse_contains(cx, cond_expr)
56+
&& find_insert_calls(cx, &contains_expr, then_expr)
57+
{
58+
span_lint_and_then(
59+
cx,
60+
HASHSET_INSERT_AFTER_CONTAINS,
61+
expr.span,
62+
"usage of `HashSet::insert` after `HashSet::contains`",
63+
|diag| {
64+
diag.note("`HashSet::insert` returns whether it was inserted")
65+
.span_help(contains_expr.span, "remove the `HashSet::contains` call");
66+
},
67+
);
68+
}
69+
}
70+
}
71+
72+
struct ContainsExpr<'tcx> {
73+
receiver: &'tcx Expr<'tcx>,
74+
value: &'tcx Expr<'tcx>,
75+
span: Span,
76+
}
77+
fn try_parse_contains<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<ContainsExpr<'tcx>> {
78+
let expr = peel_hir_expr_while(expr, |e| {
79+
if let ExprKind::Unary(UnOp::Not, e) = e.kind {
80+
Some(e)
81+
} else {
82+
None
83+
}
84+
});
85+
if let ExprKind::MethodCall(path, receiver, [value], span) = expr.kind {
86+
let value = value.peel_borrows();
87+
let receiver = receiver.peel_borrows();
88+
let receiver_ty = cx.typeck_results().expr_ty(receiver).peel_refs();
89+
if value.span.eq_ctxt(expr.span)
90+
&& is_type_diagnostic_item(cx, receiver_ty, sym::HashSet)
91+
&& path.ident.name == sym!(contains)
92+
{
93+
return Some(ContainsExpr { receiver, value, span });
94+
}
95+
}
96+
None
97+
}
98+
99+
struct InsertExpr<'tcx> {
100+
receiver: &'tcx Expr<'tcx>,
101+
value: &'tcx Expr<'tcx>,
102+
}
103+
fn try_parse_insert<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<InsertExpr<'tcx>> {
104+
if let ExprKind::MethodCall(path, receiver, [value], _) = expr.kind {
105+
let value = value.peel_borrows();
106+
let value = peel_hir_expr_while(value, |e| {
107+
if let ExprKind::Unary(UnOp::Deref, e) = e.kind {
108+
Some(e)
109+
} else {
110+
None
111+
}
112+
});
113+
114+
let receiver_ty = cx.typeck_results().expr_ty(receiver).peel_refs();
115+
if is_type_diagnostic_item(cx, receiver_ty, sym::HashSet) && path.ident.name == sym!(insert) {
116+
Some(InsertExpr { receiver, value })
117+
} else {
118+
None
119+
}
120+
} else {
121+
None
122+
}
123+
}
124+
125+
fn find_insert_calls<'tcx>(cx: &LateContext<'tcx>, contains_expr: &ContainsExpr<'tcx>, expr: &'tcx Expr<'_>) -> bool {
126+
for_each_expr(expr, |e| {
127+
if let Some(insert_expr) = try_parse_insert(cx, e)
128+
&& SpanlessEq::new(cx).eq_expr(contains_expr.receiver, insert_expr.receiver)
129+
&& SpanlessEq::new(cx).eq_expr(contains_expr.value, insert_expr.value)
130+
{
131+
ControlFlow::Break(())
132+
} else {
133+
ControlFlow::Continue(())
134+
}
135+
})
136+
.is_some()
137+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ mod from_raw_with_void_ptr;
149149
mod from_str_radix_10;
150150
mod functions;
151151
mod future_not_send;
152+
mod hashset_insert_after_contains;
152153
mod if_let_mutex;
153154
mod if_not_else;
154155
mod if_then_some_else_none;
@@ -1165,6 +1166,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
11651166
..Default::default()
11661167
})
11671168
});
1169+
store.register_late_pass(|_| Box::new(hashset_insert_after_contains::HashsetInsertAfterContains));
11681170
// add lints here, do not remove this comment, it's used in `new_lint`
11691171
}
11701172

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#![allow(unused)]
2+
#![allow(clippy::nonminimal_bool)]
3+
#![allow(clippy::needless_borrow)]
4+
#![warn(clippy::hashset_insert_after_contains)]
5+
6+
use std::collections::HashSet;
7+
8+
fn main() {
9+
should_warn_cases();
10+
11+
should_not_warn_cases();
12+
}
13+
14+
fn should_warn_cases() {
15+
let mut set = HashSet::new();
16+
let value = 5;
17+
18+
if !set.contains(&value) {
19+
set.insert(value);
20+
println!("Just a comment");
21+
}
22+
23+
if set.contains(&value) {
24+
set.insert(value);
25+
println!("Just a comment");
26+
}
27+
28+
if !set.contains(&value) {
29+
set.insert(value);
30+
}
31+
32+
if !!set.contains(&value) {
33+
set.insert(value);
34+
println!("Just a comment");
35+
}
36+
37+
if (&set).contains(&value) {
38+
set.insert(value);
39+
}
40+
41+
let borrow_value = &6;
42+
if !set.contains(borrow_value) {
43+
set.insert(*borrow_value);
44+
}
45+
46+
let borrow_set = &mut set;
47+
if !borrow_set.contains(&value) {
48+
borrow_set.insert(value);
49+
}
50+
}
51+
52+
fn should_not_warn_cases() {
53+
let mut set = HashSet::new();
54+
let value = 5;
55+
let another_value = 6;
56+
57+
if !set.contains(&value) {
58+
set.insert(another_value);
59+
}
60+
61+
if !set.contains(&value) {
62+
println!("Just a comment");
63+
}
64+
65+
if simply_true() {
66+
set.insert(value);
67+
}
68+
69+
if !set.contains(&value) {
70+
set.replace(value); //it is not insert
71+
println!("Just a comment");
72+
}
73+
}
74+
75+
fn simply_true() -> bool {
76+
true
77+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
error: usage of `HashSet::insert` after `HashSet::contains`
2+
--> tests/ui/hashset_insert_after_contains.rs:18:5
3+
|
4+
LL | / if !set.contains(&value) {
5+
LL | | set.insert(value);
6+
LL | | println!("Just a comment");
7+
LL | | }
8+
| |_____^
9+
|
10+
= note: `HashSet::insert` returns whether it was inserted
11+
help: remove the `HashSet::contains` call
12+
--> tests/ui/hashset_insert_after_contains.rs:18:13
13+
|
14+
LL | if !set.contains(&value) {
15+
| ^^^^^^^^^^^^^^^^
16+
= note: `-D clippy::hashset-insert-after-contains` implied by `-D warnings`
17+
= help: to override `-D warnings` add `#[allow(clippy::hashset_insert_after_contains)]`
18+
19+
error: usage of `HashSet::insert` after `HashSet::contains`
20+
--> tests/ui/hashset_insert_after_contains.rs:23:5
21+
|
22+
LL | / if set.contains(&value) {
23+
LL | | set.insert(value);
24+
LL | | println!("Just a comment");
25+
LL | | }
26+
| |_____^
27+
|
28+
= note: `HashSet::insert` returns whether it was inserted
29+
help: remove the `HashSet::contains` call
30+
--> tests/ui/hashset_insert_after_contains.rs:23:12
31+
|
32+
LL | if set.contains(&value) {
33+
| ^^^^^^^^^^^^^^^^
34+
35+
error: usage of `HashSet::insert` after `HashSet::contains`
36+
--> tests/ui/hashset_insert_after_contains.rs:28:5
37+
|
38+
LL | / if !set.contains(&value) {
39+
LL | | set.insert(value);
40+
LL | | }
41+
| |_____^
42+
|
43+
= note: `HashSet::insert` returns whether it was inserted
44+
help: remove the `HashSet::contains` call
45+
--> tests/ui/hashset_insert_after_contains.rs:28:13
46+
|
47+
LL | if !set.contains(&value) {
48+
| ^^^^^^^^^^^^^^^^
49+
50+
error: usage of `HashSet::insert` after `HashSet::contains`
51+
--> tests/ui/hashset_insert_after_contains.rs:32:5
52+
|
53+
LL | / if !!set.contains(&value) {
54+
LL | | set.insert(value);
55+
LL | | println!("Just a comment");
56+
LL | | }
57+
| |_____^
58+
|
59+
= note: `HashSet::insert` returns whether it was inserted
60+
help: remove the `HashSet::contains` call
61+
--> tests/ui/hashset_insert_after_contains.rs:32:14
62+
|
63+
LL | if !!set.contains(&value) {
64+
| ^^^^^^^^^^^^^^^^
65+
66+
error: usage of `HashSet::insert` after `HashSet::contains`
67+
--> tests/ui/hashset_insert_after_contains.rs:37:5
68+
|
69+
LL | / if (&set).contains(&value) {
70+
LL | | set.insert(value);
71+
LL | | }
72+
| |_____^
73+
|
74+
= note: `HashSet::insert` returns whether it was inserted
75+
help: remove the `HashSet::contains` call
76+
--> tests/ui/hashset_insert_after_contains.rs:37:15
77+
|
78+
LL | if (&set).contains(&value) {
79+
| ^^^^^^^^^^^^^^^^
80+
81+
error: usage of `HashSet::insert` after `HashSet::contains`
82+
--> tests/ui/hashset_insert_after_contains.rs:42:5
83+
|
84+
LL | / if !set.contains(borrow_value) {
85+
LL | | set.insert(*borrow_value);
86+
LL | | }
87+
| |_____^
88+
|
89+
= note: `HashSet::insert` returns whether it was inserted
90+
help: remove the `HashSet::contains` call
91+
--> tests/ui/hashset_insert_after_contains.rs:42:13
92+
|
93+
LL | if !set.contains(borrow_value) {
94+
| ^^^^^^^^^^^^^^^^^^^^^^
95+
96+
error: usage of `HashSet::insert` after `HashSet::contains`
97+
--> tests/ui/hashset_insert_after_contains.rs:47:5
98+
|
99+
LL | / if !borrow_set.contains(&value) {
100+
LL | | borrow_set.insert(value);
101+
LL | | }
102+
| |_____^
103+
|
104+
= note: `HashSet::insert` returns whether it was inserted
105+
help: remove the `HashSet::contains` call
106+
--> tests/ui/hashset_insert_after_contains.rs:47:20
107+
|
108+
LL | if !borrow_set.contains(&value) {
109+
| ^^^^^^^^^^^^^^^^
110+
111+
error: aborting due to 7 previous errors
112+

0 commit comments

Comments
 (0)