use ra_ap_syntax::{ast, ast::HasName};
use std::collections::{HashMap, HashSet};
use super::support::type_name;
use crate::{AstCtx, Example, Violation};
#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
Example {
label: "forwarding trait impl",
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}",
pass: true,
},
Example {
label: "logic in trait impl without inherent method",
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}",
pass: false,
},
Example {
label: "logic with same-named inherent method elsewhere",
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}",
pass: true,
},
Example {
label: "foreign trait impl exempt",
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}",
pass: true,
},
Example {
label: "body at threshold",
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}",
pass: true,
},
Example {
label: "logic in test module",
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}",
pass: true,
},
];
crate::ast_rule!(
trait_logic_not_inherent,
"Flag substantial logic in impls of locally-defined traits when the type has no same-named inherent method.",
"Essential functionality buried in trait impls forces users to import the trait to call it; implement inherently and forward from the trait.",
Low,
params { threshold: i64 = 3 },
);
struct LocalFacts {
traits: HashSet<String>,
inherent: HashMap<String, HashSet<String>>,
}
fn collect_local_facts(ctx: &AstCtx<'_>) -> LocalFacts {
let mut facts = LocalFacts {
traits: HashSet::default(),
inherent: HashMap::default(),
};
for item in ctx.nodes::<ast::Trait>() {
if let Some(name) = item.name() {
facts.traits.insert(name.text().to_string());
}
}
for item in ctx
.nodes::<ast::Impl>()
.filter(|item| item.trait_().is_none())
{
record_inherent_methods(&item, &mut facts);
}
facts
}
fn record_inherent_methods(item: &ast::Impl, facts: &mut LocalFacts) {
let Some(name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
return;
};
let methods = facts.inherent.entry(name).or_default();
for method in item
.assoc_item_list()
.into_iter()
.flat_map(|list| list.assoc_items())
.filter_map(|assoc| match assoc {
ast::AssocItem::Fn(method) => Some(method),
_ => None,
})
{
if let Some(name) = method.name() {
methods.insert(name.text().to_string());
}
}
}
fn check_trait_logic_not_inherent(ctx: &AstCtx<'_>) -> Vec<Violation> {
let threshold = ctx
.file
.config
.get_usize("rust_trait_logic_not_inherent", &PARAMS[0]);
let facts = collect_local_facts(ctx);
ctx.nodes::<ast::Impl>()
.filter(|item| !ctx.is_in_test(item))
.flat_map(|item| check_impl(ctx, &item, &facts, threshold))
.collect()
}
fn check_impl(
ctx: &AstCtx<'_>,
item: &ast::Impl,
facts: &LocalFacts,
threshold: usize,
) -> Vec<Violation> {
let Some(trait_name) = item.trait_().and_then(|ty| type_name(&ty)) else {
return Vec::new();
};
if !facts.traits.contains(&trait_name) {
return Vec::new();
}
let Some(self_name) = item.self_ty().and_then(|ty| type_name(&ty)) else {
return Vec::new();
};
let inherent = facts.inherent.get(&self_name);
let associated_items = item
.assoc_item_list()
.into_iter()
.flat_map(|list| list.assoc_items());
associated_items
.filter_map(|assoc| match assoc { ast::AssocItem::Fn(method) => Some(method), _ => None })
.filter_map(|method| {
let name = method.name()?;
let statements = method.body()?.stmt_list()?.statements().count();
if statements <= threshold || inherent.is_some_and(|methods| methods.contains(name.text().as_str())) { return None }
Some(ctx.violation(&name, format!(
"trait method `{trait_name}::{name}` for `{self_name}` holds {statements} statements of logic — implement inherently and forward"
)))
}).collect()
}
crate::rulewright_ast_test!(check_trait_logic_not_inherent, {
crate::example_tests!(EXAMPLES, check_trait_logic_not_inherent);
});