Skip to content

Commit c07baa4

Browse files
authored
Add manual_option_zip lint (a.and_then(|x| b.map(|y| (x, y)))) (#16600)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/rust-clippy/pull/16600)* changelog: [`manual_option_zip`]: new lint that detects `a.and_then(|a| b.map(|b| (a, b)))` and suggests using `a.zip(b)` instead This PR introduces a new lint `manual_option_zip` that identifies cases where `Option::and_then` is followed by `Option::map` in the provided closure to construct a tuple that could be more concisely shaped using `Option::zip`. The lint detects the pattern: ```rust a.and_then(|x| b.map(|y| (x, y))) ``` And suggests replacing it with: ```rust a.zip(b) ``` Closes #16599
2 parents e4683d2 + ee14362 commit c07baa4

9 files changed

Lines changed: 414 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6801,6 +6801,7 @@ Released 2018-09-13
68016801
[`manual_ok_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_err
68026802
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
68036803
[`manual_option_as_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_option_as_slice
6804+
[`manual_option_zip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_option_zip
68046805
[`manual_pattern_char_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
68056806
[`manual_pop_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_pop_if
68066807
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
419419
crate::methods::MANUAL_IS_VARIANT_AND_INFO,
420420
crate::methods::MANUAL_NEXT_BACK_INFO,
421421
crate::methods::MANUAL_OK_OR_INFO,
422+
crate::methods::MANUAL_OPTION_ZIP_INFO,
422423
crate::methods::MANUAL_REPEAT_N_INFO,
423424
crate::methods::MANUAL_SATURATING_ARITHMETIC_INFO,
424425
crate::methods::MANUAL_SPLIT_ONCE_INFO,

clippy_lints/src/loops/manual_memcpy.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ fn get_details_from_idx<'tcx>(
385385
ExprKind::Binary(op, lhs, rhs) => match op.node {
386386
BinOpKind::Add => {
387387
let offset_opt = get_start(lhs, starts)
388-
.and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, o)))
389-
.or_else(|| get_start(rhs, starts).and_then(|s| get_offset(cx, lhs, starts).map(|o| (s, o))));
388+
.zip(get_offset(cx, rhs, starts))
389+
.or_else(|| get_start(rhs, starts).zip(get_offset(cx, lhs, starts)));
390390

391391
offset_opt.map(|(s, o)| (s, Offset::positive(o)))
392392
},
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::msrvs::{self, Msrv};
3+
use clippy_utils::peel_blocks;
4+
use clippy_utils::res::{MaybeDef, MaybeResPath};
5+
use clippy_utils::source::snippet_with_applicability;
6+
use clippy_utils::visitors::for_each_expr_without_closures;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{self as hir, Expr, ExprKind, HirId, PatKind};
9+
use rustc_lint::LateContext;
10+
use rustc_span::symbol::sym;
11+
use std::ops::ControlFlow;
12+
13+
use super::MANUAL_OPTION_ZIP;
14+
15+
/// Checks for `a.and_then(|a| b.map(|b| (a, b)))` and suggests `a.zip(b)`.
16+
pub(super) fn check<'tcx>(
17+
cx: &LateContext<'tcx>,
18+
expr: &'tcx Expr<'tcx>,
19+
recv: &'tcx Expr<'_>,
20+
arg: &'tcx Expr<'_>,
21+
msrv: Msrv,
22+
) {
23+
// Looking for: `a.and_then(|a| b.map(|b| (a, b)))`.
24+
// `and_then(|a| ...)`
25+
if let ExprKind::Closure(&hir::Closure { body: outer_body_id, .. }) = arg.kind
26+
&& let hir::Body { params: [outer_param], value: outer_value, .. } = cx.tcx.hir_body(outer_body_id)
27+
&& let PatKind::Binding(_, outer_param_id, _, None) = outer_param.pat.kind
28+
&& cx.typeck_results().expr_ty(recv).is_diag_item(cx, sym::Option)
29+
// `b.map(|b| ...)`
30+
&& let ExprKind::MethodCall(method_path, map_recv, [map_arg], _) = peel_blocks(outer_value).kind
31+
&& method_path.ident.name == sym::map
32+
&& cx.typeck_results().expr_ty(map_recv).is_diag_item(cx, sym::Option)
33+
// `b` does not reference the outer closure parameter `a`.
34+
&& for_each_expr_without_closures(map_recv, |e| {
35+
if e.res_local_id() == Some(outer_param_id) {
36+
ControlFlow::Break(())
37+
} else {
38+
ControlFlow::Continue(())
39+
}
40+
}).is_none()
41+
// `|b| (a, b)`
42+
&& let ExprKind::Closure(&hir::Closure { body: inner_body_id, .. }) = map_arg.kind
43+
&& let hir::Body { params: [inner_param], value: inner_value, .. } = cx.tcx.hir_body(inner_body_id)
44+
&& let PatKind::Binding(_, inner_param_id, _, None) = inner_param.pat.kind
45+
// `(a, b)` or `(b, a)` — tuple of outer and inner param in either order.
46+
&& let ExprKind::Tup([first, second]) = peel_blocks(inner_value).kind
47+
&& let Some((zip_recv, zip_arg)) = zip_operands(first, second, outer_param_id, inner_param_id, recv, map_recv)
48+
// `Option.zip()` is available.
49+
&& msrv.meets(cx, msrvs::OPTION_ZIP)
50+
{
51+
let mut applicability = Applicability::MachineApplicable;
52+
let zip_recv_snip = snippet_with_applicability(cx, zip_recv.span, "_", &mut applicability);
53+
let zip_arg_snip = snippet_with_applicability(cx, zip_arg.span, "_", &mut applicability);
54+
let suggestion = format!("{zip_recv_snip}.zip({zip_arg_snip})");
55+
56+
span_lint_and_sugg(
57+
cx,
58+
MANUAL_OPTION_ZIP,
59+
expr.span,
60+
"manual implementation of `Option::zip`",
61+
"use",
62+
suggestion,
63+
applicability,
64+
);
65+
}
66+
}
67+
68+
/// Given the two tuple elements and the `and_then` receiver / `map` receiver, returns the
69+
/// `(zip_receiver, zip_argument)` expressions for the `.zip()` suggestion.
70+
///
71+
/// For `(outer, inner)` order the zip is `recv.zip(map_recv)`.
72+
/// For `(inner, outer)` (reversed) the zip is `map_recv.zip(recv)`.
73+
/// Returns `None` if the tuple elements don't match either order.
74+
fn zip_operands<'a>(
75+
first: &Expr<'_>,
76+
second: &Expr<'_>,
77+
outer_param_id: HirId,
78+
inner_param_id: HirId,
79+
recv: &'a Expr<'a>,
80+
map_recv: &'a Expr<'a>,
81+
) -> Option<(&'a Expr<'a>, &'a Expr<'a>)> {
82+
if first.res_local_id() == Some(outer_param_id) && second.res_local_id() == Some(inner_param_id) {
83+
Some((recv, map_recv))
84+
} else if first.res_local_id() == Some(inner_param_id) && second.res_local_id() == Some(outer_param_id) {
85+
Some((map_recv, recv))
86+
} else {
87+
None
88+
}
89+
}

clippy_lints/src/methods/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ mod manual_inspect;
6363
mod manual_is_variant_and;
6464
mod manual_next_back;
6565
mod manual_ok_or;
66+
mod manual_option_zip;
6667
mod manual_repeat_n;
6768
mod manual_saturating_arithmetic;
6869
mod manual_str_repeat;
@@ -1950,6 +1951,34 @@ declare_clippy_lint! {
19501951
"finds patterns that can be encoded more concisely with `Option::ok_or`"
19511952
}
19521953

1954+
declare_clippy_lint! {
1955+
/// ### What it does
1956+
/// Checks for usage of `a.and_then(|a| b.map(|b| (a, b)))` which can be
1957+
/// more concisely expressed as `a.zip(b)`.
1958+
///
1959+
/// ### Why is this bad?
1960+
/// `Option::zip` is more concise and directly expresses the intent of
1961+
/// combining two `Option` values into a tuple.
1962+
///
1963+
/// ### Example
1964+
/// ```no_run
1965+
/// let a: Option<i32> = Some(1);
1966+
/// let b: Option<i32> = Some(2);
1967+
/// let _ = a.and_then(|x| b.map(|y| (x, y)));
1968+
/// ```
1969+
///
1970+
/// Use instead:
1971+
/// ```no_run
1972+
/// let a: Option<i32> = Some(1);
1973+
/// let b: Option<i32> = Some(2);
1974+
/// let _ = a.zip(b);
1975+
/// ```
1976+
#[clippy::version = "1.95.0"]
1977+
pub MANUAL_OPTION_ZIP,
1978+
complexity,
1979+
"manual reimplementation of `Option::zip`"
1980+
}
1981+
19531982
declare_clippy_lint! {
19541983
/// ### What it does
19551984
///
@@ -4814,6 +4843,7 @@ impl_lint_pass!(Methods => [
48144843
MANUAL_IS_VARIANT_AND,
48154844
MANUAL_NEXT_BACK,
48164845
MANUAL_OK_OR,
4846+
MANUAL_OPTION_ZIP,
48174847
MANUAL_REPEAT_N,
48184848
MANUAL_SATURATING_ARITHMETIC,
48194849
MANUAL_SPLIT_ONCE,
@@ -5115,6 +5145,7 @@ impl Methods {
51155145
}
51165146
},
51175147
(sym::and_then, [arg]) => {
5148+
manual_option_zip::check(cx, expr, recv, arg, self.msrv);
51185149
let biom_option_linted = bind_instead_of_map::check_and_then_some(cx, expr, recv, arg);
51195150
let biom_result_linted = bind_instead_of_map::check_and_then_ok(cx, expr, recv, arg);
51205151
if !biom_option_linted && !biom_result_linted {

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ msrv_aliases! {
6060
1,51,0 { BORROW_AS_PTR, SEEK_FROM_CURRENT, UNSIGNED_ABS }
6161
1,50,0 { BOOL_THEN, CLAMP, SLICE_FILL }
6262
1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST }
63-
1,46,0 { CONST_IF_MATCH }
63+
1,46,0 { CONST_IF_MATCH, OPTION_ZIP }
6464
1,45,0 { STR_STRIP_PREFIX }
6565
1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS }
6666
1,42,0 { MATCHES_MACRO, SLICE_PATTERNS, PTR_SLICE_RAW_PARTS }

tests/ui/manual_option_zip.fixed

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#![warn(clippy::manual_option_zip)]
2+
#![allow(clippy::bind_instead_of_map)]
3+
4+
fn main() {}
5+
6+
fn should_lint() {
7+
// basic case
8+
let a: Option<i32> = Some(1);
9+
let b: Option<i32> = Some(2);
10+
let _ = a.zip(b);
11+
//~^ manual_option_zip
12+
13+
// different types
14+
let a: Option<String> = Some(String::new());
15+
let b: Option<i32> = Some(1);
16+
let _ = a.zip(b);
17+
//~^ manual_option_zip
18+
19+
// with None receiver
20+
let b: Option<i32> = Some(2);
21+
let _ = None::<i32>.zip(b);
22+
//~^ manual_option_zip
23+
24+
// with function call as map receiver
25+
let a: Option<i32> = Some(1);
26+
let _ = a.zip(get_option());
27+
//~^ manual_option_zip
28+
29+
// tuple order reversed: (inner, outer) instead of (outer, inner)
30+
let a: Option<i32> = Some(1);
31+
let b: Option<i32> = Some(2);
32+
let _ = b.zip(a);
33+
//~^ manual_option_zip
34+
35+
// closure bodies wrapped in blocks
36+
let a: Option<i32> = Some(1);
37+
let b: Option<i32> = Some(2);
38+
#[rustfmt::skip]
39+
let _ = a.zip(b);
40+
//~^ manual_option_zip
41+
#[rustfmt::skip]
42+
let _ = a.zip(b);
43+
//~^ manual_option_zip
44+
#[rustfmt::skip]
45+
let _ = a.zip(b);
46+
//~^ manual_option_zip
47+
}
48+
49+
fn should_not_lint() {
50+
let a: Option<i32> = Some(1);
51+
let b: Option<i32> = Some(2);
52+
53+
// tuple has more than 2 elements
54+
let _ = a.and_then(|a| b.map(|b| (a, b, 1)));
55+
56+
// three-element tuple but with either `a` or `b` as the elements
57+
let _ = a.and_then(|a| b.map(|b| (a, b, a)));
58+
59+
// inner closure body is not a simple tuple of the params
60+
let _ = a.and_then(|a| b.map(|b| (a, b + 1)));
61+
62+
// map receiver uses the outer closure parameter
63+
let _ = a.and_then(|a| a.checked_add(1).map(|b| (a, b)));
64+
65+
// .map receiver is not an Option type.
66+
let _ = a.and_then(|a| NotOption(Some(1)).map(|b| (a, b)));
67+
68+
// .and_then receiver is not an Option type.
69+
let _ = NotOption(Some(1)).and_then(|a| b.map(|b| (a, b)));
70+
71+
// closure body is not a map call
72+
let a: Option<i32> = Some(1);
73+
let _ = a.and_then(|a| Some((a, 1)));
74+
75+
// single-element tuple
76+
let _ = a.and_then(|a| b.map(|_b| (a,)));
77+
78+
// the outer param used in the map receiver (cannot extract)
79+
let opts: Vec<Option<i32>> = vec![Some(1), Some(2)];
80+
let _ = a.and_then(|a| opts[a as usize].map(|b| (a, b)));
81+
82+
// extra statements in outer closure body
83+
let _ = a.and_then(|a| {
84+
let _x = 1;
85+
b.map(|b| (a, b))
86+
});
87+
88+
// extra statements in inner closure body
89+
let _ = a.and_then(|a| {
90+
b.map(|b| {
91+
let _x = 1;
92+
(a, b)
93+
})
94+
});
95+
96+
// n-ary zip where n > 2, which is out of scope for this lint (for now)
97+
let c: Option<i32> = Some(3);
98+
let _ = a.and_then(|a| b.and_then(|b| c.map(|c| (a, b, c))));
99+
100+
// not Option type (Result)
101+
let a: Result<i32, &str> = Ok(1);
102+
let b: Result<i32, &str> = Ok(2);
103+
let _ = a.and_then(|a| b.map(|b| (a, b)));
104+
}
105+
106+
fn get_option() -> Option<i32> {
107+
Some(123)
108+
}
109+
110+
struct NotOption(Option<i32>);
111+
impl NotOption {
112+
fn map<U>(self, f: impl FnOnce(i32) -> U) -> Option<U> {
113+
self.0.map(f)
114+
}
115+
fn and_then<U>(self, f: impl FnOnce(i32) -> Option<U>) -> Option<U> {
116+
self.0.and_then(f)
117+
}
118+
}

0 commit comments

Comments
 (0)