Skip to content

Commit b386427

Browse files
committed
fix [manual_map] not catching type adjustment
1 parent 06758d8 commit b386427

File tree

4 files changed

+297
-16
lines changed

4 files changed

+297
-16
lines changed

clippy_lints/src/matches/manual_utils.rs

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ use rustc_ast::util::parser::PREC_UNAMBIGUOUS;
1111
use rustc_errors::Applicability;
1212
use rustc_hir::def::Res;
1313
use rustc_hir::LangItem::{OptionNone, OptionSome};
14-
use rustc_hir::{BindingMode, Expr, ExprKind, HirId, Mutability, Pat, PatKind, Path, QPath};
14+
use rustc_hir::{self as hir, BindingMode, Expr, ExprKind, HirId, Mutability, Pat, PatKind, Path, QPath};
1515
use rustc_lint::LateContext;
16+
use rustc_middle::ty::adjustment::Adjust;
1617
use rustc_span::{sym, SyntaxContext};
1718

1819
#[expect(clippy::too_many_arguments)]
@@ -65,15 +66,7 @@ where
6566

6667
let some_expr = get_some_expr_fn(cx, some_pat, some_expr, expr_ctxt)?;
6768

68-
// These two lints will go back and forth with each other.
69-
if cx.typeck_results().expr_ty(some_expr.expr) == cx.tcx.types.unit
70-
&& !is_lint_allowed(cx, OPTION_MAP_UNIT_FN, expr.hir_id)
71-
{
72-
return None;
73-
}
74-
75-
// `map` won't perform any adjustments.
76-
if !cx.typeck_results().expr_adjustments(some_expr.expr).is_empty() {
69+
if !should_lint(cx, expr, some_expr.expr) {
7770
return None;
7871
}
7972

@@ -274,3 +267,79 @@ pub(super) fn try_parse_pattern<'tcx>(
274267
fn is_none_expr(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
275268
is_res_lang_ctor(cx, path_res(cx, peel_blocks(expr)), OptionNone)
276269
}
270+
271+
fn expr_ty_adjusted(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
272+
cx.typeck_results()
273+
.expr_adjustments(expr)
274+
.iter()
275+
// We do not care about exprs with `NeverToAny` adjustments, such as `panic!` call.
276+
.filter(|adj| !matches!(adj.kind, Adjust::NeverToAny))
277+
.next()
278+
.is_some()
279+
}
280+
281+
fn expr_has_type_coercion<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
282+
if expr.span.from_expansion() {
283+
return false;
284+
}
285+
if expr_ty_adjusted(cx, expr) {
286+
return true;
287+
}
288+
289+
// Identify coercion sites and recursively check it those sites
290+
// actually has type adjustments.
291+
match expr.kind {
292+
// Function/method calls, including enum initialization.
293+
ExprKind::Call(_, args) | ExprKind::MethodCall(_, _, args, _) => {
294+
args.iter().any(|arg| expr_has_type_coercion(cx, arg))
295+
},
296+
// Struct/union initialization.
297+
ExprKind::Struct(_, fields, _) => {
298+
fields.iter().map(|expr_field| expr_field.expr).any(|ex| expr_has_type_coercion(cx, ex))
299+
},
300+
// Function results, including the final line of a block or a `return` expression.
301+
ExprKind::Block(hir::Block { expr: Some(ref ret_expr), .. }, _) |
302+
ExprKind::Ret(Some(ref ret_expr)) => expr_has_type_coercion(cx, ret_expr),
303+
304+
// ===== Coercion-propagation expressions =====
305+
306+
// Array, where the type is `[U; n]`.
307+
ExprKind::Array(elems) |
308+
// Tuple, `(U_0, U_1, ..., U_n)`.
309+
ExprKind::Tup(elems) => {
310+
elems.iter().any(|elem| expr_has_type_coercion(cx, elem))
311+
},
312+
// Array but with repeating syntax.
313+
ExprKind::Repeat(rep_elem, _) => expr_has_type_coercion(cx, rep_elem),
314+
// Others that may contain coercion sites.
315+
ExprKind::If(_, then, maybe_else) => {
316+
expr_has_type_coercion(cx, then) || maybe_else.map(|e| expr_has_type_coercion(cx, e)).unwrap_or_default()
317+
}
318+
ExprKind::Match(_, arms, _) => {
319+
arms.iter().map(|arm| arm.body).any(|body| expr_has_type_coercion(cx, body))
320+
}
321+
_ => false
322+
}
323+
}
324+
325+
fn should_lint<'tcx>(cx: &LateContext<'tcx>, match_expr: &Expr<'tcx>, some_expr: &Expr<'tcx>) -> bool {
326+
let typeck = cx.typeck_results();
327+
let some_expr_ty = typeck.expr_ty(some_expr);
328+
329+
// These two lints will go back and forth with each other.
330+
if some_expr_ty.is_unit() && !is_lint_allowed(cx, OPTION_MAP_UNIT_FN, match_expr.hir_id) {
331+
return false;
332+
}
333+
334+
// `map` won't perform any adjustments.
335+
// However, if the ty of `Some` arg is unit, type coercions should be fine.
336+
if !some_expr_ty.is_unit()
337+
// TODO: this needs to be tested
338+
&& !some_expr_ty.is_scalar()
339+
&& expr_has_type_coercion(cx, match_expr)
340+
{
341+
return false;
342+
}
343+
344+
true
345+
}

