use std::path::Path;
use crate::finding::model::package_tree::PackageTree;
use crate::reporting::offence::Offence;
use crate::reporting::rule_explanation::RuleExplanation;
use crate::rule::Rule;
use crate::source_file::SourceFile;
pub struct DirectoryFileCountRule {
limit: usize,
}
impl DirectoryFileCountRule {
pub const DEFAULT_LIMIT: usize = 20;
pub const INDEXES: [&'static str; 3] = ["all_tests.rs", "lib.rs", "mod.rs"];
pub fn new(limit: usize) -> Self {
Self { limit }
}
fn counted_in(tree: &PackageTree, directory: &Path) -> usize {
tree.files_in(directory)
.into_iter()
.filter(|path| !Self::is_index(path))
.count()
}
fn is_index(path: &Path) -> bool {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| Self::INDEXES.contains(&name))
}
fn offence(&self, tree: &PackageTree, directory: &Path, found: usize) -> Offence {
let subject = Self::shown(directory);
let carrier = tree
.registries_in(directory)
.first()
.map(|path| Self::shown(path))
.unwrap_or_else(|| subject.clone());
Offence::new(
&carrier,
1,
self.name(),
format!(
"{subject} holds {found} files, more than the {} a directory may hold",
self.limit
),
format!(
"group the files of {subject} into subfolders of at most {} each, each with its \
own mod.rs declared by this index",
self.limit
),
)
.with_subject(&subject)
}
fn shown(path: &Path) -> String {
let shown = path.to_string_lossy().replace('\\', "/");
if shown.is_empty() || shown == "." {
return "the package root".to_string();
}
shown
}
}
impl Default for DirectoryFileCountRule {
fn default() -> Self {
Self::new(Self::DEFAULT_LIMIT)
}
}
impl Rule for DirectoryFileCountRule {
fn name(&self) -> &'static str {
"directory-file-count"
}
fn check(&self, _file: &SourceFile) -> Vec<Offence> {
Vec::new()
}
fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
let tree = PackageTree::of(files);
tree.directories()
.iter()
.filter_map(|directory| {
let found = Self::counted_in(&tree, directory);
(found > self.limit).then(|| self.offence(&tree, directory, found))
})
.collect()
}
fn requirement(&self) -> Option<&'static str> {
None
}
fn is_configured(&self) -> bool {
true
}
fn explanation(&self) -> RuleExplanation {
RuleExplanation::new(
self.name(),
"A directory holds a number of files a reader can hold in their head.",
"src/parsing/ -- 24 files",
"src/parsing/ -- 12 files\nsrc/parsing/naming/ -- 12 files",
)
}
}