stern4rust/rules/layout/
directory_file_count_rule.rs1use std::path::Path;
6
7use crate::finding::model::package_tree::PackageTree;
8use crate::reporting::offence::Offence;
9use crate::rule::Rule;
10use crate::source_file::SourceFile;
11
12pub struct DirectoryFileCountRule {
31 limit: usize,
32}
33
34impl DirectoryFileCountRule {
35 pub const DEFAULT_LIMIT: usize = 20;
36 pub const INDEXES: [&'static str; 3] = ["all_tests.rs", "lib.rs", "mod.rs"];
37
38 pub fn new(limit: usize) -> Self {
39 Self { limit }
40 }
41
42 fn counted_in(tree: &PackageTree, directory: &Path) -> usize {
43 tree.files_in(directory)
44 .into_iter()
45 .filter(|path| !Self::is_index(path))
46 .count()
47 }
48
49 fn is_index(path: &Path) -> bool {
50 path.file_name()
51 .and_then(|name| name.to_str())
52 .is_some_and(|name| Self::INDEXES.contains(&name))
53 }
54
55 fn offence(&self, tree: &PackageTree, directory: &Path, found: usize) -> Offence {
59 let subject = Self::shown(directory);
60 let carrier = tree
61 .registries_in(directory)
62 .first()
63 .map(|path| Self::shown(path))
64 .unwrap_or_else(|| subject.clone());
65 Offence::new(
66 &carrier,
67 1,
68 self.name(),
69 format!(
70 "{subject} holds {found} files, more than the {} a directory may hold",
71 self.limit
72 ),
73 format!(
74 "group the files of {subject} into subfolders of at most {} each, each with its \
75 own mod.rs declared by this index",
76 self.limit
77 ),
78 )
79 .with_subject(&subject)
80 }
81
82 fn shown(path: &Path) -> String {
83 let shown = path.to_string_lossy().replace('\\', "/");
84 if shown.is_empty() || shown == "." {
85 return "the package root".to_string();
86 }
87 shown
88 }
89}
90
91impl Default for DirectoryFileCountRule {
92 fn default() -> Self {
93 Self::new(Self::DEFAULT_LIMIT)
94 }
95}
96
97impl Rule for DirectoryFileCountRule {
98 fn name(&self) -> &'static str {
99 "directory-file-count"
100 }
101
102 fn check(&self, _file: &SourceFile) -> Vec<Offence> {
103 Vec::new()
104 }
105
106 fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
109 let tree = PackageTree::of(files);
110 tree.directories()
111 .iter()
112 .filter_map(|directory| {
113 let found = Self::counted_in(&tree, directory);
114 (found > self.limit).then(|| self.offence(&tree, directory, found))
115 })
116 .collect()
117 }
118
119 fn requirement(&self) -> Option<&'static str> {
120 None
121 }
122
123 fn is_configured(&self) -> bool {
124 true
125 }
126}