tests/ui/manual_map_option_2.fixed

Lines changed: 95 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,92 @@ 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+
// FIXME: This is a known FN case.
114+
let _: Option<Box<&[u8]>> = match Some(0) {
115+
Some(_) => Some(g(x)),
116+
None => None,
117+
};
118+
}
119+
120+
fn with_fn_ret(s: &Option<String>) -> Option<(String, &str)> {
121+
// Don't lint, `map` doesn't work as the return type is adjusted.
122+
match s {
123+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
124+
None => None,
125+
}
126+
}
127+
128+
fn with_fn_ret_2(s: &Option<String>) -> Option<(String, &str)> {
129+
if true {
130+
// Don't lint, `map` doesn't work as the return type is adjusted.
131+
return match s {
132+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
133+
None => None,
134+
};
135+
}
136+
None
137+
}
138+
139+
#[allow(clippy::needless_late_init)]
140+
fn with_fn_ret_3<'a>(s: &'a Option<String>) -> Option<(String, &'a str)> {
141+
let x: Option<(String, &'a str)>;
142+
x = {
143+
match s {
144+
Some(x) => Some({ if let Some(ref s) = s { (x.clone(), s) } else { panic!() } }),
145+
None => None,
146+
}
147+
};
148+
x
149+
}
150+
}

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+
// FIXME: This is a known FN case.
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+
}

tests/ui/manual_map_option_2.stderr

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ LL | | };
3232
| |_____^ help: try: `s.as_ref().map(|x| { if let Some(ref s) = s { (x.clone(), s) } else { panic!() } })`
3333

3434
error: manual implementation of `Option::map`
35-
--> tests/ui/manual_map_option_2.rs:58:17
35+
--> tests/ui/manual_map_option_2.rs:63:17
3636
|
3737
LL | let _ = match Some(0) {
3838
| _________________^
@@ -42,7 +42,7 @@ LL | | };
4242
| |_________^ help: try: `Some(0).map(|x| f(x))`
4343

4444
error: manual implementation of `Option::map`
45-
--> tests/ui/manual_map_option_2.rs:63:13
45+
--> tests/ui/manual_map_option_2.rs:68:13
4646
|
4747
LL | let _ = match Some(0) {
4848
| _____________^
@@ -52,7 +52,7 @@ LL | | };
5252
| |_____^ help: try: `Some(0).map(|x| unsafe { f(x) })`
5353

5454
error: manual implementation of `Option::map`
55-
--> tests/ui/manual_map_option_2.rs:67:13
55+
--> tests/ui/manual_map_option_2.rs:72:13
5656
|
5757
LL | let _ = match Some(0) {
5858
| _____________^
@@ -61,5 +61,26 @@ LL | | None => None,
6161
LL | | };
6262
| |_____^ help: try: `Some(0).map(|x| unsafe { f(x) })`
6363

64-
error: aborting due to 5 previous errors
64+
error: manual implementation of `Option::map`
65+
--> tests/ui/manual_map_option_2.rs:109:17
66+
|
67+
LL | let _ = match Some(0) {
68+
| _________________^
69+
LL | | Some(_) => Some(match f() {
70+
LL | | Ok(res) => Ok(Box::new(res)),
71+
LL | | _ => Err(()),
72+
LL | | }),
73+
LL | | None => None,
74+
LL | | };
75+
| |_________^
76+
|
77+
help: try
78+
|
79+
LL ~ let _ = Some(0).map(|_| match f() {
80+
LL + Ok(res) => Ok(Box::new(res)),
81+
LL + _ => Err(()),
82+
LL ~ });
83+
|
84+
85+
error: aborting due to 6 previous errors
6586

0 commit comments

Comments
 (0)