diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d793489be2..f3557608620a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6318,6 +6318,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..c4b4399c29d9 --- /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 lint 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); + + // Only lint if exactly one side is pointer-sized + if from_is_ptr_sized == to_is_ptr_sized { + return; + } + + // Get the bit width of the fixed-size type + let (ptr_sized_ty, fixed_ty, fixed_bits, direction) = if from_is_ptr_sized { + let bits = fixed_type_bits(cast_to); + (cast_from, cast_to, bits, "to") + } else { + let bits = fixed_type_bits(cast_from); + (cast_to, cast_from, bits, "from") + }; + + let Some(fixed_bits) = fixed_bits else { + return; + }; + + // Check if this cast is always safe (no architecture dependency) + 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}`: the behavior depends on the target pointer width"); + + 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 { + // Sign changes are handled by cast_sign_loss, but we still need to consider + // whether the magnitude fits + + if from_is_ptr_sized { + // ptr-sized → fixed: safe if fixed type can hold any ptr-sized value + // usize/isize is at most 64 bits, so u64/i64 or larger is always safe + // But we need to consider sign: usize → i64 could overflow if usize > i64::MAX + if to_signed { + // usize → i64: not safe (usize could be > i64::MAX on 64-bit) + // isize → i64: safe (isize fits in i64) + // isize → i128: safe + from_signed && fixed_bits >= 64 + } else { + // usize → u64: safe (usize fits in u64) + // usize → u128: safe + // isize → u64: not safe (isize could be negative) + !from_signed && fixed_bits >= 64 + } + } else if from_signed == to_signed { + // fixed → ptr-sized: safe if fixed type fits in minimum ptr width (16 bits) + // u8, u16 → usize: always safe + // i8, i16 → isize: always safe + // Same signedness: safe if fits in 16-bit ptr + fixed_bits <= 16 + } else { + // Sign change: handled by cast_sign_loss, but from a size perspective: + // u16 → isize: safe (value fits, but could be interpreted differently) + // For this lint, we focus on size, not sign interpretation + fixed_bits <= 16 && !from_signed // only unsigned small → ptr-sized is truly safe + } +} diff --git a/clippy_lints/src/casts/mod.rs b/clippy_lints/src/casts/mod.rs index 7220a8a80066..e9efe4e1452b 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; @@ -814,6 +815,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.89.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 @@ -878,6 +915,7 @@ impl_lint_pass!(Casts => [ AS_POINTER_UNDERSCORE, MANUAL_DANGLING_PTR, CONFUSING_METHOD_TO_NUMERIC_CAST, + CAST_PTR_SIZED_INT, NEEDLESS_TYPE_CAST, ]); @@ -923,6 +961,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 6b68940c6423..c97064a35821 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..1956770cebc1 --- /dev/null +++ b/tests/ui/cast_ptr_sized_int.rs @@ -0,0 +1,89 @@ +//@no-rustfix +#![warn(clippy::cast_ptr_sized_int)] +#![allow(clippy::unnecessary_cast)] + +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..218563ff2993 --- /dev/null +++ b/tests/ui/cast_ptr_sized_int.stderr @@ -0,0 +1,229 @@ +error: casting `usize` to `u8`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:12: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:13: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:14: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:15: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:16: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:17: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:22: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:23: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:24: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:25: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:26: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:27: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:33: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:34: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:35: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:40: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:41: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:42: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:44: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:45: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:46: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:47: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:48: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:49: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`: the behavior depends on the target pointer width + --> tests/ui/cast_ptr_sized_int.rs:52: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 +