weavatrix-scan 0.4.2

Deterministic, safe repository scanner for code intelligence
Documentation
use super::{IgnoreRule, RuleAction, RuleMatch, RuleScope, RuleSet, RuleTarget};

impl RuleSet {
    pub(super) fn push(&mut self, rule: IgnoreRule) {
        let index = self.rules.len();
        if rule.scope == RuleScope::Anywhere && rule.matcher.is_literal() {
            self.exact_anywhere
                .entry(rule.pattern.clone())
                .or_default()
                .push(index);
        } else if let Some(key) = rule.matcher.prefix_key() {
            self.prefixes.entry(key).or_default().push(index);
        } else if let Some(key) = rule.matcher.suffix_key(&rule.pattern) {
            self.suffixes.entry(key).or_default().push(index);
        } else {
            self.generic.push(index);
        }
        self.rules.push(rule);
    }

    pub(super) fn matches(&self, path: &str, is_directory: bool) -> Option<RuleMatch> {
        if let Some(action) = self.matches_exact(path, is_directory) {
            return Some(RuleMatch::Exact(action));
        }
        let mut ancestor = path;
        while let Some((parent, _)) = ancestor.rsplit_once('/') {
            if let Some(action) = self.matches_exact(parent, true) {
                return Some(RuleMatch::Ancestor(action));
            }
            ancestor = parent;
        }
        None
    }

    pub(super) fn matches_exact(&self, path: &str, is_directory: bool) -> Option<RuleAction> {
        let mut best = None;
        let name = path.rsplit('/').next().unwrap_or(path);
        if let Some(indices) = self.exact_anywhere.get(name)
            && let Some(&index) = indices
                .iter()
                .rev()
                .find(|&&index| self.rules[index].matches_exact(path, is_directory))
        {
            best = Some(index);
        }
        best = self.best_match(&self.generic, path, is_directory, best);
        let name_prefix = name.as_bytes().first();
        if let Some(indices) = name_prefix.and_then(|key| self.prefixes.get(key)) {
            best = self.best_match(indices, path, is_directory, best);
        }
        if let Some(path_prefix) = path.as_bytes().first()
            && Some(path_prefix) != name_prefix
            && let Some(indices) = self.prefixes.get(path_prefix)
        {
            best = self.best_match(indices, path, is_directory, best);
        }
        if let Some(indices) = path
            .as_bytes()
            .last()
            .and_then(|key| self.suffixes.get(key))
        {
            best = self.best_match(indices, path, is_directory, best);
        }
        best.map(|index| self.rules[index].action)
    }

    fn best_match(
        &self,
        indices: &[usize],
        path: &str,
        is_directory: bool,
        mut best: Option<usize>,
    ) -> Option<usize> {
        for &index in indices.iter().rev() {
            if best.is_some_and(|best| index <= best) {
                break;
            }
            if self.rules[index].matches_exact(path, is_directory) {
                best = Some(index);
                break;
            }
        }
        best
    }
}

impl IgnoreRule {
    fn matches_exact(&self, path: &str, is_directory: bool) -> bool {
        if self.target == RuleTarget::Directory && !is_directory {
            return false;
        }
        if self.scope == RuleScope::Anywhere {
            let name = path.rsplit('/').next().unwrap_or(path);
            return self.matcher.matches(&self.pattern, name);
        }
        self.matcher.matches(&self.pattern, path)
    }
}