wowlab_tidy/languages/rust/rules/api/
trait_logic_not_inherent.rs1use ra_ap_syntax::{ast, ast::HasName};
2use wowlab_types::sim::{FastMap, FastSet};
3
4use super::support::type_name;
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "forwarding trait impl",
11 code: "trait Download { fn get(&self); }\nstruct Client;\nimpl Client {\n fn get(&self) {\n let a = 1;\n let b = a + 1;\n let c = b + 1;\n let _ = c;\n }\n}\nimpl Download for Client {\n fn get(&self) { Self::get(self) }\n}",
12 pass: true,
13 },
14 Example {
15 label: "logic in trait impl without inherent method",
16 code: "trait Download { fn get(&self); }\nstruct Client;\nimpl Download for Client {\n fn get(&self) {\n let a = 1;\n let b = a + 1;\n let c = b + 1;\n let _ = c;\n }\n}",
17 pass: false,
18 },
19 Example {
20 label: "logic with same-named inherent method elsewhere",
21 code: "trait Download { fn get(&self); }\nstruct Client;\nimpl Client {\n fn get(&self) {}\n}\nimpl Download for Client {\n fn get(&self) {\n let a = 1;\n let b = a + 1;\n let c = b + 1;\n let _ = c;\n }\n}",
22 pass: true,
23 },
24 Example {
25 label: "foreign trait impl exempt",
26 code: "struct Client;\nimpl std::fmt::Display for Client {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let a = 1;\n let b = a + 1;\n let c = b + 1;\n write!(f, \"{c}\")\n }\n}",
27 pass: true,
28 },
29 Example {
30 label: "body at threshold",
31 code: "trait Download { fn get(&self); }\nstruct Client;\nimpl Download for Client {\n fn get(&self) {\n let a = 1;\n let b = a + 1;\n let _ = b;\n }\n}",
32 pass: true,
33 },
34 Example {
35 label: "logic in test module",
36 code: "#[cfg(test)]\nmod tests {\n trait Download { fn get(&self); }\n struct Client;\n impl Download for Client {\n fn get(&self) {\n let a = 1;\n let b = a + 1;\n let c = b + 1;\n let _ = c;\n }\n }\n}",
37 pass: true,
38 },
39];
40
41crate::ast_rule!(
42 trait_logic_not_inherent,
43 "Flag substantial logic in impls of locally-defined traits when the type has no same-named inherent method.",
44 "Essential functionality buried in trait impls forces users to import the trait to call it; implement inherently and forward from the trait (M-ESSENTIAL-FN-INHERENT).",
45 Low,
46 params { threshold: i64 = 3 },
47);
48
49struct LocalFacts {
50 traits: FastSet<String>,
51 inherent: FastMap<String, FastSet<String>>,
52}
53
54fn collect_local_facts(ctx: &AstCtx<'_>) -> LocalFacts {
56 let mut facts = LocalFacts {
57 traits: FastSet::default(),
58 inherent: FastMap::default(),
59 };
60
61 for item in ctx.nodes::<ast::Trait>() {
62 if let Some(name) = item.name() {
63 facts.traits.insert(name.text().to_string());
64 }
65 }
66
67 for item in ctx
68 .nodes::<ast::Impl>()
69 .filter(|item| item.trait_().is_none())
70 {
71 record_inherent_methods(&item, &mut facts);
72 }
73
74 facts
75}
76
77fn record_inherent_methods(item: &ast::Impl, facts: &mut LocalFacts) {
79 let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
80 return;
81 };
82 let methods = facts.inherent.entry(name).or_default();
83
84 for method in item
85 .assoc_item_list()
86 .into_iter()
87 .flat_map(|list| list.assoc_items())
88 .filter_map(|assoc| match assoc {
89 ast::AssocItem::Fn(method) => Some(method),
90 _ => None,
91 })
92 {
93 if let Some(name) = method.name() {
94 methods.insert(name.text().to_string());
95 }
96 }
97}
98
99fn check_trait_logic_not_inherent(ctx: &AstCtx<'_>) -> Vec<Violation> {
100 let threshold = ctx
101 .file
102 .config
103 .get_usize("rust_trait_logic_not_inherent", &PARAMS[0]);
104 let facts = collect_local_facts(ctx);
105
106 ctx.nodes::<ast::Impl>()
107 .filter(|item| !ctx.is_in_test(item))
108 .flat_map(|item| check_impl(ctx, &item, &facts, threshold))
109 .collect()
110}
111
112fn check_impl(
113 ctx: &AstCtx<'_>,
114 item: &ast::Impl,
115 facts: &LocalFacts,
116 threshold: usize,
117) -> Vec<Violation> {
118 let Some(trait_name) = item.trait_().and_then(|ty| type_name(&ty)) else {
119 return Vec::new();
120 };
121
122 if !facts.traits.contains(&trait_name) {
123 return Vec::new();
124 }
125
126 let Some(self_name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
127 return Vec::new();
128 };
129 let inherent = facts.inherent.get(&self_name);
130
131 let associated_items = item
132 .assoc_item_list()
133 .into_iter()
134 .flat_map(|list| list.assoc_items());
135
136 associated_items
137 .filter_map(|assoc| match assoc { ast::AssocItem::Fn(method) => Some(method), _ => None })
138 .filter_map(|method| {
139 let name = method.name()?;
140 let statements = method.body()?.stmt_list()?.statements().count();
141
142 if statements <= threshold || inherent.is_some_and(|methods| methods.contains(name.text().as_str())) { return None }
143
144 Some(ctx.violation(&name, format!(
145 "trait method `{trait_name}::{name}` for `{self_name}` holds {statements} statements of logic — implement inherently and forward (M-ESSENTIAL-FN-INHERENT)"
146 )))
147 }).collect()
148}
149
150crate::tidy_ast_test!(check_trait_logic_not_inherent, {
151 crate::example_tests!(EXAMPLES, check_trait_logic_not_inherent);
152});