Skip to content

Commit 9458180

Browse files
committed
new lint truncate_with_drain
1 parent 9cf416d commit 9458180

File tree

9 files changed

+767
-2
lines changed

9 files changed

+767
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6033,6 +6033,7 @@ Released 2018-09-13
60336033
[`trim_split_whitespace`]: https://rust-lang.github.io/rust-clippy/master/index.html#trim_split_whitespace
60346034
[`trivial_regex`]: https://rust-lang.github.io/rust-clippy/master/index.html#trivial_regex
60356035
[`trivially_copy_pass_by_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref
6036+
[`truncate_with_drain`]: https://rust-lang.github.io/rust-clippy/master/index.html#truncate_with_drain
60366037
[`try_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#try_err
60376038
[`tuple_array_conversions`]: https://rust-lang.github.io/rust-clippy/master/index.html#tuple_array_conversions
60386039
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

book/src/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A collection of lints to catch common mistakes and improve your
77
[Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
Lints are divided into categories, each with a default [lint
1212
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
465465
crate::methods::SUSPICIOUS_OPEN_OPTIONS_INFO,
466466
crate::methods::SUSPICIOUS_SPLITN_INFO,
467467
crate::methods::SUSPICIOUS_TO_OWNED_INFO,
468+
crate::methods::TRUNCATE_WITH_DRAIN_INFO,
468469
crate::methods::TYPE_ID_ON_BOX_INFO,
469470
crate::methods::UNINIT_ASSUMED_INIT_INFO,
470471
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
@@ -106,6 +106,7 @@ mod suspicious_command_arg_space;
106106
mod suspicious_map;
107107
mod suspicious_splitn;
108108
mod suspicious_to_owned;
109+
mod truncate_with_drain;
109110
mod type_id_on_box;
110111
mod uninit_assumed_init;
111112
mod unit_hash;
@@ -4166,6 +4167,31 @@ declare_clippy_lint! {
41664167
"calling `.first().is_some()` or `.first().is_none()` instead of `.is_empty()`"
41674168
}
41684169

4170+
declare_clippy_lint! {
4171+
/// ### What it does
4172+
/// Checks for usage of `.drain(x..)` for the sole purpose of truncate a container.
4173+
///
4174+
/// ### Why is this bad?
4175+
/// This creates an unnecessary iterator that is dropped immediately.
4176+
///
4177+
/// Calling `.truncate(x)` also makes the intent clearer.
4178+
///
4179+
/// ### Example
4180+
/// ```no_run
4181+
/// let mut v = vec![1, 2, 3];
4182+
/// v.drain(1..);
4183+
/// ```
4184+
/// Use instead:
4185+
/// ```no_run
4186+
/// let mut v = vec![1, 2, 3];
4187+
/// v.truncate(1);
4188+
/// ```
4189+
#[clippy::version = "1.84.0"]
4190+
pub TRUNCATE_WITH_DRAIN,
4191+
nursery,
4192+
"calling `drain` in order to `truncate` a `Vec`"
4193+
}
4194+
41694195
pub struct Methods {
41704196
avoid_breaking_exported_api: bool,
41714197
msrv: Msrv,
@@ -4327,6 +4353,7 @@ impl_lint_pass!(Methods => [
43274353
NEEDLESS_CHARACTER_ITERATION,
43284354
MANUAL_INSPECT,
43294355
UNNECESSARY_MIN_OR_MAX,
4356+
TRUNCATE_WITH_DRAIN,
43304357
]);
43314358

43324359
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4634,6 +4661,7 @@ impl Methods {
46344661
&& matches!(kind, StmtKind::Semi(_))
46354662
&& args.len() <= 1
46364663
{
4664+
truncate_with_drain::check(cx, expr, recv, span, args.first());
46374665
clear_with_drain::check(cx, expr, recv, span, args.first());
46384666
} else if let [arg] = args {
46394667
iter_with_drain::check(cx, expr, recv, span, arg);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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::{self as rustc_ty};
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(cx: &LateContext<'_>, expr: &Expr<'_>, container_path: Option<&Path<'_>>) -> bool {
21+
let ty = cx.typeck_results().expr_ty(expr);
22+
if let Some(higher::Range { start, end, limits }) = higher::Range::hir(expr) {
23+
let start_is_none_or_min = start.map_or(true, |start| {
24+
if let rustc_ty::Adt(_, subst) = ty.kind()
25+
&& let bnd_ty = subst.type_at(0)
26+
&& let Some(min_val) = bnd_ty.numeric_min_val(cx.tcx)
27+
&& let Some(min_const) = mir_to_const(cx.tcx, Const::from_ty_const(min_val, bnd_ty, cx.tcx))
28+
&& let Some(start_const) = ConstEvalCtxt::new(cx).eval(start)
29+
{
30+
start_const == min_const
31+
} else {
32+
false
33+
}
34+
});
35+
let end_is_none_or_max = end.map_or(true, |end| match limits {
36+
RangeLimits::Closed => {
37+
if let rustc_ty::Adt(_, subst) = ty.kind()
38+
&& let bnd_ty = subst.type_at(0)
39+
&& let Some(max_val) = bnd_ty.numeric_max_val(cx.tcx)
40+
&& let Some(max_const) = mir_to_const(cx.tcx, Const::from_ty_const(max_val, bnd_ty, cx.tcx))
41+
&& let Some(end_const) = ConstEvalCtxt::new(cx).eval(end)
42+
{
43+
end_const == max_const
44+
} else {
45+
false
46+
}
47+
},
48+
RangeLimits::HalfOpen => {
49+
if let Some(container_path) = container_path
50+
&& let ExprKind::MethodCall(name, self_arg, [], _) = end.kind
51+
&& name.ident.name == sym::len
52+
&& let ExprKind::Path(QPath::Resolved(None, path)) = self_arg.kind
53+
{
54+
container_path.res == path.res
55+
} else {
56+
false
57+
}
58+
},
59+
});
60+
return !start_is_none_or_min && end_is_none_or_max;
61+
}
62+
false
63+
}
64+
65+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, arg: Option<&Expr<'_>>) {
66+
if let Some(arg) = arg {
67+
if match_acceptable_type(cx, recv, &ACCEPTABLE_TYPES_WITH_ARG)
68+
&& let ExprKind::Path(QPath::Resolved(None, container_path)) = recv.kind
69+
&& is_range_open_ended(cx, arg, Some(container_path))
70+
{
71+
suggest(cx, expr, recv, span, arg);
72+
}
73+
}
74+
}
75+
76+
fn match_acceptable_type(cx: &LateContext<'_>, expr: &Expr<'_>, types: &[rustc_span::Symbol]) -> bool {
77+
let expr_ty = cx.typeck_results().expr_ty(expr).peel_refs();
78+
types.iter().any(|&ty| is_type_diagnostic_item(cx, expr_ty, ty))
79+
// String type is a lang item but not a diagnostic item for now so we need a separate check
80+
|| is_type_lang_item(cx, expr_ty, LangItem::String)
81+
}
82+
83+
fn suggest(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, span: Span, arg: &Expr<'_>) {
84+
if let Some(adt) = cx.typeck_results().expr_ty(recv).ty_adt_def()
85+
// Use `opt_item_name` while `String` is not a diagnostic item
86+
&& let Some(ty_name) = cx.tcx.opt_item_name(adt.did())
87+
{
88+
if let Some(higher::Range { start: Some(start), .. }) = higher::Range::hir(arg) {
89+
span_lint_and_sugg(
90+
cx,
91+
TRUNCATE_WITH_DRAIN,
92+
span.with_hi(expr.span.hi()),
93+
format!("`drain` used to truncate a `{ty_name}`"),
94+
"try",
95+
format!("truncate({})", snippet(cx, start.span, "0")),
96+
Applicability::MachineApplicable,
97+
);
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)