Skip to main content

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

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