Skip to content

Commit bb58083

Browse files
committed
new lint: while_pop_unwrap
1 parent 3594d55 commit bb58083

File tree

8 files changed

+226
-0
lines changed

8 files changed

+226
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5160,6 +5160,7 @@ Released 2018-09-13
51605160
[`while_immutable_condition`]: https://rust-lang.github.io/rust-clippy/master/index.html#while_immutable_condition
51615161
[`while_let_loop`]: https://rust-lang.github.io/rust-clippy/master/index.html#while_let_loop
51625162
[`while_let_on_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator
5163+
[`while_pop_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#while_pop_unwrap
51635164
[`wildcard_dependencies`]: https://rust-lang.github.io/rust-clippy/master/index.html#wildcard_dependencies
51645165
[`wildcard_enum_match_arm`]: https://rust-lang.github.io/rust-clippy/master/index.html#wildcard_enum_match_arm
51655166
[`wildcard_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#wildcard_imports

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
645645
crate::useless_conversion::USELESS_CONVERSION_INFO,
646646
crate::vec::USELESS_VEC_INFO,
647647
crate::vec_init_then_push::VEC_INIT_THEN_PUSH_INFO,
648+
crate::while_pop_unwrap::WHILE_POP_UNWRAP_INFO,
648649
crate::wildcard_imports::ENUM_GLOB_USE_INFO,
649650
crate::wildcard_imports::WILDCARD_IMPORTS_INFO,
650651
crate::write::PRINTLN_EMPTY_STRING_INFO,

clippy_lints/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ mod use_self;
324324
mod useless_conversion;
325325
mod vec;
326326
mod vec_init_then_push;
327+
mod while_pop_unwrap;
327328
mod wildcard_imports;
328329
mod write;
329330
mod zero_div_zero;

clippy_lints/src/loops/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use rustc_session::{declare_lint_pass, declare_tool_lint};
2525
use rustc_span::source_map::Span;
2626
use utils::{make_iterator_snippet, IncrementVisitor, InitializeVisitor};
2727

28+
use crate::while_pop_unwrap;
29+
2830
declare_clippy_lint! {
2931
/// ### What it does
3032
/// Checks for for-loops that manually copy items between
@@ -643,6 +645,7 @@ impl<'tcx> LateLintPass<'tcx> for Loops {
643645
if let Some(higher::While { condition, body }) = higher::While::hir(expr) {
644646
while_immutable_condition::check(cx, condition, body);
645647
missing_spin_loop::check(cx, condition, body);
648+
while_pop_unwrap::check(cx, condition, body);
646649
}
647650
}
648651
}

clippy_lints/src/while_pop_unwrap.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use clippy_utils::{diagnostics::span_lint_and_then, match_def_path, paths, source::snippet};
2+
use rustc_errors::Applicability;
3+
use rustc_hir::*;
4+
use rustc_lint::LateContext;
5+
use rustc_session::{declare_lint_pass, declare_tool_lint};
6+
use rustc_span::{symbol::Ident, Span};
7+
8+
declare_clippy_lint! {
9+
/// ### What it does
10+
/// Looks for loops that check for emptiness of a `Vec` in the condition and pop an element
11+
/// in the body as a separate operation.
12+
///
13+
/// ### Why is this bad?
14+
/// Such loops can be written in a more idiomatic way by using a while..let loop and directly
15+
/// pattern matching on the return value of `Vec::pop()`.
16+
///
17+
/// ### Example
18+
/// ```rust
19+
/// let mut numbers = vec![1, 2, 3, 4, 5];
20+
/// while !numbers.is_empty() {
21+
/// let number = numbers.pop().unwrap();
22+
/// // use `number`
23+
/// }
24+
/// ```
25+
/// Use instead:
26+
/// ```rust
27+
/// let mut numbers = vec![1, 2, 3, 4, 5];
28+
/// while let Some(number) = numbers.pop() {
29+
/// // use `number`
30+
/// }
31+
/// ```
32+
#[clippy::version = "1.70.0"]
33+
pub WHILE_POP_UNWRAP,
34+
style,
35+
"checking for emptiness of a `Vec` in the loop condition and popping an element in the body"
36+
}
37+
declare_lint_pass!(WhilePopUnwrap => [WHILE_POP_UNWRAP]);
38+
39+
fn report_lint<'tcx>(
40+
cx: &LateContext<'tcx>,
41+
pop_span: Span,
42+
ident: Option<Ident>,
43+
loop_span: Span,
44+
receiver_span: Span,
45+
) {
46+
span_lint_and_then(
47+
cx,
48+
WHILE_POP_UNWRAP,
49+
pop_span,
50+
"you seem to be trying to pop elements from a `Vec` in a loop",
51+
|diag| {
52+
diag.span_suggestion(
53+
loop_span,
54+
"try",
55+
format!(
56+
"while let Some({}) = {}.pop()",
57+
ident.as_ref().map(Ident::as_str).unwrap_or("element"),
58+
snippet(cx, receiver_span, "..")
59+
),
60+
Applicability::MaybeIncorrect,
61+
)
62+
.note("this while loop can be written in a more idiomatic way");
63+
},
64+
);
65+
}
66+
67+
fn match_method_call(cx: &LateContext<'_>, expr: &Expr<'_>, method: &[&str]) -> bool {
68+
if let ExprKind::MethodCall(..) = expr.kind
69+
&& let Some(id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
70+
&& match_def_path(cx, id, method)
71+
{
72+
true
73+
} else {
74+
false
75+
}
76+
}
77+
78+
fn is_vec_pop<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool {
79+
match_method_call(cx, expr, &paths::VEC_POP)
80+
}
81+
82+
fn is_vec_pop_unwrap<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool {
83+
if let ExprKind::MethodCall(_, inner, ..) = expr.kind
84+
&& (match_method_call(cx, expr, &paths::OPTION_UNWRAP) || match_method_call(cx, expr, &paths::OPTION_EXPECT))
85+
&& is_vec_pop(cx, inner)
86+
{
87+
true
88+
} else {
89+
false
90+
}
91+
}
92+
93+
fn check_local<'tcx>(cx: &LateContext<'tcx>, stmt: &Stmt<'_>, loop_span: Span, recv_span: Span) {
94+
if let StmtKind::Local(local) = stmt.kind
95+
&& let PatKind::Binding(.., ident, _) = local.pat.kind
96+
&& let Some(init) = local.init
97+
&& let ExprKind::MethodCall(_, inner, ..) = init.kind
98+
&& is_vec_pop_unwrap(cx, init)
99+
{
100+
report_lint(cx, init.span.to(inner.span), Some(ident), loop_span, recv_span);
101+
}
102+
}
103+
104+
fn check_call_arguments<'tcx>(cx: &LateContext<'tcx>, stmt: &Stmt<'_>, loop_span: Span, recv_span: Span) {
105+
if let StmtKind::Semi(expr) | StmtKind::Expr(expr) = stmt.kind {
106+
if let ExprKind::MethodCall(_, _, args, _) | ExprKind::Call(_, args) = expr.kind {
107+
let offending_arg = args.iter().find_map(|arg| is_vec_pop_unwrap(cx, arg).then(|| arg.span));
108+
109+
if let Some(offending_arg) = offending_arg {
110+
report_lint(cx, offending_arg, None, loop_span, recv_span);
111+
}
112+
}
113+
}
114+
}
115+
116+
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, cond: &'tcx Expr<'_>, body: &'tcx Expr<'_>) {
117+
if let ExprKind::Unary(UnOp::Not, cond) = cond.kind
118+
&& let ExprKind::MethodCall(_, Expr { span: recv_span, .. }, _, _) = cond.kind
119+
&& match_method_call(cx, cond, &paths::VEC_IS_EMPTY)
120+
&& let ExprKind::Block(body, _) = body.kind
121+
&& let Some(stmt) = body.stmts.first()
122+
{
123+
check_local(cx, stmt, cond.span, *recv_span);
124+
check_call_arguments(cx, stmt, cond.span, *recv_span);
125+
}
126+
}

