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