Skip to content

Commit a2f9861

Browse files
committed
fix [manual_map] not catching type adjustment
1 parent 75e3a2e commit a2f9861

File tree

4 files changed

+302
-17
lines changed

4 files changed

+302
-17
lines changed

clippy_lints/src/matches/manual_utils.rs

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ where
7373
}
7474

7575
// `map` won't perform any adjustments.
76-
if !cx.typeck_results().expr_adjustments(some_expr.expr).is_empty() {
76+
if expr_has_type_coercion(cx, expr) {
7777
return None;
7878
}
7979

@@ -124,6 +124,12 @@ where
124124
};
125125

126126
let closure_expr_snip = some_expr.to_snippet_with_context(cx, expr_ctxt, &mut app);
127+
let closure_body = if some_expr.needs_unsafe_block {
128+
format!("unsafe {}", closure_expr_snip.blockify())
129+
} else {
130+
closure_expr_snip.to_string()
131+
};
132+
127133
let body_str = if let PatKind::Binding(annotation, id, some_binding, None) = some_pat.kind {
128134
if !some_expr.needs_unsafe_block
129135
&& let Some(func) = can_pass_as_func(cx, id, some_expr.expr)
@@ -145,20 +151,12 @@ where
145151
""
146152
};
147153

148-
if some_expr.needs_unsafe_block {
149-
format!("|{annotation}{some_binding}| unsafe {{ {closure_expr_snip} }}")
150-
} else {
151-
format!("|{annotation}{some_binding}| {closure_expr_snip}")
152-
}
154+
format!("|{annotation}{some_binding}| {closure_body}")
153155
}
154156
} else if !is_wild_none && explicit_ref.is_none() {
155157
// TODO: handle explicit reference annotations.
156158
let pat_snip = snippet_with_context(cx, some_pat.span, expr_ctxt, "..", &mut app).0;
157-
if some_expr.needs_unsafe_block {
158-
format!("|{pat_snip}| unsafe {{ {closure_expr_snip} }}")
159-
} else {
160-
format!("|{pat_snip}| {closure_expr_snip}")
161-
}
159+
format!("|{pat_snip}| {closure_body}")
162160
} else {
163161
// Refutable bindings and mixed reference annotations can't be handled by `map`.
164162
return None;
@@ -274,3 +272,71 @@ pub(super) fn try_parse_pattern<'tcx>(
274272
fn is_none_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
275273
is_res_lang_ctor(cx, path_res(cx, peel_blocks(expr)), OptionNone)
276274
}
275+
276+
fn expr_ty_adjusted(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
277+
cx.typeck_results()
278+
.expr_adjustments(expr)
279+
.iter()
280+
// We do not care about exprs with `NeverToAny` adjustments, such as `panic!` call.
281+
.any(|adj| !matches!(adj.kind, Adjust::NeverToAny))
282+
}
283+
284+
fn expr_has_type_coercion<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
285+
if expr.span.from_expansion() {
286+
return false;
287+
}
288+
if expr_ty_adjusted(cx, expr) {
289+
return true;
290+
}
291+
292+
// Identify coercion sites and recursively check it those sites
293+
// actually has type adjustments.
294+
match expr.kind {
295+
// Function/method calls, including enum initialization.
296+
ExprKind::Call(_, args) | ExprKind::MethodCall(_, _, args, _) if let Some(def_id) = fn_def_id(cx, expr) => {
297+
let fn_sig = cx.tcx.fn_sig(def_id).instantiate_identity();
298+
if !fn_sig.output().skip_binder().has_type_flags(TypeFlags::HAS_TY_PARAM) {
299+
return false;
300+
}
301+
let mut args_with_ty_param = fn_sig
302+
.inputs()
303+
.skip_binder()
304+
.iter()
305+
.zip(args)
306+
.filter_map(|(arg_ty, arg)| if arg_ty.has_type_flags(TypeFlags::HAS_TY_PARAM) {
307+
Some(arg)
308+
} else {
309+
None
310+
});
311+
args_with_ty_param.any(|arg| expr_has_type_coercion(cx, arg))
312+
},
313+
// Struct/union initialization.
314+
ExprKind::Struct(_, fields, _) => {
315+
fields.iter().map(|expr_field| expr_field.expr).any(|ex| expr_has_type_coercion(cx, ex))
316+
},
317+
// those two `ref` keywords cannot be removed
318+
#[allow(clippy::needless_borrow)]
319+
// Function results, including the final line of a block or a `return` expression.
320+
ExprKind::Block(hir::Block { expr: Some(ref ret_expr), .. }, _) |
321+
ExprKind::Ret(Some(ref ret_expr)) => expr_has_type_coercion(cx, ret_expr),
322+
323+
// ===== Coercion-propagation expressions =====
324+
325+
// Array, where the type is `[U; n]`.
326+
ExprKind::Array(elems) |
327+
// Tuple, `(U_0, U_1, ..., U_n)`.
328+
ExprKind::Tup(elems) => {
329+
elems.iter().any(|elem| expr_has_type_coercion(cx, elem))
330+
},
331+
// Array but with repeating syntax.
332+
ExprKind::Repeat(rep_elem, _) => expr_has_type_coercion(cx, rep_elem),
333+
// Others that may contain coercion sites.
334+
ExprKind::If(_, then, maybe_else) => {
335+
expr_has_type_coercion(cx, then) || maybe_else.is_some_and(|e| expr_has_type_coercion(cx, e))
336+
}
337+
ExprKind::Match(_, arms, _) => {
338+
arms.iter().map(|arm| arm.body).any(|body| expr_has_type_coercion(cx, body))
339+
}
340+
_ => false
341+
}
342+
}

