stern4rust/rules/source/
pure_traits_rule.rs1use syn::Item;
6use syn::ItemMod;
7use syn::ItemTrait;
8use syn::TraitItem;
9use syn::TraitItemFn;
10use syn::parse_file;
11
12use crate::reporting::offence::Offence;
13use crate::reporting::rule_explanation::RuleExplanation;
14use crate::rule::Rule;
15use crate::source_file::SourceFile;
16
17pub struct PureTraitsRule;
39
40impl PureTraitsRule {
41 pub const SOURCE_ROOT: &'static str = "src/";
42
43 pub fn new() -> Self {
44 Self
45 }
46
47 fn applies_to(file: &SourceFile) -> bool {
48 file.relative_path().starts_with(Self::SOURCE_ROOT)
49 }
50
51 fn declarations(items: &[Item]) -> Vec<&ItemTrait> {
54 items
55 .iter()
56 .flat_map(|item| match item {
57 Item::Trait(declaration) => vec![declaration],
58 Item::Mod(module) => Self::inside(module)
59 .map(Self::declarations)
60 .unwrap_or_default(),
61 _ => Vec::new(),
62 })
63 .collect()
64 }
65
66 fn inside(module: &ItemMod) -> Option<&[Item]> {
67 module.content.as_ref().map(|(_, items)| items.as_slice())
68 }
69
70 fn bodies(&self, file: &SourceFile, declaration: &ItemTrait) -> Vec<Offence> {
73 let declared_by = declaration.ident.to_string();
74 declaration
75 .items
76 .iter()
77 .filter_map(|item| match item {
78 TraitItem::Fn(method) if method.default.is_some() => Some(method),
79 _ => None,
80 })
81 .map(|method| self.offence(file, &declared_by, method))
82 .collect()
83 }
84
85 fn offence(&self, file: &SourceFile, declared_by: &str, method: &TraitItemFn) -> Offence {
88 let subject = format!("{declared_by}::{}", method.sig.ident);
89 Offence::new(
90 file.relative_path(),
91 method.sig.ident.span().start().line,
92 self.name(),
93 format!(
94 "`{subject}` has a default body, so an implementor that says nothing about it \
95 cannot be told from one that chose it"
96 ),
97 "move the body into each implementor".to_string(),
98 )
99 .with_subject(&subject)
100 }
101}
102
103impl Default for PureTraitsRule {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl Rule for PureTraitsRule {
110 fn name(&self) -> &'static str {
111 "pure-traits"
112 }
113
114 fn check(&self, file: &SourceFile) -> Vec<Offence> {
115 if !Self::applies_to(file) {
116 return Vec::new();
117 }
118 let Ok(syntax) = parse_file(&file.contents()) else {
119 return Vec::new();
120 };
121 Self::declarations(&syntax.items)
122 .into_iter()
123 .flat_map(|declaration| self.bodies(file, declaration))
124 .collect()
125 }
126
127 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
128 Vec::new()
129 }
130
131 fn requirement(&self) -> Option<&'static str> {
132 None
133 }
134
135 fn is_configured(&self) -> bool {
136 true
137 }
138
139 fn explanation(&self) -> RuleExplanation {
140 RuleExplanation::new(
141 self.name(),
142 "A trait declares; it does not implement.",
143 "trait Store {\n fn commit(&self) -> bool {\n true\n }\n}",
144 "trait Store {\n fn commit(&self) -> bool;\n}",
145 )
146 }
147}