1use haz_domain::path::PathPattern;
19use haz_query_lang::expr::RawAtom;
20
21use crate::expr::atom::AtomError;
22use crate::expr::glob_intersect::{GlobIntersectError, glob_intersect_non_empty};
23
24pub fn parse_path_pattern_atom(atom: RawAtom) -> Result<PathPattern, AtomError> {
35 let RawAtom { text, span } = atom;
36 PathPattern::parse(&text).map_err(|source| AtomError::InvalidPathPattern { span, source })
37}
38
39pub fn intersects(lhs: &PathPattern, rhs: &PathPattern) -> Result<bool, GlobIntersectError> {
60 match (lhs, rhs) {
61 (PathPattern::Literal(a), PathPattern::Literal(b)) => Ok(a == b),
62 (PathPattern::Literal(literal), PathPattern::Glob(g))
63 | (PathPattern::Glob(g), PathPattern::Literal(literal)) => {
64 let compiled = g.compile();
65 let literal_text = literal.to_string();
66 Ok(compiled.compile_matcher().is_match(literal_text.as_str()))
67 }
68 (PathPattern::Glob(g1), PathPattern::Glob(g2)) => {
69 glob_intersect_non_empty(&g1.compile(), &g2.compile())
70 }
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use haz_domain::path::PathPatternError;
78 use haz_query_lang::expr::Expr;
79 use haz_query_lang::parser::parse;
80 use haz_query_lang::span::Span;
81
82 fn raw(text: &str, start: usize, end: usize) -> RawAtom {
83 RawAtom {
84 text: text.to_owned(),
85 span: Span { start, end },
86 }
87 }
88
89 #[test]
92 fn qry_003_lifts_literal_path_atom() {
93 let pattern = parse_path_pattern_atom(raw("/lib/main.rs", 0, 12)).unwrap();
94 assert!(pattern.is_literal());
95 }
96
97 #[test]
98 fn qry_003_lifts_glob_path_atom() {
99 let pattern = parse_path_pattern_atom(raw("src/**/*.rs", 0, 11)).unwrap();
100 assert!(pattern.is_glob());
101 }
102
103 #[test]
104 fn qry_003_lifts_workspace_absolute_glob() {
105 let pattern = parse_path_pattern_atom(raw("/lib/src/*.rs", 0, 13)).unwrap();
106 assert!(pattern.is_glob());
107 }
108
109 #[test]
112 fn qry_003_rejects_empty_path_pattern_atom() {
113 let err = parse_path_pattern_atom(raw("", 5, 5)).unwrap_err();
114 match err {
115 AtomError::InvalidPathPattern { span, source } => {
116 assert_eq!(span, Span { start: 5, end: 5 });
117 assert!(matches!(source, PathPatternError::EmptyInput));
118 }
119 other => panic!("expected InvalidPathPattern, got {other:?}"),
120 }
121 }
122
123 #[test]
124 fn qry_003_rejects_double_star_adjacent_to_literal() {
125 let err = parse_path_pattern_atom(raw("a**b/foo.rs", 2, 13)).unwrap_err();
127 match err {
128 AtomError::InvalidPathPattern {
129 span,
130 source: PathPatternError::InvalidGlobSegment { .. },
131 } => {
132 assert_eq!(span, Span { start: 2, end: 13 });
133 }
134 other => panic!("expected InvalidPathPattern, got {other:?}"),
135 }
136 }
137
138 #[test]
141 fn qry_003_lifts_parsed_expression_to_typed_path_expression() {
142 let expr = parse("src/**/*.rs & !src/tests/**").unwrap();
143 let typed = expr.try_map(parse_path_pattern_atom).unwrap();
144 let src_all = PathPattern::parse("src/**/*.rs").unwrap();
145 let src_tests = PathPattern::parse("src/tests/**").unwrap();
146 let expected = Expr::And(
147 Box::new(Expr::Atom(src_all)),
148 Box::new(Expr::Not(Box::new(Expr::Atom(src_tests)))),
149 );
150 assert_eq!(typed, expected);
151 }
152
153 #[test]
156 fn qry_003_literal_literal_byte_equal_intersect() {
157 let lhs = PathPattern::parse("/lib/main.rs").unwrap();
158 let rhs = PathPattern::parse("/lib/main.rs").unwrap();
159 assert!(intersects(&lhs, &rhs).unwrap());
160 }
161
162 #[test]
163 fn qry_003_literal_literal_byte_inequal_disjoint() {
164 let lhs = PathPattern::parse("/lib/main.rs").unwrap();
165 let rhs = PathPattern::parse("/lib/lib.rs").unwrap();
166 assert!(!intersects(&lhs, &rhs).unwrap());
167 }
168
169 #[test]
172 fn qry_003_literal_inside_glob_matches() {
173 let lhs = PathPattern::parse("/lib/src/main.rs").unwrap();
174 let rhs = PathPattern::parse("/lib/src/*.rs").unwrap();
175 assert!(intersects(&lhs, &rhs).unwrap());
176 }
177
178 #[test]
179 fn qry_003_literal_outside_glob_does_not_match() {
180 let lhs = PathPattern::parse("/lib/src/main.rs").unwrap();
181 let rhs = PathPattern::parse("/web/src/*.rs").unwrap();
182 assert!(!intersects(&lhs, &rhs).unwrap());
183 }
184
185 #[test]
186 fn qry_003_glob_matches_literal_in_either_direction() {
187 let glob_pat = PathPattern::parse("/lib/**/*.rs").unwrap();
188 let literal = PathPattern::parse("/lib/src/deep/file.rs").unwrap();
189 assert!(intersects(&glob_pat, &literal).unwrap());
190 assert!(intersects(&literal, &glob_pat).unwrap());
191 }
192
193 #[test]
196 fn qry_003_glob_glob_overlapping_intersect() {
197 let lhs = PathPattern::parse("/lib/src/**/*.rs").unwrap();
198 let rhs = PathPattern::parse("/lib/src/foo/*.rs").unwrap();
199 assert!(intersects(&lhs, &rhs).unwrap());
200 }
201
202 #[test]
203 fn qry_003_glob_glob_disjoint_prefixes_do_not_intersect() {
204 let lhs = PathPattern::parse("/lib/src/**/*.rs").unwrap();
205 let rhs = PathPattern::parse("/web/src/**/*.rs").unwrap();
206 assert!(!intersects(&lhs, &rhs).unwrap());
207 }
208
209 #[test]
210 fn qry_003_glob_glob_disjoint_extensions_do_not_intersect() {
211 let lhs = PathPattern::parse("/lib/**/*.rs").unwrap();
212 let rhs = PathPattern::parse("/lib/**/*.js").unwrap();
213 assert!(!intersects(&lhs, &rhs).unwrap());
214 }
215
216 #[test]
217 fn qry_003_glob_glob_alternation_overlap_intersect() {
218 let lhs = PathPattern::parse("/lib/{a,b}.rs").unwrap();
219 let rhs = PathPattern::parse("/lib/{b,c}.rs").unwrap();
220 assert!(intersects(&lhs, &rhs).unwrap());
221 }
222
223 #[test]
224 fn qry_003_glob_glob_char_class_overlap_intersect() {
225 let lhs = PathPattern::parse("/lib/[abc].rs").unwrap();
226 let rhs = PathPattern::parse("/lib/[bcd].rs").unwrap();
227 assert!(intersects(&lhs, &rhs).unwrap());
228 }
229
230 #[test]
231 fn qry_003_glob_glob_char_class_disjoint_do_not_intersect() {
232 let lhs = PathPattern::parse("/lib/[ab].rs").unwrap();
233 let rhs = PathPattern::parse("/lib/[cd].rs").unwrap();
234 assert!(!intersects(&lhs, &rhs).unwrap());
235 }
236
237 #[test]
238 fn qry_003_glob_glob_double_star_absorbs_single_star_prefix() {
239 let lhs = PathPattern::parse("/lib/**/*.rs").unwrap();
240 let rhs = PathPattern::parse("/lib/*/*.rs").unwrap();
241 assert!(intersects(&lhs, &rhs).unwrap());
242 }
243}