-
Notifications
You must be signed in to change notification settings - Fork 1.7k
new lint: and_then_then_some #12981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
new lint: and_then_then_some #12981
Changes from all commits
1d78783
4bb4f13
5b520b6
37e7058
3b9bba4
7a0adc4
88ec537
5654d30
7ea79c0
2a6f6d0
360cf82
2ccce8d
b528cb7
da89528
8e9fa96
13c7361
2f343a0
4e8fe6c
1090186
de9881f
015822d
be39ee0
016653f
6c0607d
f03e8d6
bb0e265
c842e4b
122cd11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
use clippy_utils::diagnostics::span_lint_and_sugg; | ||
use clippy_utils::source::snippet_with_applicability; | ||
use clippy_utils::ty::is_copy; | ||
use clippy_utils::visitors::for_each_expr; | ||
use clippy_utils::{fn_def_id, is_from_proc_macro, match_def_path}; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::def::Res; | ||
use rustc_hir::{Block, Body, Closure, Expr, ExprKind, HirId, Node, Param, Pat, Path, QPath}; | ||
use rustc_lint::{LateContext, LateLintPass}; | ||
use rustc_middle::lint::in_external_macro; | ||
use rustc_session::declare_lint_pass; | ||
use rustc_span::Span; | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// Detects usage of `Option::and_then` and `bool::then_some` that could | ||
/// be replaced with `Option::filter`. | ||
/// | ||
/// ### Why is this bad? | ||
/// Needless complexity, uses recent and uncommon stdlib functions instead of | ||
/// one older function. | ||
/// | ||
/// ### Example | ||
/// ```no_run | ||
/// let x = Some("foo".to_string()); | ||
/// let _y = x.clone().and_then(|v| v.starts_with('f').then_some(v)); | ||
/// ``` | ||
/// Use instead: | ||
/// ```no_run | ||
/// let x = Some("foo".to_string()); | ||
/// let _y = x.clone().filter(|v| v.starts_with('f')); | ||
/// ``` | ||
#[clippy::version = "1.81.0"] | ||
pub AND_THEN_THEN_SOME, | ||
nursery, | ||
"detects usage of `and_then` and `then_some` that can be replaced by `filter`" | ||
} | ||
|
||
// note: `Option::filter` is older than `bool::then_some`, | ||
// so no msrv check is required. | ||
declare_lint_pass!(AndThenThenSome => [AND_THEN_THEN_SOME]); | ||
|
||
impl<'tcx> LateLintPass<'tcx> for AndThenThenSome { | ||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | ||
if in_external_macro(cx.tcx.sess, expr.span) { | ||
return; | ||
} | ||
match expr.kind { | ||
ExprKind::MethodCall(_, recv_or_self, [arg], _) | ExprKind::Call(_, [recv_or_self, arg]) => { | ||
// we could check if type of reciever is diagnostic item Option, | ||
// but we don't technically need to, since we're checking the path. | ||
if is_and_then(cx, expr) { | ||
if let Some((closure_arg_span, closure_arg_hid, predicate)) = then_some_closure_arg(cx, arg) { | ||
let add_ref_pattern; | ||
// check if `closure_arg_hid` implements Copy, | ||
// if so, show suggestion but with `&` added to the | ||
// argument pattern. | ||
if is_copy(cx, cx.typeck_results().node_type(closure_arg_hid)) { | ||
add_ref_pattern = true; | ||
} else if contains_only_autoref_of(cx, closure_arg_hid, predicate) { | ||
add_ref_pattern = false; | ||
} else { | ||
return; | ||
} | ||
// this check is expensive, so we do it last. | ||
if is_from_proc_macro(cx, expr) { | ||
return; | ||
} | ||
show_sugg( | ||
cx, | ||
expr.span, | ||
recv_or_self, | ||
closure_arg_span, | ||
predicate, | ||
add_ref_pattern, | ||
); | ||
} | ||
} | ||
}, | ||
_ => {}, | ||
} | ||
} | ||
} | ||
|
||
// This function returns the span and hir id of the closure arguments and the receiver of | ||
// `then_some` (usually `bool`) if the expression passed is a closure whose single expression is a | ||
// call to `then_some`. | ||
fn then_some_closure_arg<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> Option<(Span, HirId, &'tcx Expr<'tcx>)> { | ||
match expr.kind { | ||
ExprKind::Closure(Closure { body, .. }) => { | ||
if let Node::Expr(expr) = cx.tcx.hir_node(body.hir_id) | ||
&& let Body { | ||
params: | ||
[ | ||
Param { | ||
hir_id: arg_id, | ||
pat: Pat { span, .. }, | ||
.. | ||
}, | ||
], | ||
.. | ||
} = cx.tcx.hir().body(*body) | ||
{ | ||
(peel_closure_body(cx, expr, *arg_id)).map(|body| (*span, *arg_id, body)) | ||
} else { | ||
None | ||
} | ||
}, | ||
_ => None, | ||
} | ||
} | ||
|
||
fn peel_closure_body<'tcx>( | ||
cx: &LateContext<'tcx>, | ||
expr: &'tcx Expr<'tcx>, | ||
closure_arg_id: HirId, | ||
) -> Option<&'tcx Expr<'tcx>> { | ||
match expr.kind { | ||
ExprKind::Ret(Some(wrapped_expr)) => | ||
// duplicated blocks because 2023 reference statements are awkward. | ||
// "&" peels multiple layers of indirection instead of just one like we want. | ||
{ | ||
peel_closure_body(cx, wrapped_expr, closure_arg_id) | ||
}, | ||
// it would be nice if we could lift { x; y.a() } into { x; y }.a() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume this comment is saying this is a false negative in this case...? Not sure, can you elaborate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, the current code will fail if the offending lambda has a multi-stmt block as its body. unfortunately fixing this would either mean shuffling around HIR (and possibly messing up the spans in the process), or modeling the problem in a more complex way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider that change optional tbh as it'd require you to modify the suggestion to include those statements as well, otherwise it may cause the code to break, you can do it though if you want |
||
ExprKind::Block( | ||
Block { | ||
stmts: [], | ||
expr: Some(wrapped_expr), | ||
.. | ||
}, | ||
_, | ||
) => peel_closure_body(cx, wrapped_expr, closure_arg_id), | ||
ExprKind::MethodCall(_, pred, [arg], _) | ExprKind::Call(_, [pred, arg]) => { | ||
if is_then_some(cx, expr) && is_local_defined_at(cx, arg, closure_arg_id) { | ||
Some(pred) | ||
} else { | ||
None | ||
} | ||
}, | ||
_ => None, | ||
} | ||
} | ||
|
||
fn get_pat_hid(node: Node<'_>) -> Option<HirId> { | ||
match node { | ||
Node::Param(Param { | ||
pat: Pat { hir_id, .. }, | ||
.. | ||
}) | ||
| Node::Pat(Pat { hir_id, .. }) => Some(*hir_id), | ||
_ => None, | ||
} | ||
} | ||
|
||
fn is_local_defined_at(cx: &LateContext<'_>, local: &Expr<'_>, arg_hid: HirId) -> bool { | ||
match local.kind { | ||
ExprKind::Path(QPath::Resolved( | ||
_, | ||
Path { | ||
res: Res::Local(local_hid), | ||
.. | ||
}, | ||
)) => { | ||
Comment on lines
+157
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm much more familiar with the hir and mir types than all the clippy utils, and yeah, you can't use function calls in patterns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be extracted from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, one step further, rather than doing an |
||
let local_pat_id = get_pat_hid(cx.tcx.hir_node(*local_hid)); | ||
local_pat_id.is_some() && local_pat_id == get_pat_hid(cx.tcx.hir_node(arg_hid)) | ||
}, | ||
// is not local at all, so definitly isn't a local defined at the given position | ||
_ => false, | ||
} | ||
} | ||
|
||
fn show_sugg( | ||
cx: &LateContext<'_>, | ||
span: Span, | ||
selfarg: &Expr<'_>, | ||
closure_args: Span, | ||
predicate: &Expr<'_>, | ||
add_ref_pattern: bool, | ||
) { | ||
let mut appl = Applicability::MachineApplicable; | ||
let prefix = if add_ref_pattern { "&" } else { "" }; | ||
let sugg = format!( | ||
"{}.filter(|{prefix}{}| {})", | ||
snippet_with_applicability(cx, selfarg.span, "<OPTION>", &mut appl), | ||
snippet_with_applicability(cx, closure_args, "<ARGS>", &mut appl), | ||
snippet_with_applicability(cx, predicate.span, "<PREDICATE>", &mut appl) | ||
); | ||
span_lint_and_sugg( | ||
cx, | ||
AND_THEN_THEN_SOME, | ||
span, | ||
"use of `Option::and_then` and `bool::then_some` is equivelent to `filter`", | ||
"use `Option::filter` instead", | ||
sugg, | ||
appl, | ||
); | ||
} | ||
|
||
fn is_then_some(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | ||
if let Some(def_id) = fn_def_id(cx, expr) { | ||
match_def_path(cx, def_id, &["core", "bool", "<impl bool>", "then_some"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. Let's match the type's diagnostic item to |
||
} else { | ||
false | ||
} | ||
} | ||
|
||
fn is_and_then(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | ||
if let Some(def_id) = fn_def_id(cx, expr) { | ||
match_def_path(cx, def_id, &["core", "option", "Option", "and_then"]) | ||
} else { | ||
false | ||
} | ||
} | ||
|
||
/// checks that `expr` contains no references to `var` outside of autoref method calls. | ||
fn contains_only_autoref_of<'tcx>(cx: &LateContext<'tcx>, var: HirId, expr: &'tcx Expr<'tcx>) -> bool { | ||
use std::ops::ControlFlow::{Break, Continue}; | ||
for_each_expr(cx, expr, |subexpr| { | ||
if is_local_defined_at(cx, subexpr, var) { | ||
match cx.tcx.hir().parent_iter(subexpr.hir_id).next() { | ||
Some(( | ||
_hir_id, | ||
Node::Expr(Expr { | ||
kind: ExprKind::MethodCall(_, _, _, _), | ||
.. | ||
}), | ||
)) => { | ||
// TODO: check if type of method reciver has one more level | ||
// of indirection than `var` | ||
return Continue(()); | ||
}, | ||
non_method_call => { | ||
dbg!(non_method_call); | ||
}, | ||
} | ||
return Break(()); | ||
} | ||
Continue(()) | ||
}) | ||
.is_none() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#![warn(clippy::and_then_then_some)] | ||
|
||
fn main() { | ||
let x = Some("foo".to_string()); | ||
|
||
let _y = x.clone().filter(|v| v.starts_with('f')); | ||
|
||
let _z = x.clone().filter(|v| v.starts_with('f')); | ||
let _w = x.clone().filter(|v| v.starts_with('f')); | ||
#[allow(clippy::needless_return)] | ||
let _v = x.clone().filter(|v| v.starts_with('g')); | ||
#[derive(Clone)] | ||
struct NonCopy; | ||
let a = Some(NonCopy); | ||
// non-copy value, but it doesn't appear in the predicate. | ||
let _a1 = a.clone().filter(|v| true); | ||
let _ = Some(true).filter(|&v| v); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#![warn(clippy::and_then_then_some)] | ||
|
||
fn main() { | ||
let x = Some("foo".to_string()); | ||
|
||
let _y = x.clone().and_then(|v| v.starts_with('f').then_some(v)); | ||
|
||
let _z = x.clone().and_then(|v| bool::then_some(v.starts_with('f'), v)); | ||
let _w = Option::and_then(x.clone(), |v: String| bool::then_some(v.starts_with('f'), v)); | ||
#[allow(clippy::needless_return)] | ||
let _v = x.clone().and_then(|v| return v.starts_with('g').then_some(v)); | ||
#[derive(Clone)] | ||
struct NonCopy; | ||
let a = Some(NonCopy); | ||
// non-copy value, but it doesn't appear in the predicate. | ||
let _a1 = a.clone().and_then(|v| true.then_some(v)); | ||
let _ = Some(true).and_then(|v| v.then_some(v)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:6:14 | ||
| | ||
LL | let _y = x.clone().and_then(|v| v.starts_with('f').then_some(v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `x.clone().filter(|v| v.starts_with('f'))` | ||
| | ||
= note: `-D clippy::and-then-then-some` implied by `-D warnings` | ||
= help: to override `-D warnings` add `#[allow(clippy::and_then_then_some)]` | ||
|
||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:8:14 | ||
| | ||
LL | let _z = x.clone().and_then(|v| bool::then_some(v.starts_with('f'), v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `x.clone().filter(|v| v.starts_with('f'))` | ||
|
||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:9:14 | ||
| | ||
LL | let _w = Option::and_then(x.clone(), |v: String| bool::then_some(v.starts_with('f'), v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `x.clone().filter(|v| v.starts_with('f'))` | ||
|
||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:11:14 | ||
| | ||
LL | let _v = x.clone().and_then(|v| return v.starts_with('g').then_some(v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `x.clone().filter(|v| v.starts_with('g'))` | ||
|
||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:16:15 | ||
| | ||
LL | let _a1 = a.clone().and_then(|v| true.then_some(v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `a.clone().filter(|v| true)` | ||
|
||
error: use of `Option::and_then` and `bool::then_some` is equivelent to `filter` | ||
--> tests/ui/and_then_then_some.rs:17:13 | ||
| | ||
LL | let _ = Some(true).and_then(|v| v.then_some(v)); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `Option::filter` instead: `Some(true).filter(|&v| v)` | ||
|
||
error: aborting due to 6 previous errors | ||
|
Uh oh!
There was an error while loading. Please reload this page.