Skip to content

Commit 4dff96e

Browse files
committed
new lint hashset_insert_after_contains
1 parent 1eb254e commit 4dff96e

File tree

6 files changed

+289
-0
lines changed

6 files changed

+289
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4880,6 +4880,7 @@ Released 2018-09-13
48804880
[`get_first`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_first
48814881
[`get_last_with_len`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_last_with_len
48824882
[`get_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_unwrap
4883+
[`hashset_insert_after_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#hashset_insert_after_contains
48834884
[`host_endian_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#host_endian_bytes
48844885
[`identity_conversion`]: https://rust-lang.github.io/rust-clippy/master/index.html#identity_conversion
48854886
[`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
@@ -200,6 +200,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
200200
crate::functions::TOO_MANY_ARGUMENTS_INFO,
201201
crate::functions::TOO_MANY_LINES_INFO,
202202
crate::future_not_send::FUTURE_NOT_SEND_INFO,
203+
crate::hashset_insert_after_contains::HASHSET_INSERT_AFTER_CONTAINS_INFO,
203204
crate::if_let_mutex::IF_LET_MUTEX_INFO,
204205
crate::if_not_else::IF_NOT_ELSE_INFO,
205206
crate::if_then_some_else_none::IF_THEN_SOME_ELSE_NONE_INFO,
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use clippy_utils::ty::is_type_diagnostic_item;
3+
use clippy_utils::{higher, peel_hir_expr_while, SpanlessEq};
4+
use rustc_hir::intravisit::Visitor;
5+
use rustc_hir::{Block, Expr, ExprKind, Stmt, StmtKind, UnOp};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::{declare_lint_pass, declare_tool_lint};
8+
use rustc_span::sym;
9+
10+
declare_clippy_lint! {
11+
/// Checks for usage of `contains` to see if a value is not
12+
/// present on `HashSet` followed by a `insert`.
13+
///
14+
/// ### Why is this bad?
15+
/// Using just `insert` and checking the returned `bool` is more efficient.
16+
///
17+
/// ### Example
18+
/// ```rust
19+
/// use std::collections::HashSet;
20+
/// let mut set = HashSet::new();
21+
/// let value = 5;
22+
/// if !set.contains(&value) {
23+
/// set.insert(value);
24+
/// println!("inserted {value:?}");
25+
/// }
26+
/// ```
27+
/// Use instead:
28+
/// ```rust
29+
/// use std::collections::HashSet;
30+
/// let mut set = HashSet::new();
31+
/// let value = 5;
32+
/// if set.insert(&value) {
33+
/// println!("inserted {value:?}");
34+
/// }
35+
/// ```
36+
#[clippy::version = "1.73.0"]
37+
pub HASHSET_INSERT_AFTER_CONTAINS,
38+
perf,
39+
"use of `contains` to see if a value is not present on a `HashSet` followed by a `insert`"
40+
}
41+
declare_lint_pass!(HashsetInsertAfterContains => [HASHSET_INSERT_AFTER_CONTAINS]);
42+
43+
impl<'tcx> LateLintPass<'tcx> for HashsetInsertAfterContains {
44+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
45+
if expr.span.from_expansion() {
46+
return;
47+
}
48+
49+
let Some(higher::If {
50+
cond: cond_expr,
51+
then: then_expr,
52+
..
53+
}) = higher::If::hir(expr)
54+
else {
55+
return;
56+
};
57+
58+
let Some(contains_expr) = try_parse_contains(cx, cond_expr) else {
59+
return;
60+
};
61+
62+
if !find_insert_calls(cx, &contains_expr, then_expr) {
63+
return;
64+
};
65+
span_lint(
66+
cx,
67+
HASHSET_INSERT_AFTER_CONTAINS,
68+
expr.span,
69+
"usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead",
70+
);
71+
}
72+
}
73+
74+
struct ContainsExpr<'tcx> {
75+
receiver: &'tcx Expr<'tcx>,
76+
value: &'tcx Expr<'tcx>,
77+
}
78+
fn try_parse_contains<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<ContainsExpr<'tcx>> {
79+
let expr = peel_hir_expr_while(expr, |e| match e.kind {
80+
ExprKind::Unary(UnOp::Not, e) => Some(e),
81+
_ => None,
82+
});
83+
match expr.kind {
84+
ExprKind::MethodCall(
85+
path,
86+
receiver,
87+
[
88+
Expr {
89+
kind: ExprKind::AddrOf(_, _, value),
90+
span: value_span,
91+
..
92+
},
93+
],
94+
_,
95+
) => {
96+
let receiver_ty = cx.typeck_results().expr_ty(receiver);
97+
if value_span.ctxt() == expr.span.ctxt()
98+
&& is_type_diagnostic_item(cx, receiver_ty, sym::HashSet)
99+
&& path.ident.name == sym!(contains)
100+
{
101+
Some(ContainsExpr { receiver, value })
102+
} else {
103+
None
104+
}
105+
},
106+
_ => None,
107+
}
108+
}
109+
110+
struct InsertExpr<'tcx> {
111+
receiver: &'tcx Expr<'tcx>,
112+
value: &'tcx Expr<'tcx>,
113+
}
114+
fn try_parse_insert<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<InsertExpr<'tcx>> {
115+
if let ExprKind::MethodCall(path, receiver, [value], _) = expr.kind {
116+
let receiver_ty = cx.typeck_results().expr_ty(receiver);
117+
if is_type_diagnostic_item(cx, receiver_ty, sym::HashSet) && path.ident.name == sym!(insert) {
118+
Some(InsertExpr { receiver, value })
119+
} else {
120+
None
121+
}
122+
} else {
123+
None
124+
}
125+
}
126+
127+
fn find_insert_calls<'tcx>(cx: &LateContext<'tcx>, contains_expr: &ContainsExpr<'tcx>, expr: &'tcx Expr<'_>) -> bool {
128+
let mut s = InsertSearcher {
129+
cx,
130+
receiver: contains_expr.receiver,
131+
value: contains_expr.value,
132+
should_lint: false,
133+
};
134+
s.visit_expr(expr);
135+
s.should_lint
136+
}
137+
138+
struct InsertSearcher<'cx, 'tcx> {
139+
cx: &'cx LateContext<'tcx>,
140+
/// The receiver expression used in the contains call.
141+
receiver: &'tcx Expr<'tcx>,
142+
/// The value expression used in the contains call.
143+
value: &'tcx Expr<'tcx>,
144+
/// Whether or a lint shoud be emitted.
145+
should_lint: bool,
146+
}
147+
148+
impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
149+
fn visit_block(&mut self, block: &'tcx Block<'_>) {
150+
for stmt in block.stmts {
151+
self.visit_stmt(stmt);
152+
}
153+
if let Some(expr) = block.expr {
154+
self.visit_expr(expr);
155+
}
156+
}
157+
158+
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
159+
match try_parse_insert(self.cx, expr) {
160+
Some(insert_expr) => {
161+
if SpanlessEq::new(self.cx).eq_expr(self.receiver, insert_expr.receiver)
162+
&& SpanlessEq::new(self.cx).eq_expr(self.value, insert_expr.value)
163+
{
164+
self.should_lint = true;
165+
}
166+
},
167+
_ => {
168+
if let ExprKind::Block(block, _) = expr.kind {
169+
self.visit_block(block);
170+
}
171+
},
172+
}
173+
}
174+
175+
fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) {
176+
match stmt.kind {
177+
StmtKind::Semi(e) => {
178+
self.visit_expr(e);
179+
},
180+
StmtKind::Expr(e) => self.visit_expr(e),
181+
_ => (),
182+
}
183+
}
184+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ mod from_raw_with_void_ptr;
144144
mod from_str_radix_10;
145145
mod functions;
146146
mod future_not_send;
147+
mod hashset_insert_after_contains;
147148
mod if_let_mutex;
148149
mod if_not_else;
149150
mod if_then_some_else_none;
@@ -1095,6 +1096,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10951096
});
10961097
store.register_late_pass(|_| Box::new(redundant_locals::RedundantLocals));
10971098
store.register_late_pass(|_| Box::new(ignored_unit_patterns::IgnoredUnitPatterns));
1099+
store.register_late_pass(|_| Box::new(hashset_insert_after_contains::HashsetInsertAfterContains));
10981100
// add lints here, do not remove this comment, it's used in `new_lint`
10991101
}
11001102

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#![allow(unused)]
2+
#![allow(clippy::nonminimal_bool)]
3+
#![warn(clippy::hashset_insert_after_contains)]
4+
5+
use std::collections::HashSet;
6+
7+
fn main() {
8+
should_warn_cases();
9+
10+
should_not_warn_cases();
11+
}
12+
13+
fn should_warn_cases() {
14+
let mut set = HashSet::new();
15+
let value = 5;
16+
17+
if !set.contains(&value) {
18+
set.insert(value);
19+
println!("Just a comment");
20+
}
21+
22+
if set.contains(&value) {
23+
set.insert(value);
24+
println!("Just a comment");
25+
}
26+
27+
if !set.contains(&value) {
28+
set.insert(value);
29+
}
30+
31+
if !!set.contains(&value) {
32+
set.insert(value);
33+
println!("Just a comment");
34+
}
35+
}
36+
37+
fn should_not_warn_cases() {
38+
let mut set = HashSet::new();
39+
let value = 5;
40+
let another_value = 6;
41+
42+
if !set.contains(&value) {
43+
set.insert(another_value);
44+
}
45+
46+
if !set.contains(&value) {
47+
println!("Just a comment");
48+
}
49+
50+
if simply_true() {
51+
set.insert(value);
52+
}
53+
54+
if !set.contains(&value) {
55+
set.replace(value); //it is not insert
56+
println!("Just a comment");
57+
}
58+
}
59+
60+
fn simply_true() -> bool {
61+
true
62+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
error: usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead
2+
--> $DIR/hashset_insert_after_contains.rs:17: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: `-D clippy::hashset-insert-after-contains` implied by `-D warnings`
11+
12+
error: usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead
13+
--> $DIR/hashset_insert_after_contains.rs:22:5
14+
|
15+
LL | / if set.contains(&value) {
16+
LL | | set.insert(value);
17+
LL | | println!("Just a comment");
18+
LL | | }
19+
| |_____^
20+
21+
error: usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead
22+
--> $DIR/hashset_insert_after_contains.rs:27:5
23+
|
24+
LL | / if !set.contains(&value) {
25+
LL | | set.insert(value);
26+
LL | | }
27+
| |_____^
28+
29+
error: usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead
30+
--> $DIR/hashset_insert_after_contains.rs:31:5
31+
|
32+
LL | / if !!set.contains(&value) {
33+
LL | | set.insert(value);
34+
LL | | println!("Just a comment");
35+
LL | | }
36+
| |_____^
37+
38+
error: aborting due to 4 previous errors
39+

0 commit comments

Comments
 (0)