Skip to content

Commit a59236f

Browse files
committed
Auto merge of #10919 - y21:issue10579, r=blyxyas,xFrednet
[`map_unwrap_or`]: don't lint when referenced variable is moved Fixes #10579. The previous way of checking if changing `map(f).unwrap_or(a)` to `map_or(a, f)` is safe had a flaw when the argument to `unwrap_or` moves a binding and the `map` closure references that binding in some way. It used to simply check if any of the identifiers in the `unwrap_or` argument are referenced in the `map` closure, but it didn't consider the case where the moved binding is referred to through references, for example: ```rs let x = vec![1, 2]; let x_ref = &x; Some(()).map(|_| x_ref.clone()).unwrap_or(x); ``` This compiles as is, but we cannot change it to `map_or`. This lint however did suggest changing it, because the simple way of checking if `x` is referenced anywhere in the `map` closure fails here. The safest thing to do here imo (what this PR does) is check if the moved value `x` is referenced *anywhere* in the body (before the `unwrap_or` call). One can always create a reference to the value and smuggle them into the closure, without actually referring to `x`. The original, linked issue shows another one such example: ```rs let x = vec![1,2,3,0]; let y = x.strip_suffix(&[0]).map(|s| s.to_vec()).unwrap_or(x); ``` `x.strip_suffix(&[0])` creates a reference to `x` that is available through `s` inside of the `map` closure, so we can't change it to `map_or`. changelog: [`map_unwrap_or`]: don't lint when referenced variable is moved
2 parents b9b4537 + 9d58a42 commit a59236f

File tree

2 files changed

+88
-28
lines changed

2 files changed

+88
-28
lines changed

clippy_lints/src/methods/option_map_unwrap_or.rs

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ use clippy_utils::ty::is_copy;
44
use clippy_utils::ty::is_type_diagnostic_item;
55
use rustc_data_structures::fx::FxHashSet;
66
use rustc_errors::Applicability;
7+
use rustc_hir::def::Res;
78
use rustc_hir::intravisit::{walk_path, Visitor};
9+
use rustc_hir::ExprKind;
10+
use rustc_hir::Node;
11+
use rustc_hir::PatKind;
12+
use rustc_hir::QPath;
813
use rustc_hir::{self, HirId, Path};
914
use rustc_lint::LateContext;
1015
use rustc_middle::hir::nested_filter;
1116
use rustc_span::source_map::Span;
12-
use rustc_span::{sym, Symbol};
17+
use rustc_span::sym;
1318

1419
use super::MAP_UNWRAP_OR;
1520

