alint_rules/
max_files_per_directory.rs1use std::collections::BTreeMap;
9use std::path::PathBuf;
10
11use alint_core::{Context, Error, Level, Result, Rule, RuleSpec, Scope, Violation};
12use serde::Deserialize;
13
14#[derive(Debug, Deserialize)]
15#[serde(deny_unknown_fields)]
16struct Options {
17 max_files: usize,
18}
19
20#[derive(Debug)]
21pub struct MaxFilesPerDirectoryRule {
22 id: String,
23 level: Level,
24 policy_url: Option<String>,
25 message: Option<String>,
26 scope: Scope,
27 max_files: usize,
28}
29
30impl Rule for MaxFilesPerDirectoryRule {
31 fn id(&self) -> &str {
32 &self.id
33 }
34 fn level(&self) -> Level {
35 self.level
36 }
37 fn policy_url(&self) -> Option<&str> {
38 self.policy_url.as_deref()
39 }
40
41 fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
42 let mut counts: BTreeMap<PathBuf, usize> = BTreeMap::new();
44 for entry in ctx.index.files() {
45 if !self.scope.matches(&entry.path) {
46 continue;
47 }
48 let parent = entry
49 .path
50 .parent()
51 .map(Path::to_path_buf)
52 .unwrap_or_default();
53 *counts.entry(parent).or_insert(0) += 1;
54 }
55 let mut violations = Vec::new();
56 for (dir, count) in counts {
57 if count > self.max_files {
58 let pretty = if dir.as_os_str().is_empty() {
59 "<repo root>".to_string()
60 } else {
61 dir.display().to_string()
62 };
63 let msg = self.message.clone().unwrap_or_else(|| {
64 format!("{pretty} has {count} files; max is {}", self.max_files)
65 });
66 violations.push(Violation::new(msg).with_path(&dir));
67 }
68 }
69 Ok(violations)
70 }
71}
72
73pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
74 let paths = spec.paths.as_ref().ok_or_else(|| {
75 Error::rule_config(
76 &spec.id,
77 "max_files_per_directory requires a `paths` field (often `\"**\"`)",
78 )
79 })?;
80 let opts: Options = spec
81 .deserialize_options()
82 .map_err(|e| Error::rule_config(&spec.id, format!("invalid options: {e}")))?;
83 if opts.max_files == 0 {
84 return Err(Error::rule_config(
85 &spec.id,
86 "max_files_per_directory `max_files` must be > 0",
87 ));
88 }
89 if spec.fix.is_some() {
90 return Err(Error::rule_config(
91 &spec.id,
92 "max_files_per_directory has no fix op — file relocation is a human decision",
93 ));
94 }
95 Ok(Box::new(MaxFilesPerDirectoryRule {
96 id: spec.id.clone(),
97 level: spec.level,
98 policy_url: spec.policy_url.clone(),
99 message: spec.message.clone(),
100 scope: Scope::from_paths_spec(paths)?,
101 max_files: opts.max_files,
102 }))
103}
104
105use std::path::Path;