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