use std::iter::once;
use syn::Attribute;
use syn::Expr;
use syn::Item;
use syn::ItemMod;
use syn::Lit;
use syn::Meta;
use syn::parse_file;
use crate::reporting::offence::Offence;
use crate::rule::Rule;
use crate::source_file::SourceFile;
pub struct DeclaredByNameRule;
impl DeclaredByNameRule {
pub const ATTRIBUTE: &'static str = "path";
pub fn new() -> Self {
Self
}
fn declarations(items: &[Item]) -> Vec<&ItemMod> {
items
.iter()
.flat_map(|item| match item {
Item::Mod(module) => once(module)
.chain(
Self::inside(module)
.map(Self::declarations)
.unwrap_or_default(),
)
.collect::<Vec<&ItemMod>>(),
_ => Vec::new(),
})
.collect()
}
fn inside(module: &ItemMod) -> Option<&[Item]> {
module.content.as_ref().map(|(_, items)| items.as_slice())
}
fn target_of(attrs: &[Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| match &attr.meta {
Meta::NameValue(pair) if pair.path.is_ident(Self::ATTRIBUTE) => match &pair.value {
Expr::Lit(literal) => match &literal.lit {
Lit::Str(text) => Some(text.value()),
_ => None,
},
_ => None,
},
_ => None,
})
}
fn offence(&self, file: &SourceFile, module: &ItemMod, target: &str) -> Offence {
let name = module.ident.to_string();
let expected = format!("{name}.rs");
Offence::new(
file.relative_path(),
module.ident.span().start().line,
self.name(),
format!(
"`mod {name}` is reached through `#[path = \"{target}\"]`, so the file it \
declares cannot be found from its name"
),
format!(
"move `{target}` to `{expected}` beside this file and drop the `#[path]` attribute"
),
)
.with_subject(&name)
.with_expected(&expected)
}
}
impl Default for DeclaredByNameRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for DeclaredByNameRule {
fn name(&self) -> &'static str {
"declared-by-name"
}
fn check(&self, file: &SourceFile) -> Vec<Offence> {
let Ok(syntax) = parse_file(&file.contents()) else {
return Vec::new();
};
Self::declarations(&syntax.items)
.into_iter()
.filter_map(|module| {
Self::target_of(&module.attrs).map(|target| self.offence(file, module, &target))
})
.collect()
}
fn check_workspace(&self, _files: &[SourceFile]) -> Vec<Offence> {
Vec::new()
}
fn is_configured(&self) -> bool {
true
}
}