diff --git a/CHANGELOG.md b/CHANGELOG.md index 795eba1dfeaf..45272ce906e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6377,6 +6377,7 @@ Released 2018-09-13 [`cast_possible_wrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_wrap [`cast_precision_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_precision_loss [`cast_ptr_alignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_alignment +[`cast_ptr_sized_int`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_sized_int [`cast_ref_to_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ref_to_mut [`cast_sign_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_sign_loss [`cast_slice_different_sizes`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_slice_different_sizes diff --git a/clippy_lints/src/casts/cast_ptr_sized_int.rs b/clippy_lints/src/casts/cast_ptr_sized_int.rs new file mode 100644 index 000000000000..dfb4aca4aff0 --- /dev/null +++ b/clippy_lints/src/casts/cast_ptr_sized_int.rs @@ -0,0 +1,115 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::ty::is_isize_or_usize; +use rustc_hir::Expr; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; + +use super::CAST_PTR_SIZED_INT; + +/// Checks for casts between pointer-sized integer types (`usize`/`isize`) and +/// fixed-size integer types where the behavior depends on the target architecture. +/// +/// Some casts are always safe and are NOT linted: +/// - `u8`/`u16` → `usize`: always fits (usize is at least 16-bit) +/// - `i8`/`i16` → `isize`: always fits (isize is at least 16-bit) +/// - `usize` → `u64`/`u128`: always fits (usize is at most 64-bit) +/// - `isize` → `i64`/`i128`: always fits (isize is at most 64-bit) +pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>, cast_from: Ty<'tcx>, cast_to: Ty<'tcx>) { + // Only consider integer-to-integer casts. + if !cast_from.is_integral() || !cast_to.is_integral() { + return; + } + + let from_is_ptr_sized = is_isize_or_usize(cast_from); + let to_is_ptr_sized = is_isize_or_usize(cast_to); + + // We only care about casts where exactly one side is pointer-sized. + if from_is_ptr_sized == to_is_ptr_sized { + return; + } + + // Identify which side is the pointer-sized type and which is the fixed-size type. + let (ptr_sized_ty, fixed_ty, fixed_bits_opt, direction) = if from_is_ptr_sized { + (cast_from, cast_to, fixed_type_bits(cast_to), "to") + } else { + (cast_to, cast_from, fixed_type_bits(cast_from), "from") + }; + + let Some(fixed_bits) = fixed_bits_opt else { + // If the non-pointer side is not a fixed-size integer, bail out. + return; + }; + + // If this cast is always safe regardless of target pointer width, don't lint. + if is_always_safe_cast( + from_is_ptr_sized, + fixed_bits, + cast_from.is_signed(), + cast_to.is_signed(), + ) { + return; + } + + let msg = format!("casting `{cast_from}` to `{cast_to}`: will always truncate"); + + span_lint_and_then(cx, CAST_PTR_SIZED_INT, expr.span, msg, |diag| { + let help_msg = format!( + "`{ptr_sized_ty}` varies in size depending on the target, \ + so casting {direction} `{fixed_ty}` may produce different results across platforms" + ); + diag.help(help_msg); + diag.help("consider using `TryFrom` or `TryInto` for explicit fallible conversions"); + }); +} + +/// Returns the bit width of a fixed-size integer type, or None if not a fixed-size int. +fn fixed_type_bits(ty: Ty<'_>) -> Option { + match ty.kind() { + ty::Int(int_ty) => int_ty.bit_width(), + ty::Uint(uint_ty) => uint_ty.bit_width(), + _ => None, + } +} + +/// Determines if a cast between pointer-sized and fixed-size integers is always safe. +/// +/// Always safe casts (no architecture dependency): +/// - Small fixed → ptr-sized: u8/i8/u16/i16 → usize/isize (ptr-sized is at least 16-bit) +/// - Ptr-sized → large fixed: usize/isize → u64/i64/u128/i128 (ptr-sized is at most 64-bit) +/// +/// NOT safe (depends on architecture): +/// - Large fixed → ptr-sized: u32/u64/etc → usize (may truncate on smaller ptr widths) +/// - Ptr-sized → small fixed: usize → u8/u16/u32 (may truncate on larger ptr widths) +fn is_always_safe_cast(from_is_ptr_sized: bool, fixed_bits: u64, from_signed: bool, to_signed: bool) -> bool { + // Note: sign-change issues are handled by a separate lint (cast_sign_loss). Here we + // only reason about whether the numeric magnitude will always fit regardless of + // the target pointer width. + + if from_is_ptr_sized { + // Casting from pointer-sized -> fixed-size: + // - Pointer-sized integers (usize/isize) are at most 64 bits. + // - A fixed-size target with >= 64 bits can always hold the magnitude of a pointer-sized value, but + // we must respect signedness: + // * isize -> i64/i128 is safe (from_signed == true and to_signed && fixed_bits >= 64) + // * usize -> u64/u128 is safe (from_signed == false and !to_signed && fixed_bits >= 64) + if fixed_bits < 64 { + return false; + } + if to_signed { + // Target is signed: safe only if source is signed (isize -> i64) + from_signed && fixed_bits >= 64 + } else { + // Target is unsigned: safe only if source is unsigned (usize -> u64) + !from_signed && fixed_bits >= 64 + } + } else if from_signed == to_signed { + // Casting from fixed-size -> pointer-sized: + // - Pointer-sized integers are at least 16 bits. + // - Small fixed-size types (<= 16 bits) always fit in the smallest pointer width. + // Same signedness: small fixed types (<=16 bits) are always safe. + fixed_bits <= 16 + } else { + // Sign change: only the case unsigned small -> signed ptr is considered safe here. + fixed_bits <= 16 && !from_signed + } +} diff --git a/clippy_lints/src/casts/mod.rs b/clippy_lints/src/casts/mod.rs index 3c9ebef73f0d..8af5b21abd6b 100644 --- a/clippy_lints/src/casts/mod.rs +++ b/clippy_lints/src/casts/mod.rs @@ -10,6 +10,7 @@ mod cast_possible_truncation; mod cast_possible_wrap; mod cast_precision_loss; mod cast_ptr_alignment; +mod cast_ptr_sized_int; mod cast_sign_loss; mod cast_slice_different_sizes; mod cast_slice_from_raw_parts; @@ -820,6 +821,42 @@ declare_clippy_lint! { "casting a primitive method pointer to any integer type" } +declare_clippy_lint! { + /// ### What it does + /// Checks for casts between pointer-sized integer types (`usize`/`isize`) + /// and fixed-size integer types (like `u64`, `i32`, etc.). + /// + /// ### Why is this bad? + /// `usize` and `isize` have sizes that depend on the target architecture + /// (32-bit on 32-bit platforms, 64-bit on 64-bit platforms). Casting between + /// these and fixed-size integers can lead to subtle, platform-specific bugs: + /// + /// - `usize as u64`: On 64-bit platforms this is lossless, but on 32-bit + /// platforms the upper 32 bits are always zero. + /// - `u64 as usize`: On 32-bit platforms this truncates, but on 64-bit + /// platforms it's lossless. + /// + /// Using `TryFrom`/`TryInto` makes the potential for failure explicit. + /// + /// ### Example + /// ```no_run + /// pub fn foo(x: usize) -> u64 { + /// x as u64 + /// } + /// ``` + /// + /// Use instead: + /// ```no_run + /// pub fn foo(x: usize) -> u64 { + /// u64::try_from(x).expect("usize should fit in u64") + /// } + /// ``` + #[clippy::version = "1.95.0"] + pub CAST_PTR_SIZED_INT, + restriction, + "casts between pointer-sized and fixed-size integer types may behave differently across platforms" +} + declare_clippy_lint! { /// ### What it does /// Checks for bindings (constants, statics, or let bindings) that are defined @@ -884,6 +921,7 @@ impl_lint_pass!(Casts => [ AS_POINTER_UNDERSCORE, MANUAL_DANGLING_PTR, CONFUSING_METHOD_TO_NUMERIC_CAST, + CAST_PTR_SIZED_INT, NEEDLESS_TYPE_CAST, ]); @@ -929,6 +967,7 @@ impl<'tcx> LateLintPass<'tcx> for Casts { cast_sign_loss::check(cx, expr, cast_from_expr, cast_from, cast_to, self.msrv); cast_abs_to_unsigned::check(cx, expr, cast_from_expr, cast_from, cast_to, self.msrv); cast_nan_to_int::check(cx, expr, cast_from_expr, cast_from, cast_to); + cast_ptr_sized_int::check(cx, expr, cast_from, cast_to); } cast_lossless::check(cx, expr, cast_from_expr, cast_from, cast_to, cast_to_hir, self.msrv); cast_enum_constructor::check(cx, expr, cast_from_expr, cast_from); diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index a04d133b0d72..de082ba49d6f 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -61,6 +61,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::casts::CAST_POSSIBLE_WRAP_INFO, crate::casts::CAST_PRECISION_LOSS_INFO, crate::casts::CAST_PTR_ALIGNMENT_INFO, + crate::casts::CAST_PTR_SIZED_INT_INFO, crate::casts::CAST_SIGN_LOSS_INFO, crate::casts::CAST_SLICE_DIFFERENT_SIZES_INFO, crate::casts::CAST_SLICE_FROM_RAW_PARTS_INFO, diff --git a/tests/ui/cast_ptr_sized_int.rs b/tests/ui/cast_ptr_sized_int.rs new file mode 100644 index 000000000000..9568b9df6354 --- /dev/null +++ b/tests/ui/cast_ptr_sized_int.rs @@ -0,0 +1,87 @@ +#![warn(clippy::cast_ptr_sized_int)] + +fn main() { + // Architecture-dependent behavior + + let x: usize = 42; + + // usize to small fixed-size (may truncate on larger ptr widths) + let _ = x as u8; //~ cast_ptr_sized_int + let _ = x as u16; //~ cast_ptr_sized_int + let _ = x as u32; //~ cast_ptr_sized_int + let _ = x as i8; //~ cast_ptr_sized_int + let _ = x as i16; //~ cast_ptr_sized_int + let _ = x as i32; //~ cast_ptr_sized_int + + let y: isize = 42; + + // isize to small fixed-size (may truncate on larger ptr widths) + let _ = y as u8; //~ cast_ptr_sized_int + let _ = y as u16; //~ cast_ptr_sized_int + let _ = y as u32; //~ cast_ptr_sized_int + let _ = y as i8; //~ cast_ptr_sized_int + let _ = y as i16; //~ cast_ptr_sized_int + let _ = y as i32; //~ cast_ptr_sized_int + + // Large fixed-size to ptr-sized (may truncate on smaller ptr widths) + let c: u32 = 1; + let d: u64 = 1; + let e: u128 = 1; + let _ = c as usize; //~ cast_ptr_sized_int + let _ = d as usize; //~ cast_ptr_sized_int + let _ = e as usize; //~ cast_ptr_sized_int + + let h: i32 = 1; + let i: i64 = 1; + let j: i128 = 1; + let _ = h as usize; //~ cast_ptr_sized_int + let _ = i as usize; //~ cast_ptr_sized_int + let _ = j as usize; //~ cast_ptr_sized_int + + let _ = c as isize; //~ cast_ptr_sized_int + let _ = d as isize; //~ cast_ptr_sized_int + let _ = e as isize; //~ cast_ptr_sized_int + let _ = h as isize; //~ cast_ptr_sized_int + let _ = i as isize; //~ cast_ptr_sized_int + let _ = j as isize; //~ cast_ptr_sized_int + + // usize to signed (potential sign issues) + let _ = x as i64; //~ cast_ptr_sized_int +} + +// Always safe, no architecture dependency + +fn no_lint_always_safe() { + // Small fixed → ptr-sized: always safe (ptr-sized is at least 16-bit) + let a: u8 = 1; + let b: u16 = 1; + let _ = a as usize; // OK: u8 fits in any usize + let _ = b as usize; // OK: u16 fits in any usize + + let f: i8 = 1; + let g: i16 = 1; + let _ = f as isize; // OK: i8 fits in any isize + let _ = g as isize; // OK: i16 fits in any isize + + // Ptr-sized → large fixed: always safe (ptr-sized is at most 64-bit) + let x: usize = 42; + let y: isize = 42; + let _ = x as u64; // OK: usize fits in u64 + let _ = x as u128; // OK: usize fits in u128 + let _ = y as i64; // OK: isize fits in i64 + let _ = y as i128; // OK: isize fits in i128 +} + +fn no_lint_same_kind() { + // Both pointer-sized (handled by other lints) + let x: usize = 42; + let _ = x as isize; + + let y: isize = 42; + let _ = y as usize; + + // Both fixed-size (handled by other lints) + let a: u32 = 1; + let _ = a as u64; + let _ = a as i64; +} diff --git a/tests/ui/cast_ptr_sized_int.stderr b/tests/ui/cast_ptr_sized_int.stderr new file mode 100644 index 000000000000..75e4e7954041 --- /dev/null +++ b/tests/ui/cast_ptr_sized_int.stderr @@ -0,0 +1,229 @@ +error: casting `usize` to `u8`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:9:13 + | +LL | let _ = x as u8; + | ^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `u8` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + = note: `-D clippy::cast-ptr-sized-int` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::cast_ptr_sized_int)]` + +error: casting `usize` to `u16`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:10:13 + | +LL | let _ = x as u16; + | ^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `u16` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `usize` to `u32`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:11:13 + | +LL | let _ = x as u32; + | ^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `u32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `usize` to `i8`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:12:13 + | +LL | let _ = x as i8; + | ^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `i8` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `usize` to `i16`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:13:13 + | +LL | let _ = x as i16; + | ^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `i16` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `usize` to `i32`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:14:13 + | +LL | let _ = x as i32; + | ^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `i32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `u8`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:19:13 + | +LL | let _ = y as u8; + | ^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `u8` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `u16`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:20:13 + | +LL | let _ = y as u16; + | ^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `u16` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `u32`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:21:13 + | +LL | let _ = y as u32; + | ^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `u32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `i8`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:22:13 + | +LL | let _ = y as i8; + | ^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `i8` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `i16`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:23:13 + | +LL | let _ = y as i16; + | ^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `i16` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `isize` to `i32`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:24:13 + | +LL | let _ = y as i32; + | ^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting to `i32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u32` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:30:13 + | +LL | let _ = c as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `u32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u64` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:31:13 + | +LL | let _ = d as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `u64` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u128` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:32:13 + | +LL | let _ = e as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `u128` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i32` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:37:13 + | +LL | let _ = h as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `i32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i64` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:38:13 + | +LL | let _ = i as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `i64` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i128` to `usize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:39:13 + | +LL | let _ = j as usize; + | ^^^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting from `i128` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u32` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:41:13 + | +LL | let _ = c as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `u32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u64` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:42:13 + | +LL | let _ = d as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `u64` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `u128` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:43:13 + | +LL | let _ = e as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `u128` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i32` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:44:13 + | +LL | let _ = h as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `i32` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i64` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:45:13 + | +LL | let _ = i as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `i64` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `i128` to `isize`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:46:13 + | +LL | let _ = j as isize; + | ^^^^^^^^^^ + | + = help: `isize` varies in size depending on the target, so casting from `i128` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: casting `usize` to `i64`: will always truncate + --> tests/ui/cast_ptr_sized_int.rs:49:13 + | +LL | let _ = x as i64; + | ^^^^^^^^ + | + = help: `usize` varies in size depending on the target, so casting to `i64` may produce different results across platforms + = help: consider using `TryFrom` or `TryInto` for explicit fallible conversions + +error: aborting due to 25 previous errors +