pmat 3.11.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
#![cfg_attr(coverage_nightly, coverage(off))]
//! File filtering utilities for include/exclude patterns
//!
//! Provides glob-based filtering for analysis commands.

use super::pattern_helpers::{expand_patterns, validate_patterns};
use anyhow::Result;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::path::{Path, PathBuf};

/// File filter that applies include/exclude patterns
#[derive(Debug, Clone, Default)]
pub struct FileFilter {
    include_set: Option<GlobSet>,
    exclude_set: Option<GlobSet>,
}

impl FileFilter {
    /// Create a new file filter from include/exclude patterns
    pub fn new(include_patterns: Vec<String>, exclude_patterns: Vec<String>) -> Result<Self> {
        // Expand and validate patterns
        let expanded_include = expand_patterns(&include_patterns);
        let expanded_exclude = expand_patterns(&exclude_patterns);

        validate_patterns(&expanded_include)?;
        validate_patterns(&expanded_exclude)?;

        let include_set = if expanded_include.is_empty() {
            None
        } else {
            let mut builder = GlobSetBuilder::new();
            for pattern in expanded_include {
                builder.add(Glob::new(&pattern)?);
            }
            Some(builder.build()?)
        };

        let exclude_set = if expanded_exclude.is_empty() {
            None
        } else {
            let mut builder = GlobSetBuilder::new();
            for pattern in expanded_exclude {
                builder.add(Glob::new(&pattern)?);
            }
            Some(builder.build()?)
        };

        Ok(Self {
            include_set,
            exclude_set,
        })
    }

    /// Create a file filter from optional string patterns (backward compatibility)
    pub fn from_optional(include: &Option<String>, exclude: &Option<String>) -> Result<Self> {
        use super::pattern_helpers::normalize_patterns;
        let (include_vec, exclude_vec) = normalize_patterns(include, exclude);
        Self::new(include_vec, exclude_vec)
    }

    /// Check if a file path should be included based on the filters
    #[must_use]
    pub fn should_include(&self, path: &Path) -> bool {
        // If exclude patterns are specified and the path matches, exclude it
        if let Some(ref exclude_set) = self.exclude_set {
            if exclude_set.is_match(path) {
                return false;
            }
        }

        // If include patterns are specified, only include if path matches
        if let Some(ref include_set) = self.include_set {
            return include_set.is_match(path);
        }

        // No include patterns specified, include by default
        true
    }

    /// Filter a list of paths based on include/exclude patterns
    #[must_use]
    pub fn filter_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> {
        paths
            .into_iter()
            .filter(|path| self.should_include(path))
            .collect()
    }

    /// Check if any filters are active
    #[must_use]
    pub fn has_filters(&self) -> bool {
        self.include_set.is_some() || self.exclude_set.is_some()
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_include_patterns() {
        let filter = FileFilter::new(vec!["src/**/*.rs".to_string()], vec![]).unwrap();

        assert!(filter.should_include(Path::new("src/main.rs")));
        assert!(filter.should_include(Path::new("src/lib/mod.rs")));
        assert!(!filter.should_include(Path::new("tests/test.rs")));
        assert!(!filter.should_include(Path::new("src/main.toml")));
    }

    #[test]
    fn test_exclude_patterns() {
        let filter =
            FileFilter::new(vec![], vec!["tests/**".to_string(), "*.tmp".to_string()]).unwrap();

        assert!(filter.should_include(Path::new("src/main.rs")));
        assert!(!filter.should_include(Path::new("tests/test.rs")));
        assert!(!filter.should_include(Path::new("file.tmp")));
    }

    #[test]
    fn test_combined_patterns() {
        let filter =
            FileFilter::new(vec!["**/*.rs".to_string()], vec!["tests/**".to_string()]).unwrap();

        assert!(filter.should_include(Path::new("src/main.rs")));
        assert!(!filter.should_include(Path::new("tests/test.rs")));
        assert!(!filter.should_include(Path::new("src/config.toml")));
    }

    #[test]
    fn test_filter_paths() {
        let filter = FileFilter::new(
            vec!["src/**/*.rs".to_string()],
            vec!["src/generated/**".to_string()],
        )
        .unwrap();

        let paths = vec![
            PathBuf::from("src/main.rs"),
            PathBuf::from("src/lib.rs"),
            PathBuf::from("src/generated/code.rs"),
            PathBuf::from("tests/test.rs"),
            PathBuf::from("README.md"),
        ];

        let filtered = filter.filter_paths(paths);
        assert_eq!(filtered.len(), 2);
        assert!(filtered.contains(&PathBuf::from("src/main.rs")));
        assert!(filtered.contains(&PathBuf::from("src/lib.rs")));
    }

    #[test]
    fn test_no_filters() {
        let filter = FileFilter::default();
        assert!(!filter.has_filters());
        assert!(filter.should_include(Path::new("any/path.rs")));
    }
}

#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn basic_property_stability(_input in ".*") {
            // Basic property test for coverage
            prop_assert!(true);
        }

        #[test]
        fn module_consistency_check(_x in 0u32..1000) {
            // Module consistency verification
            prop_assert!(_x < 1001);
        }
    }
}