Skip to content

Commit ebf5c1a

Browse files
committed
Auto merge of #12031 - y21:eager_transmute_more_binops, r=llogiq
Lint nested binary operations and handle field projections in `eager_transmute` This PR makes the lint a bit stronger. Previously it would only lint `(x < 4).then_some(transmute(x))` (that is, a single binary op in the condition). With this change, it understands: - multiple, nested binary ops: `(x < 4 && x > 1).then_some(...)` - local references with projections: `(x.field < 4 && x.field > 1).then_some(transmute(x.field))` changelog: [`eager_transmute`]: lint nested binary operations and look through field/array accesses r? llogiq (since you reviewed my initial PR #11981, I figured you have the most context here, sorry if you are too busy with other PRs, feel free to reassign to someone else then)
2 parents 17dcd0d + 6c28f07 commit ebf5c1a

File tree

4 files changed

+225
-15
lines changed

4 files changed

+225
-15
lines changed

clippy_lints/src/transmute/eager_transmute.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::ty::is_normalizable;
3-
use clippy_utils::{path_to_local, path_to_local_id};
3+
use clippy_utils::{eq_expr_value, path_to_local};
44
use rustc_abi::WrappingRange;
55
use rustc_errors::Applicability;
66
use rustc_hir::{Expr, ExprKind, Node};
@@ -25,6 +25,52 @@ fn range_fully_contained(from: WrappingRange, to: WrappingRange) -> bool {
2525
to.contains(from.start) && to.contains(from.end)
2626
}
2727

28+
/// Checks if a given expression is a binary operation involving a local variable or is made up of
29+
/// other (nested) binary expressions involving the local. There must be at least one local
30+
/// reference that is the same as `local_expr`.
31+
///
32+
/// This is used as a heuristic to detect if a variable
33+
/// is checked to be within the valid range of a transmuted type.
34+
/// All of these would return true:
35+
/// * `x < 4`
36+
/// * `x < 4 && x > 1`
37+
/// * `x.field < 4 && x.field > 1` (given `x.field`)
38+
/// * `x.field < 4 && unrelated()`
39+
/// * `(1..=3).contains(&x)`
40+
fn binops_with_local(cx: &LateContext<'_>, local_expr: &Expr<'_>, expr: &Expr<'_>) -> bool {
41+
match expr.kind {
42+
ExprKind::Binary(_, lhs, rhs) => {
43+
binops_with_local(cx, local_expr, lhs) || binops_with_local(cx, local_expr, rhs)
44+
},
45+
ExprKind::MethodCall(path, receiver, [arg], _)
46+
if path.ident.name == sym!(contains)
47+
// ... `contains` called on some kind of range
48+
&& let Some(receiver_adt) = cx.typeck_results().expr_ty(receiver).peel_refs().ty_adt_def()
49+
&& let lang_items = cx.tcx.lang_items()
50+
&& [
51+
lang_items.range_from_struct(),
52+
lang_items.range_inclusive_struct(),
53+
lang_items.range_struct(),
54+
lang_items.range_to_inclusive_struct(),
55+
lang_items.range_to_struct()
56+
].into_iter().any(|did| did == Some(receiver_adt.did())) =>
57+
{
58+
eq_expr_value(cx, local_expr, arg.peel_borrows())
59+
},
60+
_ => eq_expr_value(cx, local_expr, expr),
61+
}
62+
}
63+
64+
/// Checks if an expression is a path to a local variable (with optional projections), e.g.
65+
/// `x.field[0].field2` would return true.
66+
fn is_local_with_projections(expr: &Expr<'_>) -> bool {
67+
match expr.kind {
68+
ExprKind::Path(_) => path_to_local(expr).is_some(),
69+
ExprKind::Field(expr, _) | ExprKind::Index(expr, ..) => is_local_with_projections(expr),
70+
_ => false,
71+
}
72+
}
73+
2874
pub(super) fn check<'tcx>(
2975
cx: &LateContext<'tcx>,
3076
expr: &'tcx Expr<'tcx>,
@@ -36,9 +82,8 @@ pub(super) fn check<'tcx>(
3682
&& let ExprKind::MethodCall(path, receiver, [arg], _) = then_some_call.kind
3783
&& cx.typeck_results().expr_ty(receiver).is_bool()
3884
&& path.ident.name == sym!(then_some)
39-
&& let ExprKind::Binary(_, lhs, rhs) = receiver.kind
40-
&& let Some(local_id) = path_to_local(transmutable)
41-
&& (path_to_local_id(lhs, local_id) || path_to_local_id(rhs, local_id))
85+
&& is_local_with_projections(transmutable)
86+
&& binops_with_local(cx, transmutable, receiver)
4287
&& is_normalizable(cx, cx.param_env, from_ty)
4388
&& is_normalizable(cx, cx.param_env, to_ty)
4489
// we only want to lint if the target type has a niche that is larger than the one of the source type

tests/ui/eager_transmute.fixed

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,49 @@ enum Opcode {
1212
Div = 3,
1313
}
1414

15+
struct Data {
16+
foo: &'static [u8],
17+
bar: &'static [u8],
18+
}
19+
1520
fn int_to_opcode(op: u8) -> Option<Opcode> {
1621
(op < 4).then(|| unsafe { std::mem::transmute(op) })
1722
}
1823

19-
fn f(op: u8, unrelated: u8) {
24+
fn f(op: u8, op2: Data, unrelated: u8) {
2025
true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2126
(unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2227
(op < 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
2328
(op > 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
2429
(op == 0).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
30+
31+
let _: Option<Opcode> = (op > 0 && op < 10).then(|| unsafe { std::mem::transmute(op) });
32+
let _: Option<Opcode> = (op > 0 && op < 10 && unrelated == 0).then(|| unsafe { std::mem::transmute(op) });
33+
34+
// lint even when the transmutable goes through field/array accesses
35+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then(|| unsafe { std::mem::transmute(op2.foo[0]) });
36+
37+
// don't lint: wrong index used in the transmute
38+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[1]) });
39+
40+
// don't lint: no check for the transmutable in the condition
41+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op2.bar[0]) });
42+
43+
// don't lint: wrong variable
44+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) });
45+
46+
// range contains checks
47+
let _: Option<Opcode> = (1..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
48+
let _: Option<Opcode> = ((1..=3).contains(&op) || op == 4).then(|| unsafe { std::mem::transmute(op) });
49+
let _: Option<Opcode> = (1..3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
50+
let _: Option<Opcode> = (1..).contains(&op).then(|| unsafe { std::mem::transmute(op) });
51+
let _: Option<Opcode> = (..3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
52+
let _: Option<Opcode> = (..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
53+
54+
// unrelated binding in contains
55+
let _: Option<Opcode> = (1..=3)
56+
.contains(&unrelated)
57+
.then_some(unsafe { std::mem::transmute(op) });
2558
}
2659

2760
unsafe fn f2(op: u8) {

tests/ui/eager_transmute.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,49 @@ enum Opcode {
1212
Div = 3,
1313
}
1414

15+
struct Data {
16+
foo: &'static [u8],
17+
bar: &'static [u8],
18+
}
19+
1520
fn int_to_opcode(op: u8) -> Option<Opcode> {
1621
(op < 4).then_some(unsafe { std::mem::transmute(op) })
1722
}
1823

19-
fn f(op: u8, unrelated: u8) {
24+
fn f(op: u8, op2: Data, unrelated: u8) {
2025
true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2126
(unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2227
(op < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2328
(op > 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2429
(op == 0).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
30+
31+
let _: Option<Opcode> = (op > 0 && op < 10).then_some(unsafe { std::mem::transmute(op) });
32+
let _: Option<Opcode> = (op > 0 && op < 10 && unrelated == 0).then_some(unsafe { std::mem::transmute(op) });
33+
34+
// lint even when the transmutable goes through field/array accesses
35+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[0]) });
36+
37+
// don't lint: wrong index used in the transmute
38+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[1]) });
39+
40+
// don't lint: no check for the transmutable in the condition
41+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op2.bar[0]) });
42+
43+
// don't lint: wrong variable
44+
let _: Option<Opcode> = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) });
45+
46+
// range contains checks
47+
let _: Option<Opcode> = (1..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
48+
let _: Option<Opcode> = ((1..=3).contains(&op) || op == 4).then_some(unsafe { std::mem::transmute(op) });
49+
let _: Option<Opcode> = (1..3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
50+
let _: Option<Opcode> = (1..).contains(&op).then_some(unsafe { std::mem::transmute(op) });
51+
let _: Option<Opcode> = (..3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
52+
let _: Option<Opcode> = (..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
53+
54+
// unrelated binding in contains
55+
let _: Option<Opcode> = (1..=3)
56+
.contains(&unrelated)
57+
.then_some(unsafe { std::mem::transmute(op) });
2558
}
2659

2760
unsafe fn f2(op: u8) {

tests/ui/eager_transmute.stderr

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: this transmute is always evaluated eagerly, even if the condition is false
2-
--> $DIR/eager_transmute.rs:16:33
2+
--> $DIR/eager_transmute.rs:21:33
33
|
44
LL | (op < 4).then_some(unsafe { std::mem::transmute(op) })
55
| ^^^^^^^^^^^^^^^^^^^^^^^
@@ -12,7 +12,7 @@ LL | (op < 4).then(|| unsafe { std::mem::transmute(op) })
1212
| ~~~~ ++
1313

1414
error: this transmute is always evaluated eagerly, even if the condition is false
15-
--> $DIR/eager_transmute.rs:22:33
15+
--> $DIR/eager_transmute.rs:27:33
1616
|
1717
LL | (op < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
1818
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -23,7 +23,7 @@ LL | (op < 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
2323
| ~~~~ ++
2424

2525
error: this transmute is always evaluated eagerly, even if the condition is false
26-
--> $DIR/eager_transmute.rs:23:33
26+
--> $DIR/eager_transmute.rs:28:33
2727
|
2828
LL | (op > 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
2929
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -34,7 +34,7 @@ LL | (op > 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
3434
| ~~~~ ++
3535

3636
error: this transmute is always evaluated eagerly, even if the condition is false
37-
--> $DIR/eager_transmute.rs:24:34
37+
--> $DIR/eager_transmute.rs:29:34
3838
|
3939
LL | (op == 0).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
4040
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -45,7 +45,106 @@ LL | (op == 0).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
4545
| ~~~~ ++
4646

4747
error: this transmute is always evaluated eagerly, even if the condition is false
48-
--> $DIR/eager_transmute.rs:28:24
48+
--> $DIR/eager_transmute.rs:31:68
49+
|
50+
LL | let _: Option<Opcode> = (op > 0 && op < 10).then_some(unsafe { std::mem::transmute(op) });
51+
| ^^^^^^^^^^^^^^^^^^^^^^^
52+
|
53+
help: consider using `bool::then` to only transmute if the condition holds
54+
|
55+
LL | let _: Option<Opcode> = (op > 0 && op < 10).then(|| unsafe { std::mem::transmute(op) });
56+
| ~~~~ ++
57+
58+
error: this transmute is always evaluated eagerly, even if the condition is false
59+
--> $DIR/eager_transmute.rs:32:86
60+
|
61+
LL | let _: Option<Opcode> = (op > 0 && op < 10 && unrelated == 0).then_some(unsafe { std::mem::transmute(op) });
62+
| ^^^^^^^^^^^^^^^^^^^^^^^
63+
|
64+
help: consider using `bool::then` to only transmute if the condition holds
65+
|
66+
LL | let _: Option<Opcode> = (op > 0 && op < 10 && unrelated == 0).then(|| unsafe { std::mem::transmute(op) });
67+
| ~~~~ ++
68+
69+
error: this transmute is always evaluated eagerly, even if the condition is false
70+
--> $DIR/eager_transmute.rs:35:84
71+
|
72+
LL | let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[0]) });
73+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74+
|
75+
help: consider using `bool::then` to only transmute if the condition holds
76+
|
77+
LL | let _: Option<Opcode> = (op2.foo[0] > 0 && op2.foo[0] < 10).then(|| unsafe { std::mem::transmute(op2.foo[0]) });
78+
| ~~~~ ++
79+
80+
error: this transmute is always evaluated eagerly, even if the condition is false
81+
--> $DIR/eager_transmute.rs:47:70
82+
|
83+
LL | let _: Option<Opcode> = (1..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
84+
| ^^^^^^^^^^^^^^^^^^^^^^^
85+
|
86+
help: consider using `bool::then` to only transmute if the condition holds
87+
|
88+
LL | let _: Option<Opcode> = (1..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
89+
| ~~~~ ++
90+
91+
error: this transmute is always evaluated eagerly, even if the condition is false
92+
--> $DIR/eager_transmute.rs:48:83
93+
|
94+
LL | let _: Option<Opcode> = ((1..=3).contains(&op) || op == 4).then_some(unsafe { std::mem::transmute(op) });
95+
| ^^^^^^^^^^^^^^^^^^^^^^^
96+
|
97+
help: consider using `bool::then` to only transmute if the condition holds
98+
|
99+
LL | let _: Option<Opcode> = ((1..=3).contains(&op) || op == 4).then(|| unsafe { std::mem::transmute(op) });
100+
| ~~~~ ++
101+
102+
error: this transmute is always evaluated eagerly, even if the condition is false
103+
--> $DIR/eager_transmute.rs:49:69
104+
|
105+
LL | let _: Option<Opcode> = (1..3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
106+
| ^^^^^^^^^^^^^^^^^^^^^^^
107+
|
108+
help: consider using `bool::then` to only transmute if the condition holds
109+
|
110+
LL | let _: Option<Opcode> = (1..3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
111+
| ~~~~ ++
112+
113+
error: this transmute is always evaluated eagerly, even if the condition is false
114+
--> $DIR/eager_transmute.rs:50:68
115+
|
116+
LL | let _: Option<Opcode> = (1..).contains(&op).then_some(unsafe { std::mem::transmute(op) });
117+
| ^^^^^^^^^^^^^^^^^^^^^^^
118+
|
119+
help: consider using `bool::then` to only transmute if the condition holds
120+
|
121+
LL | let _: Option<Opcode> = (1..).contains(&op).then(|| unsafe { std::mem::transmute(op) });
122+
| ~~~~ ++
123+
124+
error: this transmute is always evaluated eagerly, even if the condition is false
125+
--> $DIR/eager_transmute.rs:51:68
126+
|
127+
LL | let _: Option<Opcode> = (..3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
128+
| ^^^^^^^^^^^^^^^^^^^^^^^
129+
|
130+
help: consider using `bool::then` to only transmute if the condition holds
131+
|
132+
LL | let _: Option<Opcode> = (..3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
133+
| ~~~~ ++
134+
135+
error: this transmute is always evaluated eagerly, even if the condition is false
136+
--> $DIR/eager_transmute.rs:52:69
137+
|
138+
LL | let _: Option<Opcode> = (..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) });
139+
| ^^^^^^^^^^^^^^^^^^^^^^^
140+
|
141+
help: consider using `bool::then` to only transmute if the condition holds
142+
|
143+
LL | let _: Option<Opcode> = (..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) });
144+
| ~~~~ ++
145+
146+
error: this transmute is always evaluated eagerly, even if the condition is false
147+
--> $DIR/eager_transmute.rs:61:24
49148
|
50149
LL | (op < 4).then_some(std::mem::transmute::<_, Opcode>(op));
51150
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -56,7 +155,7 @@ LL | (op < 4).then(|| std::mem::transmute::<_, Opcode>(op));
56155
| ~~~~ ++
57156

58157
error: this transmute is always evaluated eagerly, even if the condition is false
59-
--> $DIR/eager_transmute.rs:57:60
158+
--> $DIR/eager_transmute.rs:90:60
60159
|
61160
LL | let _: Option<NonZeroU8> = (v1 > 0).then_some(unsafe { std::mem::transmute(v1) });
62161
| ^^^^^^^^^^^^^^^^^^^^^^^
@@ -67,7 +166,7 @@ LL | let _: Option<NonZeroU8> = (v1 > 0).then(|| unsafe { std::mem::transmut
67166
| ~~~~ ++
68167

69168
error: this transmute is always evaluated eagerly, even if the condition is false
70-
--> $DIR/eager_transmute.rs:63:86
169+
--> $DIR/eager_transmute.rs:96:86
71170
|
72171
LL | let _: Option<NonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
73172
| ^^^^^^^^^^^^^^^^^^^^^^^
@@ -78,7 +177,7 @@ LL | let _: Option<NonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then(|| u
78177
| ~~~~ ++
79178

80179
error: this transmute is always evaluated eagerly, even if the condition is false
81-
--> $DIR/eager_transmute.rs:69:93
180+
--> $DIR/eager_transmute.rs:102:93
82181
|
83182
LL | let _: Option<NonZeroNonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
84183
| ^^^^^^^^^^^^^^^^^^^^^^^
@@ -88,5 +187,5 @@ help: consider using `bool::then` to only transmute if the condition holds
88187
LL | let _: Option<NonZeroNonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then(|| unsafe { std::mem::transmute(v2) });
89188
| ~~~~ ++
90189

91-
error: aborting due to 8 previous errors
190+
error: aborting due to 17 previous errors
92191

0 commit comments

Comments
 (0)