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 @@ -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
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 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<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 {
// 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
}
}
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 @@ -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
Expand Down Expand Up @@ -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,
]);

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