wowlab_tidy/languages/rust/rules/style/
magic_numbers.rs1use 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});