Skip to content

Commit 0a2eece

Browse files
Add replace_box lint (#14953)
Adds a new lint that detects `var = Default::default()` when `var` is `Box<T>` and `T` implements `Default`. changelog: [`replace_box`]: new lint
2 parents ea54123 + 517ef60 commit 0a2eece

File tree

7 files changed

+595
-107
lines changed

7 files changed

+595
-107
lines changed

CHANGELOG.md

Lines changed: 266 additions & 107 deletions
Large diffs are not rendered by default.

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
655655
crate::regex::REGEX_CREATION_IN_LOOPS_INFO,
656656
crate::regex::TRIVIAL_REGEX_INFO,
657657
crate::repeat_vec_with_capacity::REPEAT_VEC_WITH_CAPACITY_INFO,
658+
crate::replace_box::REPLACE_BOX_INFO,
658659
crate::reserve_after_initialization::RESERVE_AFTER_INITIALIZATION_INFO,
659660
crate::return_self_not_must_use::RETURN_SELF_NOT_MUST_USE_INFO,
660661
crate::returns::LET_AND_RETURN_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ mod ref_patterns;
323323
mod reference;
324324
mod regex;
325325
mod repeat_vec_with_capacity;
326+
mod replace_box;
326327
mod reserve_after_initialization;
327328
mod return_self_not_must_use;
328329
mod returns;
@@ -832,5 +833,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
832833
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
833834
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
834835
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
836+
store.register_late_pass(|_| Box::new(replace_box::ReplaceBox));
835837
// add lints here, do not remove this comment, it's used in `new_lint`
836838
}

clippy_lints/src/replace_box.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::sugg::Sugg;
3+
use clippy_utils::ty::implements_trait;
4+
use clippy_utils::{is_default_equivalent_call, local_is_initialized, path_def_id, path_to_local};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{Expr, ExprKind, LangItem, QPath};
7+
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_middle::ty::{self, Ty};
9+
use rustc_session::declare_lint_pass;
10+
use rustc_span::sym;
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Detects assignments of `Default::default()` or `Box::new(value)`
15+
/// to a place of type `Box<T>`.
16+
///
17+
/// ### Why is this bad?
18+
/// This incurs an extra heap allocation compared to assigning the boxed
19+
/// storage.
20+
///
21+
/// ### Example
22+
/// ```no_run
23+
/// let mut b = Box::new(1u32);
24+
/// b = Default::default();
25+
/// ```
26+
/// Use instead:
27+
/// ```no_run
28+
/// let mut b = Box::new(1u32);
29+
/// *b = Default::default();
30+
/// ```
31+
#[clippy::version = "1.92.0"]
32+
pub REPLACE_BOX,
33+
perf,
34+
"assigning a newly created box to `Box<T>` is inefficient"
35+
}
36+
declare_lint_pass!(ReplaceBox => [REPLACE_BOX]);
37+
38+
impl LateLintPass<'_> for ReplaceBox {
39+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
40+
if let ExprKind::Assign(lhs, rhs, _) = &expr.kind
41+
&& !lhs.span.from_expansion()
42+
&& !rhs.span.from_expansion()
43+
{
44+
let lhs_ty = cx.typeck_results().expr_ty(lhs);
45+
46+
// No diagnostic for late-initialized locals
47+
if let Some(local) = path_to_local(lhs)
48+
&& !local_is_initialized(cx, local)
49+
{
50+
return;
51+
}
52+
53+
let Some(inner_ty) = get_box_inner_type(cx, lhs_ty) else {
54+
return;
55+
};
56+
57+
if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default)
58+
&& implements_trait(cx, inner_ty, default_trait_id, &[])
59+
&& is_default_call(cx, rhs)
60+
{
61+
span_lint_and_then(
62+
cx,
63+
REPLACE_BOX,
64+
expr.span,
65+
"creating a new box with default content",
66+
|diag| {
67+
let mut app = Applicability::MachineApplicable;
68+
let suggestion = format!(
69+
"{} = Default::default()",
70+
Sugg::hir_with_applicability(cx, lhs, "_", &mut app).deref()
71+
);
72+
73+
diag.note("this creates a needless allocation").span_suggestion(
74+
expr.span,
75+
"replace existing content with default instead",
76+
suggestion,
77+
app,
78+
);
79+
},
80+
);
81+
}
82+
83+
if inner_ty.is_sized(cx.tcx, cx.typing_env())
84+
&& let Some(rhs_inner) = get_box_new_payload(cx, rhs)
85+
{
86+
span_lint_and_then(cx, REPLACE_BOX, expr.span, "creating a new box", |diag| {
87+
let mut app = Applicability::MachineApplicable;
88+
let suggestion = format!(
89+
"{} = {}",
90+
Sugg::hir_with_applicability(cx, lhs, "_", &mut app).deref(),
91+
Sugg::hir_with_context(cx, rhs_inner, expr.span.ctxt(), "_", &mut app),
92+
);
93+
94+
diag.note("this creates a needless allocation").span_suggestion(
95+
expr.span,
96+
"replace existing content with inner value instead",
97+
suggestion,
98+
app,
99+
);
100+
});
101+
}
102+
}
103+
}
104+
}
105+
106+
fn get_box_inner_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
107+
if let ty::Adt(def, args) = ty.kind()
108+
&& cx.tcx.is_lang_item(def.did(), LangItem::OwnedBox)
109+
{
110+
Some(args.type_at(0))
111+
} else {
112+
None
113+
}
114+
}
115+
116+
fn is_default_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
117+
matches!(expr.kind, ExprKind::Call(func, _args) if is_default_equivalent_call(cx, func, Some(expr)))
118+
}
119+
120+
fn get_box_new_payload<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
121+
if let ExprKind::Call(box_new, [arg]) = expr.kind
122+
&& let ExprKind::Path(QPath::TypeRelative(ty, seg)) = box_new.kind
123+
&& seg.ident.name == sym::new
124+
&& path_def_id(cx, ty).is_some_and(|id| Some(id) == cx.tcx.lang_items().owned_box())
125+
{
126+
Some(arg)
127+
} else {
128+
None
129+
}
130+
}

