Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ document.

[df995e...master](https://github.com/rust-lang/rust-clippy/compare/df995e...master)

### New Lints

* Added [`new_instead_of_clear`] to `perf`
[#16549](https://github.com/rust-lang/rust-clippy/pull/16549)

## Rust 1.95

Current stable, released 2026-04-16
Expand Down Expand Up @@ -7053,6 +7058,7 @@ Released 2018-09-13
[`neg_multiply`]: https://rust-lang.github.io/rust-clippy/master/index.html#neg_multiply
[`negative_feature_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#negative_feature_names
[`never_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#never_loop
[`new_instead_of_clear`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_instead_of_clear
[`new_ret_no_self`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_ret_no_self
[`new_without_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_without_default
[`new_without_default_derive`]: https://rust-lang.github.io/rust-clippy/master/index.html#new_without_default_derive
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::operators::MODULO_ARITHMETIC_INFO,
crate::operators::MODULO_ONE_INFO,
crate::operators::NEEDLESS_BITWISE_BOOL_INFO,
crate::operators::NEW_INSTEAD_OF_CLEAR_INFO,
crate::operators::OP_REF_INFO,
crate::operators::REDUNDANT_COMPARISONS_INFO,
crate::operators::SELF_ASSIGNMENT_INFO,
Expand Down
35 changes: 35 additions & 0 deletions clippy_lints/src/operators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod misrefactored_assign_op;
mod modulo_arithmetic;
mod modulo_one;
mod needless_bitwise_bool;
mod new_instead_of_clear;
mod numeric_arithmetic;
mod op_ref;
mod self_assignment;
Expand Down Expand Up @@ -857,6 +858,38 @@ declare_clippy_lint! {
"Boolean expressions that use bitwise rather than lazy operators"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for assignments of `Collection::new()` or `vec![]` to a collection
/// variable that was already initialized.
///
/// ### Why is this bad?
/// The existing allocation is thrown away. `.clear()` empties the collection
/// without freeing the memory, so the next push/insert won't need to allocate.
///
/// ### Example
/// ```no_run
/// # fn f(v: &Vec<i32>) {}
/// let mut v = Vec::new();
/// v.push(1);
/// f(&v);
/// v = Vec::new();
/// ```
///
/// Use instead:
/// ```no_run
/// # fn f(v: &Vec<i32>) {}
/// let mut v = Vec::new();
/// v.push(1);
/// f(&v);
/// v.clear();
/// ```
#[clippy::version = "1.96.0"]
pub NEW_INSTEAD_OF_CLEAR,
perf,
"creating a new collection instead of calling `.clear()`"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for arguments to `==` which have their address
Expand Down Expand Up @@ -995,6 +1028,7 @@ impl_lint_pass!(Operators => [
MODULO_ARITHMETIC,
MODULO_ONE,
NEEDLESS_BITWISE_BOOL,
NEW_INSTEAD_OF_CLEAR,
OP_REF,
REDUNDANT_COMPARISONS,
SELF_ASSIGNMENT,
Expand Down Expand Up @@ -1072,6 +1106,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
ExprKind::Assign(lhs, rhs, _) => {
assign_op_pattern::check(cx, e, lhs, rhs, self.msrv);
self_assignment::check(cx, e, lhs, rhs);
new_instead_of_clear::check(cx, e);
},
ExprKind::Unary(op, arg) =>
{
Expand Down
106 changes: 106 additions & 0 deletions clippy_lints/src/operators/new_instead_of_clear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::higher::VecArgs;
use clippy_utils::macros::matching_root_macro_call;
use clippy_utils::res::MaybeDef;
use clippy_utils::source::snippet;
use clippy_utils::{local_is_initialized, sym};
use rustc_errors::Applicability;
use rustc_hir::def::Res;
use rustc_hir::{Expr, ExprKind, QPath, TyKind, UnOp};
use rustc_lint::LateContext;

use super::NEW_INSTEAD_OF_CLEAR;

pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>) {
if let ExprKind::Assign(lhs, rhs, _) = expr.kind
&& !expr.span.from_expansion()
{
check_collection_new(cx, expr, lhs, rhs);
check_vec_macro(cx, expr, lhs, rhs);
}
}

fn check_collection_new(cx: &LateContext<'_>, expr: &Expr<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>) {
// skip macro expansions (e.g. vec![] is handled separately)
if !rhs.span.from_expansion()
&& let ExprKind::Call(func, []) = rhs.kind
&& let ExprKind::Path(QPath::TypeRelative(ty, method)) = func.kind
&& method.ident.name == sym::new
// turbofish on the type (e.g. `Vec::<i32>::new()`) changes type inference,
// so replacing with `.clear()` could be wrong
&& !has_type_args(ty)
{
let rhs_ty = cx.typeck_results().node_type(ty.hir_id);
if matches!(
rhs_ty.peel_refs().opt_diag_name(cx),
Some(
sym::Vec
| sym::HashMap
| sym::HashSet
| sym::VecDeque
| sym::BTreeMap
| sym::BTreeSet
| sym::BinaryHeap
| sym::LinkedList
)
) && let Some(sugg) = local_snippet(cx, lhs)
{
emit_lint(cx, expr, &sugg);
}
}
}

fn check_vec_macro(cx: &LateContext<'_>, expr: &Expr<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>) {
if matching_root_macro_call(cx, rhs.span, sym::vec_macro).is_some()
&& matches!(VecArgs::hir(cx, rhs), Some(VecArgs::Vec([])))
&& let Some(sugg) = local_snippet(cx, lhs)
{
emit_lint(cx, expr, &sugg);
}
}

fn emit_lint(cx: &LateContext<'_>, expr: &Expr<'_>, sugg: &str) {
span_lint_and_then(
cx,
NEW_INSTEAD_OF_CLEAR,
expr.span,
"assigning a new empty collection",
|diag| {
diag.span_suggestion(
expr.span,
"consider using `.clear()` instead",
format!("{sugg}.clear()"),
Applicability::MaybeIncorrect,
);
diag.note("`.clear()` retains the allocated memory for reuse");
},
);
}

fn has_type_args(ty: &rustc_hir::Ty<'_>) -> bool {
if let TyKind::Path(QPath::Resolved(_, path)) = ty.kind {
return path.segments.last().is_some_and(|seg| seg.args.is_some());
}
false
}

/// Returns the source snippet for `expr` if it refers to an initialized local variable,
/// peeling any leading dereferences first.
fn local_snippet(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<String> {
let inner = peel_derefs(expr);
if let ExprKind::Path(QPath::Resolved(None, path)) = inner.kind
&& let Res::Local(hir_id) = path.res
&& local_is_initialized(cx, hir_id)
{
Some(snippet(cx, inner.span, "..").into_owned())
} else {
None
}
}

fn peel_derefs<'tcx>(mut expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
while let ExprKind::Unary(UnOp::Deref, inner) = expr.kind {
expr = inner;
}
expr
}
111 changes: 111 additions & 0 deletions tests/ui/new_instead_of_clear.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#![warn(clippy::new_instead_of_clear)]
#![expect(clippy::vec_init_then_push)]
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque};

fn use_vec(_: &Vec<i32>) {}
fn use_map(_: &HashMap<i32, i32>) {}
fn use_set(_: &HashSet<i32>) {}
fn use_deque(_: &VecDeque<i32>) {}
fn use_btree_map(_: &BTreeMap<i32, i32>) {}
fn use_btree_set(_: &BTreeSet<i32>) {}
fn use_heap(_: &BinaryHeap<i32>) {}
fn use_list(_: &LinkedList<i32>) {}

fn main() {
let mut v = Vec::new();
v.push(1);
use_vec(&v);
v.clear();
//~^ new_instead_of_clear

let mut map: HashMap<i32, i32> = HashMap::new();
map.insert(1, 2);
use_map(&map);
map.clear();
//~^ new_instead_of_clear

let mut set: HashSet<i32> = HashSet::new();
set.insert(1);
use_set(&set);
set.clear();
//~^ new_instead_of_clear

let mut deque: VecDeque<i32> = VecDeque::new();
deque.push_back(1);
use_deque(&deque);
deque.clear();
//~^ new_instead_of_clear

let mut btree_map: BTreeMap<i32, i32> = BTreeMap::new();
btree_map.insert(1, 2);
use_btree_map(&btree_map);
btree_map.clear();
//~^ new_instead_of_clear

let mut btree_set: BTreeSet<i32> = BTreeSet::new();
btree_set.insert(1);
use_btree_set(&btree_set);
btree_set.clear();
//~^ new_instead_of_clear

let mut heap: BinaryHeap<i32> = BinaryHeap::new();
heap.push(1);
use_heap(&heap);
heap.clear();
//~^ new_instead_of_clear

let mut list: LinkedList<i32> = LinkedList::new();
list.push_back(1);
use_list(&list);
list.clear();
//~^ new_instead_of_clear

let mut v2 = Vec::new();
v2.push(1);
use_vec(&v2);
v2.clear();
//~^ new_instead_of_clear
}

fn deref_target() {
let mut v = Box::new(Vec::<i32>::new());
v.push(1);
use_vec(&v);
v.clear();
//~^ new_instead_of_clear
}

fn block_expr() {
let mut v = Vec::new();
v.push(1);
use_vec(&v);
let _ = {
v.clear();
//~^ new_instead_of_clear
42
};
}

fn no_lint() {
// turbofish changes type inference, replacing with .clear() would be wrong
let mut v: Vec<i32> = Vec::new();
v.push(1);
use_vec(&v);
v = Vec::<i32>::new();

// first assignment to an uninitialized binding, .clear() won't compile
let mut u: Vec<u8>;
u = Vec::new();
u.push(1u8);

// inside a macro expansion
macro_rules! reset {
($x:ident) => {
$x = Vec::new()
};
}
let mut w = Vec::new();
w.push(1);
use_vec(&w);
reset!(w);
}
Loading
Loading