Skip to content

Commit 669fbcb

Browse files
Fix map_unwrap_or fail to cover Result::unwrap_or (rust-lang#15718)
Closes rust-lang/rust-clippy#15713 Closes rust-lang/rust-clippy#15714 Closes rust-lang/rust-clippy#15752 Closes rust-lang/rust-clippy#16258 changelog: [`map_unwrap_or`] add cover for `Result::unwrap_or`
2 parents bcd52c1 + 0db25db commit 669fbcb

File tree

12 files changed

+446
-244
lines changed

12 files changed

+446
-244
lines changed

clippy_lints/src/derivable_impls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ fn check_struct<'tcx>(
105105
if let TyKind::Path(QPath::Resolved(_, p)) = self_ty.kind
106106
&& let Some(PathSegment { args, .. }) = p.segments.last()
107107
{
108-
let args = args.map(|a| a.args).unwrap_or(&[]);
108+
let args = args.map(|a| a.args).unwrap_or_default();
109109

110110
// ty_args contains the generic parameters of the type declaration, while args contains the
111111
// arguments used at instantiation time. If both len are not equal, it means that some
Lines changed: 192 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,210 @@
1-
use clippy_utils::diagnostics::span_lint_and_sugg;
1+
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::msrvs::{self, Msrv};
3-
use clippy_utils::res::MaybeDef;
4-
use clippy_utils::source::snippet;
5-
use clippy_utils::usage::mutated_variables;
3+
use clippy_utils::res::{MaybeDef as _, MaybeResPath as _};
4+
use clippy_utils::source::snippet_with_applicability;
5+
use clippy_utils::ty::is_copy;
6+
use rustc_data_structures::fx::FxHashSet;
67
use rustc_errors::Applicability;
7-
use rustc_hir as hir;
8+
use rustc_hir::def::Res;
9+
use rustc_hir::intravisit::{Visitor, walk_expr, walk_path};
10+
use rustc_hir::{ExprKind, HirId, LangItem, Node, PatKind, Path, QPath};
811
use rustc_lint::LateContext;
9-
use rustc_span::symbol::sym;
12+
use rustc_middle::hir::nested_filter;
13+
use rustc_span::{Span, sym};
14+
use std::ops::ControlFlow;
1015

1116
use super::MAP_UNWRAP_OR;
1217

13-
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s
14-
///
15-
/// Returns true if the lint was emitted
18+
/// lint use of `map().unwrap_or()` for `Option`s and `Result`s
19+
#[expect(clippy::too_many_arguments)]
1620
pub(super) fn check<'tcx>(
1721
cx: &LateContext<'tcx>,
18-
expr: &'tcx hir::Expr<'_>,
19-
recv: &'tcx hir::Expr<'_>,
20-
map_arg: &'tcx hir::Expr<'_>,
21-
unwrap_arg: &'tcx hir::Expr<'_>,
22+
expr: &rustc_hir::Expr<'_>,
23+
recv: &rustc_hir::Expr<'_>,
24+
map_arg: &'tcx rustc_hir::Expr<'_>,
25+
unwrap_recv: &rustc_hir::Expr<'_>,
26+
unwrap_arg: &'tcx rustc_hir::Expr<'_>,
27+
map_span: Span,
2228
msrv: Msrv,
23-
) -> bool {
24-
// lint if the caller of `map()` is an `Option` or a `Result`.
25-
let is_option = cx.typeck_results().expr_ty(recv).is_diag_item(cx, sym::Option);
26-
let is_result = cx.typeck_results().expr_ty(recv).is_diag_item(cx, sym::Result);
29+
) {
30+
let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs();
31+
let recv_ty_kind = match recv_ty.opt_diag_name(cx) {
32+
Some(sym::Option) => sym::Option,
33+
Some(sym::Result) if msrv.meets(cx, msrvs::RESULT_MAP_OR) => sym::Result,
34+
_ => return,
35+
};
2736

28-
if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) {
29-
return false;
37+
let unwrap_arg_ty = cx.typeck_results().expr_ty(unwrap_arg);
38+
if !is_copy(cx, unwrap_arg_ty) {
39+
// Replacing `.map(<f>).unwrap_or(<a>)` with `.map_or(<a>, <f>)` can sometimes lead to
40+
// borrowck errors, see #10579 for one such instance.
41+
// In particular, if `a` causes a move and `f` references that moved binding, then we cannot lint:
42+
// ```
43+
// let x = vec![1, 2];
44+
// x.get(0..1).map(|s| s.to_vec()).unwrap_or(x);
45+
// ```
46+
// This compiles, but changing it to `map_or` will produce a compile error:
47+
// ```
48+
// let x = vec![1, 2];
49+
// x.get(0..1).map_or(x, |s| s.to_vec())
50+
// ^ moving `x` here
51+
// ^^^^^^^^^^^ while it is borrowed here (and later used in the closure)
52+
// ```
53+
// So, we have to check that `a` is not referenced anywhere (even outside of the `.map` closure!)
54+
// before the call to `unwrap_or`.
55+
56+
let mut unwrap_visitor = UnwrapVisitor {
57+
cx,
58+
identifiers: FxHashSet::default(),
59+
};
60+
unwrap_visitor.visit_expr(unwrap_arg);
61+
62+
let mut reference_visitor = ReferenceVisitor {
63+
cx,
64+
identifiers: unwrap_visitor.identifiers,
65+
unwrap_or_span: unwrap_arg.span,
66+
};
67+
68+
let body = cx.tcx.hir_body_owned_by(cx.tcx.hir_enclosing_body_owner(expr.hir_id));
69+
70+
// Visit the body, and return if we've found a reference
71+
if reference_visitor.visit_body(body).is_break() {
72+
return;
73+
}
74+
}
75+
76+
if !unwrap_arg.span.eq_ctxt(map_span) {
77+
return;
3078
}
3179

32-
if is_option || is_result {
33-
// Don't make a suggestion that may fail to compile due to mutably borrowing
34-
// the same variable twice.
35-
let map_mutated_vars = mutated_variables(recv, cx);
36-
let unwrap_mutated_vars = mutated_variables(unwrap_arg, cx);
37-
if let (Some(map_mutated_vars), Some(unwrap_mutated_vars)) = (map_mutated_vars, unwrap_mutated_vars) {
38-
if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() {
39-
return false;
40-
}
41-
} else {
42-
return false;
80+
let mut applicability = Applicability::MachineApplicable;
81+
// get snippet for unwrap_or()
82+
let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability);
83+
// lint message
84+
85+
let suggest_kind = if recv_ty_kind == sym::Option
86+
&& unwrap_arg
87+
.basic_res()
88+
.ctor_parent(cx)
89+
.is_lang_item(cx, LangItem::OptionNone)
90+
{
91+
SuggestedKind::AndThen
92+
}
93+
// is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead
94+
else if matches!(&unwrap_arg.kind, ExprKind::Lit(lit)
95+
if matches!(lit.node, rustc_ast::LitKind::Bool(false)))
96+
&& msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND)
97+
{
98+
SuggestedKind::IsVariantAnd
99+
} else {
100+
SuggestedKind::Other
101+
};
102+
103+
let arg = match suggest_kind {
104+
SuggestedKind::AndThen => "None",
105+
SuggestedKind::IsVariantAnd => "false",
106+
SuggestedKind::Other => "<a>",
107+
};
108+
109+
let suggest = match (suggest_kind, recv_ty_kind) {
110+
(SuggestedKind::AndThen, _) => "and_then(<f>)",
111+
(SuggestedKind::IsVariantAnd, sym::Result) => "is_ok_and(<f>)",
112+
(SuggestedKind::IsVariantAnd, sym::Option) => "is_some_and(<f>)",
113+
_ => "map_or(<a>, <f>)",
114+
};
115+
116+
let msg = format!(
117+
"called `map(<f>).unwrap_or({arg})` on {} `{recv_ty_kind}` value",
118+
if recv_ty_kind == sym::Option { "an" } else { "a" }
119+
);
120+
121+
span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| {
122+
let map_arg_span = map_arg.span;
123+
124+
let mut suggestion = vec![
125+
(
126+
map_span,
127+
String::from(match (suggest_kind, recv_ty_kind) {
128+
(SuggestedKind::AndThen, _) => "and_then",
129+
(SuggestedKind::IsVariantAnd, sym::Result) => "is_ok_and",
130+
(SuggestedKind::IsVariantAnd, sym::Option) => "is_some_and",
131+
(SuggestedKind::Other, _)
132+
if unwrap_arg_ty.peel_refs().is_array()
133+
&& cx.typeck_results().expr_ty_adjusted(unwrap_arg).peel_refs().is_slice() =>
134+
{
135+
return;
136+
},
137+
_ => "map_or",
138+
}),
139+
),
140+
(expr.span.with_lo(unwrap_recv.span.hi()), String::new()),
141+
];
142+
143+
if matches!(suggest_kind, SuggestedKind::Other) {
144+
suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, ")));
43145
}
44146

45-
// lint message
46-
let msg = if is_option {
47-
"called `map(<f>).unwrap_or_else(<g>)` on an `Option` value"
48-
} else {
49-
"called `map(<f>).unwrap_or_else(<g>)` on a `Result` value"
50-
};
51-
// get snippets for args to map() and unwrap_or_else()
52-
let map_snippet = snippet(cx, map_arg.span, "..");
53-
let unwrap_snippet = snippet(cx, unwrap_arg.span, "..");
54-
// lint, with note if both map() and unwrap_or_else() have the same span
55-
if map_arg.span.eq_ctxt(unwrap_arg.span) {
56-
let var_snippet = snippet(cx, recv.span, "..");
57-
span_lint_and_sugg(
58-
cx,
59-
MAP_UNWRAP_OR,
60-
expr.span,
61-
msg,
62-
"try",
63-
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"),
64-
Applicability::MachineApplicable,
65-
);
66-
return true;
147+
diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability);
148+
});
149+
}
150+
151+
#[derive(Clone, Copy, PartialEq, Eq)]
152+
enum SuggestedKind {
153+
AndThen,
154+
IsVariantAnd,
155+
Other,
156+
}
157+
158+
struct UnwrapVisitor<'a, 'tcx> {
159+
cx: &'a LateContext<'tcx>,
160+
identifiers: FxHashSet<HirId>,
161+
}
162+
163+
impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> {
164+
type NestedFilter = nested_filter::All;
165+
166+
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) {
167+
if let Res::Local(local_id) = path.res
168+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
169+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
170+
{
171+
self.identifiers.insert(local_id);
67172
}
173+
walk_path(self, path);
68174
}
69175

