Skip to content

Commit fcaa02f

Browse files
committed
fix: map_unwrap_or fail to cover Result::unwrap_or
1 parent 9bcc8a4 commit fcaa02f

File tree

9 files changed

+331
-238
lines changed

9 files changed

+331
-238
lines changed
Lines changed: 168 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,193 @@
1-
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
1+
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::msrvs::{self, Msrv};
3-
use clippy_utils::source::snippet;
4-
use clippy_utils::ty::is_type_diagnostic_item;
5-
use clippy_utils::usage::mutated_variables;
3+
use clippy_utils::source::snippet_with_applicability;
4+
use clippy_utils::ty::{is_copy, is_type_diagnostic_item};
5+
use rustc_data_structures::fx::FxHashSet;
66
use rustc_errors::Applicability;
7-
use rustc_hir as hir;
7+
use rustc_hir::def::Res;
8+
use rustc_hir::intravisit::{Visitor, walk_path};
9+
use rustc_hir::{ExprKind, HirId, Node, PatKind, Path, QPath};
810
use rustc_lint::LateContext;
9-
use rustc_span::symbol::sym;
11+
use rustc_middle::hir::nested_filter;
12+
use rustc_span::{Span, sym};
13+
use std::ops::ControlFlow;
1014

1115
use super::MAP_UNWRAP_OR;
1216

13-
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s
14-
///
15-
/// Returns true if the lint was emitted
17+
/// lint use of `map().unwrap_or()` for `Option`s and `Result`s
18+
#[expect(clippy::too_many_arguments)]
1619
pub(super) fn check<'tcx>(
1720
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<'_>,
21+
expr: &rustc_hir::Expr<'_>,
22+
recv: &rustc_hir::Expr<'_>,
23+
map_arg: &'tcx rustc_hir::Expr<'_>,
24+
unwrap_recv: &rustc_hir::Expr<'_>,
25+
unwrap_arg: &'tcx rustc_hir::Expr<'_>,
26+
map_span: Span,
2227
msrv: Msrv,
23-
) -> bool {
24-
// lint if the caller of `map()` is an `Option` or a `Result`.
28+
) {
2529
let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option);
2630
let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result);
2731

28-
if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) {
29-
return false;
32+
if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR) {
33+
return;
3034
}
3135

36+
// lint if the caller of `map()` is an `Option`
3237
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;
38+
if !is_copy(cx, cx.typeck_results().expr_ty(unwrap_arg)) {
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;
4073
}
41-
} else {
42-
return false;
4374
}
4475

76+
if !unwrap_arg.span.eq_ctxt(map_span) {
77+
return;
78+
}
79+
80+
// is_some_and is stabilised && `unwrap_or` argument is false; suggest `is_some_and` instead
81+
let suggest_is_some_and = matches!(&unwrap_arg.kind, ExprKind::Lit(lit)
82+
if matches!(lit.node, rustc_ast::LitKind::Bool(false)))
83+
&& msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND);
84+
85+
let mut applicability = Applicability::MachineApplicable;
86+
// get snippet for unwrap_or()
87+
let unwrap_snippet = snippet_with_applicability(cx, unwrap_arg.span, "..", &mut applicability);
4588
// lint message
46-
let msg = if is_option {
47-
"called `map(<f>).unwrap_or_else(<g>)` on an `Option` value"
89+
// comparing the snippet from source to raw text ("None") below is safe
90+
// because we already have checked the type.
91+
let unwrap_snippet_none = is_option && unwrap_snippet == "None";
92+
let arg = if unwrap_snippet_none {
93+
"None"
94+
} else if suggest_is_some_and {
95+
"false"
4896
} else {
49-
"called `map(<f>).unwrap_or_else(<g>)` on a `Result` value"
97+
"<a>"
5098
};
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 neither arg is > 1 line and both map() and
55-
// unwrap_or_else() have the same span
56-
let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1;
57-
let same_span = map_arg.span.eq_ctxt(unwrap_arg.span);
58-
if same_span && !multiline {
59-
let var_snippet = snippet(cx, recv.span, "..");
60-
span_lint_and_sugg(
61-
cx,
62-
MAP_UNWRAP_OR,
63-
expr.span,
64-
msg,
65-
"try",
66-
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"),
67-
Applicability::MachineApplicable,
68-
);
69-
return true;
70-
} else if same_span && multiline {
71-
span_lint(cx, MAP_UNWRAP_OR, expr.span, msg);
72-
return true;
99+
let suggest = if unwrap_snippet_none {
100+
"and_then(<f>)"
101+
} else if suggest_is_some_and {
102+
if is_result {
103+
"is_ok_and(<f>)"
104+
} else {
105+
"is_some_and(<f>)"
106+
}
107+
} else {
108+
"map_or(<a>, <f>)"
109+
};
110+
let msg = format!(
111+
"called `map(<f>).unwrap_or({arg})` on an `{}` value",
112+
if is_option { "Option" } else { "Result" }
113+
);
114+
115+
span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| {
116+
let map_arg_span = map_arg.span;
117+
118+
let mut suggestion = vec![
119+
(
120+
map_span,
121+
String::from(if unwrap_snippet_none {
122+
"and_then"
123+
} else if suggest_is_some_and {
124+
if is_result { "is_ok_and" } else { "is_some_and" }
125+
} else {
126+
"map_or"
127+
}),
128+
),
129+
(expr.span.with_lo(unwrap_recv.span.hi()), String::new()),
130+
];
131+
132+
if !unwrap_snippet_none && !suggest_is_some_and {
133+
suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, ")));
134+
}
135+
136+
diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability);
137+
});
138+
}
139+
}
140+
141+
struct UnwrapVisitor<'a, 'tcx> {
142+
cx: &'a LateContext<'tcx>,
143+
identifiers: FxHashSet<HirId>,
144+
}
145+
146+
impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> {
147+
type NestedFilter = nested_filter::All;
148+
149+
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) {
150+
if let Res::Local(local_id) = path.res
151+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
152+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
153+
{
154+
self.identifiers.insert(local_id);
73155
}
156+
walk_path(self, path);
74157
}
75158

