use ra_ap_syntax::{
AstNode,
ast::{self, HasAttrs},
};
use crate::{AstCtx, Example, Violation};
#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
Example {
label: "cast to u8",
code: "fn f() { let x = 42u64 as u8; }",
pass: false,
},
Example {
label: "cast to f32",
code: "fn f() { let x = 1.0f64 as f32; }",
pass: false,
},
Example {
label: "cast to i16",
code: "fn f() { let x = 1000i32 as i16; }",
pass: false,
},
Example {
label: "cast to u32",
code: "fn f() { let x = 42u64 as u32; }",
pass: true,
},
Example {
label: "cast to u64",
code: "fn f() { let x = 42u32 as u64; }",
pass: true,
},
Example {
label: "cast to usize",
code: "fn f() { let x = 42u32 as usize; }",
pass: true,
},
Example {
label: "cast in test module",
code: "#[cfg(test)]\nmod tests {\n fn f() { let x = 42u64 as u8; }\n}",
pass: true,
},
Example {
label: "documented conversion policy helper",
code: "#[expect(clippy::cast_possible_truncation, reason = \"wire payload deliberately stores f32\")]\nfn payload(value: f64) -> f32 { value as f32 }",
pass: true,
},
];
crate::ast_rule!(
lossy_cast,
"Flag `as` casts to types that lose precision (`f32`, `u8`, `u16`, `i8`, `i16`).",
"Casting to a smaller integer silently truncates, so use `TryFrom` or byte-level extraction. Float narrowing has no fallible standard conversion: centralize it in a named policy helper with bounds or rounding behavior, and document the deliberate Clippy cast expectation there instead of scattering casts.",
Medium,
);
const LOSSY_TARGETS: &[&str] = &["f32", "u8", "u16", "i8", "i16"];
fn check_lossy_cast(ctx: &AstCtx<'_>) -> Vec<Violation> {
ctx.nodes::<ast::CastExpr>()
.filter(|cast| !ctx.is_in_test(cast) && !has_documented_cast_policy(cast))
.filter_map(|cast| {
let ast::Type::PathType(path) = cast.ty()? else {
return None;
};
let ident = path.path()?.segment()?.name_ref()?;
LOSSY_TARGETS
.contains(&ident.text().as_str())
.then(|| {
ctx.violation(
&cast,
format!(
"potentially lossy cast to `{ident}` — use explicit conversion (e.g. try_into(), try_from())"
),
)
})
})
.collect()
}
fn has_documented_cast_policy(cast: &ast::CastExpr) -> bool {
cast.syntax()
.ancestors()
.find_map(ast::Fn::cast)
.is_some_and(|function| {
function.attrs().any(|attribute| {
let source = attribute.syntax().text().to_string();
source.contains("expect(")
&& source.contains("reason")
&& (source.contains("clippy::cast_possible_truncation")
|| source.contains("clippy::cast_precision_loss")
|| source.contains("clippy::cast_sign_loss"))
})
})
}
crate::rulewright_ast_test!(check_lossy_cast, {
crate::example_tests!(EXAMPLES, check_lossy_cast);
});