tests/ui/manual_map_option_2.fixed

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ fn main() {
4040
None => None,
4141
};
4242

43-
// Lint. `s` is captured by reference, so no lifetime issues.
4443
let s = Some(String::new());
44+
// Lint. `s` is captured by reference, so no lifetime issues.
4545
let _ = s.as_ref().map(|x| { if let Some(ref s) = s { (x.clone(), s) } else { panic!() } });
46+
// Don't lint this, type of `s` is coercioned from `&String` to `&str`
47+
let x: Option<(String, &str)> = match &s {
48+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
49+
None => None,
50+
};
4651

4752
// Issue #7820
4853
unsafe fn f(x: u32) -> u32 {
@@ -54,3 +59,89 @@ fn main() {
5459
let _ = Some(0).map(|x| unsafe { f(x) });
5560
let _ = Some(0).map(|x| unsafe { f(x) });
5661
}
62+
63+
// issue #12659
64+
mod with_type_coercion {
65+
trait DummyTrait {}
66+
67+
fn foo<T: DummyTrait, F: Fn() -> Result<T, ()>>(f: F) {
68+
// Don't lint
69+
let _: Option<Result<Box<dyn DummyTrait>, ()>> = match Some(0) {
70+
Some(_) => Some(match f() {
71+
Ok(res) => Ok(Box::new(res)),
72+
_ => Err(()),
73+
}),
74+
None => None,
75+
};
76+
77+
let _: Option<Box<&[u8]>> = match Some(()) {
78+
Some(_) => Some(Box::new(b"1234")),
79+
None => None,
80+
};
81+
82+
let x = String::new();
83+
let _: Option<Box<&str>> = match Some(()) {
84+
Some(_) => Some(Box::new(&x)),
85+
None => None,
86+
};
87+
88+
let _: Option<&str> = match Some(()) {
89+
Some(_) => Some(&x),
90+
None => None,
91+
};
92+
93+
//~v ERROR: manual implementation of `Option::map`
94+
let _ = Some(0).map(|_| match f() {
95+
Ok(res) => Ok(Box::new(res)),
96+
_ => Err(()),
97+
});
98+
}
99+
100+
#[allow(clippy::redundant_allocation)]
101+
fn bar() {
102+
fn f(_: Option<Box<&[u8]>>) {}
103+
fn g(b: &[u8]) -> Box<&[u8]> {
104+
Box::new(b)
105+
}
106+
107+
let x: &[u8; 4] = b"1234";
108+
f(match Some(()) {
109+
Some(_) => Some(Box::new(x)),
110+
None => None,
111+
});
112+
113+
//~v ERROR: manual implementation of `Option::map`
114+
let _: Option<Box<&[u8]>> = Some(0).map(|_| g(x));
115+
}
116+
117+
fn with_fn_ret(s: &Option<String>) -> Option<(String, &str)> {
118+
// Don't lint, `map` doesn't work as the return type is adjusted.
119+
match s {
120+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
121+
None => None,
122+
}
123+
}
124+
125+
fn with_fn_ret_2(s: &Option<String>) -> Option<(String, &str)> {
126+
if true {
127+
// Don't lint, `map` doesn't work as the return type is adjusted.
128+
return match s {
129+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
130+
None => None,
131+
};
132+
}
133+
None
134+
}
135+
136+
#[allow(clippy::needless_late_init)]
137+
fn with_fn_ret_3<'a>(s: &'a Option<String>) -> Option<(String, &'a str)> {
138+
let x: Option<(String, &'a str)>;
139+
x = {
140+
match s {
141+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
142+
None => None,
143+
}
144+
};
145+
x
146+
}
147+
}

tests/ui/manual_map_option_2.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@ fn main() {
4343
None => None,
4444
};
4545

46-
// Lint. `s` is captured by reference, so no lifetime issues.
4746
let s = Some(String::new());
47+
// Lint. `s` is captured by reference, so no lifetime issues.
4848
let _ = match &s {
4949
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
5050
None => None,
5151
};
52+
// Don't lint this, type of `s` is coercioned from `&String` to `&str`
53+
let x: Option<(String, &str)> = match &s {
54+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
55+
None => None,
56+
};
5257

5358
// Issue #7820
5459
unsafe fn f(x: u32) -> u32 {
@@ -69,3 +74,95 @@ fn main() {
6974
None => None,
7075
};
7176
}
77+
78+
// issue #12659
79+
mod with_type_coercion {
80+
trait DummyTrait {}
81+
82+
fn foo<T: DummyTrait, F: Fn() -> Result<T, ()>>(f: F) {
83+
// Don't lint
84+
let _: Option<Result<Box<dyn DummyTrait>, ()>> = match Some(0) {
85+
Some(_) => Some(match f() {
86+
Ok(res) => Ok(Box::new(res)),
87+
_ => Err(()),
88+
}),
89+
None => None,
90+
};
91+
92+
let _: Option<Box<&[u8]>> = match Some(()) {
93+
Some(_) => Some(Box::new(b"1234")),
94+
None => None,
95+
};
96+
97+
let x = String::new();
98+
let _: Option<Box<&str>> = match Some(()) {
99+
Some(_) => Some(Box::new(&x)),
100+
None => None,
101+
};
102+
103+
let _: Option<&str> = match Some(()) {
104+
Some(_) => Some(&x),
105+
None => None,
106+
};
107+
108+
//~v ERROR: manual implementation of `Option::map`
109+
let _ = match Some(0) {
110+
Some(_) => Some(match f() {
111+
Ok(res) => Ok(Box::new(res)),
112+
_ => Err(()),
113+
}),
114+
None => None,
115+
};
116+
}
117+
118+
#[allow(clippy::redundant_allocation)]
119+
fn bar() {
120+
fn f(_: Option<Box<&[u8]>>) {}
121+
fn g(b: &[u8]) -> Box<&[u8]> {
122+
Box::new(b)
123+
}
124+
125+
let x: &[u8; 4] = b"1234";
126+
f(match Some(()) {
127+
Some(_) => Some(Box::new(x)),
128+
None => None,
129+
});
130+
131+
//~v ERROR: manual implementation of `Option::map`
132+
let _: Option<Box<&[u8]>> = match Some(0) {
133+
Some(_) => Some(g(x)),
134+
None => None,
135+
};
136+
}
137+
138+
fn with_fn_ret(s: &Option<String>) -> Option<(String, &str)> {
139+
// Don't lint, `map` doesn't work as the return type is adjusted.
140+
match s {
141+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
142+
None => None,
143+
}
144+
}
145+
146+
fn with_fn_ret_2(s: &Option<String>) -> Option<(String, &str)> {
147+
if true {
148+
// Don't lint, `map` doesn't work as the return type is adjusted.
149+
return match s {
150+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
151+
None => None,
152+
};
153+
}
154+
None
155+
}
156+
157+
#[allow(clippy::needless_late_init)]
158+
fn with_fn_ret_3<'a>(s: &'a Option<String>) -> Option<(String, &'a str)> {
159+
let x: Option<(String, &'a str)>;
160+
x = {
161+
match s {
162+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
163+
None => None,
164+
}
165+
};
166+
x
167+
}
168+
}

0 commit comments

Comments
 (0)