Skip to content

Commit 95c395b

Browse files
committed
Ban saturating arithmetic in tests
1 parent 63ae469 commit 95c395b

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed

build.rs

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn main() {
3131

3232
// Parse each rust file with syn and run the linting suite on it in parallel
3333
rust_files.par_iter().for_each_with(tx.clone(), |tx, file| {
34+
let is_test = file.display().to_string().contains("test");
3435
let Ok(content) = fs::read_to_string(file) else {
3536
return;
3637
};
@@ -63,6 +64,10 @@ fn main() {
6364
track_lint(ForbidKeysRemoveCall::lint(&parsed_file));
6465
track_lint(RequireFreezeStruct::lint(&parsed_file));
6566
track_lint(RequireExplicitPalletIndex::lint(&parsed_file));
67+
68+
if is_test {
69+
track_lint(ForbidSaturatingMath::lint(&parsed_file));
70+
}
6671
});
6772

6873
// Collect and print all errors after the parallel processing is done
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use super::*;
2+
use syn::{Expr, ExprCall, ExprMethodCall, ExprPath, File, Path, spanned::Spanned, visit::Visit};
3+
4+
pub struct ForbidSaturatingMath;
5+
6+
impl Lint for ForbidSaturatingMath {
7+
fn lint(source: &File) -> Result {
8+
let mut visitor = SaturatingMathBanVisitor::default();
9+
visitor.visit_file(source);
10+
11+
if visitor.errors.is_empty() {
12+
Ok(())
13+
} else {
14+
Err(visitor.errors)
15+
}
16+
}
17+
}
18+
19+
#[derive(Default)]
20+
struct SaturatingMathBanVisitor {
21+
errors: Vec<syn::Error>,
22+
}
23+
24+
impl<'ast> Visit<'ast> for SaturatingMathBanVisitor {
25+
fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
26+
let ExprMethodCall { method, .. } = node;
27+
28+
if method.to_string().starts_with("saturating_") {
29+
let msg = "Safe math is banned to encourage tests to panic";
30+
self.errors.push(syn::Error::new(method.span(), msg));
31+
}
32+
}
33+
34+
fn visit_expr_call(&mut self, node: &'ast ExprCall) {
35+
let ExprCall { func, .. } = node;
36+
37+
if is_saturating_math_call(func) {
38+
let msg = "Safe math is banned to encourage tests to panic";
39+
self.errors.push(syn::Error::new(node.func.span(), msg));
40+
}
41+
}
42+
}
43+
44+
fn is_saturating_math_call(func: &Expr) -> bool {
45+
let Expr::Path(ExprPath {
46+
path: Path { segments: path, .. },
47+
..
48+
}) = func
49+
else {
50+
return false;
51+
};
52+
53+
path.last().map_or(false, |seg| {
54+
seg.ident.to_string().starts_with("saturating_")
55+
})
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
use quote::quote;
62+
63+
fn lint(input: proc_macro2::TokenStream) -> Result {
64+
let mut visitor = SaturatingMathBanVisitor::default();
65+
let expr: syn::Expr = syn::parse2(input).expect("should be a valid expression");
66+
67+
match &expr {
68+
syn::Expr::MethodCall(call) => visitor.visit_expr_method_call(call),
69+
syn::Expr::Call(call) => visitor.visit_expr_call(call),
70+
_ => panic!("should be a valid method call or function call"),
71+
}
72+
73+
if visitor.errors.is_empty() {
74+
Ok(())
75+
} else {
76+
Err(visitor.errors)
77+
}
78+
}
79+
80+
#[test]
81+
fn test_saturating_forbidden() {
82+
let input = quote! { stake.saturating_add(alpha) };
83+
assert!(lint(input).is_err());
84+
let input = quote! { alpha_price.saturating_mul(float_alpha_block_emission) };
85+
assert!(lint(input).is_err());
86+
let input = quote! { alpha_out_i.saturating_sub(root_alpha) };
87+
assert!(lint(input).is_err());
88+
}
89+
90+
#[test]
91+
fn test_saturating_ufcs_forbidden() {
92+
let input = quote! { SaturatingAdd::saturating_add(stake, alpha) };
93+
assert!(lint(input).is_err());
94+
let input = quote! { core::num::SaturatingAdd::saturating_add(stake, alpha) };
95+
assert!(lint(input).is_err());
96+
let input =
97+
quote! { SaturatingMul::saturating_mul(alpha_price, float_alpha_block_emission) };
98+
assert!(lint(input).is_err());
99+
let input = quote! { core::num::SaturatingMul::saturating_mul(alpha_price, float_alpha_block_emission) };
100+
assert!(lint(input).is_err());
101+
let input = quote! { SaturatingSub::saturating_sub(alpha_out_i, root_alpha) };
102+
assert!(lint(input).is_err());
103+
let input = quote! { core::num::SaturatingSub::saturating_sub(alpha_out_i, root_alpha) };
104+
assert!(lint(input).is_err());
105+
}
106+
107+
#[test]
108+
fn test_saturating_to_from_num_forbidden() {
109+
let input = quote! { I96F32::saturating_from_num(u64::MAX) };
110+
assert!(lint(input).is_err());
111+
let input = quote! { remaining_emission.saturating_to_num::<u64>() };
112+
assert!(lint(input).is_err());
113+
}
114+
}

support/linting/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ pub use lint::*;
33

44
mod forbid_as_primitive;
55
mod forbid_keys_remove;
6+
mod forbid_saturating_math;
67
mod pallet_index;
78
mod require_freeze_struct;
89

910
pub use forbid_as_primitive::ForbidAsPrimitiveConversion;
1011
pub use forbid_keys_remove::ForbidKeysRemoveCall;
12+
pub use forbid_saturating_math::ForbidSaturatingMath;
1113
pub use pallet_index::RequireExplicitPalletIndex;
1214
pub use require_freeze_struct::RequireFreezeStruct;

0 commit comments

Comments
 (0)