Skip to main content

wowlab_tidy/languages/rust/rules/style/
magic_numbers.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, LiteralKind, UnaryOp},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "magic number in function",
12        code: "fn f() { let x = 42; }",
13        pass: false,
14    },
15    Example {
16        label: "zero allowed by default",
17        code: "fn f() { let x = 0; }",
18        pass: true,
19    },
20    Example {
21        label: "one allowed by default",
22        code: "fn f() { let x = 1; }",
23        pass: true,
24    },
25    Example {
26        label: "power of two is magic",
27        code: "fn f() { let x = 256; }",
28        pass: false,
29    },
30    Example {
31        label: "small int is magic",
32        code: "fn f() { let x = 2; }",
33        pass: false,
34    },
35    Example {
36        label: "round number is magic",
37        code: "fn f() { let x = 100; }",
38        pass: false,
39    },
40    Example {
41        label: "const passes",
42        code: "const N: i32 = 42;",
43        pass: true,
44    },
45    Example {
46        label: "static passes",
47        code: "static N: i32 = 42;",
48        pass: true,
49    },
50    Example {
51        label: "enum discriminant passes",
52        code: "enum E { A = 42 }",
53        pass: true,
54    },
55    Example {
56        label: "float magic number",
57        code: "fn f() { let x = 3.14; }",
58        pass: false,
59    },
60    Example {
61        label: "float zero allowed by default",
62        code: "fn f() { let x = 0.0; }",
63        pass: true,
64    },
65    Example {
66        label: "magic number in test module",
67        code: "#[cfg(test)]\nmod tests {\n    fn f() { let x = 42; }\n}",
68        pass: true,
69    },
70    Example {
71        label: "negative magic number",
72        code: "fn f() { let x = -42; }",
73        pass: false,
74    },
75    Example {
76        label: "negative one allowed by default",
77        code: "fn f() { let x = -1; }",
78        pass: true,
79    },
80    Example {
81        label: "underscored literal is magic",
82        code: "fn f() { let x = 1_000; }",
83        pass: false,
84    },
85    Example {
86        label: "hex literal is magic",
87        code: "fn f() { let x = 0xff; }",
88        pass: false,
89    },
90    Example {
91        label: "integer match patterns are not expressions",
92        code: "fn f(x: i32) { match x { 2 | 42 => {}, _ => {} } }",
93        pass: true,
94    },
95    Example {
96        label: "negative integer match pattern is not an expression",
97        code: "fn f(x: i32) { match x { -42 => {}, _ => {} } }",
98        pass: true,
99    },
100    Example {
101        label: "literal in match arm body remains magic",
102        code: "fn f(x: i32) -> i32 { match x { 0 => 42, _ => 1 } }",
103        pass: false,
104    },
105];
106
107crate::ast_rule!(
108    magic_numbers,
109    "Flag unnamed numeric literals — extract into named constants.",
110    "Unnamed numeric literals obscure intent. Named constants make code self-documenting and easier to update.",
111    params {
112        allowed: [String] = ["0", "1", "0.0", "1.0"]
113    },
114);
115
116fn check_magic_numbers(ctx: &AstCtx<'_>) -> Vec<Violation> {
117    let allowed = ctx
118        .file
119        .config
120        .get_str_array("rust_magic_numbers", &PARAMS[0]);
121
122    ctx.nodes::<ast::Literal>()
123        .filter(|literal| {
124            !ctx.is_in_test(literal)
125                && !is_pattern_literal(literal)
126                && !is_in_const_context(literal)
127        })
128        .filter_map(|literal| match literal.kind() {
129            LiteralKind::IntNumber(number) => {
130                let digits = number.value().ok()?.to_string();
131
132                if is_negated(&literal) {
133                    (!allowed.iter().any(|allowed| allowed == &digits)).then(|| {
134                        let prefix = literal
135                            .syntax()
136                            .parent()
137                            .and_then(ast::PrefixExpr::cast)
138                            .expect("negated literal has a prefix expression");
139
140                        ctx.violation(
141                            &prefix,
142                            format!("magic number `-{digits}` — extract into a named constant"),
143                        )
144                    })
145                } else {
146                    (!allowed.iter().any(|allowed| allowed == &digits)).then(|| {
147                        ctx.violation(
148                            &literal,
149                            format!("magic number `{digits}` — extract into a named constant"),
150                        )
151                    })
152                }
153            }
154            LiteralKind::FloatNumber(number) => {
155                let digits = number.value_string();
156
157                (!allowed.iter().any(|allowed| allowed == &digits)).then(|| {
158                    ctx.violation(
159                        &literal,
160                        format!("magic number `{digits}` — extract into a named constant"),
161                    )
162                })
163            }
164            _ => None,
165        })
166        .collect()
167}
168
169fn is_pattern_literal(literal: &ast::Literal) -> bool {
170    literal
171        .syntax()
172        .parent()
173        .is_some_and(|parent| ast::LiteralPat::can_cast(parent.kind()))
174}
175
176fn is_negated(literal: &ast::Literal) -> bool {
177    literal
178        .syntax()
179        .parent()
180        .and_then(ast::PrefixExpr::cast)
181        .is_some_and(|prefix| prefix.op_kind() == Some(UnaryOp::Neg))
182}
183
184fn is_in_const_context(literal: &ast::Literal) -> bool {
185    literal.syntax().ancestors().any(|ancestor| {
186        ast::Static::can_cast(ancestor.kind())
187            || ast::Enum::can_cast(ancestor.kind())
188            || ast::Const::cast(ancestor).is_some_and(|item| !is_trait_associated_const(&item))
189    })
190}
191
192fn is_trait_associated_const(item: &ast::Const) -> bool {
193    item.syntax()
194        .parent()
195        .and_then(ast::AssocItemList::cast)
196        .and_then(|list| list.syntax().parent())
197        .and_then(ast::Trait::cast)
198        .is_some()
199}
200
201crate::tidy_ast_test!(check_magic_numbers, {
202    crate::example_tests!(EXAMPLES, check_magic_numbers);
203});