Skip to content

Commit d89877a

Browse files
committed
fix: map_unwrap_or fail to cover Result::unwrap_or
1 parent 2a6197e commit d89877a

File tree

7 files changed

+295
-236
lines changed

7 files changed

+295
-236
lines changed
Lines changed: 164 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,189 @@
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+
"is_some_and(<f>)"
103+
} else {
104+
"map_or(<a>, <f>)"
105+
};
106+
let msg = format!(
107+
"called `map(<f>).unwrap_or({arg})` on an `{}` value",
108+
if is_option { "Option" } else { "Result" }
109+
);
110+
111+
span_lint_and_then(cx, MAP_UNWRAP_OR, expr.span, msg, |diag| {
112+
let map_arg_span = map_arg.span;
113+
114+
let mut suggestion = vec![
115+
(
116+
map_span,
117+
String::from(if unwrap_snippet_none {
118+
"and_then"
119+
} else if suggest_is_some_and {
120+
"is_some_and"
121+
} else {
122+
"map_or"
123+
}),
124+
),
125+
(expr.span.with_lo(unwrap_recv.span.hi()), String::new()),
126+
];
127+
128+
if !unwrap_snippet_none && !suggest_is_some_and {
129+
suggestion.push((map_arg_span.with_hi(map_arg_span.lo()), format!("{unwrap_snippet}, ")));
130+
}
131+
132+
diag.multipart_suggestion(format!("use `{suggest}` instead"), suggestion, applicability);
133+
});
134+
}
135+
}
136+
137+
struct UnwrapVisitor<'a, 'tcx> {
138+
cx: &'a LateContext<'tcx>,
139+
identifiers: FxHashSet<HirId>,
140+
}
141+
142+
impl<'tcx> Visitor<'tcx> for UnwrapVisitor<'_, 'tcx> {
143+
type NestedFilter = nested_filter::All;
144+
145+
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) {
146+
if let Res::Local(local_id) = path.res
147+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
148+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
149+
{
150+
self.identifiers.insert(local_id);
151+
}
152+
walk_path(self, path);
153+
}
154+
155+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
156+
self.cx.tcx
157+
}
158+
}
159+
160+
struct ReferenceVisitor<'a, 'tcx> {
161+
cx: &'a LateContext<'tcx>,
162+
identifiers: FxHashSet<HirId>,
163+
unwrap_or_span: Span,
164+
}
165+
166+
impl<'tcx> Visitor<'tcx> for ReferenceVisitor<'_, 'tcx> {
167+
type NestedFilter = nested_filter::All;
168+
type Result = ControlFlow<()>;
169+
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) -> ControlFlow<()> {
170+
// If we haven't found a reference yet, check if this references
171+
// one of the locals that was moved in the `unwrap_or` argument.
172+
// We are only interested in exprs that appear before the `unwrap_or` call.
173+
if expr.span < self.unwrap_or_span
174+
&& let ExprKind::Path(ref path) = expr.kind
175+
&& let QPath::Resolved(_, path) = path
176+
&& let Res::Local(local_id) = path.res
177+
&& let Node::Pat(pat) = self.cx.tcx.hir_node(local_id)
178+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
179+
&& self.identifiers.contains(&local_id)
180+
{
181+
return ControlFlow::Break(());
73182
}
183+
rustc_hir::intravisit::walk_expr(self, expr)
74184
}
75185

76-
false
186+
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
187+
self.cx.tcx
188+
}
77189
}
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;
@@ -5525,7 +5525,7 @@ impl Methods {
55255525
);
55265526
},
55275527
Some((sym::map, m_recv, [m_arg], span, _)) => {
5528-
option_map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
5528+
map_unwrap_or::check(cx, expr, m_recv, m_arg, recv, u_arg, span, self.msrv);
55295529
},
55305530
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
55315531
obfuscated_if_else::check(cx, expr, t_recv, t_arg, Some(u_arg), then_method, name);
@@ -5557,7 +5557,7 @@ impl Methods {
55575557
(sym::unwrap_or_else, [u_arg]) => {
55585558
match method_call(recv) {
55595559
Some((sym::map, recv, [map_arg], _, _))
5560-
if map_unwrap_or::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
5560+
if map_unwrap_or_else::check(cx, expr, recv, map_arg, u_arg, self.msrv) => {},
55615561
Some((then_method @ (sym::then | sym::then_some), t_recv, [t_arg], _, _)) => {
55625562
obfuscated_if_else::check(
55635563
cx,

0 commit comments

Comments
 (0)