use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::ty::is_normalizable; use clippy_utils::{eq_expr_value, path_to_local}; use rustc_abi::WrappingRange; use rustc_errors::Applicability; use rustc_hir::{Expr, ExprKind, Node}; use rustc_lint::LateContext; use rustc_middle::ty::Ty; use super::EAGER_TRANSMUTE; fn peel_parent_unsafe_blocks<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { for (_, parent) in cx.tcx.hir().parent_iter(expr.hir_id) { match parent { Node::Block(_) => {}, Node::Expr(e) if let ExprKind::Block(..) = e.kind => {}, Node::Expr(e) => return Some(e), _ => break, } } None } 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()` /// * `(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), } } /// 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>, transmutable: &'tcx Expr<'tcx>, from_ty: Ty<'tcx>, to_ty: Ty<'tcx>, ) -> bool { if let Some(then_some_call) = peel_parent_unsafe_blocks(cx, expr) && let ExprKind::MethodCall(path, receiver, [arg], _) = then_some_call.kind && cx.typeck_results().expr_ty(receiver).is_bool() && path.ident.name == sym!(then_some) && 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 // e.g. `u8` to `NonZeroU8` should lint, but `NonZeroU8` to `u8` should not && let Ok(from_layout) = cx.tcx.layout_of(cx.param_env.and(from_ty)) && let Ok(to_layout) = cx.tcx.layout_of(cx.param_env.and(to_ty)) && match (from_layout.largest_niche, to_layout.largest_niche) { (Some(from_niche), Some(to_niche)) => !range_fully_contained(from_niche.valid_range, to_niche.valid_range), (None, Some(_)) => true, (_, None) => false, } { span_lint_and_then( cx, EAGER_TRANSMUTE, expr.span, "this transmute is always evaluated eagerly, even if the condition is false", |diag| { diag.multipart_suggestion( "consider using `bool::then` to only transmute if the condition holds", vec![ (path.ident.span, "then".into()), (arg.span.shrink_to_lo(), "|| ".into()), ], Applicability::MaybeIncorrect, ); }, ); true } else { false } }