Skip to content

Commit 6ce05bf

Browse files
committed
Auto merge of #5332 - DevinR528:if-let-else-mutex, r=flip1995
If let else mutex changelog: Adds lint to catch incorrect use of `Mutex::lock` in `if let` expressions with lock calls in any of the blocks. closes: #5219
2 parents 1336558 + 3fbe321 commit 6ce05bf

File tree

8 files changed

+248
-2
lines changed

8 files changed

+248
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ Released 2018-09-13
12861286
[`get_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#get_unwrap
12871287
[`identity_conversion`]: https://rust-lang.github.io/rust-clippy/master/index.html#identity_conversion
12881288
[`identity_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#identity_op
1289+
[`if_let_mutex`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_let_mutex
12891290
[`if_let_redundant_pattern_matching`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_let_redundant_pattern_matching
12901291
[`if_let_some_result`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_let_some_result
12911292
[`if_not_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_not_else

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 350 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are 362 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:
1111

clippy_lints/src/if_let_mutex.rs

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use crate::utils::{match_type, paths, span_lint_and_help, SpanlessEq};
2+
use if_chain::if_chain;
3+
use rustc_hir::intravisit::{self as visit, NestedVisitorMap, Visitor};
4+
use rustc_hir::{Expr, ExprKind, MatchSource};
5+
use rustc_lint::{LateContext, LateLintPass};
6+
use rustc_middle::hir::map::Map;
7+
use rustc_session::{declare_lint_pass, declare_tool_lint};
8+
9+
declare_clippy_lint! {
10+
/// **What it does:** Checks for `Mutex::lock` calls in `if let` expression
11+
/// with lock calls in any of the else blocks.
12+
///
13+
/// **Why is this bad?** The Mutex lock remains held for the whole
14+
/// `if let ... else` block and deadlocks.
15+
///
16+
/// **Known problems:** None.
17+
///
18+
/// **Example:**
19+
///
20+
/// ```rust,ignore
21+
/// if let Ok(thing) = mutex.lock() {
22+
/// do_thing();
23+
/// } else {
24+
/// mutex.lock();
25+
/// }
26+
/// ```
27+
/// Should be written
28+
/// ```rust,ignore
29+
/// let locked = mutex.lock();
30+
/// if let Ok(thing) = locked {
31+
/// do_thing(thing);
32+
/// } else {
33+
/// use_locked(locked);
34+
/// }
35+
/// ```
36+
pub IF_LET_MUTEX,
37+
correctness,
38+
"locking a `Mutex` in an `if let` block can cause deadlocks"
39+
}
40+
41+
declare_lint_pass!(IfLetMutex => [IF_LET_MUTEX]);
42+
43+
impl LateLintPass<'_, '_> for IfLetMutex {
44+
fn check_expr(&mut self, cx: &LateContext<'_, '_>, ex: &'_ Expr<'_>) {
45+
let mut arm_visit = ArmVisitor {
46+
mutex_lock_called: false,
47+
found_mutex: None,
48+
cx,
49+
};
50+
let mut op_visit = OppVisitor {
51+
mutex_lock_called: false,
52+
found_mutex: None,
53+
cx,
54+
};
55+
if let ExprKind::Match(
56+
ref op,
57+
ref arms,
58+
MatchSource::IfLetDesugar {
59+
contains_else_clause: true,
60+
},
61+
) = ex.kind
62+
{
63+
op_visit.visit_expr(op);
64+
if op_visit.mutex_lock_called {
65+
for arm in *arms {
66+
arm_visit.visit_arm(arm);
67+
}
68+
69+
if arm_visit.mutex_lock_called && arm_visit.same_mutex(cx, op_visit.found_mutex.unwrap()) {
70+
span_lint_and_help(
71+
cx,
72+
IF_LET_MUTEX,
73+
ex.span,
74+
"calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock",
75+
None,
76+
"move the lock call outside of the `if let ...` expression",
77+
);
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
/// Checks if `Mutex::lock` is called in the `if let _ = expr.
85+
pub struct OppVisitor<'tcx, 'l> {
86+
mutex_lock_called: bool,
87+
found_mutex: Option<&'tcx Expr<'tcx>>,
88+
cx: &'tcx LateContext<'tcx, 'l>,
89+
}
90+
91+
impl<'tcx, 'l> Visitor<'tcx> for OppVisitor<'tcx, 'l> {
92+
type Map = Map<'tcx>;
93+
94+
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
95+
if_chain! {
96+
if let Some(mutex) = is_mutex_lock_call(self.cx, expr);
97+
then {
98+
self.found_mutex = Some(mutex);
99+
self.mutex_lock_called = true;
100+
return;
101+
}
102+
}
103+
visit::walk_expr(self, expr);
104+
}
105+
106+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
107+
NestedVisitorMap::None
108+
}
109+
}
110+
111+
/// Checks if `Mutex::lock` is called in any of the branches.
112+
pub struct ArmVisitor<'tcx, 'l> {
113+
mutex_lock_called: bool,
114+
found_mutex: Option<&'tcx Expr<'tcx>>,
115+
cx: &'tcx LateContext<'tcx, 'l>,
116+
}
117+
118+
impl<'tcx, 'l> Visitor<'tcx> for ArmVisitor<'tcx, 'l> {
119+
type Map = Map<'tcx>;
120+
121+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
122+
if_chain! {
123+
if let Some(mutex) = is_mutex_lock_call(self.cx, expr);
124+
then {
125+
self.found_mutex = Some(mutex);
126+
self.mutex_lock_called = true;
127+
return;
128+
}
129+
}
130+
visit::walk_expr(self, expr);
131+
}
132+
133+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
134+
NestedVisitorMap::None
135+
}
136+
}
137+
138+
impl<'tcx, 'l> ArmVisitor<'tcx, 'l> {
139+
fn same_mutex(&self, cx: &LateContext<'_, '_>, op_mutex: &Expr<'_>) -> bool {
140+
if let Some(arm_mutex) = self.found_mutex {
141+
SpanlessEq::new(cx).eq_expr(op_mutex, arm_mutex)
142+
} else {
143+
false
144+
}
145+
}
146+
}
147+
148+
fn is_mutex_lock_call<'a>(cx: &LateContext<'a, '_>, expr: &'a Expr<'_>) -> Option<&'a Expr<'a>> {
149+
if_chain! {
150+
if let ExprKind::MethodCall(path, _span, args) = &expr.kind;
151+
if path.ident.to_string() == "lock";
152+
let ty = cx.tables.expr_ty(&args[0]);
153+
if match_type(cx, ty, &paths::MUTEX);
154+
then {
155+
Some(&args[0])
156+
} else {
157+
None
158+
}
159+
}
160+
}

clippy_lints/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ mod future_not_send;
222222
mod get_last_with_len;
223223
mod identity_conversion;
224224
mod identity_op;
225+
mod if_let_mutex;
225226
mod if_let_some_result;
226227
mod if_not_else;
227228
mod implicit_return;
@@ -573,6 +574,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
573574
&get_last_with_len::GET_LAST_WITH_LEN,
574575
&identity_conversion::IDENTITY_CONVERSION,
575576
&identity_op::IDENTITY_OP,
577+
&if_let_mutex::IF_LET_MUTEX,
576578
&if_let_some_result::IF_LET_SOME_RESULT,
577579
&if_not_else::IF_NOT_ELSE,
578580
&implicit_return::IMPLICIT_RETURN,
@@ -1054,6 +1056,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10541056
store.register_late_pass(|| box dereference::Dereferencing);
10551057
store.register_late_pass(|| box future_not_send::FutureNotSend);
10561058
store.register_late_pass(|| box utils::internal_lints::CollapsibleCalls);
1059+
store.register_late_pass(|| box if_let_mutex::IfLetMutex);
10571060

10581061
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
10591062
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1233,6 +1236,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12331236
LintId::of(&get_last_with_len::GET_LAST_WITH_LEN),
12341237
LintId::of(&identity_conversion::IDENTITY_CONVERSION),
12351238
LintId::of(&identity_op::IDENTITY_OP),
1239+
LintId::of(&if_let_mutex::IF_LET_MUTEX),
12361240
LintId::of(&if_let_some_result::IF_LET_SOME_RESULT),
12371241
LintId::of(&indexing_slicing::OUT_OF_BOUNDS_INDEXING),
12381242
LintId::of(&infinite_iter::INFINITE_ITER),
@@ -1620,6 +1624,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16201624
LintId::of(&erasing_op::ERASING_OP),
16211625
LintId::of(&formatting::POSSIBLE_MISSING_COMMA),
16221626
LintId::of(&functions::NOT_UNSAFE_PTR_ARG_DEREF),
1627+
LintId::of(&if_let_mutex::IF_LET_MUTEX),
16231628
LintId::of(&indexing_slicing::OUT_OF_BOUNDS_INDEXING),
16241629
LintId::of(&infinite_iter::INFINITE_ITER),
16251630
LintId::of(&inherent_to_string::INHERENT_TO_STRING_SHADOW_DISPLAY),

doc/adding_lints.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ Once we are satisfied with the output, we need to run
101101
Please note that, we should run `TESTNAME=foo_functions cargo uitest`
102102
every time before running `tests/ui/update-all-references.sh`.
103103
Running `TESTNAME=foo_functions cargo uitest` should pass then. When we commit
104-
our lint, we need to commit the generated `.stderr` files, too.
104+
our lint, we need to commit the generated `.stderr` files, too. In general, you
105+
should only commit files changed by `tests/ui/update-all-references.sh` for the
106+
specific lint you are creating/editing.
105107

106108
## Rustfix tests
107109

src/lintlist/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,13 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
731731
deprecation: None,
732732
module: "identity_op",
733733
},
734+
Lint {
735+
name: "if_let_mutex",
736+
group: "correctness",
737+
desc: "locking a `Mutex` in an `if let` block can cause deadlocks",
738+
deprecation: None,
739+
module: "if_let_mutex",
740+
},
734741
Lint {
735742
name: "if_let_some_result",
736743
group: "style",

tests/ui/if_let_mutex.rs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![warn(clippy::if_let_mutex)]
2+
3+
use std::ops::Deref;
4+
use std::sync::Mutex;
5+
6+
fn do_stuff<T>(_: T) {}
7+
8+
fn if_let() {
9+
let m = Mutex::new(1_u8);
10+
if let Err(locked) = m.lock() {
11+
do_stuff(locked);
12+
} else {
13+
let lock = m.lock().unwrap();
14+
do_stuff(lock);
15+
};
16+
}
17+
18+
// This is the most common case as the above case is pretty
19+
// contrived.
20+
fn if_let_option() {
21+
let m = Mutex::new(Some(0_u8));
22+
if let Some(locked) = m.lock().unwrap().deref() {
23+
do_stuff(locked);
24+
} else {
25+
let lock = m.lock().unwrap();
26+
do_stuff(lock);
27+
};
28+
}
29+
30+
// When mutexs are different don't warn
31+
fn if_let_different_mutex() {
32+
let m = Mutex::new(Some(0_u8));
33+
let other = Mutex::new(None::<u8>);
34+
if let Some(locked) = m.lock().unwrap().deref() {
35+
do_stuff(locked);
36+
} else {
37+
let lock = other.lock().unwrap();
38+
do_stuff(lock);
39+
};
40+
}
41+
42+
fn main() {}

tests/ui/if_let_mutex.stderr

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
error: calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock
2+
--> $DIR/if_let_mutex.rs:10:5
3+
|
4+
LL | / if let Err(locked) = m.lock() {
5+
LL | | do_stuff(locked);
6+
LL | | } else {
7+
LL | | let lock = m.lock().unwrap();
8+
LL | | do_stuff(lock);
9+
LL | | };
10+
| |_____^
11+
|
12+
= note: `-D clippy::if-let-mutex` implied by `-D warnings`
13+
= help: move the lock call outside of the `if let ...` expression
14+
15+
error: calling `Mutex::lock` inside the scope of another `Mutex::lock` causes a deadlock
16+
--> $DIR/if_let_mutex.rs:22:5
17+
|
18+
LL | / if let Some(locked) = m.lock().unwrap().deref() {
19+
LL | | do_stuff(locked);
20+
LL | | } else {
21+
LL | | let lock = m.lock().unwrap();
22+
LL | | do_stuff(lock);
23+
LL | | };
24+
| |_____^
25+
|
26+
= help: move the lock call outside of the `if let ...` expression
27+
28+
error: aborting due to 2 previous errors
29+

0 commit comments

Comments
 (0)