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