wowlab_tidy/languages/rust/rules/correctness/
exotic_numeric_api.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self, HasName, HasVisibility, VisibilityKind},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "pub fn NonZero param",
12 code: "pub fn window(n: std::num::NonZeroUsize) -> usize { n.get() }",
13 pass: false,
14 },
15 Example {
16 label: "pub fn Wrapping return",
17 code: "pub fn count() -> std::num::Wrapping<u32> { std::num::Wrapping(0) }",
18 pass: false,
19 },
20 Example {
21 label: "pub fn Saturating param",
22 code: "pub fn add(x: std::num::Saturating<u32>) -> u32 { x.0 }",
23 pass: false,
24 },
25 Example {
26 label: "pub method NonZero param",
27 code: "struct S;\nimpl S {\n pub fn set(&self, n: std::num::NonZeroU8) -> u8 { n.get() }\n}",
28 pass: false,
29 },
30 Example {
31 label: "private fn NonZero param",
32 code: "fn helper(n: std::num::NonZeroU8) -> u8 { n.get() }",
33 pass: true,
34 },
35 Example {
36 label: "pub fn plain numbers",
37 code: "pub fn window_size(n: usize) -> usize { n }",
38 pass: true,
39 },
40 Example {
41 label: "exotic type only inside body",
42 code: "pub fn f(n: u32) -> u32 { let w = std::num::Wrapping(n); w.0 }",
43 pass: true,
44 },
45 Example {
46 label: "pub fn in test module",
47 code: "#[cfg(test)]\nmod tests {\n pub fn window(n: std::num::NonZeroUsize) -> usize { n.get() }\n}",
48 pass: true,
49 },
50];
51
52crate::ast_rule!(
53 exotic_numeric_api,
54 "Flag `Saturating`/`Wrapping`/`NonZero*` in pub fn signatures.",
55 "Std convention is plain numbers at public numeric boundaries; exotic wrappers belong to internal arithmetic.",
56 Low,
57);
58
59fn exotic_numeric_name(ty: &ast::Type) -> Option<String> {
60 ty.syntax()
61 .descendants()
62 .filter_map(ast::NameRef::cast)
63 .map(|name| name.text().to_string())
64 .find(|name| name == "Saturating" || name == "Wrapping" || name.starts_with("NonZero"))
65}
66
67fn check_exotic_numeric_api(ctx: &AstCtx<'_>) -> Vec<Violation> {
68 let mut violations = Vec::new();
69
70 for function in ctx.nodes::<ast::Fn>().filter(|function| {
71 !ctx.is_in_test(function)
72 && function
73 .visibility()
74 .is_some_and(|visibility| matches!(visibility.kind(), VisibilityKind::Pub))
75 }) {
76 let Some(function_name) = function.name() else {
77 continue;
78 };
79 let parameter_types = function
80 .param_list()
81 .into_iter()
82 .flat_map(|parameters| parameters.params())
83 .filter_map(|parameter| parameter.ty());
84 let return_type = function.ret_type().and_then(|ret| ret.ty());
85
86 for ty in parameter_types.chain(return_type) {
87 if let Some(name) = exotic_numeric_name(&ty) {
88 violations.push(ctx.violation(
89 &function_name,
90 format!(
91 "pub fn `{function_name}` uses `{name}` in its signature — public numeric boundaries take plain numbers"
92 ),
93 ));
94 }
95 }
96 }
97
98 violations
99}
100
101crate::tidy_ast_test!(check_exotic_numeric_api, {
102 crate::example_tests!(EXAMPLES, check_exotic_numeric_api);
103});