Skip to main content

stern4rust/rules/
directory_subfolder_count_rule.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::path::Path;
6
7use crate::finding::package_tree::PackageTree;
8use crate::reporting::offence::Offence;
9use crate::rule::Rule;
10use crate::source_file::SourceFile;
11
12// A directory holds a number of subfolders a reader can hold in their head.
13//
14// The counterweight to `directory-file-count`. That rule creates folders; this
15// one stops the creating from being the answer to everything, because a
16// directory with twenty subfolders is exactly as unreadable as one with a
17// hundred files and looks tidier while being worse.
18//
19// It is checked at every level rather than only at the root, so pushing the
20// sprawl one directory down does not escape it.
21//
22// Measured across eight repositories at the time it was written it finds
23// **nothing**: the deepest tree is two levels and no directory has more than one
24// subfolder. That is stated plainly because a rule that has never fired has not
25// yet earned the reader's trust, and this one is a guard against a shape the
26// family has not reached rather than a description of a problem it has.
27pub struct DirectorySubfolderCountRule {
28    limit: usize,
29}
30
31impl DirectorySubfolderCountRule {
32    pub const DEFAULT_LIMIT: usize = 5;
33
34    pub fn new(limit: usize) -> Self {
35        Self { limit }
36    }
37
38    fn offence(&self, tree: &PackageTree, directory: &Path, found: usize) -> Offence {
39        let subject = Self::shown(directory);
40        let carrier = tree
41            .registries_in(directory)
42            .first()
43            .map(|path| Self::shown(path))
44            .unwrap_or_else(|| subject.clone());
45        Offence::new(
46            &carrier,
47            1,
48            self.name(),
49            format!(
50                "{subject} holds {found} subfolders, more than the {} a directory may hold",
51                self.limit
52            ),
53            format!(
54                "group the subfolders of {subject} so that no directory holds more than {}",
55                self.limit
56            ),
57        )
58        .with_subject(&subject)
59    }
60
61    fn shown(path: &Path) -> String {
62        let shown = path.to_string_lossy().replace('\\', "/");
63        if shown.is_empty() || shown == "." {
64            return "the package root".to_string();
65        }
66        shown
67    }
68}
69
70impl Default for DirectorySubfolderCountRule {
71    fn default() -> Self {
72        Self::new(Self::DEFAULT_LIMIT)
73    }
74}
75
76impl Rule for DirectorySubfolderCountRule {
77    fn name(&self) -> &'static str {
78        "directory-subfolder-count"
79    }
80
81    fn check_workspace(&self, files: &[SourceFile]) -> Vec<Offence> {
82        let tree = PackageTree::of(files);
83        tree.directories()
84            .iter()
85            .filter_map(|directory| {
86                let found = tree.subdirectories_of(directory).len();
87                (found > self.limit).then(|| self.offence(&tree, directory, found))
88            })
89            .collect()
90    }
91}