Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions clippy_lints/src/casts/cast_ptr_sized_int.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
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
}
}
39 changes: 39 additions & 0 deletions clippy_lints/src/casts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
]);

Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
87 changes: 87 additions & 0 deletions tests/ui/cast_ptr_sized_int.rs
Original file line number Diff line number Diff line change
@@ -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;
}
Loading