Skip to content

Commit fa57af9

Browse files
committed
feat: add cast_ptr_sized_int lint
Introduce a new lint to detect casts between pointer-sized integers (`usize`, `isize`) and fixed-size integer types. - Identifies architecture-dependent casts that may lead to truncation or unexpected behavior when moving between 32-bit and 64-bit platforms. - Encourages the use of `TryFrom` or `TryInto` to make potential conversion failures explicit and handled. - Includes optimization logic to ignore always-safe casts, such as widening conversions from small fixed-size integers to pointer-sized types.
1 parent 34fab5c commit fa57af9

File tree

6 files changed

+474
-0
lines changed

6 files changed

+474
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6318,6 +6318,7 @@ Released 2018-09-13
63186318
[`cast_possible_wrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_wrap
63196319
[`cast_precision_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_precision_loss
63206320
[`cast_ptr_alignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_alignment
6321+
[`cast_ptr_sized_int`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_sized_int
63216322
[`cast_ref_to_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ref_to_mut
63226323
[`cast_sign_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_sign_loss
63236324
[`cast_slice_different_sizes`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_slice_different_sizes
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::ty::is_isize_or_usize;
3+
use rustc_hir::Expr;
4+
use rustc_lint::LateContext;
5+
use rustc_middle::ty::{self, Ty};
6+
7+
use super::CAST_PTR_SIZED_INT;
8+
9+
/// Checks for casts between pointer-sized integer types (`usize`/`isize`) and
10+
/// fixed-size integer types where the behavior depends on the target architecture.
11+
///
12+
/// Some casts are always safe and are NOT linted:
13+
/// - `u8`/`u16` → `usize`: always fits (usize is at least 16-bit)
14+
/// - `i8`/`i16` → `isize`: always fits (isize is at least 16-bit)
15+
/// - `usize` → `u64`/`u128`: always fits (usize is at most 64-bit)
16+
/// - `isize` → `i64`/`i128`: always fits (isize is at most 64-bit)
17+
pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>, cast_from: Ty<'tcx>, cast_to: Ty<'tcx>) {
18+
// Only lint integer-to-integer casts
19+
if !cast_from.is_integral() || !cast_to.is_integral() {
20+
return;
21+
}
22+
23+
let from_is_ptr_sized = is_isize_or_usize(cast_from);
24+
let to_is_ptr_sized = is_isize_or_usize(cast_to);
25+
26+
// Only lint if exactly one side is pointer-sized
27+
if from_is_ptr_sized == to_is_ptr_sized {
28+
return;
29+
}
30+
31+
// Get the bit width of the fixed-size type
32+
let (ptr_sized_ty, fixed_ty, fixed_bits, direction) = if from_is_ptr_sized {
33+
let bits = fixed_type_bits(cast_to);
34+
(cast_from, cast_to, bits, "to")
35+
} else {
36+
let bits = fixed_type_bits(cast_from);
37+
(cast_to, cast_from, bits, "from")
38+
};
39+
40+
let Some(fixed_bits) = fixed_bits else {
41+
return;
42+
};
43+
44+
// Check if this cast is always safe (no architecture dependency)
45+
if is_always_safe_cast(
46+
from_is_ptr_sized,
47+
fixed_bits,
48+
cast_from.is_signed(),
49+
cast_to.is_signed(),
50+
) {
51+
return;
52+
}
53+
54+
let msg = format!("casting `{cast_from}` to `{cast_to}`: the behavior depends on the target pointer width");
55+
56+
span_lint_and_then(cx, CAST_PTR_SIZED_INT, expr.span, msg, |diag| {
57+
let help_msg = format!(
58+
"`{ptr_sized_ty}` varies in size depending on the target, \
59+
so casting {direction} `{fixed_ty}` may produce different results across platforms"
60+
);
61+
diag.help(help_msg);
62+
diag.help("consider using `TryFrom` or `TryInto` for explicit fallible conversions");
63+
});
64+
}
65+
66+
/// Returns the bit width of a fixed-size integer type, or None if not a fixed-size int.
67+
fn fixed_type_bits(ty: Ty<'_>) -> Option<u64> {
68+
match ty.kind() {
69+
ty::Int(int_ty) => int_ty.bit_width(),
70+
ty::Uint(uint_ty) => uint_ty.bit_width(),
71+
_ => None,
72+
}
73+
}
74+
75+
/// Determines if a cast between pointer-sized and fixed-size integers is always safe.
76+
///
77+
/// Always safe casts (no architecture dependency):
78+
/// - Small fixed → ptr-sized: u8/i8/u16/i16 → usize/isize (ptr-sized is at least 16-bit)
79+
/// - Ptr-sized → large fixed: usize/isize → u64/i64/u128/i128 (ptr-sized is at most 64-bit)
80+
///
81+
/// NOT safe (depends on architecture):
82+
/// - Large fixed → ptr-sized: u32/u64/etc → usize (may truncate on smaller ptr widths)
83+
/// - Ptr-sized → small fixed: usize → u8/u16/u32 (may truncate on larger ptr widths)
84+
fn is_always_safe_cast(from_is_ptr_sized: bool, fixed_bits: u64, from_signed: bool, to_signed: bool) -> bool {
85+
// Sign changes are handled by cast_sign_loss, but we still need to consider
86+
// whether the magnitude fits
87+
88+
if from_is_ptr_sized {
89+
// ptr-sized → fixed: safe if fixed type can hold any ptr-sized value
90+
// usize/isize is at most 64 bits, so u64/i64 or larger is always safe
91+
// But we need to consider sign: usize → i64 could overflow if usize > i64::MAX
92+
if to_signed {
93+
// usize → i64: not safe (usize could be > i64::MAX on 64-bit)
94+
// isize → i64: safe (isize fits in i64)
95+
// isize → i128: safe
96+
from_signed && fixed_bits >= 64
97+
} else {
98+
// usize → u64: safe (usize fits in u64)
99+
// usize → u128: safe
100+
// isize → u64: not safe (isize could be negative)
101+
!from_signed && fixed_bits >= 64
102+
}
103+
} else if from_signed == to_signed {
104+
// fixed → ptr-sized: safe if fixed type fits in minimum ptr width (16 bits)
105+
// u8, u16 → usize: always safe
106+
// i8, i16 → isize: always safe
107+
// Same signedness: safe if fits in 16-bit ptr
108+
fixed_bits <= 16
109+
} else {
110+
// Sign change: handled by cast_sign_loss, but from a size perspective:
111+
// u16 → isize: safe (value fits, but could be interpreted differently)
112+
// For this lint, we focus on size, not sign interpretation
113+
fixed_bits <= 16 && !from_signed // only unsigned small → ptr-sized is truly safe
114+
}
115+
}

clippy_lints/src/casts/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod cast_possible_truncation;
1010
mod cast_possible_wrap;
1111
mod cast_precision_loss;
1212
mod cast_ptr_alignment;
13+
mod cast_ptr_sized_int;
1314
mod cast_sign_loss;
1415
mod cast_slice_different_sizes;
1516
mod cast_slice_from_raw_parts;
@@ -814,6 +815,42 @@ declare_clippy_lint! {
814815
"casting a primitive method pointer to any integer type"
815816
}
816817

818+
declare_clippy_lint! {
819+
/// ### What it does
820+
/// Checks for casts between pointer-sized integer types (`usize`/`isize`)
821+
/// and fixed-size integer types (like `u64`, `i32`, etc.).
822+
///
823+
/// ### Why is this bad?
824+
/// `usize` and `isize` have sizes that depend on the target architecture
825+
/// (32-bit on 32-bit platforms, 64-bit on 64-bit platforms). Casting between
826+
/// these and fixed-size integers can lead to subtle, platform-specific bugs:
827+
///
828+
/// - `usize as u64`: On 64-bit platforms this is lossless, but on 32-bit
829+
/// platforms the upper 32 bits are always zero.
830+
/// - `u64 as usize`: On 32-bit platforms this truncates, but on 64-bit
831+
/// platforms it's lossless.
832+
///
833+
/// Using `TryFrom`/`TryInto` makes the potential for failure explicit.
834+
///
835+
/// ### Example
836+
/// ```no_run
837+
/// pub fn foo(x: usize) -> u64 {
838+
/// x as u64
839+
/// }
840+
/// ```
841+
///
842+
/// Use instead:
843+
/// ```no_run
844+
/// pub fn foo(x: usize) -> u64 {
845+
/// u64::try_from(x).expect("usize should fit in u64")
846+
/// }
847+
/// ```
848+
#[clippy::version = "1.89.0"]
849+
pub CAST_PTR_SIZED_INT,
850+
restriction,
851+
"casts between pointer-sized and fixed-size integer types may behave differently across platforms"
852+
}
853+
817854
declare_clippy_lint! {
818855
/// ### What it does
819856
/// Checks for bindings (constants, statics, or let bindings) that are defined
@@ -878,6 +915,7 @@ impl_lint_pass!(Casts => [
878915
AS_POINTER_UNDERSCORE,
879916
MANUAL_DANGLING_PTR,
880917
CONFUSING_METHOD_TO_NUMERIC_CAST,
918+
CAST_PTR_SIZED_INT,
881919
NEEDLESS_TYPE_CAST,
882920
]);
883921

@@ -923,6 +961,7 @@ impl<'tcx> LateLintPass<'tcx> for Casts {
923961
cast_sign_loss::check(cx, expr, cast_from_expr, cast_from, cast_to, self.msrv);
924962
cast_abs_to_unsigned::check(cx, expr, cast_from_expr, cast_from, cast_to, self.msrv);
925963
cast_nan_to_int::check(cx, expr, cast_from_expr, cast_from, cast_to);
964+
cast_ptr_sized_int::check(cx, expr, cast_from, cast_to);
926965
}
927966
cast_lossless::check(cx, expr, cast_from_expr, cast_from, cast_to, cast_to_hir, self.msrv);
928967
cast_enum_constructor::check(cx, expr, cast_from_expr, cast_from);

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
6161
crate::casts::CAST_POSSIBLE_WRAP_INFO,
6262
crate::casts::CAST_PRECISION_LOSS_INFO,
6363
crate::casts::CAST_PTR_ALIGNMENT_INFO,
64+
crate::casts::CAST_PTR_SIZED_INT_INFO,
6465
crate::casts::CAST_SIGN_LOSS_INFO,
6566
crate::casts::CAST_SLICE_DIFFERENT_SIZES_INFO,
6667
crate::casts::CAST_SLICE_FROM_RAW_PARTS_INFO,

tests/ui/cast_ptr_sized_int.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//@no-rustfix
2+
#![warn(clippy::cast_ptr_sized_int)]
3+
#![allow(clippy::unnecessary_cast)]
4+
5+
fn main() {
6+
// Architecture-dependent behavior
7+
8+
let x: usize = 42;
9+
10+
// usize to small fixed-size (may truncate on larger ptr widths)
11+
let _ = x as u8; //~ cast_ptr_sized_int
12+
let _ = x as u16; //~ cast_ptr_sized_int
13+
let _ = x as u32; //~ cast_ptr_sized_int
14+
let _ = x as i8; //~ cast_ptr_sized_int
15+
let _ = x as i16; //~ cast_ptr_sized_int
16+
let _ = x as i32; //~ cast_ptr_sized_int
17+
18+
let y: isize = 42;
19+
20+
// isize to small fixed-size (may truncate on larger ptr widths)
21+
let _ = y as u8; //~ cast_ptr_sized_int
22+
let _ = y as u16; //~ cast_ptr_sized_int
23+
let _ = y as u32; //~ cast_ptr_sized_int
24+
let _ = y as i8; //~ cast_ptr_sized_int
25+
let _ = y as i16; //~ cast_ptr_sized_int
26+
let _ = y as i32; //~ cast_ptr_sized_int
27+
28+
// Large fixed-size to ptr-sized (may truncate on smaller ptr widths)
29+
let c: u32 = 1;
30+
let d: u64 = 1;
31+
let e: u128 = 1;
32+
let _ = c as usize; //~ cast_ptr_sized_int
33+
let _ = d as usize; //~ cast_ptr_sized_int
34+
let _ = e as usize; //~ cast_ptr_sized_int
35+
36+
let h: i32 = 1;
37+
let i: i64 = 1;
38+
let j: i128 = 1;
39+
let _ = h as usize; //~ cast_ptr_sized_int
40+
let _ = i as usize; //~ cast_ptr_sized_int
41+
let _ = j as usize; //~ cast_ptr_sized_int
42+
43+
let _ = c as isize; //~ cast_ptr_sized_int
44+
let _ = d as isize; //~ cast_ptr_sized_int
45+
let _ = e as isize; //~ cast_ptr_sized_int
46+
let _ = h as isize; //~ cast_ptr_sized_int
47+
let _ = i as isize; //~ cast_ptr_sized_int
48+
let _ = j as isize; //~ cast_ptr_sized_int
49+
50+
// usize to signed (potential sign issues)
51+
let _ = x as i64; //~ cast_ptr_sized_int
52+
}
53+
54+
// Always safe, no architecture dependency
55+
56+
fn no_lint_always_safe() {
57+
// Small fixed → ptr-sized: always safe (ptr-sized is at least 16-bit)
58+
let a: u8 = 1;
59+
let b: u16 = 1;
60+
let _ = a as usize; // OK: u8 fits in any usize
61+
let _ = b as usize; // OK: u16 fits in any usize
62+
63+
let f: i8 = 1;
64+
let g: i16 = 1;
65+
let _ = f as isize; // OK: i8 fits in any isize
66+
let _ = g as isize; // OK: i16 fits in any isize
67+
68+
// Ptr-sized → large fixed: always safe (ptr-sized is at most 64-bit)
69+
let x: usize = 42;
70+
let y: isize = 42;
71+
let _ = x as u64; // OK: usize fits in u64
72+
let _ = x as u128; // OK: usize fits in u128
73+
let _ = y as i64; // OK: isize fits in i64
74+
let _ = y as i128; // OK: isize fits in i128
75+
}
76+
77+
fn no_lint_same_kind() {
78+
// Both pointer-sized (handled by other lints)
79+
let x: usize = 42;
80+
let _ = x as isize;
81+
82+
let y: isize = 42;
83+
let _ = y as usize;
84+
85+
// Both fixed-size (handled by other lints)
86+
let a: u32 = 1;
87+
let _ = a as u64;
88+
let _ = a as i64;
89+
}

0 commit comments

Comments
 (0)