Skip to content

Commit 7bc3da9

Browse files
committed
Auto merge of #10647 - y21:while_pop_unwrap, r=llogiq
new lint: `manual_while_let_some` This PR implements the lint I suggested [on zulip](https://rust-lang.zulipchat.com/#narrow/stream/257328-clippy/topic/lint.20on.20while.20pop.20unwrap). It looks for while loops like these: ```rs let mut numbers = vec![0, 1, 2]; while !numbers.is_empty() { let number = numbers.pop().unwrap(); // use `number` } ``` and suggests replacing it with a while-let loop, like this: ```rs let mut numbers = vec![0, 1, 2]; while let Some(number) = numbers.pop() { // use `number` } ``` ... which is more concise and idiomatic. It only looks for `Vec::pop()` calls in the first statement of the loop body in an attempt to not trigger FPs (as pop might only be called conditionally). changelog: new lint [`manual_while_let_some`]
2 parents 3594d55 + 9613ea8 commit 7bc3da9

10 files changed

+428
-4
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4797,6 +4797,7 @@ Released 2018-09-13
47974797
[`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
47984798
[`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
47994799
[`manual_unwrap_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_unwrap_or
4800+
[`manual_while_let_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_while_let_some
48004801
[`many_single_char_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#many_single_char_names
48014802
[`map_clone`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_clone
48024803
[`map_collect_result_unit`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_collect_result_unit

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
249249
crate::loops::MANUAL_FIND_INFO,
250250
crate::loops::MANUAL_FLATTEN_INFO,
251251
crate::loops::MANUAL_MEMCPY_INFO,
252+
crate::loops::MANUAL_WHILE_LET_SOME_INFO,
252253
crate::loops::MISSING_SPIN_LOOP_INFO,
253254
crate::loops::MUT_RANGE_BOUND_INFO,
254255
crate::loops::NEEDLESS_RANGE_LOOP_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use clippy_utils::{
2+
diagnostics::{multispan_sugg_with_applicability, span_lint_and_then},
3+
match_def_path, paths,
4+
source::snippet,
5+
SpanlessEq,
6+
};
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{Expr, ExprKind, Pat, Stmt, StmtKind, UnOp};
9+
use rustc_lint::LateContext;
10+
use rustc_span::Span;
11+
use std::borrow::Cow;
12+
13+
use super::MANUAL_WHILE_LET_SOME;
14+
15+
/// The kind of statement that the `pop()` call appeared in.
16+
///
17+
/// Depending on whether the value was assigned to a variable or not changes what pattern
18+
/// we use for the suggestion.
19+
#[derive(Copy, Clone)]
20+
enum PopStmt<'hir> {
21+
/// `x.pop().unwrap()` was and assigned to a variable.
22+
/// The pattern of this local variable will be used and the local statement
23+
/// is deleted in the suggestion.
24+
Local(&'hir Pat<'hir>),
25+
/// `x.pop().unwrap()` appeared in an arbitrary expression and was not assigned to a variable.
26+
/// The suggestion will use some placeholder identifier and the `x.pop().unwrap()` expression
27+
/// is replaced with that identifier.
28+
Anonymous,
29+
}
30+
31+
fn report_lint(cx: &LateContext<'_>, pop_span: Span, pop_stmt_kind: PopStmt<'_>, loop_span: Span, receiver_span: Span) {
32+
span_lint_and_then(
33+
cx,
34+
MANUAL_WHILE_LET_SOME,
35+
pop_span,
36+
"you seem to be trying to pop elements from a `Vec` in a loop",
37+
|diag| {
38+
let (pat, pop_replacement) = match pop_stmt_kind {
39+
PopStmt::Local(pat) => (snippet(cx, pat.span, ".."), String::new()),
40+
PopStmt::Anonymous => (Cow::Borrowed("element"), "element".into()),
41+
};
42+
43+
let loop_replacement = format!("while let Some({}) = {}.pop()", pat, snippet(cx, receiver_span, ".."));
44+
multispan_sugg_with_applicability(
45+
diag,
46+
"consider using a `while..let` loop",
47+
Applicability::MachineApplicable,
48+
[(loop_span, loop_replacement), (pop_span, pop_replacement)],
49+
);
50+
},
51+
);
52+
}
53+
54+
fn match_method_call(cx: &LateContext<'_>, expr: &Expr<'_>, method: &[&str]) -> bool {
55+
if let ExprKind::MethodCall(..) = expr.kind
56+
&& let Some(id) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
57+
{
58+
match_def_path(cx, id, method)
59+
} else {
60+
false
61+
}
62+
}
63+
64+
fn is_vec_pop_unwrap(cx: &LateContext<'_>, expr: &Expr<'_>, is_empty_recv: &Expr<'_>) -> bool {
65+
if (match_method_call(cx, expr, &paths::OPTION_UNWRAP) || match_method_call(cx, expr, &paths::OPTION_EXPECT))
66+
&& let ExprKind::MethodCall(_, unwrap_recv, ..) = expr.kind
67+
&& match_method_call(cx, unwrap_recv, &paths::VEC_POP)
68+
&& let ExprKind::MethodCall(_, pop_recv, ..) = unwrap_recv.kind
69+
{
70+
// make sure they're the same `Vec`
71+
SpanlessEq::new(cx).eq_expr(pop_recv, is_empty_recv)
72+
} else {
73+
false
74+
}
75+
}
76+
77+
fn check_local(cx: &LateContext<'_>, stmt: &Stmt<'_>, is_empty_recv: &Expr<'_>, loop_span: Span) {
78+
if let StmtKind::Local(local) = stmt.kind
79+
&& let Some(init) = local.init
80+
&& is_vec_pop_unwrap(cx, init, is_empty_recv)
81+
{
82+
report_lint(cx, stmt.span, PopStmt::Local(local.pat), loop_span, is_empty_recv.span);
83+
}
84+
}
85+
86+
fn check_call_arguments(cx: &LateContext<'_>, stmt: &Stmt<'_>, is_empty_recv: &Expr<'_>, loop_span: Span) {
87+
if let StmtKind::Semi(expr) | StmtKind::Expr(expr) = stmt.kind {
88+
if let ExprKind::MethodCall(.., args, _) | ExprKind::Call(_, args) = expr.kind {
89+
let offending_arg = args
90+
.iter()
91+
.find_map(|arg| is_vec_pop_unwrap(cx, arg, is_empty_recv).then_some(arg.span));
92+
93+
if let Some(offending_arg) = offending_arg {
94+
report_lint(cx, offending_arg, PopStmt::Anonymous, loop_span, is_empty_recv.span);
95+
}
96+
}
97+
}
98+
}
99+
100+
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, full_cond: &'tcx Expr<'_>, body: &'tcx Expr<'_>, loop_span: Span) {
101+
if let ExprKind::Unary(UnOp::Not, cond) = full_cond.kind
102+
&& let ExprKind::MethodCall(_, is_empty_recv, _, _) = cond.kind
103+
&& match_method_call(cx, cond, &paths::VEC_IS_EMPTY)
104+
&& let ExprKind::Block(body, _) = body.kind
105+
&& let Some(stmt) = body.stmts.first()
106+
{
107+
check_local(cx, stmt, is_empty_recv, loop_span);
108+
check_call_arguments(cx, stmt, is_empty_recv, loop_span);
109+
}
110+
}

clippy_lints/src/loops/mod.rs

+34-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod iter_next_loop;
77
mod manual_find;
88
mod manual_flatten;
99
mod manual_memcpy;
10+
mod manual_while_let_some;
1011
mod missing_spin_loop;
1112
mod mut_range_bound;
1213
mod needless_range_loop;
@@ -575,6 +576,36 @@ declare_clippy_lint! {
575576
"manual implementation of `Iterator::find`"
576577
}
577578

579+
declare_clippy_lint! {
580+
/// ### What it does
581+
/// Looks for loops that check for emptiness of a `Vec` in the condition and pop an element
582+
/// in the body as a separate operation.
583+
///
584+
/// ### Why is this bad?
585+
/// Such loops can be written in a more idiomatic way by using a while-let loop and directly
586+
/// pattern matching on the return value of `Vec::pop()`.
587+
///
588+
/// ### Example
589+
/// ```rust
590+
/// let mut numbers = vec![1, 2, 3, 4, 5];
591+
/// while !numbers.is_empty() {
592+
/// let number = numbers.pop().unwrap();
593+
/// // use `number`
594+
/// }
595+
/// ```
596+
/// Use instead:
597+
/// ```rust
598+
/// let mut numbers = vec![1, 2, 3, 4, 5];
599+
/// while let Some(number) = numbers.pop() {
600+
/// // use `number`
601+
/// }
602+
/// ```
603+
#[clippy::version = "1.70.0"]
604+
pub MANUAL_WHILE_LET_SOME,
605+
style,
606+
"checking for emptiness of a `Vec` in the loop condition and popping an element in the body"
607+
}
608+
578609
declare_lint_pass!(Loops => [
579610
MANUAL_MEMCPY,
580611
MANUAL_FLATTEN,
@@ -594,6 +625,7 @@ declare_lint_pass!(Loops => [
594625
SINGLE_ELEMENT_LOOP,
595626
MISSING_SPIN_LOOP,
596627
MANUAL_FIND,
628+
MANUAL_WHILE_LET_SOME
597629
]);
598630

599631
impl<'tcx> LateLintPass<'tcx> for Loops {
@@ -640,9 +672,10 @@ impl<'tcx> LateLintPass<'tcx> for Loops {
640672

641673
while_let_on_iterator::check(cx, expr);
642674

643-
if let Some(higher::While { condition, body }) = higher::While::hir(expr) {
675+
if let Some(higher::While { condition, body, span }) = higher::While::hir(expr) {
644676
while_immutable_condition::check(cx, condition, body);
645677
missing_spin_loop::check(cx, condition, body);
678+
manual_while_let_some::check(cx, condition, body, span);
646679
}
647680
}
648681
}

clippy_lints/src/utils/author.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ impl<'a, 'tcx> PrintVisitor<'a, 'tcx> {
333333

334334
#[allow(clippy::too_many_lines)]
335335
fn expr(&self, expr: &Binding<&hir::Expr<'_>>) {
336-
if let Some(higher::While { condition, body }) = higher::While::hir(expr.value) {
336+
if let Some(higher::While { condition, body, .. }) = higher::While::hir(expr.value) {
337337
bind!(self, condition, body);
338338
chain!(
339339
self,

clippy_utils/src/higher.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ pub struct While<'hir> {
311311
pub condition: &'hir Expr<'hir>,
312312
/// `while` loop body
313313
pub body: &'hir Expr<'hir>,
314+
/// Span of the loop header
315+
pub span: Span,
314316
}
315317

316318
impl<'hir> While<'hir> {
@@ -336,10 +338,10 @@ impl<'hir> While<'hir> {
336338
},
337339
_,
338340
LoopSource::While,
339-
_,
341+
span,
340342
) = expr.kind
341343
{
342-
return Some(Self { condition, body });
344+
return Some(Self { condition, body, span });
343345
}
344346
None
345347
}

clippy_utils/src/paths.rs

+4
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/manual_while_let_some.fixed

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//@run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::manual_while_let_some)]
5+
6+
struct VecInStruct {
7+
numbers: Vec<i32>,
8+
unrelated: String,
9+
}
10+
11+
struct Foo {
12+
a: i32,
13+
b: i32,
14+
}
15+
16+
fn accept_i32(_: i32) {}
17+
fn accept_optional_i32(_: Option<i32>) {}
18+
fn accept_i32_tuple(_: (i32, i32)) {}
19+
20+
fn main() {
21+
let mut numbers = vec![1, 2, 3, 4, 5];
22+
while let Some(number) = numbers.pop() {
23+
24+
}
25+
26+
let mut val = VecInStruct {
27+
numbers: vec![1, 2, 3, 4, 5],
28+
unrelated: String::new(),
29+
};
30+
while let Some(number) = val.numbers.pop() {
31+
32+
}
33+
34+
while let Some(element) = numbers.pop() {
35+
accept_i32(element);
36+
}
37+
38+
while let Some(element) = numbers.pop() {
39+
accept_i32(element);
40+
}
41+
42+
// This should not warn. It "conditionally" pops elements.
43+
while !numbers.is_empty() {
44+
if true {
45+
accept_i32(numbers.pop().unwrap());
46+
}
47+
}
48+
49+
// This should also not warn. It conditionally pops elements.
50+
while !numbers.is_empty() {
51+
if false {
52+
continue;
53+
}
54+
accept_i32(numbers.pop().unwrap());
55+
}
56+
57+
// This should not warn. It pops elements, but does not unwrap it.
58+
// Might handle the Option in some other arbitrary way.
59+
while !numbers.is_empty() {
60+
accept_optional_i32(numbers.pop());
61+
}
62+
63+
let unrelated_vec: Vec<String> = Vec::new();
64+
// This should not warn. It pops elements from a different vector.
65+
while !unrelated_vec.is_empty() {
66+
accept_i32(numbers.pop().unwrap());
67+
}
68+
69+
macro_rules! generate_loop {
70+
() => {
71+
while !numbers.is_empty() {
72+
accept_i32(numbers.pop().unwrap());
73+
}
74+
};
75+
}
76+
// Do not warn if the loop comes from a macro.
77+
generate_loop!();
78+
79+
// Try other kinds of patterns
80+
let mut numbers = vec![(0, 0), (1, 1), (2, 2)];
81+
while let Some((a, b)) = numbers.pop() {
82+
83+
}
84+
85+
while let Some(element) = numbers.pop() {
86+
accept_i32_tuple(element);
87+
}
88+
89+
let mut results = vec![Foo { a: 1, b: 2 }, Foo { a: 3, b: 4 }];
90+
while let Some(Foo { a, b }) = results.pop() {
91+
92+
}
93+
}

0 commit comments

Comments
 (0)