Skip to content

Commit 3a6e6b3

Browse files
committed
feat(clippy_lints): add cast_ptr_sized_int lint
Implement a new lint to detect casts between pointer-sized integers (`usize`, `isize`) and fixed-size integers that exhibit architecture-dependent behavior.
1 parent 62c5f16 commit 3a6e6b3

File tree

6 files changed

+472
-0
lines changed

6 files changed

+472
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6377,6 +6377,7 @@ Released 2018-09-13
63776377
[`cast_possible_wrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_possible_wrap
63786378
[`cast_precision_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_precision_loss
63796379
[`cast_ptr_alignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_alignment
6380+
[`cast_ptr_sized_int`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ptr_sized_int
63806381
[`cast_ref_to_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_ref_to_mut
63816382
[`cast_sign_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#cast_sign_loss
63826383
[`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 consider 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+
// We only care about casts where exactly one side is pointer-sized.
27+
if from_is_ptr_sized == to_is_ptr_sized {
28+
return;
29+
}
30+
31+
// Identify which side is the pointer-sized type and which is the fixed-size type.
32+
let (ptr_sized_ty, fixed_ty, fixed_bits_opt, direction) = if from_is_ptr_sized {
33+
(cast_from, cast_to, fixed_type_bits(cast_to), "to")
34+
} else {
35+
(cast_to, cast_from, fixed_type_bits(cast_from), "from")
36+
};
37+
38+
let Some(fixed_bits) = fixed_bits_opt else {
39+
// If the non-pointer side is not a fixed-size integer, bail out.
40+
return;
41+
};
42+
43+
// If this cast is always safe regardless of target pointer width, don't lint.
44+
if is_always_safe_cast(
45+
from_is_ptr_sized,
46+
fixed_bits,
47+
cast_from.is_signed(),
48+
cast_to.is_signed(),
49+
) {
50+
return;
51+
}
52+
53+
let msg = format!("casting `{cast_from}` to `{cast_to}`: will always truncate");
54+
55+
span_lint_and_then(cx, CAST_PTR_SIZED_INT, expr.span, msg, |diag| {
56+
let help_msg = format!(
57+
"`{ptr_sized_ty}` varies in size depending on the target, \
58+
so casting {direction} `{fixed_ty}` may produce different results across platforms"
59+
);
60+
diag.help(help_msg);
61+
diag.help("consider using `TryFrom` or `TryInto` for explicit fallible conversions");
62+
});
63+
}
64+
65+
/// Returns the bit width of a fixed-size integer type, or None if not a fixed-size int.
66+
fn fixed_type_bits(ty: Ty<'_>) -> Option<u64> {
67+
match ty.kind() {
68+
ty::Int(int_ty) => int_ty.bit_width(),
69+
ty::Uint(uint_ty) => uint_ty.bit_width(),
70+
_ => None,
71+
}
72+
}
73+
74+
/// Determines if a cast between pointer-sized and fixed-size integers is always safe.
75+
///
76+
/// Always safe casts (no architecture dependency):
77+
/// - Small fixed → ptr-sized: u8/i8/u16/i16 → usize/isize (ptr-sized is at least 16-bit)
78+
/// - Ptr-sized → large fixed: usize/isize → u64/i64/u128/i128 (ptr-sized is at most 64-bit)
79+
///
80+
/// NOT safe (depends on architecture):
81+
/// - Large fixed → ptr-sized: u32/u64/etc → usize (may truncate on smaller ptr widths)
82+
/// - Ptr-sized → small fixed: usize → u8/u16/u32 (may truncate on larger ptr widths)
83+
fn is_always_safe_cast(from_is_ptr_sized: bool, fixed_bits: u64, from_signed: bool, to_signed: bool) -> bool {
84+
// Note: sign-change issues are handled by a separate lint (cast_sign_loss). Here we
85+
// only reason about whether the numeric magnitude will always fit regardless of
86+
// the target pointer width.
87+
88+
if from_is_ptr_sized {
89+
// Casting from pointer-sized -> fixed-size:
90+
// - Pointer-sized integers (usize/isize) are at most 64 bits.
91+
// - A fixed-size target with >= 64 bits can always hold the magnitude of a pointer-sized value, but
92+
// we must respect signedness:
93+
// * isize -> i64/i128 is safe (from_signed == true and to_signed && fixed_bits >= 64)
94+
// * usize -> u64/u128 is safe (from_signed == false and !to_signed && fixed_bits >= 64)
95+
if fixed_bits < 64 {
96+
return false;
97+
}
98+
if to_signed {
99+
// Target is signed: safe only if source is signed (isize -> i64)
100+
from_signed && fixed_bits >= 64
101+
} else {
102+
// Target is unsigned: safe only if source is unsigned (usize -> u64)
103+
!from_signed && fixed_bits >= 64
104+
}
105+
} else if from_signed == to_signed {
106+
// Casting from fixed-size -> pointer-sized:
107+
// - Pointer-sized integers are at least 16 bits.
108+
// - Small fixed-size types (<= 16 bits) always fit in the smallest pointer width.
109+
// Same signedness: small fixed types (<=16 bits) are always safe.
110+
fixed_bits <= 16
111+
} else {
112+
// Sign change: only the case unsigned small -> signed ptr is considered safe here.
113+
fixed_bits <= 16 && !from_signed
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;
@@ -820,6 +821,42 @@ declare_clippy_lint! {
820821
"casting a primitive method pointer to any integer type"
821822
}
822823

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

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

0 commit comments

Comments
 (0)