use syn::Attribute;
use syn::Item;
use syn::ItemMod;
use syn::parse_file;
use crate::reporting::offence::Offence;
use crate::rule::Rule;
use crate::source_file::SourceFile;
pub struct TestFileNamePostfixRule;
impl TestFileNamePostfixRule {
pub const POSTFIX: &'static str = "_tests.rs";
pub const REGISTRIES: [&'static str; 2] = ["all_tests.rs", "mod.rs"];
pub const TESTS_ROOT: &'static str = "tests/";
pub fn new() -> Self {
Self
}
fn applies_to(file: &SourceFile) -> bool {
let path = file.relative_path();
path.starts_with(Self::TESTS_ROOT)
&& !path
.rsplit('/')
.next()
.is_some_and(|name| Self::REGISTRIES.contains(&name))
}
fn tests_in(items: &[Item]) -> usize {
items
.iter()
.map(|item| match item {
Item::Fn(function) if Self::is_test(&function.attrs) => 1,
Item::Mod(module) => Self::inside(module).map(Self::tests_in).unwrap_or_default(),
_ => 0,
})
.sum()
}
fn inside(module: &ItemMod) -> Option<&[Item]> {
module.content.as_ref().map(|(_, items)| items.as_slice())
}
fn is_test(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
attr.path()
.segments
.last()
.is_some_and(|segment| segment.ident == "test")
})
}
fn suggested_name(relative_path: &str) -> String {
let stem = relative_path
.strip_suffix(".rs")
.unwrap_or(relative_path)
.to_string();
format!("{stem}{}", Self::POSTFIX)
}
fn offence(&self, file: &SourceFile, found: usize) -> Offence {
let path = file.relative_path();
let suggested = Self::suggested_name(path);
Offence::new(
path,
1,
self.name(),
format!(
"{path} holds {found} test(s) but its name does not end in `{}`, so nothing \
pairs it with the source file it exercises",
Self::POSTFIX
),
format!("rename it `{suggested}`"),
)
.with_subject(path)
.with_expected(&suggested)
}
}
impl Default for TestFileNamePostfixRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for TestFileNamePostfixRule {
fn name(&self) -> &'static str {
"test-file-name-postfix"
}
fn check(&self, file: &SourceFile) -> Vec<Offence> {
if !Self::applies_to(file) || file.relative_path().ends_with(Self::POSTFIX) {
return Vec::new();
}
let Ok(syntax) = parse_file(&file.contents()) else {
return Vec::new();
};
match Self::tests_in(&syntax.items) {
0 => Vec::new(),
found => vec![self.offence(file, found)],
}
}
fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
Vec::new()
}
fn requirement(&self) -> Option<&'static str> {
None
}
fn is_configured(&self) -> bool {
true
}
}