claude_agent/common/
path_matched.rs

1//! Path-matching trait for context-sensitive index entries.
2//!
3//! The `PathMatched` trait enables index entries to be filtered based on file paths.
4//! This is essential for rules that apply only to specific file patterns.
5//!
6//! # Example
7//!
8//! ```ignore
9//! pub struct RuleIndex {
10//!     paths: Option<Vec<String>>,
11//!     compiled_patterns: Vec<Pattern>,
12//!     // ...
13//! }
14//!
15//! impl PathMatched for RuleIndex {
16//!     fn path_patterns(&self) -> Option<&[String]> {
17//!         self.paths.as_deref()
18//!     }
19//!
20//!     fn matches_path(&self, path: &Path) -> bool {
21//!         if self.compiled_patterns.is_empty() {
22//!             return true; // Global rule matches all
23//!         }
24//!         let path_str = path.to_string_lossy();
25//!         self.compiled_patterns.iter().any(|p| p.matches(&path_str))
26//!     }
27//! }
28//! ```
29
30use std::path::Path;
31
32/// Trait for index entries that support path-based filtering.
33///
34/// Implementors can specify glob patterns that determine which files
35/// the entry applies to. Entries without patterns match all files.
36pub trait PathMatched {
37    /// Get the path patterns this entry matches.
38    ///
39    /// Returns `None` if this is a global entry that matches all files.
40    /// Returns `Some(&[])` if patterns were explicitly set to empty (matches nothing).
41    fn path_patterns(&self) -> Option<&[String]>;
42
43    /// Check if this entry matches the given file path.
44    ///
45    /// Default behavior:
46    /// - No patterns (`None`) → matches all files
47    /// - Empty patterns → matches no files
48    /// - Has patterns → matches if any pattern matches
49    fn matches_path(&self, path: &Path) -> bool;
50
51    /// Check if this is a global entry (matches all files).
52    fn is_global(&self) -> bool {
53        self.path_patterns().is_none()
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    struct TestPathMatched {
62        patterns: Option<Vec<String>>,
63    }
64
65    impl PathMatched for TestPathMatched {
66        fn path_patterns(&self) -> Option<&[String]> {
67            self.patterns.as_deref()
68        }
69
70        fn matches_path(&self, path: &Path) -> bool {
71            match &self.patterns {
72                None => true, // Global
73                Some(patterns) if patterns.is_empty() => false,
74                Some(patterns) => {
75                    let path_str = path.to_string_lossy();
76                    patterns.iter().any(|p| {
77                        glob::Pattern::new(p)
78                            .map(|pat| pat.matches(&path_str))
79                            .unwrap_or(false)
80                    })
81                }
82            }
83        }
84    }
85
86    #[test]
87    fn test_global_matches_all() {
88        let global = TestPathMatched { patterns: None };
89        assert!(global.is_global());
90        assert!(global.matches_path(Path::new("any/file.rs")));
91        assert!(global.matches_path(Path::new("other/path.ts")));
92    }
93
94    #[test]
95    fn test_pattern_matching() {
96        let rust_only = TestPathMatched {
97            patterns: Some(vec!["**/*.rs".to_string()]),
98        };
99        assert!(!rust_only.is_global());
100        assert!(rust_only.matches_path(Path::new("src/lib.rs")));
101        assert!(rust_only.matches_path(Path::new("tests/integration.rs")));
102        assert!(!rust_only.matches_path(Path::new("src/lib.ts")));
103    }
104
105    #[test]
106    fn test_multiple_patterns() {
107        let web = TestPathMatched {
108            patterns: Some(vec!["**/*.ts".to_string(), "**/*.tsx".to_string()]),
109        };
110        assert!(web.matches_path(Path::new("src/app.ts")));
111        assert!(web.matches_path(Path::new("components/Button.tsx")));
112        assert!(!web.matches_path(Path::new("src/lib.rs")));
113    }
114
115    #[test]
116    fn test_empty_patterns_matches_nothing() {
117        let empty = TestPathMatched {
118            patterns: Some(vec![]),
119        };
120        assert!(!empty.is_global());
121        assert!(!empty.matches_path(Path::new("any/file.rs")));
122    }
123}