Add needless_type_cast lint

This commit is contained in:
Samuel Onoja 2025-12-09 22:56:23 +01:00
parent 0cf51b1b73
commit 1232c81a3e
No known key found for this signature in database
9 changed files with 760 additions and 2 deletions

View file

@ -6688,6 +6688,7 @@ Released 2018-09-13
[`needless_return`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_return
[`needless_return_with_question_mark`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_return_with_question_mark
[`needless_splitn`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_splitn
[`needless_type_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_type_cast
[`needless_update`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_update
[`neg_cmp_op_on_partial_ord`]: https://rust-lang.github.io/rust-clippy/master/index.html#neg_cmp_op_on_partial_ord
[`neg_multiply`]: https://rust-lang.github.io/rust-clippy/master/index.html#neg_multiply

View file

@ -4,7 +4,7 @@
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are over 800 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

View file

@ -5,7 +5,7 @@
A collection of lints to catch common mistakes and improve your
[Rust](https://github.com/rust-lang/rust) code.
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are over 800 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
Lints are divided into categories, each with a default [lint
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

View file

@ -19,6 +19,7 @@ mod fn_to_numeric_cast;
mod fn_to_numeric_cast_any;
mod fn_to_numeric_cast_with_truncation;
mod manual_dangling_ptr;
mod needless_type_cast;
mod ptr_as_ptr;
mod ptr_cast_constness;
mod ref_as_ptr;
@ -813,6 +814,32 @@ declare_clippy_lint! {
"casting a primitive method pointer to any integer type"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for bindings (constants, statics, or let bindings) that are defined
/// with one numeric type but are consistently cast to a different type in all usages.
///
/// ### Why is this bad?
/// If a binding is always cast to a different type when used, it would be clearer
/// and more efficient to define it with the target type from the start.
///
/// ### Example
/// ```no_run
/// const SIZE: u16 = 15;
/// let arr: [u8; SIZE as usize] = [0; SIZE as usize];
/// ```
///
/// Use instead:
/// ```no_run
/// const SIZE: usize = 15;
/// let arr: [u8; SIZE] = [0; SIZE];
/// ```
#[clippy::version = "1.93.0"]
pub NEEDLESS_TYPE_CAST,
pedantic,
"binding defined with one type but always cast to another"
}
pub struct Casts {
msrv: Msrv,
}
@ -851,6 +878,7 @@ impl_lint_pass!(Casts => [
AS_POINTER_UNDERSCORE,
MANUAL_DANGLING_PTR,
CONFUSING_METHOD_TO_NUMERIC_CAST,
NEEDLESS_TYPE_CAST,
]);
impl<'tcx> LateLintPass<'tcx> for Casts {
@ -920,4 +948,8 @@ impl<'tcx> LateLintPass<'tcx> for Casts {
cast_slice_different_sizes::check(cx, expr, self.msrv);
ptr_cast_constness::check_null_ptr_cast_method(cx, expr);
}
fn check_body(&mut self, cx: &LateContext<'tcx>, body: &rustc_hir::Body<'tcx>) {
needless_type_cast::check(cx, body);
}
}

View file

@ -0,0 +1,289 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::visitors::{Descend, for_each_expr, for_each_expr_without_closures};
use core::ops::ControlFlow;
use rustc_data_structures::fx::FxHashMap;
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::{BlockCheckMode, Body, Expr, ExprKind, HirId, LetStmt, PatKind, StmtKind, UnsafeSource};
use rustc_lint::LateContext;
use rustc_middle::ty::{Ty, TypeVisitableExt};
use rustc_span::Span;
use super::NEEDLESS_TYPE_CAST;
struct BindingInfo<'a> {
source_ty: Ty<'a>,
ty_span: Span,
}
struct UsageInfo<'a> {
cast_to: Option<Ty<'a>>,
in_generic_context: bool,
}
pub(super) fn check<'a>(cx: &LateContext<'a>, body: &Body<'a>) {
let mut bindings: FxHashMap<HirId, BindingInfo<'a>> = FxHashMap::default();
for_each_expr_without_closures(body.value, |expr| {
match expr.kind {
ExprKind::Block(block, _) => {
for stmt in block.stmts {
if let StmtKind::Let(let_stmt) = stmt.kind {
collect_binding_from_local(cx, let_stmt, &mut bindings);
}
}
},
ExprKind::Let(let_expr) => {
collect_binding_from_let(cx, let_expr, &mut bindings);
},
_ => {},
}
ControlFlow::<()>::Continue(())
});
#[allow(rustc::potential_query_instability)]
let mut binding_vec: Vec<_> = bindings.into_iter().collect();
binding_vec.sort_by_key(|(_, info)| info.ty_span.lo());
for (hir_id, binding_info) in binding_vec {
check_binding_usages(cx, body, hir_id, &binding_info);
}
}
fn collect_binding_from_let<'a>(
cx: &LateContext<'a>,
let_expr: &rustc_hir::LetExpr<'a>,
bindings: &mut FxHashMap<HirId, BindingInfo<'a>>,
) {
if let_expr.ty.is_none()
|| let_expr.span.from_expansion()
|| has_generic_return_type(cx, let_expr.init)
|| contains_unsafe(let_expr.init)
{
return;
}
if let PatKind::Binding(_, hir_id, _, _) = let_expr.pat.kind
&& let Some(ty_hir) = let_expr.ty
{
let ty = cx.typeck_results().pat_ty(let_expr.pat);
if ty.is_numeric() {
bindings.insert(
hir_id,
BindingInfo {
source_ty: ty,
ty_span: ty_hir.span,
},
);
}
}
}
fn collect_binding_from_local<'a>(
cx: &LateContext<'a>,
let_stmt: &LetStmt<'a>,
bindings: &mut FxHashMap<HirId, BindingInfo<'a>>,
) {
if let_stmt.ty.is_none()
|| let_stmt.span.from_expansion()
|| let_stmt
.init
.is_some_and(|init| has_generic_return_type(cx, init) || contains_unsafe(init))
{
return;
}
if let PatKind::Binding(_, hir_id, _, _) = let_stmt.pat.kind
&& let Some(ty_hir) = let_stmt.ty
{
let ty = cx.typeck_results().pat_ty(let_stmt.pat);
if ty.is_numeric() {
bindings.insert(
hir_id,
BindingInfo {
source_ty: ty,
ty_span: ty_hir.span,
},
);
}
}
}
fn contains_unsafe(expr: &Expr<'_>) -> bool {
for_each_expr_without_closures(expr, |e| {
if let ExprKind::Block(block, _) = e.kind
&& let BlockCheckMode::UnsafeBlock(UnsafeSource::UserProvided) = block.rules
{
return ControlFlow::Break(());
}
ControlFlow::Continue(())
})
.is_some()
}
fn has_generic_return_type(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
match &expr.kind {
ExprKind::Block(block, _) => {
if let Some(tail_expr) = block.expr {
return has_generic_return_type(cx, tail_expr);
}
false
},
ExprKind::If(_, then_block, else_expr) => {
has_generic_return_type(cx, then_block) || else_expr.is_some_and(|e| has_generic_return_type(cx, e))
},
ExprKind::Match(_, arms, _) => arms.iter().any(|arm| has_generic_return_type(cx, arm.body)),
ExprKind::Loop(block, label, ..) => for_each_expr_without_closures(*block, |e| {
match e.kind {
ExprKind::Loop(..) => {
// Unlabeled breaks inside nested loops target the inner loop, not ours
return ControlFlow::Continue(Descend::No);
},
ExprKind::Break(dest, Some(break_expr)) => {
let targets_this_loop =
dest.label.is_none() || dest.label.map(|l| l.ident) == label.map(|l| l.ident);
if targets_this_loop && has_generic_return_type(cx, break_expr) {
return ControlFlow::Break(());
}
},
_ => {},
}
ControlFlow::Continue(Descend::Yes)
})
.is_some(),
ExprKind::MethodCall(..) => {
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id) {
let sig = cx.tcx.fn_sig(def_id).instantiate_identity();
let ret_ty = sig.output().skip_binder();
return ret_ty.has_param();
}
false
},
ExprKind::Call(callee, _) => {
if let ExprKind::Path(qpath) = &callee.kind {
let res = cx.qpath_res(qpath, callee.hir_id);
if let Res::Def(DefKind::Fn | DefKind::AssocFn, def_id) = res {
let sig = cx.tcx.fn_sig(def_id).instantiate_identity();
let ret_ty = sig.output().skip_binder();
return ret_ty.has_param();
}
}
false
},
_ => false,
}
}
fn is_generic_res(cx: &LateContext<'_>, res: Res) -> bool {
let has_type_params = |def_id| {
cx.tcx
.generics_of(def_id)
.own_params
.iter()
.any(|p| p.kind.is_ty_or_const())
};
match res {
Res::Def(DefKind::Fn | DefKind::AssocFn, def_id) => has_type_params(def_id),
// Ctor → Variant → ADT: constructor's parent is variant, variant's parent is the ADT
Res::Def(DefKind::Ctor(..), def_id) => has_type_params(cx.tcx.parent(cx.tcx.parent(def_id))),
_ => false,
}
}
fn is_cast_in_generic_context<'a>(cx: &LateContext<'a>, cast_expr: &Expr<'a>) -> bool {
let mut current_id = cast_expr.hir_id;
loop {
let parent_id = cx.tcx.parent_hir_id(current_id);
if parent_id == current_id {
return false;
}
let parent = cx.tcx.hir_node(parent_id);
match parent {
rustc_hir::Node::Expr(parent_expr) => {
match &parent_expr.kind {
ExprKind::Closure(_) => return false,
ExprKind::Call(callee, _) => {
if let ExprKind::Path(qpath) = &callee.kind {
let res = cx.qpath_res(qpath, callee.hir_id);
if is_generic_res(cx, res) {
return true;
}
}
},
ExprKind::MethodCall(..) => {
if let Some(def_id) = cx.typeck_results().type_dependent_def_id(parent_expr.hir_id)
&& cx
.tcx
.generics_of(def_id)
.own_params
.iter()
.any(|p| p.kind.is_ty_or_const())
{
return true;
}
},
_ => {},
}
current_id = parent_id;
},
_ => return false,
}
}
}
fn check_binding_usages<'a>(cx: &LateContext<'a>, body: &Body<'a>, hir_id: HirId, binding_info: &BindingInfo<'a>) {
let mut usages = Vec::new();
for_each_expr(cx, body.value, |expr| {
if let ExprKind::Path(ref qpath) = expr.kind
&& !expr.span.from_expansion()
&& let Res::Local(id) = cx.qpath_res(qpath, expr.hir_id)
&& id == hir_id
{
let parent_id = cx.tcx.parent_hir_id(expr.hir_id);
let parent = cx.tcx.hir_node(parent_id);
let usage = if let rustc_hir::Node::Expr(parent_expr) = parent
&& let ExprKind::Cast(..) = parent_expr.kind
&& !parent_expr.span.from_expansion()
{
UsageInfo {
cast_to: Some(cx.typeck_results().expr_ty(parent_expr)),
in_generic_context: is_cast_in_generic_context(cx, parent_expr),
}
} else {
UsageInfo {
cast_to: None,
in_generic_context: false,
}
};
usages.push(usage);
}
ControlFlow::<()>::Continue(())
});
let Some(first_target) = usages
.first()
.and_then(|u| u.cast_to)
.filter(|&t| t != binding_info.source_ty)
.filter(|&t| usages.iter().all(|u| u.cast_to == Some(t) && !u.in_generic_context))
else {
return;
};
span_lint_and_sugg(
cx,
NEEDLESS_TYPE_CAST,
binding_info.ty_span,
format!(
"this binding is defined as `{}` but is always cast to `{}`",
binding_info.source_ty, first_target
),
"consider defining it as",
first_target.to_string(),
Applicability::MaybeIncorrect,
);
}

