Skip to main content

stern4rust/
exclusion_set.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use anyhow::Result;
6use std::path::Path;
7use std::path::PathBuf;
8
9use crate::exclusion::Exclusion;
10use crate::exclusion_outcome::ExclusionOutcome;
11
12// Every `--exclude` pattern this run was given, applied to the walked paths.
13//
14// Exclusion happens after the walk rather than by pruning it. Pruning would be
15// faster on a large vendored tree and would cost the one thing that makes an
16// exclusion acceptable at all: knowing how many files each pattern removed. A
17// tree that is never entered cannot be counted, and an exclusion nobody can
18// count is the silent skip this tool removed from the walker in 0.4.0.
19//
20// A path is attributed to the **first** pattern that covers it, so two
21// overlapping patterns do not both claim the same file and inflate the total.
22pub struct ExclusionSet {
23    exclusions: Vec<Exclusion>,
24}
25
26impl ExclusionSet {
27    pub fn new(patterns: &[String]) -> Result<Self> {
28        let exclusions = patterns
29            .iter()
30            .map(|pattern| Exclusion::new(pattern))
31            .collect::<Result<Vec<_>>>()?;
32        Ok(Self { exclusions })
33    }
34
35    pub fn is_empty(&self) -> bool {
36        self.exclusions.is_empty()
37    }
38
39    pub fn apply(&self, paths: Vec<PathBuf>, root: &Path) -> ExclusionOutcome {
40        let mut counts = vec![0usize; self.exclusions.len()];
41        let mut kept = Vec::new();
42        for path in paths {
43            match self.covering(&path, root) {
44                Some(index) => counts[index] += 1,
45                None => kept.push(path),
46            }
47        }
48        let excluded = self
49            .exclusions
50            .iter()
51            .zip(counts)
52            .map(|(exclusion, count)| (exclusion.pattern().to_string(), count))
53            .collect();
54        ExclusionOutcome::new(kept, excluded)
55    }
56
57    fn covering(&self, path: &Path, root: &Path) -> Option<usize> {
58        let relative = path.strip_prefix(root).unwrap_or(path);
59        self.exclusions
60            .iter()
61            .position(|exclusion| exclusion.matches(relative))
62    }
63}