@@ -26,23 +31,41 @@ pub(super) fn check<'tcx>(
2631
// lint if the caller of `map()` is an `Option`
2732
if is_type_diagnostic_item(cx, cx.typeck_results().expr_ty(recv), sym::Option) {
2833
if !is_copy(cx, cx.typeck_results().expr_ty(unwrap_arg)) {
29-
// Do not lint if the `map` argument uses identifiers in the `map`
30-
// argument that are also used in the `unwrap_or` argument
34+
// Replacing `.map(<f>).unwrap_or(<a>)` with `.map_or(<a>, <f>)` can sometimes lead to
35+
// borrowck errors, see #10579 for one such instance.
36+
// In particular, if `a` causes a move and `f` references that moved binding, then we cannot lint:
37+
// ```
38+
// let x = vec![1, 2];
39+
// x.get(0..1).map(|s| s.to_vec()).unwrap_or(x);
40+
// ```
41+
// This compiles, but changing it to `map_or` will produce a compile error:
42+
// ```
43+
// let x = vec![1, 2];
44+
// x.get(0..1).map_or(x, |s| s.to_vec())
45+
// ^ moving `x` here
46+
// ^^^^^^^^^^^ while it is borrowed here (and later used in the closure)
47+
// ```
48+
// So, we have to check that `a` is not referenced anywhere (even outside of the `.map` closure!)
49+
// before the call to `unwrap_or`.
3150

3251
let mut unwrap_visitor = UnwrapVisitor {
3352
cx,
3453
identifiers: FxHashSet::default(),
3554
};
3655
unwrap_visitor.visit_expr(unwrap_arg);
3756

38-
let mut map_expr_visitor = MapExprVisitor {
57+
let mut reference_visitor = ReferenceVisitor {
3958
cx,
4059
identifiers: unwrap_visitor.identifiers,
41-
found_identifier: false,
60+
found_reference: false,
61+
unwrap_or_span: unwrap_arg.span,
4262
};
43-
map_expr_visitor.visit_expr(map_arg);
4463

45-
if map_expr_visitor.found_identifier {
64+
let map = cx.tcx.hir();
65+
let body = map.body(map.body_owned_by(map.enclosing_body_owner(expr.hir_id)));
66+
reference_visitor.visit_body(body);
67+
68+
if reference_visitor.found_reference {
4669
return;
4770
}
4871
}
@@ -91,14 +114,19 @@ pub(super) fn check<'tcx>(
91114

92115
struct UnwrapVisitor<'a, 'tcx> {
93116
cx: &'a LateContext<'tcx>,
94-
identifiers: FxHashSet<Symbol>,
117+
identifiers: FxHashSet<HirId>,
95118
}
96119

97120
impl<'a, 'tcx> Visitor<'tcx> for UnwrapVisitor<'a, 'tcx> {
98121
type NestedFilter = nested_filter::All;
99122

100-
fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) {
101-
self.identifiers.insert(ident(path));
123+
fn visit_path(&mut self, path: &Path<'tcx>, _: HirId) {
124+
if let Res::Local(local_id) = path.res
125+
&& let Some(Node::Pat(pat)) = self.cx.tcx.hir().find(local_id)
126+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
127+
{
128+
self.identifiers.insert(local_id);
129+
}
102130
walk_path(self, path);
103131
}
104132

@@ -107,32 +135,35 @@ impl<'a, 'tcx> Visitor<'tcx> for UnwrapVisitor<'a, 'tcx> {
107135
}
108136
}
109137

110-
struct MapExprVisitor<'a, 'tcx> {
138+
struct ReferenceVisitor<'a, 'tcx> {
111139
cx: &'a LateContext<'tcx>,
112-
identifiers: FxHashSet<Symbol>,
113-
found_identifier: bool,
140+
identifiers: FxHashSet<HirId>,
141+
found_reference: bool,
142+
unwrap_or_span: Span,
114143
}
115144

116-
impl<'a, 'tcx> Visitor<'tcx> for MapExprVisitor<'a, 'tcx> {
145+
impl<'a, 'tcx> Visitor<'tcx> for ReferenceVisitor<'a, 'tcx> {
117146
type NestedFilter = nested_filter::All;
118-
119-
fn visit_path(&mut self, path: &Path<'tcx>, _id: HirId) {
120-
if self.identifiers.contains(&ident(path)) {
121-
self.found_identifier = true;
122-
return;
147+
fn visit_expr(&mut self, expr: &'tcx rustc_hir::Expr<'_>) {
148+
// If we haven't found a reference yet, check if this references
149+
// one of the locals that was moved in the `unwrap_or` argument.
150+
// We are only interested in exprs that appear before the `unwrap_or` call.
151+
if !self.found_reference {
152+
if expr.span < self.unwrap_or_span
153+
&& let ExprKind::Path(ref path) = expr.kind
154+
&& let QPath::Resolved(_, path) = path
155+
&& let Res::Local(local_id) = path.res
156+
&& let Some(Node::Pat(pat)) = self.cx.tcx.hir().find(local_id)
157+
&& let PatKind::Binding(_, local_id, ..) = pat.kind
158+
&& self.identifiers.contains(&local_id)
159+
{
160+
self.found_reference = true;
161+
}
162+
rustc_hir::intravisit::walk_expr(self, expr);
123163
}
124-
walk_path(self, path);
125164
}
126165

127166
fn nested_visit_map(&mut self) -> Self::Map {
128167
self.cx.tcx.hir()
129168
}
130169
}
131-
132-
fn ident(path: &Path<'_>) -> Symbol {
133-
path.segments
134-
.last()
135-
.expect("segments should be composed of at least 1 element")
136-
.ident
137-
.name
138-
}

tests/ui/map_unwrap_or.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,32 @@ fn msrv_1_41() {
9494

9595
let _ = res.map(|x| x + 1).unwrap_or_else(|_e| 0);
9696
}
97+
98+
mod issue_10579 {
99+
// Different variations of the same issue.
100+
fn v1() {
101+
let x = vec![1, 2, 3, 0];
102+
let y = x.strip_suffix(&[0]).map(|s| s.to_vec()).unwrap_or(x);
103+
println!("{y:?}");
104+
}
105+
fn v2() {
106+
let x = vec![1, 2, 3, 0];
107+
let y = Some(()).map(|_| x.to_vec()).unwrap_or(x);
108+
println!("{y:?}");
109+
}
110+
fn v3() {
111+
let x = vec![1, 2, 3, 0];
112+
let xref = &x;
113+
let y = Some(()).map(|_| xref.to_vec()).unwrap_or(x);
114+
println!("{y:?}");
115+
}
116+
fn v4() {
117+
struct VecInStruct {
118+
v: Vec<u8>,
119+
}
120+
let s = VecInStruct { v: vec![1, 2, 3, 0] };
121+
122+
let y = Some(()).map(|_| s.v.clone()).unwrap_or(s.v);
123+
println!("{y:?}");
124+
}
125+
}

0 commit comments

Comments
 (0)