View file

@ -70,6 +70,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::casts::FN_TO_NUMERIC_CAST_ANY_INFO,
crate::casts::FN_TO_NUMERIC_CAST_WITH_TRUNCATION_INFO,
crate::casts::MANUAL_DANGLING_PTR_INFO,
crate::casts::NEEDLESS_TYPE_CAST_INFO,
crate::casts::PTR_AS_PTR_INFO,
crate::casts::PTR_CAST_CONSTNESS_INFO,
crate::casts::REF_AS_PTR_INFO,

View file

@ -0,0 +1,182 @@
#![warn(clippy::needless_type_cast)]
#![allow(clippy::no_effect, clippy::unnecessary_cast, unused)]
fn takes_i32(x: i32) -> i32 {
x
}
fn generic<T>(x: T) -> T {
x
}
fn main() {
let a: i32 = 10;
//~^ needless_type_cast
let _ = a as i32 + 5;
let _ = a as i32 * 2;
let b: u16 = 20;
let _ = b;
let _ = b as u32;
let c: u8 = 5;
let _ = c as u16;
let _ = c as u32;
let d: i32 = 100;
let _ = d + 1;
let e = 42u8;
let _ = e as i64;
let _ = e as i64 + 10;
let f: usize = 1;
//~^ needless_type_cast
let _ = f as usize;
}
fn test_function_call() {
let a: i32 = 10;
//~^ needless_type_cast
let _ = takes_i32(a as i32);
let _ = takes_i32(a as i32);
}
fn test_generic_call() {
let a: u8 = 10;
let _ = generic(a as i32);
let _ = generic(a as i32);
}
fn test_method_on_cast() {
let a: i32 = 10;
//~^ needless_type_cast
let _ = (a as i32).checked_add(5);
let _ = (a as i32).saturating_mul(2);
}
fn test_iterator_sum() {
let a: i32 = 10;
//~^ needless_type_cast
let arr = [a as i32, a as i32];
let _: i32 = arr.iter().copied().sum();
}
fn test_closure() {
let a: i32 = 10;
//~^ needless_type_cast
let _: i32 = [1i32, 2].iter().map(|x| x + a as i32).sum();
}
fn test_struct_field() {
struct S {
x: i32,
y: i32,
}
let a: i32 = 10;
//~^ needless_type_cast
let _ = S {
x: a as i32,
y: a as i32,
};
}
fn test_option() {
let a: u8 = 10;
let _: Option<i32> = Some(a as i32);
let _: Option<i32> = Some(a as i32);
}
fn test_mixed_context() {
let a: u8 = 10;
let _ = takes_i32(a as i32);
let _ = generic(a as i32);
}
fn test_nested_block() {
if true {
let a: i32 = 10;
//~^ needless_type_cast
let _ = a as i32 + 1;
let _ = a as i32 * 2;
}
}
fn test_match_expr() {
let a: i32 = 10;
//~^ needless_type_cast
let _ = match 1 {
1 => a as i32,
_ => a as i32,
};
}
fn test_return_expr() -> i32 {
let a: i32 = 10;
//~^ needless_type_cast
a as i32
}
fn test_closure_always_cast() {
let a: i32 = 10;
//~^ needless_type_cast
let _ = [1, 2].iter().map(|_| a as i32).sum::<i32>();
let _ = a as i32;
}
fn test_closure_mixed_usage() {
let a: u8 = 10;
let _ = [1, 2].iter().map(|_| a as i32).sum::<i32>();
let _ = a + 1;
}
fn test_nested_generic_call() {
let a: u8 = 10;
let _ = generic(takes_i32(a as i32));
let _ = generic(takes_i32(a as i32));
}
fn test_generic_initializer() {
// Should not lint: changing type would affect what generic() returns
let a: u8 = generic(10u8);
let _ = a as i32;
let _ = a as i32;
}
fn test_unsafe_transmute() {
// Should not lint: initializer contains unsafe block
#[allow(clippy::useless_transmute)]
let x: u32 = unsafe { std::mem::transmute(0u32) };
let _ = x as u64;
}
fn test_if_with_generic() {
// Should not lint: one branch has generic return type
let x: u8 = if true { generic(1) } else { 2 };
let _ = x as i32;
}
fn test_match_with_generic() {
// Should not lint: one branch has generic return type
let x: u8 = match 1 {
1 => generic(1),
_ => 2,
};
let _ = x as i32;
}
fn test_default() {
// Should not lint: Default::default() has generic return type
let x: u8 = Default::default();
let _ = x as i32;
}
fn test_loop_with_generic() {
// Should not lint: loop break has generic return type
#[allow(clippy::never_loop)]
let x: u8 = loop {
break generic(1);
};
let _ = x as i32;
}

