Skip to main content

wowlab_tidy/languages/rust/rules/correctness/
raw_rng.rs

1use ra_ap_syntax::ast::{self, BinaryOp, CmpOp, HasArgList};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "raw rng() less-than comparison",
9        code: "fn f(rng: &mut dyn FnMut() -> f64, chance: f64) -> bool { rng() < chance }",
10        pass: false,
11    },
12    Example {
13        label: "raw rng() greater-than comparison",
14        code: "fn f(rng: &mut dyn FnMut() -> f64, chance: f64) -> bool { chance > rng() }",
15        pass: false,
16    },
17    Example {
18        label: "proc_chance is fine",
19        code: "fn f(rng: &mut dyn FnMut() -> f64, chance: f64) -> bool { proc_chance(rng, chance) }",
20        pass: true,
21    },
22    Example {
23        label: "raw rng() in test module",
24        code: "#[cfg(test)]\nmod tests {\n    fn t(rng: &mut dyn FnMut() -> f64) -> bool { rng() < 0.5 }\n}",
25        pass: true,
26    },
27];
28
29crate::ast_rule!(
30    raw_rng,
31    "Flag raw `rng() < chance` stochastic gating — use `proc_chance(rng, chance)`.",
32    "Comparing a raw `rng()` draw against a probability is easy to get backwards and inconsistent across hooks. Route every stochastic gate through `proc_chance` so the convention is uniform.",
33    Medium,
34);
35
36fn check_raw_rng(ctx: &AstCtx<'_>) -> Vec<Violation> {
37    let comparisons = ctx
38        .nodes::<ast::BinExpr>()
39        .filter(|expr| !ctx.is_in_test(expr))
40        .filter(|expr| matches!(expr.op_kind(), Some(BinaryOp::CmpOp(CmpOp::Ord { .. }))));
41
42    comparisons
43        .filter(|expr| {
44            let (left, right) = expr.sub_exprs();
45
46            left.as_ref().is_some_and(is_rng_call) || right.as_ref().is_some_and(is_rng_call)
47        })
48        .map(|expr| {
49            ctx.violation(
50                &expr,
51                "raw rng() comparison — use proc_chance(rng, chance) instead",
52            )
53        })
54        .collect()
55}
56
57fn is_rng_call(expr: &ast::Expr) -> bool {
58    let ast::Expr::CallExpr(call) = expr else {
59        return false;
60    };
61
62    if call
63        .arg_list()
64        .is_some_and(|args| args.args().next().is_some())
65    {
66        return false;
67    }
68
69    let Some(ast::Expr::PathExpr(path)) = call.expr() else {
70        return false;
71    };
72
73    let name = path
74        .path()
75        .and_then(|path| path.segment())
76        .and_then(|segment| segment.name_ref());
77
78    name.is_some_and(|name| name.text() == "rng")
79}
80
81crate::tidy_ast_test!(check_raw_rng, {
82    crate::example_tests!(EXAMPLES, check_raw_rng);
83});