Skip to main content

stern4rust/rules/source/
pure_traits_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use 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
17// A trait declares; it does not implement.
18//
19// A default body reads as a convenience and works as a decision nobody made.
20// The implementor that says nothing about a method is indistinguishable from the
21// one that considered it and found the default right, so the question of which
22// of the two you are looking at cannot be answered by reading either file. Make
23// the body a declaration and every implementor has to answer, in its own file,
24// where the answer is.
25//
26// The other half of the requirement -- that every implementor implements every
27// method -- needs no rule. With no default to fall back on, `rustc` refuses to
28// compile an incomplete impl (`E0046`), immediately and more precisely than this
29// tool could. Only the half the compiler is silent about is checked here.
30//
31// Only methods are reported. An associated type and an associated constant may
32// carry a default without any of this being true of them: neither is behaviour,
33// so neither lets an implementor inherit a decision while appearing to have made
34// one.
35//
36// tests/ is exempt. A test file declares traits to stand in for real ones, and
37// a stand-in with a body is the shape those fakes are supposed to have.
38pub 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    // Descends into inline modules: a body does not stop being a body for
52    // sitting one level down.
53    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    // Every body is reported rather than the trait once, because each one is a
71    // separate edit in a different set of files.
72    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    // Reported against the method rather than the trait, because the body is
86    // what has to go.
87    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}