Skip to main content

wowlab_tidy/languages/rust/rules/performance/
from_instead_of_as.rs

1use 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});