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