clippy_utils/src/paths.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,7 @@ pub const WEAK_RC: [&str; 3] = ["alloc", "rc", "Weak"];
159159
pub const PTR_NON_NULL: [&str; 4] = ["core", "ptr", "non_null", "NonNull"];
160160
pub const INSTANT_NOW: [&str; 4] = ["std", "time", "Instant", "now"];
161161
pub const INSTANT: [&str; 3] = ["std", "time", "Instant"];
162+
pub const VEC_IS_EMPTY: [&str; 4] = ["alloc", "vec", "Vec", "is_empty"];
163+
pub const VEC_POP: [&str; 4] = ["alloc", "vec", "Vec", "pop"];
164+
pub const OPTION_UNWRAP: [&str; 4] = ["core", "option", "Option", "unwrap"];
165+
pub const OPTION_EXPECT: [&str; 4] = ["core", "option", "Option", "expect"];

tests/ui/while_pop_unwrap.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#![allow(unused)]
2+
#![warn(clippy::while_pop_unwrap)]
3+
4+
struct VecInStruct {
5+
numbers: Vec<i32>,
6+
unrelated: String,
7+
}
8+
9+
fn accept_i32(_: i32) {}
10+
11+
fn main() {
12+
let mut numbers = vec![1, 2, 3, 4, 5];
13+
while !numbers.is_empty() {
14+
let number = numbers.pop().unwrap();
15+
}
16+
17+
let mut val = VecInStruct {
18+
numbers: vec![1, 2, 3, 4, 5],
19+
unrelated: String::new(),
20+
};
21+
while !val.numbers.is_empty() {
22+
let number = val.numbers.pop().unwrap();
23+
}
24+
25+
while !numbers.is_empty() {
26+
accept_i32(numbers.pop().unwrap());
27+
}
28+
29+
while !numbers.is_empty() {
30+
accept_i32(numbers.pop().expect(""));
31+
}
32+
33+
// This should not warn. It "conditionally" pops elements.
34+
while !numbers.is_empty() {
35+
if true {
36+
accept_i32(numbers.pop().unwrap());
37+
}
38+
}
39+
40+
// This should also not warn. It conditionally pops elements.
41+
while !numbers.is_empty() {
42+
if false {
43+
continue;
44+
}
45+
accept_i32(numbers.pop().unwrap());
46+
}
47+
}

