Skip to main content

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

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