Skip to main content

alint_rules/
file_absent.rs

1//! `file_absent` — emit a violation for every file matching `paths`.
2
3use alint_core::{
4    Context, Error, FixSpec, Fixer, Level, PathsSpec, Result, Rule, RuleSpec, Scope, Violation,
5};
6
7use crate::fixers::FileRemoveFixer;
8
9#[derive(Debug)]
10pub struct FileAbsentRule {
11    id: String,
12    level: Level,
13    policy_url: Option<String>,
14    message: Option<String>,
15    scope: Scope,
16    patterns: Vec<String>,
17    fixer: Option<FileRemoveFixer>,
18}
19
20impl Rule for FileAbsentRule {
21    fn id(&self) -> &str {
22        &self.id
23    }
24    fn level(&self) -> Level {
25        self.level
26    }
27    fn policy_url(&self) -> Option<&str> {
28        self.policy_url.as_deref()
29    }
30
31    fn evaluate(&self, ctx: &Context<'_>) -> Result<Vec<Violation>> {
32        let mut violations = Vec::new();
33        for entry in ctx.index.files() {
34            if self.scope.matches(&entry.path) {
35                let msg = self.message.clone().unwrap_or_else(|| {
36                    format!(
37                        "file is forbidden (matches [{}]): {}",
38                        self.patterns.join(", "),
39                        entry.path.display()
40                    )
41                });
42                violations.push(Violation::new(msg).with_path(&entry.path));
43            }
44        }
45        Ok(violations)
46    }
47
48    fn fixer(&self) -> Option<&dyn Fixer> {
49        self.fixer.as_ref().map(|f| f as &dyn Fixer)
50    }
51}
52
53pub fn build(spec: &RuleSpec) -> Result<Box<dyn Rule>> {
54    let Some(paths) = &spec.paths else {
55        return Err(Error::rule_config(
56            &spec.id,
57            "file_absent requires a `paths` field",
58        ));
59    };
60    let fixer = match &spec.fix {
61        Some(FixSpec::FileRemove { .. }) => Some(FileRemoveFixer),
62        Some(other) => {
63            return Err(Error::rule_config(
64                &spec.id,
65                format!("fix.{} is not compatible with file_absent", other.op_name()),
66            ));
67        }
68        None => None,
69    };
70    Ok(Box::new(FileAbsentRule {
71        id: spec.id.clone(),
72        level: spec.level,
73        policy_url: spec.policy_url.clone(),
74        message: spec.message.clone(),
75        scope: Scope::from_paths_spec(paths)?,
76        patterns: patterns_of(paths),
77        fixer,
78    }))
79}
80
81fn patterns_of(spec: &PathsSpec) -> Vec<String> {
82    match spec {
83        PathsSpec::Single(s) => vec![s.clone()],
84        PathsSpec::Many(v) => v.clone(),
85        PathsSpec::IncludeExclude { include, .. } => include.clone(),
86    }
87}