Skip to main content

alint_core/
scope.rs

1use std::path::Path;
2
3use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
4
5use crate::config::PathsSpec;
6use crate::error::{Error, Result};
7
8/// Compiled include/exclude matcher built from a [`PathsSpec`] or raw pattern list.
9///
10/// Patterns prefixed with `!` are treated as excludes when passed as a flat list.
11/// Paths are matched relative to the repository root. Globs are compiled with
12/// `literal_separator(true)` — i.e., Git-style semantics where `*` never
13/// crosses a path separator. `**` is required to descend into subdirectories.
14#[derive(Debug, Clone)]
15pub struct Scope {
16    include: GlobSet,
17    exclude: GlobSet,
18    has_include: bool,
19}
20
21fn compile(pattern: &str) -> Result<Glob> {
22    GlobBuilder::new(pattern)
23        .literal_separator(true)
24        .build()
25        .map_err(|source| Error::Glob {
26            pattern: pattern.to_string(),
27            source,
28        })
29}
30
31impl Scope {
32    pub fn from_patterns(patterns: &[String]) -> Result<Self> {
33        let mut include = GlobSetBuilder::new();
34        let mut exclude = GlobSetBuilder::new();
35        let mut has_include = false;
36        for pattern in patterns {
37            if let Some(rest) = pattern.strip_prefix('!') {
38                exclude.add(compile(rest)?);
39            } else {
40                include.add(compile(pattern)?);
41                has_include = true;
42            }
43        }
44        Ok(Self {
45            include: include.build().map_err(|source| Error::Glob {
46                pattern: patterns.join(","),
47                source,
48            })?,
49            exclude: exclude.build().map_err(|source| Error::Glob {
50                pattern: patterns.join(","),
51                source,
52            })?,
53            has_include,
54        })
55    }
56
57    pub fn from_paths_spec(spec: &PathsSpec) -> Result<Self> {
58        match spec {
59            PathsSpec::Single(s) => Self::from_patterns(std::slice::from_ref(s)),
60            PathsSpec::Many(v) => Self::from_patterns(v),
61            PathsSpec::IncludeExclude { include, exclude } => {
62                let mut combined = include.clone();
63                for e in exclude {
64                    combined.push(format!("!{e}"));
65                }
66                Self::from_patterns(&combined)
67            }
68        }
69    }
70
71    /// Match-all scope (used when no `paths` is configured on a rule).
72    pub fn match_all() -> Self {
73        let mut include = GlobSetBuilder::new();
74        include.add(compile("**").expect("`**` must compile"));
75        Self {
76            include: include.build().expect("`**` GlobSet must build"),
77            exclude: GlobSet::empty(),
78            has_include: true,
79        }
80    }
81
82    pub fn matches(&self, path: &Path) -> bool {
83        if self.exclude.is_match(path) {
84            return false;
85        }
86        if !self.has_include {
87            return true;
88        }
89        self.include.is_match(path)
90    }
91}