alint_rules/
for_each_file.rs1use alint_core::{Context, Error, Level, NestedRuleSpec, Result, Rule, RuleSpec, Scope, Violation};
20use serde::Deserialize;
21
22use crate::for_each_dir::{IterateMode, evaluate_for_each};
23
24#[derive(Debug, Deserialize)]
25#[serde(deny_unknown_fields)]
26struct Options {
27 select: String,
28 require: Vec<NestedRuleSpec>,
29}
30
31#[derive(Debug)]
32pub struct ForEachFileRule {
33 id: String,
34 level: Level,
35 policy_url: Option<String>,
36 select_scope: Scope,
37 require: Vec<NestedRuleSpec>,
38}
39
40impl Rule for ForEachFileRule {
41 fn id(&self) -> &str {
42 &self.id
43 }
44 fn level(&self) -> Level {
45 self.level
46 }
47 fn policy_url(&self) -> Option<&str> {
48 self.policy_url.as_deref()
49 }
50
51 fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
52 evaluate_for_each(
53 &self.id,
54 self.level,
55 &self.select_scope,
56 &self.require,
57 ctx,
58 IterateMode::Files,
59 )
60 }
61}
62
63pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
64 let opts: Options = spec
65 .deserialize_options()
66 .map_err(|e| Error::rule_config(&spec.id, format!("invalid options: {e}")))?;
67 if opts.require.is_empty() {
68 return Err(Error::rule_config(
69 &spec.id,
70 "for_each_file requires at least one nested rule under `require:`",
71 ));
72 }
73 let select_scope = Scope::from_patterns(&[opts.select])?;
74 Ok(Box::new(ForEachFileRule {
75 id: spec.id.clone(),
76 level: spec.level,
77 policy_url: spec.policy_url.clone(),
78 select_scope,
79 require: opts.require,
80 }))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use alint_core::{FileEntry, FileIndex, RuleRegistry};
87 use std::path::{Path, PathBuf};
88
89 fn index(entries: &[(&str, bool)]) -> FileIndex {
90 FileIndex {
91 entries: entries
92 .iter()
93 .map(|(p, is_dir)| FileEntry {
94 path: PathBuf::from(p),
95 is_dir: *is_dir,
96 size: 1,
97 })
98 .collect(),
99 }
100 }
101
102 fn registry() -> RuleRegistry {
103 crate::builtin_registry()
104 }
105
106 #[test]
107 fn passes_when_every_file_has_required_sibling() {
108 let require: Vec<NestedRuleSpec> = vec![
109 serde_yaml_ng::from_str("kind: file_exists\npaths: \"{dir}/{stem}.h\"\n").unwrap(),
110 ];
111 let r = ForEachFileRule {
112 id: "t".into(),
113 level: Level::Error,
114 policy_url: None,
115 select_scope: Scope::from_patterns(&["**/*.c".to_string()]).unwrap(),
116 require,
117 };
118 let idx = index(&[
119 ("src/foo.c", false),
120 ("src/foo.h", false),
121 ("src/bar.c", false),
122 ("src/bar.h", false),
123 ]);
124 let reg = registry();
125 let ctx = Context {
126 root: Path::new("/"),
127 index: &idx,
128 registry: Some(®),
129 facts: None,
130 vars: None,
131 git_tracked: None,
132 };
133 let v = r.evaluate(&ctx).unwrap();
134 assert!(v.is_empty(), "unexpected: {v:?}");
135 }
136
137 #[test]
138 fn violates_per_missing_sibling() {
139 let require: Vec<NestedRuleSpec> = vec![
140 serde_yaml_ng::from_str("kind: file_exists\npaths: \"{dir}/{stem}.h\"\n").unwrap(),
141 ];
142 let r = ForEachFileRule {
143 id: "t".into(),
144 level: Level::Error,
145 policy_url: None,
146 select_scope: Scope::from_patterns(&["**/*.c".to_string()]).unwrap(),
147 require,
148 };
149 let idx = index(&[
150 ("src/foo.c", false),
151 ("src/foo.h", false), ("src/bar.c", false), ("src/baz.c", false), ]);
155 let reg = registry();
156 let ctx = Context {
157 root: Path::new("/"),
158 index: &idx,
159 registry: Some(®),
160 facts: None,
161 vars: None,
162 git_tracked: None,
163 };
164 let v = r.evaluate(&ctx).unwrap();
165 assert_eq!(v.len(), 2);
166 }
167}