70-
false
176+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
177+
self.cx.tcx
178+
}
179+
}
180+
181+
struct ReferenceVisitor<'a, 'tcx> {
182+
cx: &'a LateContext<'tcx>,
183+
identifiers: FxHashSet<HirId>,
184+
unwrap_or_span: Span,
185+
}
186+
187+
impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> {
188+
type NestedFilter = nested_filter::All;
189+
type Result = ControlFlow<()>;
190+
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> {
191+
// If we haven't found a reference yet, check if this references
192+
// one of the locals that was moved in the `unwrap_or` argument.
193+
// We are only interested in exprs that appear before the `unwrap_or` call.
194+
if expr.span < self.unwrap_or_span
195+
&& let ExprKind::Path(ref path) = expr.kind
196+
&& let QPath::Resolved(_, path) = path
197+
&& let Res::Local(local_id) = path.res
198+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
199+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
200+
&& self.identifiers.contains(&local_id)
201+
{
202+
return ControlFlow::Break(());
203+
}
204+
walk_expr(self, expr)
205+
}
206+
207+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
208+
self.cx.tcx
209+
}
71210
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::msrvs::{self, Msrv};
3+
use clippy_utils::res::MaybeDef as _;
4+
use clippy_utils::source::snippet;
5+
use clippy_utils::sym;
6+
use clippy_utils::usage::mutated_variables;
7+
use rustc_errors::Applicability;
8+
use rustc_hir as hir;
9+
use rustc_lint::LateContext;
10+
11+
use super::MAP_UNWRAP_OR;
12+
13+
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s
14+
///
15+
/// Is part of the `map_unwrap_or` lint, split into separate files for readability.
16+
pub(super) fn check<'tcx>(
17+
cx: &LateContext<'tcx>,
18+
expr: &'tcx hir::Expr<'_>,
19+
recv: &'tcx hir::Expr<'_>,
20+
map_arg: &'tcx hir::Expr<'_>,
21+
unwrap_arg: &'tcx hir::Expr<'_>,
22+
msrv: Msrv,
23+
) -> bool {
24+
let recv_ty = cx.typeck_results().expr_ty(recv).peel_refs();
25+
let recv_ty_kind = match recv_ty.opt_diag_name(cx) {
26+
Some(sym::Option) => sym::Option,
27+
Some(sym::Result) if msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) => sym::Result,
28+
_ => return false,
29+
};
30+
31+
// Don't make a suggestion that may fail to compile due to mutably borrowing
32+
// the same variable twice.
33+
let Some(map_mutated_vars) = mutated_variables(recv, cx) else {
34+
return false;
35+
};
36+
let Some(unwrap_mutated_vars) = mutated_variables(unwrap_arg, cx) else {
37+
return false;
38+
};
39+
if map_mutated_vars.intersection(&unwrap_mutated_vars).next().is_some() {
40+
return false;
41+
}
42+
43+
// lint message
44+
let msg = if recv_ty_kind == sym::Option {
45+
"called `map(<f>).unwrap_or_else(<g>)` on an `Option` value"
46+
} else {
47+
"called `map(<f>).unwrap_or_else(<g>)` on a `Result` value"
48+
};
49+
// get snippets for args to map() and unwrap_or_else()
50+
let map_snippet = snippet(cx, map_arg.span, "..");
51+
let unwrap_snippet = snippet(cx, unwrap_arg.span, "..");
52+
// lint, with note if both map() and unwrap_or_else() have the same span
53+
if map_arg.span.eq_ctxt(unwrap_arg.span) {
54+
let var_snippet = snippet(cx, recv.span, "..");
55+
span_lint_and_sugg(
56+
cx,
57+
MAP_UNWRAP_OR,
58+
expr.span,
59+
msg,
60+
"try",
61+
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"),
62+
Applicability::MachineApplicable,
63+
);
64+
return true;
65+
}
66+
67+
false
68+
}

