Skip to content

Commit 61197cc

Browse files
committed
new lint hashset_insert_after_contains
1 parent 1eb254e commit 61197cc

File tree

6 files changed

+288
-0
lines changed

6 files changed

+288
-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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
/// if set.insert(&value) {
32+
/// println!("inserted {value:?}");
33+
/// }
34+
/// ```
35+
#[clippy::version = "1.73.0"]
36+
pub HASHSET_INSERT_AFTER_CONTAINS,
37+
perf,
38+
"use of `contains` to see if a value is not present on a `HashSet` followed by a `insert`"
39+
}
40+
declare_lint_pass!(HashsetInsertAfterContains => [HASHSET_INSERT_AFTER_CONTAINS]);
41+
42+
impl<'tcx> LateLintPass<'tcx> for HashsetInsertAfterContains {
43+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
44+
if expr.span.from_expansion() {
45+
return;
46+
}
47+
48+
let Some(higher::If {
49+
cond: cond_expr,
50+
then: then_expr,
51+
..
52+
}) = higher::If::hir(expr)
53+
else {
54+
return;
55+
};
56+
57+
let Some(contains_expr) = try_parse_contains(cx, cond_expr) else {
58+
return;
59+
};
60+
61+
if !find_insert_calls(cx, &contains_expr, then_expr) {
62+
return;
63+
};
64+
span_lint(
65+
cx,
66+
HASHSET_INSERT_AFTER_CONTAINS,
67+
expr.span,
68+
"usage of `HashSet::insert` after `HashSet::contains`. Remove the usage of `HashSet::contains` and just call `HashSet::contains` instead",
69+
);
70+
}
71+
}
72+
73+
struct ContainsExpr<'tcx> {
74+
receiver: &'tcx Expr<'tcx>,
75+
value: &'tcx Expr<'tcx>,
76+
}
77+
fn try_parse_contains<'tcx>(cx: &LateContext<'_>, expr: &'tcx Expr<'_>) -> Option<ContainsExpr<'tcx>> {
78+
let expr = peel_hir_expr_while(expr, |e| match e.kind {
79+
ExprKind::Unary(UnOp::Not, e) => Some(e),
80+
_ => None,
81+
});
82+
match expr.kind {
83+
ExprKind::MethodCall(
84+
path,
85+
receiver,
86+
[
87+
Expr {
88+
kind: ExprKind::AddrOf(_, _, value),
89+
span: value_span,
90+
..
91+
},
92+
],
93+
_,
94+
) => {
95+
let receiver_ty = cx.typeck_results().expr_ty(receiver);
96+
if value_span.ctxt() == expr.span.ctxt()
97+
&& is_type_diagnostic_item(cx, receiver_ty, sym::HashSet)
98+
&& path.ident.name == sym!(contains)
99+
{
100+
Some(ContainsExpr { receiver, value })
101+
} else {
102+
None
103+
}
104+
},
105+
_ => None,
106+
}
107+
}
108+
109+
struct InsertExpr<'tcx> {
110+
receiver: &'tcx Expr<'tcx>,
111+
value: &'tcx Expr<'tcx>,
112+
}
113+
fn try_parse_insert<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<InsertExpr<'tcx>> {
114+
if let ExprKind::MethodCall(path, receiver, [value], _) = expr.kind {
115+
let receiver_ty = cx.typeck_results().expr_ty(receiver);
116+
if is_type_diagnostic_item(cx, receiver_ty, sym::HashSet) && path.ident.name == sym!(insert) {
117+
Some(InsertExpr { receiver, value })
118+
} else {
119+
None
120+
}
121+
} else {
122+
None
123+
}
124+
}
125+
126+
fn find_insert_calls<'tcx>(cx: &LateContext<'tcx>, contains_expr: &ContainsExpr<'tcx>, expr: &'tcx Expr<'_>) -> bool {
127+
let mut s = InsertSearcher {
128+
cx,
129+
receiver: contains_expr.receiver,
130+
value: contains_expr.value,
131+
should_lint: false,
132+
};
133+
s.visit_expr(expr);
134+
s.should_lint
135+
}
136+
137+
struct InsertSearcher<'cx, 'tcx> {
138+
cx: &'cx LateContext<'tcx>,
139+
/// The receiver expression used in the contains call.
140+
receiver: &'tcx Expr<'tcx>,
141+
/// The value expression used in the contains call.
142+
value: &'tcx Expr<'tcx>,
143+
/// Whether or a lint shoud be emitted.
144+
should_lint: bool,
145+
}
146+
147+
impl<'tcx> Visitor<'tcx> for InsertSearcher<'_, 'tcx> {
148+
fn visit_block(&mut self, block: &'tcx Block<'_>) {
149+
for stmt in block.stmts {
150+
self.visit_stmt(stmt);
151+
}
152+
if let Some(expr) = block.expr {
153+
self.visit_expr(expr);
154+
}
155+
}
156+
157+
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
158+
match try_parse_insert(self.cx, expr) {
159+
Some(insert_expr) => {
160+
if SpanlessEq::new(self.cx).eq_expr(self.receiver, insert_expr.receiver)
161+
&& SpanlessEq::new(self.cx).eq_expr(self.value, insert_expr.value)
162+
{
163+
self.should_lint = true;
164+
}
165+
},
166+
_ => {
167+
if let ExprKind::Block(block, _) = expr.kind {
168+
self.visit_block(block);
169+
}
170+
},
171+
}
172+
}
173+
174+
fn visit_stmt(&mut self, stmt: &'tcx Stmt<'_>) {
175+
match stmt.kind {
176+
StmtKind::Semi(e) => {
177+
self.visit_expr(e);
178+
},
179+
StmtKind::Expr(e) => self.visit_expr(e),
180+
_ => (),
181+
}
182+
}
183+
}

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)