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