clippy_lints/src/methods/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ mod map_err_ignore;
7474
mod map_flatten;
7575
mod map_identity;
7676
mod map_unwrap_or;
77+
mod map_unwrap_or_else;
7778
mod map_with_unused_argument_over_ranges;
7879
mod mut_mutex_lock;
7980
mod needless_as_bytes;
@@ -89,7 +90,6 @@ mod open_options;
8990
mod option_as_ref_cloned;
9091
mod option_as_ref_deref;
9192
mod option_map_or_none;
92-
mod option_map_unwrap_or;
9393
mod or_fun_call;
9494
mod or_then_unwrap;
9595
mod path_buf_push_overwrite;
@@ -5607,7 +5607,7 @@ impl Methods {
56075607
manual_saturating_arithmetic::check_unwrap_or(cx, expr, lhs, rhs, u_arg, arith);
56085608
},
56095609
Some((sym::map, m_recv, [m_arg], span, _)) => {
5610-
option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
5610+
map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
56115611
},
56125612
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
56135613
obfuscated_if_else::check(
@@ -5648,7 +5648,7 @@ impl Methods {
56485648
(sym::unwrap_or_else, [u_arg]) => {
56495649
match method_call(recv) {
56505650
Some((sym::map, recv, [map_arg], _, _))
5651-
if map_unwrap_or::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
5651+
if map_unwrap_or_else::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
56525652
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
56535653
obfuscated_if_else::check(
56545654
cx,

0 commit comments

Comments
 (0)