Skip to content

Commit 6f1b342

Browse files
committed
truncate_with_drain
1 parent 5f05ce4 commit 6f1b342

9 files changed

+794
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6037,6 +6037,7 @@ Released 2018-09-13
60376037
[`trim_split_whitespace`]: https://rust-lang.github.io/rust-clippy/master/index.html#trim_split_whitespace
60386038
[`trivial_regex`]: https://rust-lang.github.io/rust-clippy/master/index.html#trivial_regex
60396039
[`trivially_copy_pass_by_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref
6040+
[`truncate_with_drain`]: https://rust-lang.github.io/rust-clippy/master/index.html#truncate_with_drain
60406041
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
60416042
[`tuple_array_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#tuple_array_conversions
60426043
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
469469
crate::methods::SUSPICIOUS_OPEN_OPTIONS_INFO,
470470
crate::methods::SUSPICIOUS_SPLITN_INFO,
471471
crate::methods::SUSPICIOUS_TO_OWNED_INFO,
472+
crate::methods::TRUNCATE_WITH_DRAIN_INFO,
472473
crate::methods::TYPE_ID_ON_BOX_INFO,
473474
crate::methods::UNINIT_ASSUMED_INIT_INFO,
474475
crate::methods::UNIT_HASH_INFO,

clippy_lints/src/methods/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ mod suspicious_command_arg_space;
109109
mod suspicious_map;
110110
mod suspicious_splitn;
111111
mod suspicious_to_owned;
112+
mod truncate_with_drain;
112113
mod type_id_on_box;
113114
mod uninit_assumed_init;
114115
mod unit_hash;
@@ -4281,6 +4282,31 @@ declare_clippy_lint! {
42814282
"map of a trivial closure (not dependent on parameter) over a range"
42824283
}
42834284

4285+
declare_clippy_lint! {
4286+
/// ### What it does
4287+
/// Checks for usage of `.drain(x..)` for the sole purpose of truncating a container.
4288+
///
4289+
/// ### Why is this bad?
4290+
/// This creates an unnecessary iterator that is dropped immediately.
4291+
///
4292+
/// Calling `.truncate(x)` also makes the intent clearer.
4293+
///
4294+
/// ### Example
4295+
/// ```no_run
4296+
/// let mut v = vec![1, 2, 3];
4297+
/// v.drain(1..);
4298+
/// ```
4299+
/// Use instead:
4300+
/// ```no_run
4301+
/// let mut v = vec![1, 2, 3];
4302+
/// v.truncate(1);
4303+
/// ```
4304+
#[clippy::version = "1.84.0"]
4305+
pub TRUNCATE_WITH_DRAIN,
4306+
style,
4307+
"calling `drain` in order to truncate a `Vec`"
4308+
}
4309+
42844310
pub struct Methods {
42854311
avoid_breaking_exported_api: bool,
42864312
msrv: Msrv,
@@ -4446,6 +4472,7 @@ impl_lint_pass!(Methods => [
44464472
MAP_ALL_ANY_IDENTITY,
44474473
MAP_WITH_UNUSED_ARGUMENT_OVER_RANGES,
44484474
UNNECESSARY_MAP_OR,
4475+
TRUNCATE_WITH_DRAIN,
44494476
]);
44504477

44514478
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4762,6 +4789,7 @@ impl Methods {
47624789
&& matches!(kind, StmtKind::Semi(_))
47634790
&& args.len() <= 1
47644791
{
4792+
truncate_with_drain::check(cx, expr, recv, span, args.first());
47654793
clear_with_drain::check(cx, expr, recv, span, args.first());
47664794
} else if let [arg] = args {
47674795
iter_with_drain::check(cx, expr, recv, span, arg);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use clippy_utils::consts::{ConstEvalCtxt, mir_to_const};
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::higher;
4+
use clippy_utils::source::snippet;
5+
use clippy_utils::ty::{is_type_diagnostic_item, is_type_lang_item};
6+
use rustc_ast::ast::RangeLimits;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{Expr, ExprKind, LangItem, Path, QPath};
9+
use rustc_lint::LateContext;
10+
use rustc_middle::mir::Const;
11+
use rustc_middle::ty::{Adt, Ty, TypeckResults};
12+
use rustc_span::Span;
13+
use rustc_span::symbol::sym;
14+
15+
use super::TRUNCATE_WITH_DRAIN;
16+
17+
// Add `String` here when it is added to diagnostic items
18+
const ACCEPTABLE_TYPES_WITH_ARG: [rustc_span::Symbol; 2] = [sym::Vec, sym::VecDeque];
19+
20+
pub fn is_range_open_ended<'a>(
21+
cx: &LateContext<'a>,
22+
range: higher::Range<'_>,
23+
ty: Ty<'a>,
24+
container_path: Option<&Path<'_>>,
25+
) -> bool {
26+
let higher::Range { start, end, limits } = range;
27+
let start_is_none_or_min = start.map_or(true, |start| {
28+
if let Adt(_, subst) = ty.kind()
29+
&& let bnd_ty = subst.type_at(0)
30+
&& let Some(min_val) = bnd_ty.numeric_min_val(cx.tcx)
31+
&& let Some(min_const) = mir_to_const(cx.tcx, Const::from_ty_const(min_val, bnd_ty, cx.tcx))
32+
&& let Some(start_const) = ConstEvalCtxt::new(cx).eval(start)
33+
{
34+
start_const == min_const
35+
} else {
36+
false
37+
}
38+
});
39+
let end_is_none_or_max = end.map_or(true, |end| match limits {
40+
RangeLimits::Closed => {
41+
if let Adt(_, subst) = ty.kind()
42+
&& let bnd_ty = subst.type_at(0)
43+
&& let Some(max_val) = bnd_ty.numeric_max_val(cx.tcx)
44+
&& let Some(max_const) = mir_to_const(cx.tcx, Const::from_ty_const(max_val, bnd_ty, cx.tcx))
45+
&& let Some(end_const) = ConstEvalCtxt::new(cx).eval(end)
46+
{
47+
end_const == max_const
48+
} else {
49+
false
50+
}
51+
},
52+
RangeLimits::HalfOpen => {
53+
if let Some(container_path) = container_path
54+
&& let ExprKind::MethodCall(name, self_arg, [], _) = end.kind
55+
&& name.ident.name == sym::len
56+
&& let ExprKind::Path(QPath::Resolved(None, path)) = self_arg.kind
57+
{
58+
container_path.res == path.res
59+
} else {
60+
false
61+
}
62+
},
63+
});
64+
!start_is_none_or_min && end_is_none_or_max
65+
}
66+
67+
fn match_acceptable_type(
68+
cx: &LateContext<'_>,
69+
expr: &Expr<'_>,
70+
typeck_results: &TypeckResults<'_>,
71+
types: &[rustc_span::Symbol],
72+
) -> bool {
73+
let expr_ty = typeck_results.expr_ty(expr).peel_refs();
74+
types.iter().any(|&ty| is_type_diagnostic_item(cx, expr_ty, ty))
75+
// String type is a lang item but not a diagnostic item for now so we need a separate check
76+
|| is_type_lang_item(cx, expr_ty, LangItem::String)
77+
}
78+
79+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, arg: Option<&Expr<'_>>) {
80+
if let Some(arg) = arg {
81+
let typeck_results = cx.typeck_results();
82+
if match_acceptable_type(cx, recv, typeck_results, &ACCEPTABLE_TYPES_WITH_ARG)
83+
&& let ExprKind::Path(QPath::Resolved(None, container_path)) = recv.kind
84+
&& let Some(range) = higher::Range::hir(arg)
85+
&& let higher::Range { start: Some(start), .. } = range
86+
&& is_range_open_ended(cx, range, typeck_results.expr_ty(arg), Some(container_path))
87+
&& let Some(adt) = typeck_results.expr_ty(recv).ty_adt_def()
88+
// Use `opt_item_name` while `String` is not a diagnostic item
89+
&& let Some(ty_name) = cx.tcx.opt_item_name(adt.did())
90+
{
91+
span_lint_and_sugg(
92+
cx,
93+
TRUNCATE_WITH_DRAIN,
94+
span.with_hi(expr.span.hi()),
95+
format!("`drain` used to truncate a `{ty_name}`"),
96+
"try",
97+
format!("truncate({})", snippet(cx, start.span, "0")),
98+
Applicability::MachineApplicable,
99+
);
100+
}
101+
}
102+
}

tests/ui/clear_with_drain.fixed

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn vec_partial_drains() {
8686
// Do not lint any of these because the ranges are not full
8787

8888
let mut v = vec![1, 2, 3];
89-
v.drain(1..);
89+
v.truncate(1);
9090
let mut v = vec![1, 2, 3];
9191
v.drain(1..).max();
9292

@@ -184,7 +184,7 @@ fn vec_deque_partial_drains() {
184184
// Do not lint any of these because the ranges are not full
185185

186186
let mut deque = VecDeque::from([1, 2, 3]);
187-
deque.drain(1..);
187+
deque.truncate(1);
188188
let mut deque = VecDeque::from([1, 2, 3]);
189189
deque.drain(1..).max();
190190

@@ -282,7 +282,7 @@ fn string_partial_drains() {
282282
// Do not lint any of these because the ranges are not full
283283

284284
let mut s = String::from("Hello, world!");
285-
s.drain(1..);
285+
s.truncate(1);
286286
let mut s = String::from("Hello, world!");
287287
s.drain(1..).max();
288288

tests/ui/clear_with_drain.stderr

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ error: `drain` used to clear a `Vec`
3737
LL | v.drain(..v.len());
3838
| ^^^^^^^^^^^^^^^^ help: try: `clear()`
3939

40+
error: `drain` used to truncate a `Vec`
41+
--> tests/ui/clear_with_drain.rs:89:7
42+
|
43+
LL | v.drain(1..);
44+
| ^^^^^^^^^^ help: try: `truncate(1)`
45+
|
46+
= note: `-D clippy::truncate-with-drain` implied by `-D warnings`
47+
= help: to override `-D warnings` add `#[allow(clippy::truncate_with_drain)]`
48+
4049
error: `drain` used to clear a `VecDeque`
4150
--> tests/ui/clear_with_drain.rs:120:11
4251
|
@@ -73,6 +82,12 @@ error: `drain` used to clear a `VecDeque`
7382
LL | deque.drain(..deque.len());
7483
| ^^^^^^^^^^^^^^^^^^^^ help: try: `clear()`
7584

85+
error: `drain` used to truncate a `VecDeque`
86+
--> tests/ui/clear_with_drain.rs:187:11
87+
|
88+
LL | deque.drain(1..);
89+
| ^^^^^^^^^^ help: try: `truncate(1)`
90+
7691
error: `drain` used to clear a `String`
7792
--> tests/ui/clear_with_drain.rs:218:7
7893
|
@@ -109,6 +124,12 @@ error: `drain` used to clear a `String`
109124
LL | s.drain(..s.len());
110125
| ^^^^^^^^^^^^^^^^ help: try: `clear()`
111126

127+
error: `drain` used to truncate a `String`
128+
--> tests/ui/clear_with_drain.rs:285:7
129+
|
130+
LL | s.drain(1..);
131+
| ^^^^^^^^^^ help: try: `truncate(1)`
132+
112133
error: `drain` used to clear a `HashSet`
113134
--> tests/ui/clear_with_drain.rs:316:9
114135
|
@@ -127,5 +148,5 @@ error: `drain` used to clear a `BinaryHeap`
127148
LL | heap.drain();
128149
| ^^^^^^^ help: try: `clear()`
129150

130-
error: aborting due to 21 previous errors
151+
error: aborting due to 24 previous errors
131152

0 commit comments

Comments
 (0)