76-
false
159+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
160+
self.cx.tcx
161+
}
162+
}
163+
164+
struct ReferenceVisitor<'a, 'tcx> {
165+
cx: &'a LateContext<'tcx>,
166+
identifiers: FxHashSet<HirId>,
167+
unwrap_or_span: Span,
168+
}
169+
170+
impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> {
171+
type NestedFilter = nested_filter::All;
172+
type Result = ControlFlow<()>;
173+
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> {
174+
// If we haven't found a reference yet, check if this references
175+
// one of the locals that was moved in the `unwrap_or` argument.
176+
// We are only interested in exprs that appear before the `unwrap_or` call.
177+
if expr.span < self.unwrap_or_span
178+
&& let ExprKind::Path(ref path) = expr.kind
179+
&& let QPath::Resolved(_, path) = path
180+
&& let Res::Local(local_id) = path.res
181+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
182+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
183+
&& self.identifiers.contains(&local_id)
184+
{
185+
return ControlFlow::Break(());
186+
}
187+
rustc_hir::intravisit::walk_expr(self, expr)
188+
}
189+
190+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
191+
self.cx.tcx
192+
}
77193
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_sugg};
2+
use clippy_utils::msrvs::{self, Msrv};
3+
use clippy_utils::source::snippet;
4+
use clippy_utils::ty::is_type_diagnostic_item;
5+
use clippy_utils::usage::mutated_variables;
6+
use rustc_errors::Applicability;
7+
use rustc_hir as hir;
8+
use rustc_lint::LateContext;
9+
use rustc_span::symbol::sym;
10+
11+
use super::MAP_UNWRAP_OR;
12+
13+
/// lint use of `map().unwrap_or_else()` for `Option`s and `Result`s
14+
///
15+
/// Returns true if the lint was emitted
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+
// lint if the caller of `map()` is an `Option` or a `Result`.
25+
let is_option = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option);
26+
let is_result = is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Result);
27+
28+
if is_result && !msrv.meets(cx, msrvs::RESULT_MAP_OR_ELSE) {
29+
return false;
30+
}
31+
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;
43+
}
44+
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 neither arg is > 1 line and both map() and
55+
// unwrap_or_else() have the same span
56+
let multiline = map_snippet.lines().count() > 1 || unwrap_snippet.lines().count() > 1;
57+
let same_span = map_arg.span.eq_ctxt(unwrap_arg.span);
58+
if same_span && !multiline {
59+
let var_snippet = snippet(cx, recv.span, "..");
60+
span_lint_and_sugg(
61+
cx,
62+
MAP_UNWRAP_OR,
63+
expr.span,
64+
msg,
65+
"try",
66+
format!("{var_snippet}.map_or_else({unwrap_snippet}, {map_snippet})"),
67+
Applicability::MachineApplicable,
68+
);
69+
return true;
70+
} else if same_span && multiline {
71+
span_lint(cx, MAP_UNWRAP_OR, expr.span, msg);
72+
return true;
73+
}
74+
}
75+
76+
false
77+
}

clippy_lints/src/methods/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ mod map_err_ignore;
7272
mod map_flatten;
7373
mod map_identity;
7474
mod map_unwrap_or;
75+
mod map_unwrap_or_else;
7576
mod map_with_unused_argument_over_ranges;
7677
mod mut_mutex_lock;
7778
mod needless_as_bytes;
@@ -86,7 +87,6 @@ mod open_options;
8687
mod option_as_ref_cloned;
8788
mod option_as_ref_deref;
8889
mod option_map_or_none;
89-
mod option_map_unwrap_or;
9090
mod or_fun_call;
9191
mod or_then_unwrap;
9292
mod path_buf_push_overwrite;
@@ -5551,7 +5551,7 @@ impl Methods {
55515551
);
55525552
},
55535553
Some((sym::map, m_recv, [m_arg], span, _)) => {
5554-
option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
5554+
map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
55555555
},
55565556
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
55575557
obfuscated_if_else::check(cx, expr, t_recv, t_arg, Some(u_arg), then_method, name);
@@ -5583,7 +5583,7 @@ impl Methods {
55835583
(sym::unwrap_or_else, [u_arg]) => {
55845584
match method_call(recv) {
55855585
Some((sym::map, recv, [map_arg], _, _))
5586-
if map_unwrap_or::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
5586+
if map_unwrap_or_else::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
55875587
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
55885588
obfuscated_if_else::check(
55895589
cx,

0 commit comments

Comments
 (0)