Skip to main content

wowlab_tidy/languages/rust/rules/api/
assoc_fn_no_self.rs

1use ra_ap_syntax::{
2    AstNode,
3    ast::{self, HasName},
4};
5
6use super::support::type_name;
7use crate::{AstCtx, Example, Violation};
8
9#[rustfmt::skip]
10const EXAMPLES: &[Example] = &[
11    Example {
12        label: "unrelated helper in impl",
13        code: "struct Db;\nimpl Db {\n    fn check_parameters(input: &str) -> bool {\n        input.is_empty()\n    }\n}",
14        pass: false,
15    },
16    Example {
17        label: "constructor",
18        code: "struct Db;\nimpl Db {\n    fn new() -> Self {\n        Db\n    }\n}",
19        pass: true,
20    },
21    Example {
22        label: "returns the impl type",
23        code: "struct Db;\nimpl Db {\n    fn connect(url: &str) -> Db {\n        Db\n    }\n}",
24        pass: true,
25    },
26    Example {
27        label: "takes Self parameters",
28        code: "struct Db;\nimpl Db {\n    fn merge(a: Self, b: Self) -> u32 {\n        0\n    }\n}",
29        pass: true,
30    },
31    Example {
32        label: "method with receiver",
33        code: "struct Db;\nimpl Db {\n    fn query(&self) {}\n}",
34        pass: true,
35    },
36    Example {
37        label: "from_ prefixed",
38        code: "struct Db;\nimpl Db {\n    fn from_code(code: u32) -> u32 {\n        code\n    }\n}",
39        pass: true,
40    },
41    Example {
42        label: "parameter type mentioning impl type",
43        code: "struct Config;\nimpl Config {\n    fn validate_rule(rule: &RuleConfig) -> bool {\n        true\n    }\n}",
44        pass: true,
45    },
46    Example {
47        label: "trait impl",
48        code: "struct S;\ntrait Calc {\n    fn calc(x: u32) -> u32;\n}\nimpl Calc for S {\n    fn calc(x: u32) -> u32 {\n        x\n    }\n}",
49        pass: true,
50    },
51    Example {
52        label: "unrelated helper in test module",
53        code: "#[cfg(test)]\nmod tests {\n    struct Db;\n    impl Db {\n        fn check_parameters(input: &str) -> bool {\n            input.is_empty()\n        }\n    }\n}",
54        pass: true,
55    },
56];
57
58const EXEMPT_NAMES: &[&str] = &["new", "default", "builder"];
59const EXEMPT_PREFIXES: &[&str] = &["from_", "with_", "try_", "new_"];
60
61crate::ast_rule!(
62    assoc_fn_no_self,
63    "Flag inherent associated fns that neither take nor return the impl type — make them free functions.",
64    "Regular functions are first-class in Rust; computation unrelated to a receiver hosted in an impl block adds Type:: noise for no benefit.",
65    Low,
66);
67
68fn check_assoc_fn_no_self(ctx: &AstCtx<'_>) -> Vec<Violation> {
69    ctx.nodes::<ast::Impl>()
70        .filter(|item| !ctx.is_in_test(item) && item.trait_().is_none())
71        .flat_map(|item| {
72            let Some(ty) = item.self_ty().and_then(|ty| type_name(&ty)) else {
73                return Vec::new();
74            };
75
76            let associated_items = item
77                .assoc_item_list()
78                .into_iter()
79                .flat_map(|list| list.assoc_items());
80            let unrelated_functions = associated_items
81                .filter_map(|assoc| match assoc { ast::AssocItem::Fn(function) => Some(function), _ => None })
82                .filter(|function| is_unrelated_assoc_fn(function, &ty));
83
84            unrelated_functions
85                .filter_map(|function| {
86                    let name = function.name()?;
87
88                    Some(ctx.violation(
89                        &name,
90                        format!(
91                            "associated fn `{ty}::{name}` neither takes nor returns `{ty}` — make it a free function"
92                        ),
93                    ))
94                })
95                .collect::<Vec<_>>()
96        })
97        .collect()
98}
99
100fn is_unrelated_assoc_fn(function: &ast::Fn, ty: &str) -> bool {
101    if function
102        .param_list()
103        .is_some_and(|params| params.self_param().is_some())
104    {
105        return false;
106    }
107
108    let Some(name) = function.name().map(|name| name.text().to_string()) else {
109        return false;
110    };
111
112    if EXEMPT_NAMES.contains(&name.as_str())
113        || EXEMPT_PREFIXES
114            .iter()
115            .any(|prefix| name.starts_with(prefix))
116    {
117        return false;
118    }
119
120    !signature_mentions_type(function, ty)
121}
122
123fn signature_mentions_type(function: &ast::Fn, ty: &str) -> bool {
124    let inputs = function
125        .param_list()
126        .map_or_else(String::new, |params| params.syntax().text().to_string());
127
128    if inputs.contains("Self") || inputs.contains(ty) {
129        return true;
130    }
131
132    function.ret_type().is_some_and(|ret| {
133        let text = ret.syntax().text().to_string();
134
135        text.contains("Self") || text.contains(ty)
136    })
137}
138
139crate::tidy_ast_test!(check_assoc_fn_no_self, {
140    crate::example_tests!(EXAMPLES, check_assoc_fn_no_self);
141});