From affde93041299c195649ca2c076bafb744b55c7e Mon Sep 17 00:00:00 2001 From: y21 <30553356+y21@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:38:20 +0100 Subject: [PATCH 1/2] lint on nested binary operations involving local and handle projections --- clippy_lints/src/transmute/eager_transmute.rs | 37 ++++++++++++-- tests/ui/eager_transmute.fixed | 22 +++++++- tests/ui/eager_transmute.rs | 22 +++++++- tests/ui/eager_transmute.stderr | 51 +++++++++++++++---- 4 files changed, 117 insertions(+), 15 deletions(-) diff --git a/clippy_lints/src/transmute/eager_transmute.rs b/clippy_lints/src/transmute/eager_transmute.rs index 01a23c515..7539b6af3 100644 --- a/clippy_lints/src/transmute/eager_transmute.rs +++ b/clippy_lints/src/transmute/eager_transmute.rs @@ -1,6 +1,6 @@ use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::ty::is_normalizable; -use clippy_utils::{path_to_local, path_to_local_id}; +use clippy_utils::{eq_expr_value, path_to_local}; use rustc_abi::WrappingRange; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind, Node}; @@ -25,6 +25,36 @@ fn range_fully_contained(from: WrappingRange, to: WrappingRange) -> bool { to.contains(from.start) && to.contains(from.end) } +/// Checks if a given expression is a binary operation involving a local variable or is made up of +/// other (nested) binary expressions involving the local. There must be at least one local +/// reference that is the same as `local_expr`. +/// +/// This is used as a heuristic to detect if a variable +/// is checked to be within the valid range of a transmuted type. +/// All of these would return true: +/// * `x < 4` +/// * `x < 4 && x > 1` +/// * `x.field < 4 && x.field > 1` (given `x.field`) +/// * `x.field < 4 && unrelated()` +fn binops_with_local(cx: &LateContext<'_>, local_expr: &Expr<'_>, expr: &Expr<'_>) -> bool { + match expr.kind { + ExprKind::Binary(_, lhs, rhs) => { + binops_with_local(cx, local_expr, lhs) || binops_with_local(cx, local_expr, rhs) + }, + _ => eq_expr_value(cx, local_expr, expr), + } +} + +/// Checks if an expression is a path to a local variable (with optional projections), e.g. +/// `x.field[0].field2` would return true. +fn is_local_with_projections(expr: &Expr<'_>) -> bool { + match expr.kind { + ExprKind::Path(_) => path_to_local(expr).is_some(), + ExprKind::Field(expr, _) | ExprKind::Index(expr, ..) => is_local_with_projections(expr), + _ => false, + } +} + pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, @@ -36,9 +66,8 @@ pub(super) fn check<'tcx>( && let ExprKind::MethodCall(path, receiver, [arg], _) = then_some_call.kind && cx.typeck_results().expr_ty(receiver).is_bool() && path.ident.name == sym!(then_some) - && let ExprKind::Binary(_, lhs, rhs) = receiver.kind - && let Some(local_id) = path_to_local(transmutable) - && (path_to_local_id(lhs, local_id) || path_to_local_id(rhs, local_id)) + && is_local_with_projections(transmutable) + && binops_with_local(cx, transmutable, receiver) && is_normalizable(cx, cx.param_env, from_ty) && is_normalizable(cx, cx.param_env, to_ty) // we only want to lint if the target type has a niche that is larger than the one of the source type diff --git a/tests/ui/eager_transmute.fixed b/tests/ui/eager_transmute.fixed index e06aa2cc9..58c6ee914 100644 --- a/tests/ui/eager_transmute.fixed +++ b/tests/ui/eager_transmute.fixed @@ -12,16 +12,36 @@ enum Opcode { Div = 3, } +struct Data { + foo: &'static [u8], + bar: &'static [u8], +} + fn int_to_opcode(op: u8) -> Option { (op < 4).then(|| unsafe { std::mem::transmute(op) }) } -fn f(op: u8, unrelated: u8) { +fn f(op: u8, op2: Data, unrelated: u8) { true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (op < 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); (op > 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); (op == 0).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); + + let _: Option = (op > 0 && op < 10).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = (op > 0 && op < 10 && unrelated == 0).then(|| unsafe { std::mem::transmute(op) }); + + // lint even when the transmutable goes through field/array accesses + let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then(|| unsafe { std::mem::transmute(op2.foo[0]) }); + + // don't lint: wrong index used in the transmute + let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[1]) }); + + // don't lint: no check for the transmutable in the condition + let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op2.bar[0]) }); + + // don't lint: wrong variable + let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) }); } unsafe fn f2(op: u8) { diff --git a/tests/ui/eager_transmute.rs b/tests/ui/eager_transmute.rs index 89ccdf583..2af311d6c 100644 --- a/tests/ui/eager_transmute.rs +++ b/tests/ui/eager_transmute.rs @@ -12,16 +12,36 @@ enum Opcode { Div = 3, } +struct Data { + foo: &'static [u8], + bar: &'static [u8], +} + fn int_to_opcode(op: u8) -> Option { (op < 4).then_some(unsafe { std::mem::transmute(op) }) } -fn f(op: u8, unrelated: u8) { +fn f(op: u8, op2: Data, unrelated: u8) { true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (op < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (op > 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); (op == 0).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); + + let _: Option = (op > 0 && op < 10).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = (op > 0 && op < 10 && unrelated == 0).then_some(unsafe { std::mem::transmute(op) }); + + // lint even when the transmutable goes through field/array accesses + let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[0]) }); + + // don't lint: wrong index used in the transmute + let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[1]) }); + + // don't lint: no check for the transmutable in the condition + let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op2.bar[0]) }); + + // don't lint: wrong variable + let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) }); } unsafe fn f2(op: u8) { diff --git a/tests/ui/eager_transmute.stderr b/tests/ui/eager_transmute.stderr index 5eb163c5f..65dd6815c 100644 --- a/tests/ui/eager_transmute.stderr +++ b/tests/ui/eager_transmute.stderr @@ -1,5 +1,5 @@ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:16:33 + --> $DIR/eager_transmute.rs:21:33 | LL | (op < 4).then_some(unsafe { std::mem::transmute(op) }) | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -12,7 +12,7 @@ LL | (op < 4).then(|| unsafe { std::mem::transmute(op) }) | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:22:33 + --> $DIR/eager_transmute.rs:27:33 | LL | (op < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -23,7 +23,7 @@ LL | (op < 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:23:33 + --> $DIR/eager_transmute.rs:28:33 | LL | (op > 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -34,7 +34,7 @@ LL | (op > 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:24:34 + --> $DIR/eager_transmute.rs:29:34 | LL | (op == 0).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -45,7 +45,40 @@ LL | (op == 0).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) }); | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:28:24 + --> $DIR/eager_transmute.rs:31:68 + | +LL | let _: Option = (op > 0 && op < 10).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (op > 0 && op < 10).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:32:86 + | +LL | let _: Option = (op > 0 && op < 10 && unrelated == 0).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (op > 0 && op < 10 && unrelated == 0).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:35:84 + | +LL | let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then_some(unsafe { std::mem::transmute(op2.foo[0]) }); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then(|| unsafe { std::mem::transmute(op2.foo[0]) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:48:24 | LL | (op < 4).then_some(std::mem::transmute::<_, Opcode>(op)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -56,7 +89,7 @@ LL | (op < 4).then(|| std::mem::transmute::<_, Opcode>(op)); | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:57:60 + --> $DIR/eager_transmute.rs:77:60 | LL | let _: Option = (v1 > 0).then_some(unsafe { std::mem::transmute(v1) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -67,7 +100,7 @@ LL | let _: Option = (v1 > 0).then(|| unsafe { std::mem::transmut | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:63:86 + --> $DIR/eager_transmute.rs:83:86 | LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -78,7 +111,7 @@ LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then(|| u | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:69:93 + --> $DIR/eager_transmute.rs:89:93 | LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -88,5 +121,5 @@ help: consider using `bool::then` to only transmute if the condition holds LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then(|| unsafe { std::mem::transmute(v2) }); | ~~~~ ++ -error: aborting due to 8 previous errors +error: aborting due to 11 previous errors From 6c28f0765ea6e7c819292e1ed800895aac9f00ae Mon Sep 17 00:00:00 2001 From: y21 <30553356+y21@users.noreply.github.com> Date: Thu, 28 Dec 2023 05:27:56 +0100 Subject: [PATCH 2/2] recognize `contains` calls on ranges --- clippy_lints/src/transmute/eager_transmute.rs | 16 ++++ tests/ui/eager_transmute.fixed | 13 ++++ tests/ui/eager_transmute.rs | 13 ++++ tests/ui/eager_transmute.stderr | 76 +++++++++++++++++-- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/clippy_lints/src/transmute/eager_transmute.rs b/clippy_lints/src/transmute/eager_transmute.rs index 7539b6af3..c44f5150d 100644 --- a/clippy_lints/src/transmute/eager_transmute.rs +++ b/clippy_lints/src/transmute/eager_transmute.rs @@ -36,11 +36,27 @@ fn range_fully_contained(from: WrappingRange, to: WrappingRange) -> bool { /// * `x < 4 && x > 1` /// * `x.field < 4 && x.field > 1` (given `x.field`) /// * `x.field < 4 && unrelated()` +/// * `(1..=3).contains(&x)` fn binops_with_local(cx: &LateContext<'_>, local_expr: &Expr<'_>, expr: &Expr<'_>) -> bool { match expr.kind { ExprKind::Binary(_, lhs, rhs) => { binops_with_local(cx, local_expr, lhs) || binops_with_local(cx, local_expr, rhs) }, + ExprKind::MethodCall(path, receiver, [arg], _) + if path.ident.name == sym!(contains) + // ... `contains` called on some kind of range + && let Some(receiver_adt) = cx.typeck_results().expr_ty(receiver).peel_refs().ty_adt_def() + && let lang_items = cx.tcx.lang_items() + && [ + lang_items.range_from_struct(), + lang_items.range_inclusive_struct(), + lang_items.range_struct(), + lang_items.range_to_inclusive_struct(), + lang_items.range_to_struct() + ].into_iter().any(|did| did == Some(receiver_adt.did())) => + { + eq_expr_value(cx, local_expr, arg.peel_borrows()) + }, _ => eq_expr_value(cx, local_expr, expr), } } diff --git a/tests/ui/eager_transmute.fixed b/tests/ui/eager_transmute.fixed index 58c6ee914..bece09bba 100644 --- a/tests/ui/eager_transmute.fixed +++ b/tests/ui/eager_transmute.fixed @@ -42,6 +42,19 @@ fn f(op: u8, op2: Data, unrelated: u8) { // don't lint: wrong variable let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) }); + + // range contains checks + let _: Option = (1..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = ((1..=3).contains(&op) || op == 4).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = (1..3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = (1..).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = (..3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + let _: Option = (..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + + // unrelated binding in contains + let _: Option = (1..=3) + .contains(&unrelated) + .then_some(unsafe { std::mem::transmute(op) }); } unsafe fn f2(op: u8) { diff --git a/tests/ui/eager_transmute.rs b/tests/ui/eager_transmute.rs index 2af311d6c..a82bd578f 100644 --- a/tests/ui/eager_transmute.rs +++ b/tests/ui/eager_transmute.rs @@ -42,6 +42,19 @@ fn f(op: u8, op2: Data, unrelated: u8) { // don't lint: wrong variable let _: Option = (op2.foo[0] > 0 && op2.bar[1] < 10).then_some(unsafe { std::mem::transmute(op) }); + + // range contains checks + let _: Option = (1..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = ((1..=3).contains(&op) || op == 4).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = (1..3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = (1..).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = (..3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + let _: Option = (..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + + // unrelated binding in contains + let _: Option = (1..=3) + .contains(&unrelated) + .then_some(unsafe { std::mem::transmute(op) }); } unsafe fn f2(op: u8) { diff --git a/tests/ui/eager_transmute.stderr b/tests/ui/eager_transmute.stderr index 65dd6815c..5f42eec54 100644 --- a/tests/ui/eager_transmute.stderr +++ b/tests/ui/eager_transmute.stderr @@ -78,7 +78,73 @@ LL | let _: Option = (op2.foo[0] > 0 && op2.foo[0] < 10).then(|| uns | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:48:24 + --> $DIR/eager_transmute.rs:47:70 + | +LL | let _: Option = (1..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (1..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:48:83 + | +LL | let _: Option = ((1..=3).contains(&op) || op == 4).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = ((1..=3).contains(&op) || op == 4).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:49:69 + | +LL | let _: Option = (1..3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (1..3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:50:68 + | +LL | let _: Option = (1..).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (1..).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:51:68 + | +LL | let _: Option = (..3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (..3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:52:69 + | +LL | let _: Option = (..=3).contains(&op).then_some(unsafe { std::mem::transmute(op) }); + | ^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider using `bool::then` to only transmute if the condition holds + | +LL | let _: Option = (..=3).contains(&op).then(|| unsafe { std::mem::transmute(op) }); + | ~~~~ ++ + +error: this transmute is always evaluated eagerly, even if the condition is false + --> $DIR/eager_transmute.rs:61:24 | LL | (op < 4).then_some(std::mem::transmute::<_, Opcode>(op)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -89,7 +155,7 @@ LL | (op < 4).then(|| std::mem::transmute::<_, Opcode>(op)); | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:77:60 + --> $DIR/eager_transmute.rs:90:60 | LL | let _: Option = (v1 > 0).then_some(unsafe { std::mem::transmute(v1) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -100,7 +166,7 @@ LL | let _: Option = (v1 > 0).then(|| unsafe { std::mem::transmut | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:83:86 + --> $DIR/eager_transmute.rs:96:86 | LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -111,7 +177,7 @@ LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then(|| u | ~~~~ ++ error: this transmute is always evaluated eagerly, even if the condition is false - --> $DIR/eager_transmute.rs:89:93 + --> $DIR/eager_transmute.rs:102:93 | LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) }); | ^^^^^^^^^^^^^^^^^^^^^^^ @@ -121,5 +187,5 @@ help: consider using `bool::then` to only transmute if the condition holds LL | let _: Option = (v2 < NonZeroU8::new(255).unwrap()).then(|| unsafe { std::mem::transmute(v2) }); | ~~~~ ++ -error: aborting due to 11 previous errors +error: aborting due to 17 previous errors