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.

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

### New Lints

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

## Rust 1.96

Current stable, released 2026-05-28
Expand Down Expand Up @@ -7151,6 +7156,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 @@ -620,6 +620,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 @@ -22,6 +22,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 @@ -882,6 +883,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.98.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 @@ -1021,6 +1054,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 @@ -1099,6 +1133,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
111 changes: 111 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,111 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::higher::VecArgs;
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::{eq_expr_value, is_integer_literal, local_is_initialized, peel_ref_operators, sym};
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, LangItem, QPath, TyKind};
use rustc_lint::LateContext;
use rustc_span::Symbol;

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<'_>) {
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)
// `.clear()` only keeps the backing allocation for contiguously allocated types;
// node-based collections (`BTreeMap`, `BTreeSet`, `LinkedList`) don't preallocate,
// so the note about retained memory must not be shown for them
&& let Some(retains_allocation) = match rhs_ty.opt_diag_name(cx) {
Some(sym::Vec | sym::HashMap | sym::HashSet | sym::VecDeque | sym::BinaryHeap) => Some(true),
Some(sym::BTreeMap | sym::BTreeSet | sym::LinkedList) => Some(false),
_ if rhs_ty.is_lang_item(cx, LangItem::String) => Some(true),
_ => None,
}
&& let Some(name) = local_name(cx, lhs)
{
emit_lint(cx, expr, name, retains_allocation, None);
}
}

fn check_vec_macro(cx: &LateContext<'_>, expr: &Expr<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>) {
// `VecArgs::hir` already verifies that the `vec!` macro is used
let discarded_init = match VecArgs::hir(cx, rhs) {
Some(VecArgs::Vec([])) => None,
Some(VecArgs::Repeat(init, count)) if is_integer_literal(count, 0) => Some(init),
_ => return,
};
if let Some(name) = local_name(cx, lhs) {
// an empty `vec![]` is a `Vec`, which keeps its allocation on `.clear()`
emit_lint(cx, expr, name, true, discarded_init);
}
}

fn emit_lint(
cx: &LateContext<'_>,
expr: &Expr<'_>,
name: Symbol,
retains_allocation: bool,
discarded_init: Option<&Expr<'_>>,
) {
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!("{name}.clear()"),
Applicability::MaybeIncorrect,
);
if retains_allocation {
diag.note("`.clear()` retains the allocated memory for reuse");
}
// for `vec![init; 0]`, `init` is evaluated once but would be skipped by `.clear()`
if let Some(init) = discarded_init
&& !eq_expr_value(cx, expr.span.ctxt(), init, init)
{
diag.span_note(init.span, "side effects of this expression will no longer be executed");
}
},
);
}

/// Returns `true` if the type-relative path `ty` carries explicit type arguments,
/// e.g. `Vec::<i32>` in `Vec::<i32>::new()`.
fn has_type_args(ty: &rustc_hir::Ty<'_>) -> bool {
if let TyKind::Path(QPath::Resolved(_, path)) = ty.kind {
path.segments.last().is_some_and(|seg| seg.args.is_some())
} else {
false
}
}

/// If `expr` is a path to an initialized local, returns the local's name.
///
/// Reference operators (`&`/`*` on references) are peeled, but smart-pointer `Deref`s
/// such as `Box` are deliberately left in place: a `*boxed = Vec::new()` must not be
/// rewritten to `boxed.clear()`, since the smart pointer may have its own `clear` with
/// different semantics.
fn local_name(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<Symbol> {
peel_ref_operators(cx, expr)
.res_local_id_and_ident()
.filter(|(hir_id, _)| local_is_initialized(cx, *hir_id))
.map(|(_, ident)| ident.name)
}
2 changes: 1 addition & 1 deletion tests/ui/collection_is_never_read.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![expect(clippy::useless_vec)]
#![expect(clippy::useless_vec, clippy::new_instead_of_clear)]
#![warn(clippy::collection_is_never_read)]

use std::collections::{HashMap, HashSet};
Expand Down
146 changes: 146 additions & 0 deletions tests/ui/new_instead_of_clear.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#![warn(clippy::new_instead_of_clear)]
#![expect(clippy::vec_init_then_push)]
#![allow(clippy::zero_repeat_side_effects)]
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 use_string(_: &String) {}

fn side_effect() -> i32 {
println!("called");
0
}

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 heap: BinaryHeap<i32> = BinaryHeap::new();
heap.push(1);
use_heap(&heap);
heap.clear();
//~^ new_instead_of_clear

let mut s = String::new();
s.push('a');
use_string(&s);
s.clear();
//~^ new_instead_of_clear

// node-based collections: still linted, but without the "retains memory" note
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 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

// `vec![init; 0]` is also empty; `0` has no side effect, so no extra note
let mut v3 = Vec::new();
v3.push(1);
use_vec(&v3);
v3.clear();
//~^ new_instead_of_clear

// here the discarded initializer has a side effect that would no longer run
let mut v4 = Vec::new();
v4.push(1);
use_vec(&v4);
v4.clear();
//~^ new_instead_of_clear
}

fn deref_through_reference() {
let mut v = Vec::new();
v.push(1);
use_vec(&v);
let r = &mut v;
r.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);

// smart-pointer deref: `Box` could have a `clear` with different semantics
let mut boxed = Box::new(Vec::new());
boxed.push(1);
use_vec(&boxed);
*boxed = Vec::new();

// 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