wowlab_tidy/languages/rust/rules/style/
reinvented_constant.rs1use ra_ap_syntax::ast::{self, HasName, LiteralKind};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "reinvented milliseconds value",
9 code: "const TICK_MILLIS: f64 = 1000.0;",
10 pass: false,
11 },
12 Example {
13 label: "reinvented percent value",
14 code: "const SCALE: f32 = 100.0_f32;",
15 pass: false,
16 },
17 Example {
18 label: "reserved shared constant name",
19 code: "const MS_PER_SECOND: u32 = duration();",
20 pass: false,
21 },
22 Example {
23 label: "name match is independent of value",
24 code: "const HUNDRED: f64 = 99.0;",
25 pass: false,
26 },
27 Example {
28 label: "unrelated local constant",
29 code: "const MAX_TARGETS: u32 = 8;",
30 pass: true,
31 },
32 Example {
33 label: "integer thousand is not a configured float value",
34 code: "const WINDOW_MS: u32 = 1000;",
35 pass: true,
36 },
37];
38
39crate::ast_rule!(
40 reinvented_constant,
41 "Flag local constants that reinvent shared numeric constants.",
42 "Canonical units and scaling constants belong in crates/types; duplicate names or values invite inconsistent conversions.",
43 Low,
44 params {
45 values: [String] = ["1000.0", "100.0"],
46 names: [String] = [
47 "MS_PER_SEC",
48 "MS_PER_SECOND",
49 "MILLIS_PER_SECOND",
50 "PERCENT",
51 "HUNDRED",
52 "THOUSAND"
53 ],
54 },
55);
56
57fn check_reinvented_constant(ctx: &AstCtx<'_>) -> Vec<Violation> {
58 if is_types_source(ctx.file.rel) && !cfg!(test) {
59 return Vec::new();
60 }
61
62 let values = ctx
63 .file
64 .config
65 .get_str_array("rust_reinvented_constant", &PARAMS[0]);
66 let values: Vec<f64> = values
67 .iter()
68 .filter_map(|value| value.parse::<f64>().ok())
69 .collect();
70 let names = ctx
71 .file
72 .config
73 .get_str_array("rust_reinvented_constant", &PARAMS[1]);
74
75 ctx.nodes::<ast::Const>()
76 .filter(|item| !ctx.is_in_test(item))
77 .filter_map(|item| {
78 let name = item.name()?;
79 let name_text = name.text();
80 let name_match = names.iter().any(|configured| configured == &name_text);
81 let value_match = item
82 .body()
83 .and_then(|body| float_literal(&body))
84 .is_some_and(|number| {
85 number
86 .value_string()
87 .parse::<f64>()
88 .is_ok_and(|value| values.contains(&value))
89 });
90
91 (name_match || value_match).then(|| {
92 ctx.violation(
93 &name,
94 format!(
95 "constant `{name_text}` reinvents a shared constant — use the canonical crates/types definition"
96 ),
97 )
98 })
99 })
100 .collect()
101}
102
103fn is_types_source(rel: &str) -> bool {
104 rel.starts_with("crates/types/")
105}
106
107fn float_literal(expr: &ast::Expr) -> Option<ast::FloatNumber> {
108 let mut expr = expr.clone();
109
110 loop {
111 match expr {
112 ast::Expr::Literal(literal) => match literal.kind() {
113 LiteralKind::FloatNumber(number) => return Some(number),
114 _ => return None,
115 },
116 ast::Expr::ParenExpr(paren) => expr = paren.expr()?,
117 _ => return None,
118 }
119 }
120}
121
122crate::tidy_ast_test!(check_reinvented_constant, {
123 crate::example_tests!(EXAMPLES, check_reinvented_constant);
124});