Skip to content

Commit 779e3f1

Browse files
committed
Initial implementation of mut_refcell_borrow
Fixes #9044
1 parent 99ab5fe commit 779e3f1

11 files changed

+323
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3908,6 +3908,7 @@ Released 2018-09-13
39083908
[`mut_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_mut
39093909
[`mut_mutex_lock`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_mutex_lock
39103910
[`mut_range_bound`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_range_bound
3911+
[`mut_refcell_borrow`]: https://rust-lang.github.io/rust-clippy/master/index.html#mut_refcell_borrow
39113912
[`mutable_key_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutable_key_type
39123913
[`mutex_atomic`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_atomic
39133914
[`mutex_integer`]: https://rust-lang.github.io/rust-clippy/master/index.html#mutex_integer

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
186186
LintId::of(methods::MAP_FLATTEN),
187187
LintId::of(methods::MAP_IDENTITY),
188188
LintId::of(methods::MUT_MUTEX_LOCK),
189+
LintId::of(methods::MUT_REFCELL_BORROW),
189190
LintId::of(methods::NEEDLESS_OPTION_AS_DEREF),
190191
LintId::of(methods::NEEDLESS_OPTION_TAKE),
191192
LintId::of(methods::NEEDLESS_SPLITN),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ store.register_lints(&[
334334
methods::MAP_IDENTITY,
335335
methods::MAP_UNWRAP_OR,
336336
methods::MUT_MUTEX_LOCK,
337+
methods::MUT_REFCELL_BORROW,
337338
methods::NAIVE_BYTECOUNT,
338339
methods::NEEDLESS_OPTION_AS_DEREF,
339340
methods::NEEDLESS_OPTION_TAKE,

clippy_lints/src/lib.register_style.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ store.register_group(true, "clippy::style", Some("clippy_style"), vec![
7171
LintId::of(methods::MAP_CLONE),
7272
LintId::of(methods::MAP_COLLECT_RESULT_UNIT),
7373
LintId::of(methods::MUT_MUTEX_LOCK),
74+
LintId::of(methods::MUT_REFCELL_BORROW),
7475
LintId::of(methods::NEW_RET_NO_SELF),
7576
LintId::of(methods::OBFUSCATED_IF_ELSE),
7677
LintId::of(methods::OK_EXPECT),

clippy_lints/src/methods/mod.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod map_flatten;
5353
mod map_identity;
5454
mod map_unwrap_or;
5555
mod mut_mutex_lock;
56+
mod mut_refcell_borrow;
5657
mod needless_option_as_deref;
5758
mod needless_option_take;
5859
mod no_effect_replace;
@@ -2765,6 +2766,53 @@ declare_clippy_lint! {
27652766
"`&mut Mutex::lock` does unnecessary locking"
27662767
}
27672768

2769+
declare_clippy_lint! {
2770+
/// ### What it does
2771+
/// Checks for `&mut std::cell::RefCell` method calls which perform
2772+
/// runtime-checks that the compiler can statically guarantee
2773+
///
2774+
/// ### Why is this bad?
2775+
/// Methods on `RefCell` explicitly or implicitly perform a runtime-check
2776+
/// to guarantee the borrowing rules. If called on a `&mut RefCell` we
2777+
/// can statically guarantee that the borrowing rules are upheld.
2778+
///
2779+
/// ### Example
2780+
/// ```
2781+
/// use std::cell::RefCell;
2782+
///
2783+
/// fn foo(x: &mut RefCell<i32>) -> i32 {
2784+
/// // This implicitly panics if the value is already borrowed. But it
2785+
/// // can't be borrowed because we have an exclusive reference to it
2786+
/// x.replace(42)
2787+
/// }
2788+
///
2789+
/// fn bar(x: &mut RefCell<i32>) {
2790+
/// // This check can never fail
2791+
/// if let Ok(mut value) = x.try_borrow_mut() {
2792+
/// *value = 42;
2793+
/// }
2794+
/// }
2795+
/// ```
2796+
/// Use instead:
2797+
/// ```
2798+
/// use std::cell::RefCell;
2799+
///
2800+
/// fn foo(x: &mut RefCell<i32>) -> i32 {
2801+
/// // No need for an implicit check
2802+
/// std::mem::replace(x.get_mut(), 42)
2803+
/// }
2804+
///
2805+
/// fn bar(x: &mut RefCell<i32>) {
2806+
/// // No need for an error path
2807+
/// *x.get_mut() = 42;
2808+
/// }
2809+
/// ```
2810+
#[clippy::version = "1.64.0"]
2811+
pub MUT_REFCELL_BORROW,
2812+
style,
2813+
"method call to `&mut RefCell` performs unnecessary runtime-check"
2814+
}
2815+
27682816
declare_clippy_lint! {
27692817
/// ### What it does
27702818
/// Checks for duplicate open options as well as combinations
@@ -3149,6 +3197,7 @@ impl_lint_pass!(Methods => [
31493197
MAP_CLONE,
31503198
MAP_ERR_IGNORE,
31513199
MUT_MUTEX_LOCK,
3200+
MUT_REFCELL_BORROW,
31523201
NONSENSICAL_OPEN_OPTIONS,
31533202
PATH_BUF_PUSH_OVERWRITE,
31543203
RANGE_ZIP_WITH_LEN,
@@ -3492,6 +3541,8 @@ impl Methods {
34923541
("lock", []) => {
34933542
mut_mutex_lock::check(cx, expr, recv, span);
34943543
},
3544+
(name @ ("replace" | "replace_with"), [arg]) => mut_refcell_borrow::check(cx, expr, recv, span, name, Some(arg)),
3545+
(name @ ("borrow" | "try_borrow" | "borrow_mut" | "try_borrow_mut"), []) => mut_refcell_borrow::check(cx, expr, recv, span, name, None),
34953546
(name @ ("map" | "map_err"), [m_arg]) => {
34963547
if name == "map" {
34973548
map_clone::check(cx, expr, recv, m_arg, self.msrv);
@@ -3597,7 +3648,10 @@ impl Methods {
35973648
}
35983649
}
35993650
},
3600-
("take", []) => needless_option_take::check(cx, expr, recv),
3651+
("take", []) => {
3652+
needless_option_take::check(cx, expr, recv);
3653+
mut_refcell_borrow::check(cx, expr, recv, span, "take", None);
3654+
},
36013655
("then", [arg]) => {
36023656
if !meets_msrv(self.msrv, msrvs::BOOL_THEN_SOME) {
36033657
return;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
2+
use clippy_utils::{expr_custom_deref_adjustment, match_def_path, paths};
3+
use rustc_errors::Applicability;
4+
use rustc_hir::{Expr, Mutability};
5+
use rustc_lint::LateContext;
6+
use rustc_middle::ty;
7+
use rustc_span::Span;
8+
9+
use super::MUT_REFCELL_BORROW;
10+
11+
//TODO calls to RefCell's `Clone`-impl could be replaced by `RefCell::new(foo.get_mut().clone())`
12+
//to circumvent the runtime-check
13+
14+
fn emit_replace(cx: &LateContext<'_>, name_span: Span) {
15+
span_lint_and_help(
16+
cx,
17+
MUT_REFCELL_BORROW,
18+
name_span,
19+
"calling `&mut RefCell::replace()` unnecessarily performs a runtime-check that can never fail",
20+
None,
21+
"use `.get_mut()` to get a mutable reference to the value, and replace the value using `std::mem::replace()` or direct assignment",
22+
);
23+
}
24+
25+
fn emit_replace_with(cx: &LateContext<'_>, name_span: Span) {
26+
span_lint_and_help(
27+
cx,
28+
MUT_REFCELL_BORROW,
29+
name_span,
30+
"calling `&mut RefCell::replace_with()` unnecessarily performs a runtime-check that can never fail",
31+
None,
32+
"use `.get_mut()` to get a mutable reference to the value, and replace the value using `std::mem::replace()` or direct assignment",
33+
);
34+
}
35+
36+
fn emit_borrow(cx: &LateContext<'_>, name_span: Span) {
37+
// This is not MachineApplicable as `borrow` returns a `Ref` while `get_mut` returns a
38+
// `&mut T`, and we don't check surrounding types
39+
span_lint_and_sugg(
40+
cx,
41+
MUT_REFCELL_BORROW,
42+
name_span,
43+
"calling `&mut RefCell::borrow()` unnecessarily performs a runtime-check that can never fail",
44+
"change this to",
45+
"get_mut".to_owned(),
46+
Applicability::MaybeIncorrect,
47+
);
48+
}
49+
50+
fn emit_try_borrow(cx: &LateContext<'_>, name_span: Span) {
51+
span_lint_and_help(
52+
cx,
53+
MUT_REFCELL_BORROW,
54+
name_span,
55+
"calling `&mut RefCell::try_borrow()` unnecessarily performs a runtime-check that can never fail",
56+
None,
57+
"use `.get_mut()` instead of `.try_borrow()` to get a reference to the value; remove the error-handling",
58+
);
59+
}
60+
61+
fn emit_borrow_mut(cx: &LateContext<'_>, name_span: Span) {
62+
// This is not MachineApplicable as `borrow_mut` returns a different type than `get_mut`, for
63+
// which we don't check
64+
span_lint_and_sugg(
65+
cx,
66+
MUT_REFCELL_BORROW,
67+
name_span,
68+
"calling `&mut RefCell::borrow_mut()` unnecessarily performs a runtime-check that can never fail",
69+
"change this to",
70+
"get_mut".to_owned(),
71+
Applicability::MaybeIncorrect,
72+
);
73+
}
74+
75+
fn emit_try_borrow_mut(cx: &LateContext<'_>, name_span: Span) {
76+
span_lint_and_help(
77+
cx,
78+
MUT_REFCELL_BORROW,
79+
name_span,
80+
"calling `&mut RefCell::try_borrow_mut()` unnecessarily performs a runtime-check that can never fail",
81+
None,
82+
"use `.get_mut()` instead of `.try_borrow_mut()` to get a mutable reference to the value; remove the error-handling",
83+
);
84+
}
85+
86+
fn emit_take(cx: &LateContext<'_>, name_span: Span) {
87+
span_lint_and_help(
88+
cx,
89+
MUT_REFCELL_BORROW,
90+
name_span,
91+
"calling `&mut RefCell::take()` unnecessarily performs a runtime-check that can never fail",
92+
None,
93+
"use `.get_mut()` to get a mutable reference to the value, and `std::mem::take()` to get ownership via that reference",
94+
);
95+
}
96+
97+
pub(super) fn check<'tcx>(
98+
cx: &LateContext<'tcx>,
99+
ex: &'tcx Expr<'tcx>,
100+
recv: &'tcx Expr<'tcx>,
101+
name_span: Span,
102+
name: &'tcx str,
103+
arg: Option<&'tcx Expr<'tcx>>,
104+
) {
105+
if matches!(expr_custom_deref_adjustment(cx, recv), None | Some(Mutability::Mut))
106+
&& let ty::Ref(_, _, Mutability::Mut) = cx.typeck_results().expr_ty(recv).kind()
107+
&& let Some(method_id) = cx.typeck_results().type_dependent_def_id(ex.hir_id)
108+
&& let Some(impl_id) = cx.tcx.impl_of_method(method_id)
109+
&& match_def_path(cx, impl_id, &paths::REFCELL)
110+
{
111+
//TODO: Use `arg` to emit better suggestions
112+
match (name, arg) {
113+
("replace", Some(_arg)) => emit_replace(cx, name_span),
114+
("replace_with", Some(_arg)) => emit_replace_with(cx, name_span),
115+
("borrow", None) => emit_borrow(cx, name_span),
116+
("try_borrow", None) => emit_try_borrow(cx, name_span),
117+
("borrow_mut", None) => emit_borrow_mut(cx, name_span),
118+
("try_borrow_mut", None) => emit_try_borrow_mut(cx, name_span),
119+
("take", None) => emit_take(cx, name_span),
120+
_ => unreachable!(),
121+
};
122+
}
123+
}

clippy_utils/src/paths.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ pub const PTR_WRITE_VOLATILE: [&str; 3] = ["core", "ptr", "write_volatile"];
122122
pub const PUSH_STR: [&str; 4] = ["alloc", "string", "String", "push_str"];
123123
pub const RANGE_ARGUMENT_TRAIT: [&str; 3] = ["core", "ops", "RangeBounds"];
124124
pub const RC_PTR_EQ: [&str; 4] = ["alloc", "rc", "Rc", "ptr_eq"];
125+
pub const REFCELL: [&str; 3] = ["core", "cell", "RefCell"];
125126
pub const REFCELL_REF: [&str; 3] = ["core", "cell", "Ref"];
126127
pub const REFCELL_REFMUT: [&str; 3] = ["core", "cell", "RefMut"];
127128
#[expect(clippy::invalid_paths)] // internal lints do not know about all external crates

src/docs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ docs! {
326326
"mut_mut",
327327
"mut_mutex_lock",
328328
"mut_range_bound",
329+
"mut_refcell_borrow",
329330
"mutable_key_type",
330331
"mutex_atomic",
331332
"mutex_integer",

src/docs/mut_refcell_borrow.txt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
### What it does
2+
Checks for `&mut std::cell::RefCell` method calls which perform
3+
runtime-checks that the compiler can statically guarantee
4+
5+
### Why is this bad?
6+
Methods on `RefCell` explicitly or implicitly perform a runtime-check
7+
to guarantee the borrowing rules. If called on a `&mut RefCell` we
8+
can statically guarantee that the borrowing rules are upheld.
9+
10+
### Example
11+
```
12+
use std::cell::RefCell;
13+
14+
fn foo(x: &mut RefCell<i32>) -> i32 {
15+
// This implicitly panics if the value is already borrowed. But it
16+
// can't be borrowed because we have an exclusive reference to it
17+
x.replace(42)
18+
}
19+
20+
fn bar(x: &mut RefCell<i32>) {
21+
// This check can never fail
22+
if let Ok(value) = x.try_borrow_mut() {
23+
*value = 42;
24+
}
25+
}
26+
```
27+
Use instead:
28+
```
29+
use std::cell::RefCell;
30+
31+
fn foo(x: &mut RefCell<i32>) -> i32 {
32+
// No need for an implicit check
33+
std::mem::replace(x.get_mut(), 42)
34+
}
35+
36+
fn bar(x: &mut RefCell<i32>) {
37+
// No need for an error path
38+
*x.get_mut() = 42;
39+
}
40+
```

tests/ui/mut_refcell_borrow.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![warn(clippy::mut_mutex_lock)]
2+
3+
use std::cell::RefCell;
4+
use std::rc::Rc;
5+
6+
pub fn replace(x: &mut RefCell<i32>) {
7+
x.replace(0);
8+
}
9+
10+
pub fn replace_with(x: &mut RefCell<i32>) {
11+
x.replace_with(|&mut old| old + 1);
12+
}
13+
14+
pub fn borrow(x: &mut RefCell<i32>) {
15+
let _: i32 = *x.borrow();
16+
}
17+
18+
pub fn try_borrow(x: &mut RefCell<i32>) {
19+
let _: i32 = *x.try_borrow().unwrap();
20+
}
21+
22+
pub fn borrow_mut(x: &mut RefCell<i32>) {
23+
*x.borrow_mut() += 1;
24+
}
25+
26+
pub fn try_borrow_mut(x: &mut RefCell<i32>) {
27+
*x.try_borrow_mut().unwrap() += 1;
28+
}
29+
30+
pub fn take(x: &mut RefCell<i32>) {
31+
let _: i32 = x.take();
32+
}
33+
34+
// must not lint
35+
pub fn deref_refcell(x: Rc<RefCell<i32>>) {
36+
*x.borrow_mut() += 1;
37+
}
38+
39+
// must not lint
40+
pub fn mut_deref_refcell(x: &mut Rc<RefCell<i32>>) {
41+
*x.borrow_mut() += 1;
42+
}
43+
44+
fn main() {}

0 commit comments

Comments
 (0)