blob: 638f6dc1532bb0a8ac17a06c06fb68ae7a070720 [file] [log] [blame] [edit]
use clippy_utils::diagnostics::span_lint_and_then;
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::{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_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
/// Detects assignments of `Default::default()` or `Box::new(value)`
/// to a place of type `Box<T>`.
///
/// ### Why is this bad?
/// This incurs an extra heap allocation compared to assigning the boxed
/// storage.
///
/// ### Example
/// ```no_run
/// let mut b = Box::new(1u32);
/// b = Default::default();
/// ```
/// Use instead:
/// ```no_run
/// let mut b = Box::new(1u32);
/// *b = Default::default();
/// ```
#[clippy::version = "1.92.0"]
pub REPLACE_BOX,
perf,
"assigning a newly created box to `Box<T>` is inefficient"
}
#[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))
// 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, &[])
&& is_default_call(cx, rhs)
{
span_lint_and_then(
cx,
REPLACE_BOX,
expr.span,
"creating a new box with default content",
|diag| {
let mut app = Applicability::MachineApplicable;
let suggestion = format!(
"{} = Default::default()",
Sugg::hir_with_applicability(cx, lhs, "_", &mut app).deref()
);
diag.note("this creates a needless allocation").span_suggestion(
expr.span,
"replace existing content with default instead",
suggestion,
app,
);
},
);
}
if inner_ty.is_sized(cx.tcx, cx.typing_env())
&& let Some(rhs_inner) = get_box_new_payload(cx, rhs)
{
span_lint_and_then(cx, REPLACE_BOX, expr.span, "creating a new box", |diag| {
let mut app = Applicability::MachineApplicable;
let suggestion = format!(
"{} = {}",
Sugg::hir_with_applicability(cx, lhs, "_", &mut app).deref(),
Sugg::hir_with_context(cx, rhs_inner, expr.span.ctxt(), "_", &mut app),
);
diag.note("this creates a needless allocation").span_suggestion(
expr.span,
"replace existing content with inner value instead",
suggestion,
app,
);
});
}
}
}
}
fn is_default_call(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
matches!(expr.kind, ExprKind::Call(func, _args) if is_default_equivalent_call(cx, func, Some(expr)))
}
fn get_box_new_payload<'tcx>(cx: &LateContext<'_>, expr: &Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
if let ExprKind::Call(box_new, [arg]) = expr.kind
&& let ExprKind::Path(QPath::TypeRelative(ty, seg)) = box_new.kind
&& seg.ident.name == sym::new
&& ty.basic_res().is_lang_item(cx, LangItem::OwnedBox)
{
Some(arg)
} else {
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,
}
}