fix: replace_box FP when the box is moved

This commit is contained in:
Linshu Yang 2025-11-01 00:26:34 +00:00
parent d05f74e5f7
commit 098ded3160
5 changed files with 286 additions and 7 deletions

View file

@ -820,6 +820,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
store.register_late_pass(|_| Box::new(coerce_container_to_any::CoerceContainerToAny));
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
store.register_late_pass(|_| Box::new(replace_box::ReplaceBox));
store.register_late_pass(|_| Box::new(replace_box::ReplaceBox::default()));
// add lints here, do not remove this comment, it's used in `new_lint`
}

View file

@ -3,11 +3,17 @@ use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::implements_trait;
use clippy_utils::{is_default_equivalent_call, local_is_initialized};
use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::smallvec::SmallVec;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, LangItem, QPath};
use rustc_hir::{Body, BodyId, Expr, ExprKind, HirId, LangItem, QPath};
use rustc_hir_typeck::expr_use_visitor::{Delegate, ExprUseVisitor, PlaceBase, PlaceWithHirId};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::sym;
use rustc_middle::hir::place::ProjectionKind;
use rustc_middle::mir::FakeReadCause;
use rustc_middle::ty;
use rustc_session::impl_lint_pass;
use rustc_span::{Symbol, sym};
declare_clippy_lint! {
/// ### What it does
@ -33,17 +39,57 @@ declare_clippy_lint! {
perf,
"assigning a newly created box to `Box<T>` is inefficient"
}
declare_lint_pass!(ReplaceBox => [REPLACE_BOX]);
#[derive(Default)]
pub struct ReplaceBox {
consumed_locals: FxHashSet<HirId>,
loaded_bodies: SmallVec<[BodyId; 2]>,
}
impl ReplaceBox {
fn get_consumed_locals(&mut self, cx: &LateContext<'_>) -> &FxHashSet<HirId> {
if let Some(body_id) = cx.enclosing_body
&& !self.loaded_bodies.contains(&body_id)
{
self.loaded_bodies.push(body_id);
ExprUseVisitor::for_clippy(
cx,
cx.tcx.hir_body_owner_def_id(body_id),
MovedVariablesCtxt {
consumed_locals: &mut self.consumed_locals,
},
)
.consume_body(cx.tcx.hir_body(body_id))
.into_ok();
}
&self.consumed_locals
}
}
impl_lint_pass!(ReplaceBox => [REPLACE_BOX]);
impl LateLintPass<'_> for ReplaceBox {
fn check_body_post(&mut self, _: &LateContext<'_>, body: &Body<'_>) {
if self.loaded_bodies.first().is_some_and(|&x| x == body.id()) {
self.consumed_locals.clear();
self.loaded_bodies.clear();
}
}
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &'_ Expr<'_>) {
if let ExprKind::Assign(lhs, rhs, _) = &expr.kind
&& !lhs.span.from_expansion()
&& !rhs.span.from_expansion()
&& let lhs_ty = cx.typeck_results().expr_ty(lhs)
&& let Some(inner_ty) = lhs_ty.boxed_ty()
// No diagnostic for late-initialized locals
&& lhs.res_local_id().is_none_or(|local| local_is_initialized(cx, local))
&& let Some(inner_ty) = lhs_ty.boxed_ty()
// No diagnostic if this is a local that has been moved, or the field
// of a local that has been moved, or several chained field accesses of a local
&& local_base(lhs).is_none_or(|(base_id, _)| {
!self.get_consumed_locals(cx).contains(&base_id)
})
{
if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default)
&& implements_trait(cx, inner_ty, default_trait_id, &[])
@ -109,3 +155,46 @@ fn get_box_new_payload<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<
None
}
}
struct MovedVariablesCtxt<'a> {
consumed_locals: &'a mut FxHashSet<HirId>,
}
impl<'tcx> Delegate<'tcx> for MovedVariablesCtxt<'_> {
fn consume(&mut self, cmt: &PlaceWithHirId<'tcx>, _: HirId) {
if let PlaceBase::Local(id) = cmt.place.base
&& let mut projections = cmt
.place
.projections
.iter()
.filter(|x| matches!(x.kind, ProjectionKind::Deref))
// Either no deref or multiple derefs
&& (projections.next().is_none() || projections.next().is_some())
{
self.consumed_locals.insert(id);
}
}
fn use_cloned(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {}
fn borrow(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId, _: ty::BorrowKind) {}
fn mutate(&mut self, _: &PlaceWithHirId<'tcx>, _: HirId) {}
fn fake_read(&mut self, _: &PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
}
/// A local place followed by optional fields
type IdFields = (HirId, Vec<Symbol>);
/// If `expr` is a local variable with optional field accesses, return it.
fn local_base(expr: &Expr<'_>) -> Option<IdFields> {
match expr.kind {
ExprKind::Path(qpath) => qpath.res_local_id().map(|id| (id, Vec::new())),
ExprKind::Field(expr, field) => local_base(expr).map(|(id, mut fields)| {
fields.push(field.name);
(id, fields)
}),
_ => None,
}
}

View file

@ -70,3 +70,74 @@ fn main() {
let bb: Box<u32>;
bb = Default::default();
}
fn issue15951() {
struct Foo {
inner: String,
}
fn embedded_body() {
let mut x = Box::new(());
let y = x;
x = Box::new(());
let mut x = Box::new(Foo { inner: String::new() });
let y = x.inner;
*x = Foo { inner: String::new() };
//~^ replace_box
}
let mut x = Box::new(Foo { inner: String::new() });
let in_closure = || {
*x = Foo { inner: String::new() };
//~^ replace_box
};
}
static R: fn(&mut Box<String>) = |x| **x = String::new();
//~^ replace_box
fn field() {
struct T {
content: String,
}
impl T {
fn new() -> Self {
Self { content: String::new() }
}
}
struct S {
b: Box<T>,
}
let mut s = S { b: Box::new(T::new()) };
let _b = s.b;
s.b = Box::new(T::new());
// Interestingly, the lint and fix are valid here as `s.b` is not really moved
let mut s = S { b: Box::new(T::new()) };
_ = s.b;
*s.b = T::new();
//~^ replace_box
let mut s = S { b: Box::new(T::new()) };
*s.b = T::new();
//~^ replace_box
struct Q(Box<T>);
let mut q = Q(Box::new(T::new()));
let _b = q.0;
q.0 = Box::new(T::new());
let mut q = Q(Box::new(T::new()));
_ = q.0;
*q.0 = T::new();
//~^ replace_box
// This one is a false negative, but it will need MIR analysis to work properly
let mut x = Box::new(String::new());
x = Box::new(String::new());
x;
}

View file

@ -70,3 +70,74 @@ fn main() {
let bb: Box<u32>;
bb = Default::default();
}
fn issue15951() {
struct Foo {
inner: String,
}
fn embedded_body() {
let mut x = Box::new(());
let y = x;
x = Box::new(());
let mut x = Box::new(Foo { inner: String::new() });
let y = x.inner;
x = Box::new(Foo { inner: String::new() });
//~^ replace_box
}
let mut x = Box::new(Foo { inner: String::new() });
let in_closure = || {
x = Box::new(Foo { inner: String::new() });
//~^ replace_box
};
}
static R: fn(&mut Box<String>) = |x| *x = Box::new(String::new());
//~^ replace_box
fn field() {
struct T {
content: String,
}
impl T {
fn new() -> Self {
Self { content: String::new() }
}
}
struct S {
b: Box<T>,
}
let mut s = S { b: Box::new(T::new()) };
let _b = s.b;
s.b = Box::new(T::new());
// Interestingly, the lint and fix are valid here as `s.b` is not really moved
let mut s = S { b: Box::new(T::new()) };
_ = s.b;
s.b = Box::new(T::new());
//~^ replace_box
let mut s = S { b: Box::new(T::new()) };
s.b = Box::new(T::new());
//~^ replace_box
struct Q(Box<T>);
let mut q = Q(Box::new(T::new()));
let _b = q.0;
q.0 = Box::new(T::new());
let mut q = Q(Box::new(T::new()));
_ = q.0;
q.0 = Box::new(T::new());
//~^ replace_box
// This one is a false negative, but it will need MIR analysis to work properly
let mut x = Box::new(String::new());
x = Box::new(String::new());
x;
}

View file

@ -48,5 +48,53 @@ LL | b = Box::new(mac!(three));
|
= note: this creates a needless allocation
error: aborting due to 6 previous errors
error: creating a new box
--> tests/ui/replace_box.rs:86:9
|
LL | x = Box::new(Foo { inner: String::new() });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*x = Foo { inner: String::new() }`
|
= note: this creates a needless allocation
error: creating a new box
--> tests/ui/replace_box.rs:92:9
|
LL | x = Box::new(Foo { inner: String::new() });
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*x = Foo { inner: String::new() }`
|
= note: this creates a needless allocation
error: creating a new box
--> tests/ui/replace_box.rs:97:38
|
LL | static R: fn(&mut Box<String>) = |x| *x = Box::new(String::new());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `**x = String::new()`
|
= note: this creates a needless allocation
error: creating a new box
--> tests/ui/replace_box.rs:122:5
|
LL | s.b = Box::new(T::new());
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*s.b = T::new()`
|
= note: this creates a needless allocation
error: creating a new box
--> tests/ui/replace_box.rs:126:5
|
LL | s.b = Box::new(T::new());
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*s.b = T::new()`
|
= note: this creates a needless allocation
error: creating a new box
--> tests/ui/replace_box.rs:136:5
|
LL | q.0 = Box::new(T::new());
| ^^^^^^^^^^^^^^^^^^^^^^^^ help: replace existing content with inner value instead: `*q.0 = T::new()`
|
= note: this creates a needless allocation
error: aborting due to 12 previous errors