Skip to content

Commit 5309578

Browse files
committed
New lint: copy_then_borrow_mut
1 parent e1c1ac1 commit 5309578

17 files changed

+522
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5539,6 +5539,7 @@ Released 2018-09-13
55395539
[`const_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_is_empty
55405540
[`const_static_lifetime`]: https://rust-lang.github.io/rust-clippy/master/index.html#const_static_lifetime
55415541
[`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator
5542+
[`copy_then_borrow_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_then_borrow_mut
55425543
[`crate_in_macro_def`]: https://rust-lang.github.io/rust-clippy/master/index.html#crate_in_macro_def
55435544
[`create_dir`]: https://rust-lang.github.io/rust-clippy/master/index.html#create_dir
55445545
[`crosspointer_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#crosspointer_transmute
@@ -6343,6 +6344,7 @@ Released 2018-09-13
63436344
[`avoid-breaking-exported-api`]: https://doc.rust-lang.org/clippy/lint_configuration.html#avoid-breaking-exported-api
63446345
[`await-holding-invalid-types`]: https://doc.rust-lang.org/clippy/lint_configuration.html#await-holding-invalid-types
63456346
[`cargo-ignore-publish`]: https://doc.rust-lang.org/clippy/lint_configuration.html#cargo-ignore-publish
6347+
[`check-copy-then-borrow-mut-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#check-copy-then-borrow-mut-in-tests
63466348
[`check-incompatible-msrv-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#check-incompatible-msrv-in-tests
63476349
[`check-private-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#check-private-items
63486350
[`cognitive-complexity-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#cognitive-complexity-threshold

book/src/lint_configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,16 @@ For internal testing only, ignores the current `publish` settings in the Cargo m
415415
* [`cargo_common_metadata`](https://rust-lang.github.io/rust-clippy/master/index.html#cargo_common_metadata)
416416

417417

418+
## `check-copy-then-borrow-mut-in-tests`
419+
Whether to search for mutable borrows of freshly copied data in tests.
420+
421+
**Default Value:** `true`
422+
423+
---
424+
**Affected lints:**
425+
* [`copy_then_borrow_mut`](https://rust-lang.github.io/rust-clippy/master/index.html#copy_then_borrow_mut)
426+
427+
418428
## `check-incompatible-msrv-in-tests`
419429
Whether to check MSRV compatibility in `#[test]` and `#[cfg(test)]` code.
420430

clippy_config/src/conf.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,9 @@ define_Conf! {
464464
/// For internal testing only, ignores the current `publish` settings in the Cargo manifest.
465465
#[lints(cargo_common_metadata)]
466466
cargo_ignore_publish: bool = false,
467+
/// Whether to search for mutable borrows of freshly copied data in tests.
468+
#[lints(copy_then_borrow_mut)]
469+
check_copy_then_borrow_mut_in_tests: bool = true,
467470
/// Whether to check MSRV compatibility in `#[test]` and `#[cfg(test)]` code.
468471
#[lints(incompatible_msrv)]
469472
check_incompatible_msrv_in_tests: bool = false,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::ty::is_copy;
4+
use clippy_utils::{get_enclosing_block, is_in_test, path_to_local};
5+
use rustc_ast::BindingMode;
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{Block, BorrowKind, Expr, ExprKind, Mutability, Node, PatKind};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::impl_lint_pass;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks for mutable reference on a freshly copied variable due to
14+
/// the use of a block to return an value implementing `Copy`.
15+
///
16+
/// ### Why is this bad?
17+
/// Using a block will make a copy of the block result if its type
18+
/// implements `Copy`. This might be an indication of a failed attempt
19+
/// to borrow a variable, or part of a variable.
20+
///
21+
/// ### Example
22+
/// ```no_run
23+
/// # unsafe fn unsafe_func(_: &mut i32) {}
24+
/// let mut a = 10;
25+
/// let double_a_ref = &mut unsafe { // Unsafe block needed to call `unsafe_func`
26+
/// unsafe_func(&mut a);
27+
/// a
28+
/// };
29+
/// ```
30+
/// If you intend to take a reference on `a` and you need the block,
31+
/// create the reference inside the block instead:
32+
/// ```no_run
33+
/// # unsafe fn unsafe_func(_: &mut i32) {}
34+
/// let mut a = 10;
35+
/// let double_a_ref = unsafe { // Unsafe block needed to call `unsafe_func`
36+
/// unsafe_func(&mut a);
37+
/// &mut a
38+
/// };
39+
/// ```
40+
#[clippy::version = "1.87.0"]
41+
pub COPY_THEN_BORROW_MUT,
42+
suspicious,
43+
"mutable borrow of a data which was just copied"
44+
}
45+
46+
pub struct CopyThenBorrowMut {
47+
check_in_tests: bool,
48+
}
49+
50+
impl CopyThenBorrowMut {
51+
pub const fn new(conf: &Conf) -> Self {
52+
Self {
53+
check_in_tests: conf.check_copy_then_borrow_mut_in_tests,
54+
}
55+
}
56+
}
57+
58+
impl_lint_pass!(CopyThenBorrowMut => [COPY_THEN_BORROW_MUT]);
59+
60+
impl LateLintPass<'_> for CopyThenBorrowMut {
61+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
62+
if !expr.span.from_expansion()
63+
&& let ExprKind::AddrOf(BorrowKind::Ref, Mutability::Mut, sub_expr) = expr.kind
64+
&& let ExprKind::Block(block, _) = sub_expr.kind
65+
&& !block.targeted_by_break
66+
&& block.span.eq_ctxt(expr.span)
67+
&& let Some(block_expr) = block.expr
68+
&& let block_ty = cx.typeck_results().expr_ty_adjusted(block_expr)
69+
&& is_copy(cx, block_ty)
70+
&& is_expr_mutable_outside_block(cx, block_expr, block)
71+
&& (self.check_in_tests || !is_in_test(cx.tcx, expr.hir_id))
72+
{
73+
span_lint_and_then(
74+
cx,
75+
COPY_THEN_BORROW_MUT,
76+
expr.span,
77+
"mutable borrow of a value which was just copied",
78+
|diag| {
79+
diag.multipart_suggestion(
80+
"try building the reference inside the block",
81+
vec![
82+
(expr.span.until(block.span), String::new()),
83+
(block_expr.span.shrink_to_lo(), String::from("&mut ")),
84+
],
85+
Applicability::MachineApplicable,
86+
);
87+
},
88+
);
89+
}
90+
}
91+
}
92+
93+
/// Checks if `expr` denotes a mutable variable defined outside `block`, or, recursively, a field or
94+
/// index of such a variable.
95+
fn is_expr_mutable_outside_block(cx: &LateContext<'_>, mut expr: &Expr<'_>, block: &Block<'_>) -> bool {
96+
while let ExprKind::Field(base, _) | ExprKind::Index(base, _, _) = expr.kind {
97+
expr = base;
98+
}
99+
if let Some(mut hir_id) = path_to_local(expr)
100+
&& let Node::Pat(pat) = cx.tcx.hir_node(hir_id)
101+
&& matches!(pat.kind, PatKind::Binding(BindingMode::MUT, ..))
102+
{
103+
// Scan enclosing blocks until we find `block`, or we loop or can't find
104+
// blocks anymore.
105+
loop {
106+
match get_enclosing_block(cx, hir_id).map(|b| b.hir_id) {
107+
Some(block_id) if block_id == block.hir_id => return false,
108+
Some(block_id) if block_id != hir_id => hir_id = block_id,
109+
_ => return true,
110+
}
111+
}
112+
}
113+
false
114+
}

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
113113
crate::copies::IF_SAME_THEN_ELSE_INFO,
114114
crate::copies::SAME_FUNCTIONS_IN_IF_CONDITION_INFO,
115115
crate::copy_iterator::COPY_ITERATOR_INFO,
116+
crate::copy_then_borrow_mut::COPY_THEN_BORROW_MUT_INFO,
116117
crate::crate_in_macro_def::CRATE_IN_MACRO_DEF_INFO,
117118
crate::create_dir::CREATE_DIR_INFO,
118119
crate::dbg_macro::DBG_MACRO_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ mod collection_is_never_read;
103103
mod comparison_chain;
104104
mod copies;
105105
mod copy_iterator;
106+
mod copy_then_borrow_mut;
106107
mod crate_in_macro_def;
107108
mod create_dir;
108109
mod dbg_macro;
@@ -984,5 +985,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
984985
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
985986
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
986987
store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
988+
store.register_late_pass(|_| Box::new(copy_then_borrow_mut::CopyThenBorrowMut::new(conf)));
987989
// add lints here, do not remove this comment, it's used in `new_lint`
988990
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
check-copy-then-borrow-mut-in-tests = false
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[test]
2+
fn in_test() {
3+
let mut a = [10; 2];
4+
let _ = &mut { a }; // Do not lint
5+
}
6+
7+
fn main() {
8+
let mut a = [10; 2];
9+
let _ = { &mut a }; //~ ERROR: mutable borrow
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[test]
2+
fn in_test() {
3+
let mut a = [10; 2];
4+
let _ = &mut { a }; // Do not lint
5+
}
6+
7+
fn main() {
8+
let mut a = [10; 2];
9+
let _ = &mut { a }; //~ ERROR: mutable borrow
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
error: mutable borrow of a value which was just copied
2+
--> tests/ui-toml/copy_then_borrow_mut/copy_then_borrow_mut.rs:9:13
3+
|
4+
LL | let _ = &mut { a };
5+
| ^^^^^^^^^^
6+
|
7+
= note: `-D clippy::copy-then-borrow-mut` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::copy_then_borrow_mut)]`
9+
help: try building the reference inside the block
10+
|
11+
LL - let _ = &mut { a };
12+
LL + let _ = { &mut a };
13+
|
14+
15+
error: aborting due to 1 previous error
16+

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
3131
await-holding-invalid-types
3232
blacklisted-names
3333
cargo-ignore-publish
34+
check-copy-then-borrow-mut-in-tests
3435
check-incompatible-msrv-in-tests
3536
check-private-items
3637
cognitive-complexity-threshold
@@ -123,6 +124,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
123124
await-holding-invalid-types
124125
blacklisted-names
125126
cargo-ignore-publish
127+
check-copy-then-borrow-mut-in-tests
126128
check-incompatible-msrv-in-tests
127129
check-private-items
128130
cognitive-complexity-threshold
@@ -215,6 +217,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
215217
await-holding-invalid-types
216218
blacklisted-names
217219
cargo-ignore-publish
220+
check-copy-then-borrow-mut-in-tests
218221
check-incompatible-msrv-in-tests
219222
check-private-items
220223
cognitive-complexity-threshold

tests/ui/copy_then_borrow_mut.fixed

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#![warn(clippy::copy_then_borrow_mut)]
2+
#![allow(clippy::deref_addrof)]
3+
4+
fn main() {
5+
let mut a = [0u8; 2];
6+
let _ = { &mut a }; //~ ERROR: mutable borrow
7+
8+
let _ = &mut 'label: {
9+
// Block is targeted by break
10+
if a[0] == 1 {
11+
break 'label 42u8;
12+
}
13+
a[0]
14+
};
15+
16+
let mut a = vec![0u8; 2];
17+
let _ = &mut { a }; // `a` is not `Copy`
18+
19+
let a = [0u8; 2];
20+
let _ = &mut { a }; // `a` is not mutable
21+
22+
let _ = &mut { 42 }; // Do not lint on non-place expression
23+
24+
let _ = &mut {}; // Do not lint on empty block
25+
26+
macro_rules! mac {
27+
($a:expr) => {{ a }};
28+
}
29+
let _ = &mut mac!(a); // Do not lint on borrowed macro result
30+
31+
macro_rules! mac2 {
32+
// Do not lint, as it depends on `Copy`-ness of `a`
33+
($x:expr) => {
34+
&mut unsafe { $x }
35+
};
36+
}
37+
let mut a = 0u8;
38+
let _ = &mut mac2!(a);
39+
40+
let _ = &mut {
41+
// Do not lint, the variable is defined inside the block
42+
let mut a: [i32; 5] = (1, 2, 3, 4, 5).into();
43+
a
44+
};
45+
46+
let _ = &mut {
47+
// Do not lint, the variable is defined inside the block
48+
{
49+
let mut a: [i32; 5] = (1, 2, 3, 4, 5).into();
50+
a
51+
}
52+
};
53+
54+
struct S {
55+
a: u32,
56+
}
57+
58+
let mut s = S { a: 0 };
59+
let _ = {
60+
//~^ ERROR: mutable borrow
61+
s.a = 32;
62+
&mut s.a
63+
};
64+
65+
let _ = &mut {
66+
// Do not lint, the variable is defined inside the block
67+
let mut s = S { a: 0 };
68+
s.a
69+
};
70+
71+
let mut c = (10, 20);
72+
let _ = {
73+
//~^ ERROR: mutable borrow
74+
&mut c.0
75+
};
76+
77+
let _ = &mut {
78+
// Do not lint, the variable is defined inside the block
79+
let mut c = (10, 20);
80+
c.0
81+
};
82+
83+
let mut t = [10, 20];
84+
let _ = {
85+
//~^ ERROR: mutable borrow
86+
&mut t[0]
87+
};
88+
89+
let _ = &mut {
90+
// Do not lint, the variable is defined inside the block
91+
let mut t = [10, 20];
92+
t[0]
93+
};
94+
95+
unsafe fn unsafe_func(_: &mut i32) {}
96+
let mut a = 10;
97+
// Unsafe block needed to call `unsafe_func`
98+
let double_a_ref = unsafe {
99+
//~^ ERROR: mutable borrow
100+
unsafe_func(&mut a);
101+
&mut a
102+
};
103+
}
104+
105+
#[test]
106+
fn in_test() {
107+
let mut a = [10; 2];
108+
let _ = { &mut a }; //~ ERROR: mutable borrow
109+
}

0 commit comments

Comments
 (0)