tests/ui/replace_box.fixed

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#![warn(clippy::replace_box)]
2+
3+
fn with_default<T: Default>(b: &mut Box<T>) {
4+
**b = T::default();
5+
//~^ replace_box
6+
}
7+
8+
fn with_sized<T>(b: &mut Box<T>, t: T) {
9+
**b = t;
10+
//~^ replace_box
11+
}
12+
13+
fn with_unsized<const N: usize>(b: &mut Box<[u32]>) {
14+
// No lint for assigning to Box<T> where T: !Default
15+
*b = Box::new([42; N]);
16+
}
17+
18+
macro_rules! create_default {
19+
() => {
20+
Default::default()
21+
};
22+
}
23+
24+
macro_rules! create_zero_box {
25+
() => {
26+
Box::new(0)
27+
};
28+
}
29+
30+
macro_rules! same {
31+
($v:ident) => {
32+
$v
33+
};
34+
}
35+
36+
macro_rules! mac {
37+
(three) => {
38+
3u32
39+
};
40+
}
41+
42+
fn main() {
43+
let mut b = Box::new(1u32);
44+
*b = Default::default();
45+
//~^ replace_box
46+
*b = Default::default();
47+
//~^ replace_box
48+
49+
// No lint for assigning to the storage
50+
*b = Default::default();
51+
*b = u32::default();
52+
53+
// No lint if either LHS or RHS originates in macro
54+
b = create_default!();
55+
b = create_zero_box!();
56+
same!(b) = Default::default();
57+
58+
*b = 5;
59+
//~^ replace_box
60+
61+
*b = mac!(three);
62+
//~^ replace_box
63+
64+
// No lint for assigning to Box<T> where T: !Default
65+
let mut b = Box::<str>::from("hi".to_string());
66+
b = Default::default();
67+
68+
// No lint for late initializations
69+
#[allow(clippy::needless_late_init)]
70+
let bb: Box<u32>;
71+
bb = Default::default();
72+
}

