Skip to main content

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

1use 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
54// #t(fn: rust_alloc_in_loop) the collector owns each trait name it stores.
55fn 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
77// #t(fn: rust_alloc_in_loop) the collector owns each method name it stores.
78fn 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});