Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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`]
- Loading branch information
Showing
10 changed files
with
428 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
use clippy_utils::{ | ||
diagnostics::{multispan_sugg_with_applicability, span_lint_and_then}, | ||
match_def_path, paths, | ||
source::snippet, | ||
SpanlessEq, | ||
}; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::{Expr, ExprKind, Pat, Stmt, StmtKind, UnOp}; | ||
use rustc_lint::LateContext; | ||
use rustc_span::Span; | ||
use std::borrow::Cow; | ||
|
||
use super::MANUAL_WHILE_LET_SOME; | ||
|
||
/// The kind of statement that the `pop()` call appeared in. | ||
/// | ||
/// Depending on whether the value was assigned to a variable or not changes what pattern | ||
/// we use for the suggestion. | ||
#[derive(Copy, Clone)] | ||
enum PopStmt<'hir> { | ||
/// `x.pop().unwrap()` was and assigned to a variable. | ||
/// The pattern of this local variable will be used and the local statement | ||
/// is deleted in the suggestion. | ||
Local(&'hir Pat<'hir>), | ||
/// `x.pop().unwrap()` appeared in an arbitrary expression and was not assigned to a variable. | ||
/// The suggestion will use some placeholder identifier and the `x.pop().unwrap()` expression | ||
/// is replaced with that identifier. | ||
Anonymous, | ||
} | ||
|
||
fn report_lint(cx: &LateContext<'_>, pop_span: Span, pop_stmt_kind: PopStmt<'_>, loop_span: Span, receiver_span: Span) { | ||
span_lint_and_then( | ||
cx, | ||
MANUAL_WHILE_LET_SOME, | ||
pop_span, | ||
"you seem to be trying to pop elements from a `Vec` in a loop", | ||
|diag| { | ||
let (pat, pop_replacement) = match pop_stmt_kind { | ||
PopStmt::Local(pat) => (snippet(cx, pat.span, ".."), String::new()), | ||
PopStmt::Anonymous => (Cow::Borrowed("element"), "element".into()), | ||
}; | ||
|
||
let loop_replacement = format!("while let Some({}) = {}.pop()", pat, snippet(cx, receiver_span, "..")); | ||
multispan_sugg_with_applicability( | ||
diag, | ||
"consider using a `while..let` loop", | ||
Applicability::MachineApplicable, | ||
[(loop_span, loop_replacement), (pop_span, pop_replacement)], | ||
); | ||
}, | ||
); | ||
} | ||
|
||
fn match_method_call(cx: &LateContext<'_>, expr: &Expr<'_>, method: &[&str]) -> bool { | ||
if let ExprKind::MethodCall(..) = expr.kind | ||
&& let Some(id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) | ||
{ | ||
match_def_path(cx, id, method) | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
fn is_vec_pop_unwrap(cx: &LateContext<'_>, expr: &Expr<'_>, is_empty_recv: &Expr<'_>) -> bool { | ||
if (match_method_call(cx, expr, &paths::OPTION_UNWRAP) || match_method_call(cx, expr, &paths::OPTION_EXPECT)) | ||
&& let ExprKind::MethodCall(_, unwrap_recv, ..) = expr.kind | ||
&& match_method_call(cx, unwrap_recv, &paths::VEC_POP) | ||
&& let ExprKind::MethodCall(_, pop_recv, ..) = unwrap_recv.kind | ||
{ | ||
// make sure they're the same `Vec` | ||
SpanlessEq::new(cx).eq_expr(pop_recv, is_empty_recv) | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
fn check_local(cx: &LateContext<'_>, stmt: &Stmt<'_>, is_empty_recv: &Expr<'_>, loop_span: Span) { | ||
if let StmtKind::Local(local) = stmt.kind | ||
&& let Some(init) = local.init | ||
&& is_vec_pop_unwrap(cx, init, is_empty_recv) | ||
{ | ||
report_lint(cx, stmt.span, PopStmt::Local(local.pat), loop_span, is_empty_recv.span); | ||
} | ||
} | ||
|
||
fn check_call_arguments(cx: &LateContext<'_>, stmt: &Stmt<'_>, is_empty_recv: &Expr<'_>, loop_span: Span) { | ||
if let StmtKind::Semi(expr) | StmtKind::Expr(expr) = stmt.kind { | ||
if let ExprKind::MethodCall(.., args, _) | ExprKind::Call(_, args) = expr.kind { | ||
let offending_arg = args | ||
.iter() | ||
.find_map(|arg| is_vec_pop_unwrap(cx, arg, is_empty_recv).then_some(arg.span)); | ||
|
||
if let Some(offending_arg) = offending_arg { | ||
report_lint(cx, offending_arg, PopStmt::Anonymous, loop_span, is_empty_recv.span); | ||
} | ||
} | ||
} | ||
} | ||
|
||
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, full_cond: &'tcx Expr<'_>, body: &'tcx Expr<'_>, loop_span: Span) { | ||
if let ExprKind::Unary(UnOp::Not, cond) = full_cond.kind | ||
&& let ExprKind::MethodCall(_, is_empty_recv, _, _) = cond.kind | ||
&& match_method_call(cx, cond, &paths::VEC_IS_EMPTY) | ||
&& let ExprKind::Block(body, _) = body.kind | ||
&& let Some(stmt) = body.stmts.first() | ||
{ | ||
check_local(cx, stmt, is_empty_recv, loop_span); | ||
check_call_arguments(cx, stmt, is_empty_recv, loop_span); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
//@run-rustfix | ||
|
||
#![allow(unused)] | ||
#![warn(clippy::manual_while_let_some)] | ||
|
||
struct VecInStruct { | ||
numbers: Vec<i32>, | ||
unrelated: String, | ||
} | ||
|
||
struct Foo { | ||
a: i32, | ||
b: i32, | ||
} | ||
|
||
fn accept_i32(_: i32) {} | ||
fn accept_optional_i32(_: Option<i32>) {} | ||
fn accept_i32_tuple(_: (i32, i32)) {} | ||
|
||
fn main() { | ||
let mut numbers = vec![1, 2, 3, 4, 5]; | ||
while let Some(number) = numbers.pop() { | ||
|
||
} | ||
|
||
let mut val = VecInStruct { | ||
numbers: vec![1, 2, 3, 4, 5], | ||
unrelated: String::new(), | ||
}; | ||
while let Some(number) = val.numbers.pop() { | ||
|
||
} | ||
|
||
while let Some(element) = numbers.pop() { | ||
accept_i32(element); | ||
} | ||
|
||
while let Some(element) = numbers.pop() { | ||
accept_i32(element); | ||
} | ||
|
||
// This should not warn. It "conditionally" pops elements. | ||
while !numbers.is_empty() { | ||
if true { | ||
accept_i32(numbers.pop().unwrap()); | ||
} | ||
} | ||
|
||
// This should also not warn. It conditionally pops elements. | ||
while !numbers.is_empty() { | ||
if false { | ||
continue; | ||
} | ||
accept_i32(numbers.pop().unwrap()); | ||
} | ||
|
||
// This should not warn. It pops elements, but does not unwrap it. | ||
// Might handle the Option in some other arbitrary way. | ||
while !numbers.is_empty() { | ||
accept_optional_i32(numbers.pop()); | ||
} | ||
|
||
let unrelated_vec: Vec<String> = Vec::new(); | ||
// This should not warn. It pops elements from a different vector. | ||
while !unrelated_vec.is_empty() { | ||
accept_i32(numbers.pop().unwrap()); | ||
} | ||
|
||
macro_rules! generate_loop { | ||
() => { | ||
while !numbers.is_empty() { | ||
accept_i32(numbers.pop().unwrap()); | ||
} | ||
}; | ||
} | ||
// Do not warn if the loop comes from a macro. | ||
generate_loop!(); | ||
|
||
// Try other kinds of patterns | ||
let mut numbers = vec![(0, 0), (1, 1), (2, 2)]; | ||
while let Some((a, b)) = numbers.pop() { | ||
|
||
} | ||
|
||
while let Some(element) = numbers.pop() { | ||
accept_i32_tuple(element); | ||
} | ||
|
||
let mut results = vec![Foo { a: 1, b: 2 }, Foo { a: 3, b: 4 }]; | ||
while let Some(Foo { a, b }) = results.pop() { | ||
|
||
} | ||
} |
Oops, something went wrong.