tests/ui/replace_box.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#![warn(clippy::replace_box)]
2+
3+
fn with_default<T: Default>(b: &mut Box<T>) {
4+
*b = Box::new(T::default());
5+
//~^ replace_box
6+
}
7+
8+
fn with_sized<T>(b: &mut Box<T>, t: T) {
9+
*b = Box::new(t);
10+
//~^ replace_box
11+
}
12+
13+
fn with_unsized<const N: usize>(b: &mut Box<[u32]>) {
14+
// No lint for assigning to Box<T> where T: !Default
15+
*b = Box::new([42; N]);
16+
}
17+
18+
macro_rules! create_default {
19+
() => {
20+
Default::default()
21+
};
22+
}
23+
24+
macro_rules! create_zero_box {
25+
() => {
26+
Box::new(0)
27+
};
28+
}
29+
30+
macro_rules! same {
31+
($v:ident) => {
32+
$v
33+
};
34+
}
35+
36+
macro_rules! mac {
37+
(three) => {
38+
3u32
39+
};
40+
}
41+
42+
fn main() {
43+
let mut b = Box::new(1u32);
44+
b = Default::default();
45+
//~^ replace_box
46+
b = Box::default();
47+
//~^ replace_box
48+
49+
// No lint for assigning to the storage
50+
*b = Default::default();
51+
*b = u32::default();
52+
53+
// No lint if either LHS or RHS originates in macro
54+
b = create_default!();
55+
b = create_zero_box!();
56+
same!(b) = Default::default();
57+
58+
b = Box::new(5);
59+
//~^ replace_box
60+
61+
b = Box::new(mac!(three));
62+
//~^ replace_box
63+
64+
// No lint for assigning to Box<T> where T: !Default
65+
let mut b = Box::<str>::from("hi".to_string());
66+
b = Default::default();
67+
68+
// No lint for late initializations
69+
#[allow(clippy::needless_late_init)]
70+
let bb: Box<u32>;
71+
bb = Default::default();
72+
}

tests/ui/replace_box.stderr

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
error: creating a new box
2+
--> tests/ui/replace_box.rs:4:5
3+
|
4+
LL | *b = Box::new(T::default());
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `**b = T::default()`
6+
|
7+
= note: this creates a needless allocation
8+
= note: `-D clippy::replace-box` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::replace_box)]`
10+
11+
error: creating a new box
12+
--> tests/ui/replace_box.rs:9:5
13+
|
14+
LL | *b = Box::new(t);
15+
| ^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `**b = t`
16+
|
17+
= note: this creates a needless allocation
18+
19+
error: creating a new box with default content
20+
--> tests/ui/replace_box.rs:44:5
21+
|
22+
LL | b = Default::default();
23+
| ^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with default instead: `*b = Default::default()`
24+
|
25+
= note: this creates a needless allocation
26+
27+
error: creating a new box with default content
28+
--> tests/ui/replace_box.rs:46:5
29+
|
30+
LL | b = Box::default();
31+
| ^^^^^^^^^^^^^^^^^^ help: replace existing content with default instead: `*b = Default::default()`
32+
|
33+
= note: this creates a needless allocation
34+
35+
error: creating a new box
36+
--> tests/ui/replace_box.rs:58:5
37+
|
38+
LL | b = Box::new(5);
39+
| ^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*b = 5`
40+
|
41+
= note: this creates a needless allocation
42+
43+
error: creating a new box
44+
--> tests/ui/replace_box.rs:61:5
45+
|
46+
LL | b = Box::new(mac!(three));
47+
| ^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*b = mac!(three)`
48+
|
49+
= note: this creates a needless allocation
50+
51+
error: aborting due to 6 previous errors
52+

0 commit comments

Comments
 (0)