wowlab_tidy/languages/rust/rules/performance/
from_instead_of_as.rs1use ra_ap_syntax::{AstNode, ast, ast::LiteralKind};
2
3use crate::{AstCtx, Example, Fix, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "suffixed literal cast",
9 code: "fn f() { let x = 1u8 as u32; }",
10 pass: false,
11 },
12 Example {
13 label: "unsuffixed literal",
14 code: "fn f() { let x = 1 as u32; }",
15 pass: true,
16 },
17 Example {
18 label: "unknown source type",
19 code: "fn f(a: u8) { let x = a as u32; }",
20 pass: true,
21 },
22 Example {
23 label: "const context",
24 code: "const X: u32 = 1u8 as u32;",
25 pass: true,
26 },
27 Example {
28 label: "static context",
29 code: "static X: u32 = 1u8 as u32;",
30 pass: true,
31 },
32 Example {
33 label: "cast in test module",
34 code: "#[cfg(test)]\nmod tests {\n fn f() { let x = 1u8 as u32; }\n}",
35 pass: true,
36 },
37];
38
39crate::ast_rule!(
40 from_instead_of_as,
41 "Flag `as` casts on suffixed literals — use `From`/`Into` instead.",
42 "From/Into conversions are checked at compile time. 'as' casts silently truncate, which hides bugs.",
43 Low,
44 fix_from_instead_of_as,
45);
46
47fn check_from_instead_of_as(ctx: &AstCtx<'_>) -> Vec<Violation> {
48 let numeric_casts = ctx
49 .nodes::<ast::CastExpr>()
50 .filter(|cast| !ctx.is_in_test(cast) && !is_in_const_context(cast))
51 .filter(|cast| {
52 cast.expr().is_some_and(|expr| has_numeric_suffix(&expr))
53 && cast.ty().is_some_and(|ty| is_numeric_type(&ty))
54 });
55
56 numeric_casts
57 .map(|cast| {
58 ctx.violation(
59 &cast,
60 "prefer `From`/`Into` instead of `as` cast on suffixed literal",
61 )
62 })
63 .collect()
64}
65
66fn has_numeric_suffix(expr: &ast::Expr) -> bool {
67 match expr {
68 ast::Expr::Literal(literal) => match literal.kind() {
69 LiteralKind::IntNumber(number) => number.suffix().is_some(),
70 LiteralKind::FloatNumber(number) => number.suffix().is_some(),
71 _ => false,
72 },
73 _ => false,
74 }
75}
76
77fn is_numeric_type(ty: &ast::Type) -> bool {
78 let ast::Type::PathType(path_type) = ty else {
79 return false;
80 };
81
82 let name = path_type
83 .path()
84 .and_then(|path| path.segment())
85 .and_then(|segment| segment.name_ref());
86
87 name.is_some_and(|name| NUMERIC_TYPES.contains(&name.text().as_str()))
88}
89
90fn is_in_const_context<N>(node: &N) -> bool
91where
92 N: AstNode,
93{
94 node.syntax().ancestors().any(|ancestor| {
95 ast::Const::can_cast(ancestor.kind()) || ast::Static::can_cast(ancestor.kind())
96 })
97}
98
99const NUMERIC_TYPES: &[&str] = &[
100 "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32",
101 "f64",
102];
103
104fn fix_from_instead_of_as(ctx: &AstCtx<'_>, v: &Violation) -> Option<Fix> {
105 let line = ctx.file.line(v.line)?;
106 let (before_as, after_as) = single_cast(line)?;
107 let target_type = numeric_prefix(after_as)?;
108 let suffix = numeric_suffix(before_as)?;
109 let number_part = before_as.strip_suffix(suffix)?;
110 let lit_start = literal_start(number_part)?;
111 let literal = before_as.get(lit_start..)?;
112 let before_literal = line.get(..lit_start)?;
113 let after_type = after_as.strip_prefix(target_type)?;
114
115 let fixed = format!("{before_literal}{target_type}::from({literal}){after_type}");
116
117 Some(Fix::replace_line(v.line, fixed))
118}
119
120fn single_cast(line: &str) -> Option<(&str, &str)> {
121 let (before, after) = line.split_once(" as ")?;
122
123 (!after.contains(" as ")).then_some((before, after))
124}
125
126fn numeric_prefix(value: &str) -> Option<&'static str> {
127 NUMERIC_TYPES.iter().copied().find(|numeric_type| {
128 value.strip_prefix(numeric_type).is_some_and(|rest| {
129 rest.chars()
130 .next()
131 .is_none_or(|character| !character.is_alphanumeric() && character != '_')
132 })
133 })
134}
135
136fn numeric_suffix(value: &str) -> Option<&'static str> {
137 NUMERIC_TYPES
138 .iter()
139 .copied()
140 .find(|numeric_type| value.ends_with(numeric_type))
141}
142
143fn literal_start(number_part: &str) -> Option<usize> {
144 number_part
145 .char_indices()
146 .rev()
147 .take_while(|&(_, c)| {
148 c.is_ascii_digit()
149 || c == '.'
150 || c == '_'
151 || c == 'x'
152 || c == 'b'
153 || c == 'o'
154 || c.is_ascii_hexdigit()
155 })
156 .last()
157 .map(|(index, _)| index)
158}
159
160crate::tidy_ast_test!(check_from_instead_of_as, {
161 crate::example_tests!(EXAMPLES, check_from_instead_of_as);
162 crate::fix_tests!(ast, check_from_instead_of_as, fix_from_instead_of_as);
163});