tests/ui/while_pop_unwrap.stderr

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
error: you seem to be trying to pop elements from a `Vec` in a loop
2+
--> $DIR/while_pop_unwrap.rs:14:22
3+
|
4+
LL | while !numbers.is_empty() {
5+
| ------------------ help: try: `while let Some(number) = numbers.pop()`
6+
LL | let number = numbers.pop().unwrap();
7+
| ^^^^^^^^^^^^^^^^^^^^^^
8+
|
9+
= note: this while loop can be written in a more idiomatic way
10+
= note: `-D clippy::while-pop-unwrap` implied by `-D warnings`
11+
12+
error: you seem to be trying to pop elements from a `Vec` in a loop
13+
--> $DIR/while_pop_unwrap.rs:22:22
14+
|
15+
LL | while !val.numbers.is_empty() {
16+
| ---------------------- help: try: `while let Some(number) = val.numbers.pop()`
17+
LL | let number = val.numbers.pop().unwrap();
18+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
19+
|
20+
= note: this while loop can be written in a more idiomatic way
21+
22+
error: you seem to be trying to pop elements from a `Vec` in a loop
23+
--> $DIR/while_pop_unwrap.rs:26:20
24+
|
25+
LL | while !numbers.is_empty() {
26+
| ------------------ help: try: `while let Some(element) = numbers.pop()`
27+
LL | accept_i32(numbers.pop().unwrap());
28+
| ^^^^^^^^^^^^^^^^^^^^^^
29+
|
30+
= note: this while loop can be written in a more idiomatic way
31+
32+
error: you seem to be trying to pop elements from a `Vec` in a loop
33+
--> $DIR/while_pop_unwrap.rs:30:20
34+
|
35+
LL | while !numbers.is_empty() {
36+
| ------------------ help: try: `while let Some(element) = numbers.pop()`
37+
LL | accept_i32(numbers.pop().expect(""));
38+
| ^^^^^^^^^^^^^^^^^^^^^^^^
39+
|
40+
= note: this while loop can be written in a more idiomatic way
41+
42+
error: aborting due to 4 previous errors
43+

0 commit comments

Comments
 (0)