stern4rust/rules/testing/
test_naming_rule.rs1use syn::Attribute;
6use syn::Item;
7use syn::ItemFn;
8use syn::parse_file;
9
10use crate::reporting::offence::Offence;
11use crate::rule::Rule;
12use crate::source_file::SourceFile;
13
14pub struct TestNamingRule;
32
33impl TestNamingRule {
34 pub const MINIMUM_PARTS: usize = 3;
35 pub const REGISTRIES: [&'static str; 2] = ["all_tests.rs", "mod.rs"];
36 pub const TESTS_ROOT: &'static str = "tests/";
37
38 pub fn new() -> Self {
39 Self
40 }
41
42 fn applies_to(file: &SourceFile) -> bool {
43 let path = file.relative_path();
44 path.starts_with(Self::TESTS_ROOT)
45 && !path
46 .rsplit('/')
47 .next()
48 .is_some_and(|name| Self::REGISTRIES.contains(&name))
49 }
50
51 fn is_test(attrs: &[Attribute]) -> bool {
52 attrs.iter().any(|attr| {
53 attr.path()
54 .segments
55 .last()
56 .is_some_and(|segment| segment.ident == "test")
57 })
58 }
59
60 fn offence(&self, file: &SourceFile, function: &ItemFn) -> Option<Offence> {
61 let name = function.sig.ident.to_string();
62 if name.split('_').count() >= Self::MINIMUM_PARTS {
63 return None;
64 }
65 Some(self.shape_offence(file, &name, function.sig.ident.span().start().line))
66 }
67
68 fn shape_offence(&self, file: &SourceFile, name: &str, line: usize) -> Offence {
69 Offence::new(
70 file.relative_path(),
71 line,
72 self.name(),
73 format!(
74 "`{name}` has fewer than {} parts, so it cannot say what it calls, under what \
75 conditions, and with what result",
76 Self::MINIMUM_PARTS
77 ),
78 format!(
79 "rename it `<method>_<conditions>_<result>`, starting with the method {name} calls"
80 ),
81 )
82 .with_subject(name)
83 }
84}
85
86impl Default for TestNamingRule {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Rule for TestNamingRule {
93 fn name(&self) -> &'static str {
94 "test-naming"
95 }
96
97 fn check(&self, file: &SourceFile) -> Vec<Offence> {
98 if !Self::applies_to(file) {
99 return Vec::new();
100 }
101 let Ok(syntax) = parse_file(&file.contents()) else {
102 return Vec::new();
103 };
104 syntax
105 .items
106 .iter()
107 .filter_map(|item| match item {
108 Item::Fn(function) if Self::is_test(&function.attrs) => Some(function),
109 _ => None,
110 })
111 .filter_map(|function| self.offence(file, function))
112 .collect()
113 }
114
115 fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
116 Vec::new()
117 }
118
119 fn requirement(&self) -> Option<&'static str> {
120 None
121 }
122
123 fn is_configured(&self) -> bool {
124 true
125 }
126}