alint_rules/
max_directory_depth.rs1use alint_core::{Context, Error, Level, Result, Rule, RuleSpec, Scope, Violation};
11use serde::Deserialize;
12
13#[derive(Debug, Deserialize)]
14#[serde(deny_unknown_fields)]
15struct Options {
16 max_depth: usize,
17}
18
19#[derive(Debug)]
20pub struct MaxDirectoryDepthRule {
21 id: String,
22 level: Level,
23 policy_url: Option<String>,
24 message: Option<String>,
25 scope: Scope,
26 max_depth: usize,
27}
28
29impl Rule for MaxDirectoryDepthRule {
30 fn id(&self) -> &str {
31 &self.id
32 }
33 fn level(&self) -> Level {
34 self.level
35 }
36 fn policy_url(&self) -> Option<&str> {
37 self.policy_url.as_deref()
38 }
39
40 fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
41 let mut violations = Vec::new();
42 for entry in ctx.index.files() {
43 if !self.scope.matches(&entry.path) {
44 continue;
45 }
46 let depth = entry.path.components().count();
47 if depth > self.max_depth {
48 let msg = self.message.clone().unwrap_or_else(|| {
49 format!(
50 "{} is at depth {depth}; max is {}",
51 entry.path.display(),
52 self.max_depth
53 )
54 });
55 violations.push(Violation::new(msg).with_path(&entry.path));
56 }
57 }
58 Ok(violations)
59 }
60}
61
62pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
63 let paths = spec.paths.as_ref().ok_or_else(|| {
64 Error::rule_config(
65 &spec.id,
66 "max_directory_depth requires a `paths` field (often `\"**\"`)",
67 )
68 })?;
69 let opts: Options = spec
70 .deserialize_options()
71 .map_err(|e| Error::rule_config(&spec.id, format!("invalid options: {e}")))?;
72 if opts.max_depth == 0 {
73 return Err(Error::rule_config(
74 &spec.id,
75 "max_directory_depth `max_depth` must be > 0",
76 ));
77 }
78 if spec.fix.is_some() {
79 return Err(Error::rule_config(
80 &spec.id,
81 "max_directory_depth has no fix op — moving files is a human decision",
82 ));
83 }
84 Ok(Box::new(MaxDirectoryDepthRule {
85 id: spec.id.clone(),
86 level: spec.level,
87 policy_url: spec.policy_url.clone(),
88 message: spec.message.clone(),
89 scope: Scope::from_paths_spec(paths)?,
90 max_depth: opts.max_depth,
91 }))
92}
93
94#[cfg(test)]
95mod tests {
96 use std::path::PathBuf;
97
98 fn depth_of(s: &str) -> usize {
99 PathBuf::from(s).components().count()
100 }
101
102 #[test]
103 fn depth_counts_components() {
104 assert_eq!(depth_of("README.md"), 1);
105 assert_eq!(depth_of("src/lib.rs"), 2);
106 assert_eq!(depth_of("a/b/c/d.rs"), 4);
107 }
108}