View file

@ -0,0 +1,182 @@
#![warn(clippy::needless_type_cast)]
#![allow(clippy::no_effect, clippy::unnecessary_cast, unused)]
fn takes_i32(x: i32) -> i32 {
x
}
fn generic<T>(x: T) -> T {
x
}
fn main() {
let a: u8 = 10;
//~^ needless_type_cast
let _ = a as i32 + 5;
let _ = a as i32 * 2;
let b: u16 = 20;
let _ = b;
let _ = b as u32;
let c: u8 = 5;
let _ = c as u16;
let _ = c as u32;
let d: i32 = 100;
let _ = d + 1;
let e = 42u8;
let _ = e as i64;
let _ = e as i64 + 10;
let f: u8 = 1;
//~^ needless_type_cast
let _ = f as usize;
}
fn test_function_call() {
let a: u8 = 10;
//~^ needless_type_cast
let _ = takes_i32(a as i32);
let _ = takes_i32(a as i32);
}
fn test_generic_call() {
let a: u8 = 10;
let _ = generic(a as i32);
let _ = generic(a as i32);
}
fn test_method_on_cast() {
let a: u8 = 10;
//~^ needless_type_cast
let _ = (a as i32).checked_add(5);
let _ = (a as i32).saturating_mul(2);
}
fn test_iterator_sum() {
let a: u8 = 10;
//~^ needless_type_cast
let arr = [a as i32, a as i32];
let _: i32 = arr.iter().copied().sum();
}
fn test_closure() {
let a: u8 = 10;
//~^ needless_type_cast
let _: i32 = [1i32, 2].iter().map(|x| x + a as i32).sum();
}
fn test_struct_field() {
struct S {
x: i32,
y: i32,
}
let a: u8 = 10;
//~^ needless_type_cast
let _ = S {
x: a as i32,
y: a as i32,
};
}
fn test_option() {
let a: u8 = 10;
let _: Option<i32> = Some(a as i32);
let _: Option<i32> = Some(a as i32);
}
fn test_mixed_context() {
let a: u8 = 10;
let _ = takes_i32(a as i32);
let _ = generic(a as i32);
}
fn test_nested_block() {
if true {
let a: u8 = 10;
//~^ needless_type_cast
let _ = a as i32 + 1;
let _ = a as i32 * 2;
}
}
fn test_match_expr() {
let a: u8 = 10;
//~^ needless_type_cast
let _ = match 1 {
1 => a as i32,
_ => a as i32,
};
}
fn test_return_expr() -> i32 {
let a: u8 = 10;
//~^ needless_type_cast
a as i32
}
fn test_closure_always_cast() {
let a: u8 = 10;
//~^ needless_type_cast
let _ = [1, 2].iter().map(|_| a as i32).sum::<i32>();
let _ = a as i32;
}
fn test_closure_mixed_usage() {
let a: u8 = 10;
let _ = [1, 2].iter().map(|_| a as i32).sum::<i32>();
let _ = a + 1;
}
fn test_nested_generic_call() {
let a: u8 = 10;
let _ = generic(takes_i32(a as i32));
let _ = generic(takes_i32(a as i32));
}
fn test_generic_initializer() {
// Should not lint: changing type would affect what generic() returns
let a: u8 = generic(10u8);
let _ = a as i32;
let _ = a as i32;
}
fn test_unsafe_transmute() {
// Should not lint: initializer contains unsafe block
#[allow(clippy::useless_transmute)]
let x: u32 = unsafe { std::mem::transmute(0u32) };
let _ = x as u64;
}
fn test_if_with_generic() {
// Should not lint: one branch has generic return type
let x: u8 = if true { generic(1) } else { 2 };
let _ = x as i32;
}
fn test_match_with_generic() {
// Should not lint: one branch has generic return type
let x: u8 = match 1 {
1 => generic(1),
_ => 2,
};
let _ = x as i32;
}
fn test_default() {
// Should not lint: Default::default() has generic return type
let x: u8 = Default::default();
let _ = x as i32;
}
fn test_loop_with_generic() {
// Should not lint: loop break has generic return type
#[allow(clippy::never_loop)]
let x: u8 = loop {
break generic(1);
};
let _ = x as i32;
}

View file

@ -0,0 +1,71 @@
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:13:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
|
= note: `-D clippy::needless-type-cast` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::needless_type_cast)]`
error: this binding is defined as `u8` but is always cast to `usize`
--> tests/ui/needless_type_cast.rs:33:12
|
LL | let f: u8 = 1;
| ^^ help: consider defining it as: `usize`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:39:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:52:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:59:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:66:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:77:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:99:16
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:107:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:116:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: this binding is defined as `u8` but is always cast to `i32`
--> tests/ui/needless_type_cast.rs:122:12
|
LL | let a: u8 = 10;
| ^^ help: consider defining it as: `i32`
error: aborting due to 11 previous errors