1use std::collections::HashMap;
18use std::iter;
19use std::path;
20use std::slice;
21
22use itertools::Itertools as _;
23use once_cell::sync::Lazy;
24use thiserror::Error;
25
26use crate::dsl_util::collect_similar;
27use crate::fileset_parser;
28use crate::fileset_parser::BinaryOp;
29use crate::fileset_parser::ExpressionKind;
30use crate::fileset_parser::ExpressionNode;
31pub use crate::fileset_parser::FilesetDiagnostics;
32pub use crate::fileset_parser::FilesetParseError;
33pub use crate::fileset_parser::FilesetParseErrorKind;
34pub use crate::fileset_parser::FilesetParseResult;
35use crate::fileset_parser::FunctionCallNode;
36use crate::fileset_parser::UnaryOp;
37use crate::matchers::DifferenceMatcher;
38use crate::matchers::EverythingMatcher;
39use crate::matchers::FileGlobsMatcher;
40use crate::matchers::FilesMatcher;
41use crate::matchers::IntersectionMatcher;
42use crate::matchers::Matcher;
43use crate::matchers::NothingMatcher;
44use crate::matchers::PrefixMatcher;
45use crate::matchers::UnionMatcher;
46use crate::repo_path::RelativePathParseError;
47use crate::repo_path::RepoPath;
48use crate::repo_path::RepoPathBuf;
49use crate::repo_path::RepoPathUiConverter;
50use crate::repo_path::UiPathParseError;
51
52#[derive(Debug, Error)]
54pub enum FilePatternParseError {
55 #[error("Invalid file pattern kind `{0}:`")]
57 InvalidKind(String),
58 #[error(transparent)]
60 UiPath(#[from] UiPathParseError),
61 #[error(transparent)]
63 RelativePath(#[from] RelativePathParseError),
64 #[error(transparent)]
66 GlobPattern(#[from] glob::PatternError),
67}
68
69#[derive(Clone, Debug)]
71pub enum FilePattern {
72 FilePath(RepoPathBuf),
74 PrefixPath(RepoPathBuf),
76 FileGlob {
78 dir: RepoPathBuf,
80 pattern: glob::Pattern,
82 },
83 }
87
88impl FilePattern {
89 pub fn from_str_kind(
91 path_converter: &RepoPathUiConverter,
92 input: &str,
93 kind: &str,
94 ) -> Result<Self, FilePatternParseError> {
95 match kind {
110 "cwd" => Self::cwd_prefix_path(path_converter, input),
111 "cwd-file" | "file" => Self::cwd_file_path(path_converter, input),
112 "cwd-glob" | "glob" => Self::cwd_file_glob(path_converter, input),
113 "root" => Self::root_prefix_path(input),
114 "root-file" => Self::root_file_path(input),
115 "root-glob" => Self::root_file_glob(input),
116 _ => Err(FilePatternParseError::InvalidKind(kind.to_owned())),
117 }
118 }
119
120 pub fn cwd_file_path(
122 path_converter: &RepoPathUiConverter,
123 input: impl AsRef<str>,
124 ) -> Result<Self, FilePatternParseError> {
125 let path = path_converter.parse_file_path(input.as_ref())?;
126 Ok(FilePattern::FilePath(path))
127 }
128
129 pub fn cwd_prefix_path(
131 path_converter: &RepoPathUiConverter,
132 input: impl AsRef<str>,
133 ) -> Result<Self, FilePatternParseError> {
134 let path = path_converter.parse_file_path(input.as_ref())?;
135 Ok(FilePattern::PrefixPath(path))
136 }
137
138 pub fn cwd_file_glob(
140 path_converter: &RepoPathUiConverter,
141 input: impl AsRef<str>,
142 ) -> Result<Self, FilePatternParseError> {
143 let (dir, pattern) = split_glob_path(input.as_ref());
144 let dir = path_converter.parse_file_path(dir)?;
145 Self::file_glob_at(dir, pattern)
146 }
147
148 pub fn root_file_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
150 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
152 Ok(FilePattern::FilePath(path))
153 }
154
155 pub fn root_prefix_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
157 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
158 Ok(FilePattern::PrefixPath(path))
159 }
160
161 pub fn root_file_glob(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
163 let (dir, pattern) = split_glob_path(input.as_ref());
164 let dir = RepoPathBuf::from_relative_path(dir)?;
165 Self::file_glob_at(dir, pattern)
166 }
167
168 fn file_glob_at(dir: RepoPathBuf, input: &str) -> Result<Self, FilePatternParseError> {
169 if input.is_empty() {
170 return Ok(FilePattern::FilePath(dir));
171 }
172 let normalized = RepoPathBuf::from_relative_path(input)?;
174 let pattern = glob::Pattern::new(normalized.as_internal_file_string())?;
175 Ok(FilePattern::FileGlob { dir, pattern })
176 }
177
178 pub fn as_path(&self) -> Option<&RepoPath> {
181 match self {
182 FilePattern::FilePath(path) => Some(path),
183 FilePattern::PrefixPath(path) => Some(path),
184 FilePattern::FileGlob { .. } => None,
185 }
186 }
187}
188
189fn split_glob_path(input: &str) -> (&str, &str) {
191 const GLOB_CHARS: &[char] = &['?', '*', '[', ']']; let prefix_len = input
193 .split_inclusive(path::is_separator)
194 .take_while(|component| !component.contains(GLOB_CHARS))
195 .map(|component| component.len())
196 .sum();
197 input.split_at(prefix_len)
198}
199
200#[derive(Clone, Debug)]
202pub enum FilesetExpression {
203 None,
205 All,
207 Pattern(FilePattern),
209 UnionAll(Vec<FilesetExpression>),
214 Intersection(Box<FilesetExpression>, Box<FilesetExpression>),
216 Difference(Box<FilesetExpression>, Box<FilesetExpression>),
218}
219
220impl FilesetExpression {
221 pub fn none() -> Self {
223 FilesetExpression::None
224 }
225
226 pub fn all() -> Self {
228 FilesetExpression::All
229 }
230
231 pub fn pattern(pattern: FilePattern) -> Self {
233 FilesetExpression::Pattern(pattern)
234 }
235
236 pub fn file_path(path: RepoPathBuf) -> Self {
238 FilesetExpression::Pattern(FilePattern::FilePath(path))
239 }
240
241 pub fn prefix_path(path: RepoPathBuf) -> Self {
243 FilesetExpression::Pattern(FilePattern::PrefixPath(path))
244 }
245
246 pub fn union_all(expressions: Vec<FilesetExpression>) -> Self {
248 match expressions.len() {
249 0 => FilesetExpression::none(),
250 1 => expressions.into_iter().next().unwrap(),
251 _ => FilesetExpression::UnionAll(expressions),
252 }
253 }
254
255 pub fn intersection(self, other: Self) -> Self {
257 FilesetExpression::Intersection(Box::new(self), Box::new(other))
258 }
259
260 pub fn difference(self, other: Self) -> Self {
262 FilesetExpression::Difference(Box::new(self), Box::new(other))
263 }
264
265 fn as_union_all(&self) -> &[Self] {
267 match self {
268 FilesetExpression::None => &[],
269 FilesetExpression::UnionAll(exprs) => exprs,
270 _ => slice::from_ref(self),
271 }
272 }
273
274 fn dfs_pre(&self) -> impl Iterator<Item = &Self> {
275 let mut stack: Vec<&Self> = vec![self];
276 iter::from_fn(move || {
277 let expr = stack.pop()?;
278 match expr {
279 FilesetExpression::None
280 | FilesetExpression::All
281 | FilesetExpression::Pattern(_) => {}
282 FilesetExpression::UnionAll(exprs) => stack.extend(exprs.iter().rev()),
283 FilesetExpression::Intersection(expr1, expr2)
284 | FilesetExpression::Difference(expr1, expr2) => {
285 stack.push(expr2);
286 stack.push(expr1);
287 }
288 }
289 Some(expr)
290 })
291 }
292
293 pub fn explicit_paths(&self) -> impl Iterator<Item = &RepoPath> {
298 self.dfs_pre().filter_map(|expr| match expr {
301 FilesetExpression::Pattern(pattern) => pattern.as_path(),
302 _ => None,
303 })
304 }
305
306 pub fn to_matcher(&self) -> Box<dyn Matcher> {
308 build_union_matcher(self.as_union_all())
309 }
310}
311
312fn build_union_matcher(expressions: &[FilesetExpression]) -> Box<dyn Matcher> {
317 let mut file_paths = Vec::new();
318 let mut prefix_paths = Vec::new();
319 let mut file_globs = Vec::new();
320 let mut matchers: Vec<Option<Box<dyn Matcher>>> = Vec::new();
321 for expr in expressions {
322 let matcher: Box<dyn Matcher> = match expr {
323 FilesetExpression::None => Box::new(NothingMatcher),
325 FilesetExpression::All => Box::new(EverythingMatcher),
326 FilesetExpression::Pattern(pattern) => {
327 match pattern {
328 FilePattern::FilePath(path) => file_paths.push(path),
329 FilePattern::PrefixPath(path) => prefix_paths.push(path),
330 FilePattern::FileGlob { dir, pattern } => {
331 file_globs.push((dir, pattern.clone()));
332 }
333 }
334 continue;
335 }
336 FilesetExpression::UnionAll(exprs) => build_union_matcher(exprs),
338 FilesetExpression::Intersection(expr1, expr2) => {
339 let m1 = build_union_matcher(expr1.as_union_all());
340 let m2 = build_union_matcher(expr2.as_union_all());
341 Box::new(IntersectionMatcher::new(m1, m2))
342 }
343 FilesetExpression::Difference(expr1, expr2) => {
344 let m1 = build_union_matcher(expr1.as_union_all());
345 let m2 = build_union_matcher(expr2.as_union_all());
346 Box::new(DifferenceMatcher::new(m1, m2))
347 }
348 };
349 matchers.push(Some(matcher));
350 }
351
352 if !file_paths.is_empty() {
353 matchers.push(Some(Box::new(FilesMatcher::new(file_paths))));
354 }
355 if !prefix_paths.is_empty() {
356 matchers.push(Some(Box::new(PrefixMatcher::new(prefix_paths))));
357 }
358 if !file_globs.is_empty() {
359 matchers.push(Some(Box::new(FileGlobsMatcher::new(file_globs))));
360 }
361 union_all_matchers(&mut matchers)
362}
363
364fn union_all_matchers(matchers: &mut [Option<Box<dyn Matcher>>]) -> Box<dyn Matcher> {
369 match matchers {
370 [] => Box::new(NothingMatcher),
371 [matcher] => matcher.take().expect("matcher should still be available"),
372 _ => {
373 let (left, right) = matchers.split_at_mut(matchers.len() / 2);
375 let m1 = union_all_matchers(left);
376 let m2 = union_all_matchers(right);
377 Box::new(UnionMatcher::new(m1, m2))
378 }
379 }
380}
381
382type FilesetFunction = fn(
383 &mut FilesetDiagnostics,
384 &RepoPathUiConverter,
385 &FunctionCallNode,
386) -> FilesetParseResult<FilesetExpression>;
387
388static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, FilesetFunction>> = Lazy::new(|| {
389 let mut map: HashMap<&'static str, FilesetFunction> = HashMap::new();
392 map.insert("none", |_diagnostics, _path_converter, function| {
393 function.expect_no_arguments()?;
394 Ok(FilesetExpression::none())
395 });
396 map.insert("all", |_diagnostics, _path_converter, function| {
397 function.expect_no_arguments()?;
398 Ok(FilesetExpression::all())
399 });
400 map
401});
402
403fn resolve_function(
404 diagnostics: &mut FilesetDiagnostics,
405 path_converter: &RepoPathUiConverter,
406 function: &FunctionCallNode,
407) -> FilesetParseResult<FilesetExpression> {
408 if let Some(func) = BUILTIN_FUNCTION_MAP.get(function.name) {
409 func(diagnostics, path_converter, function)
410 } else {
411 Err(FilesetParseError::new(
412 FilesetParseErrorKind::NoSuchFunction {
413 name: function.name.to_owned(),
414 candidates: collect_similar(function.name, BUILTIN_FUNCTION_MAP.keys()),
415 },
416 function.name_span,
417 ))
418 }
419}
420
421fn resolve_expression(
422 diagnostics: &mut FilesetDiagnostics,
423 path_converter: &RepoPathUiConverter,
424 node: &ExpressionNode,
425) -> FilesetParseResult<FilesetExpression> {
426 let wrap_pattern_error =
427 |err| FilesetParseError::expression("Invalid file pattern", node.span).with_source(err);
428 match &node.kind {
429 ExpressionKind::Identifier(name) => {
430 let pattern =
431 FilePattern::cwd_prefix_path(path_converter, name).map_err(wrap_pattern_error)?;
432 Ok(FilesetExpression::pattern(pattern))
433 }
434 ExpressionKind::String(name) => {
435 let pattern =
436 FilePattern::cwd_prefix_path(path_converter, name).map_err(wrap_pattern_error)?;
437 Ok(FilesetExpression::pattern(pattern))
438 }
439 ExpressionKind::StringPattern { kind, value } => {
440 let pattern = FilePattern::from_str_kind(path_converter, value, kind)
441 .map_err(wrap_pattern_error)?;
442 Ok(FilesetExpression::pattern(pattern))
443 }
444 ExpressionKind::Unary(op, arg_node) => {
445 let arg = resolve_expression(diagnostics, path_converter, arg_node)?;
446 match op {
447 UnaryOp::Negate => Ok(FilesetExpression::all().difference(arg)),
448 }
449 }
450 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
451 let lhs = resolve_expression(diagnostics, path_converter, lhs_node)?;
452 let rhs = resolve_expression(diagnostics, path_converter, rhs_node)?;
453 match op {
454 BinaryOp::Intersection => Ok(lhs.intersection(rhs)),
455 BinaryOp::Difference => Ok(lhs.difference(rhs)),
456 }
457 }
458 ExpressionKind::UnionAll(nodes) => {
459 let expressions = nodes
460 .iter()
461 .map(|node| resolve_expression(diagnostics, path_converter, node))
462 .try_collect()?;
463 Ok(FilesetExpression::union_all(expressions))
464 }
465 ExpressionKind::FunctionCall(function) => {
466 resolve_function(diagnostics, path_converter, function)
467 }
468 }
469}
470
471pub fn parse(
473 diagnostics: &mut FilesetDiagnostics,
474 text: &str,
475 path_converter: &RepoPathUiConverter,
476) -> FilesetParseResult<FilesetExpression> {
477 let node = fileset_parser::parse_program(text)?;
478 resolve_expression(diagnostics, path_converter, &node)
480}
481
482pub fn parse_maybe_bare(
487 diagnostics: &mut FilesetDiagnostics,
488 text: &str,
489 path_converter: &RepoPathUiConverter,
490) -> FilesetParseResult<FilesetExpression> {
491 let node = fileset_parser::parse_program_or_bare_string(text)?;
492 resolve_expression(diagnostics, path_converter, &node)
494}
495
496#[cfg(test)]
497mod tests {
498 use std::path::PathBuf;
499
500 use super::*;
501
502 fn repo_path_buf(value: impl Into<String>) -> RepoPathBuf {
503 RepoPathBuf::from_internal_string(value).unwrap()
504 }
505
506 fn insta_settings() -> insta::Settings {
507 let mut settings = insta::Settings::clone_current();
508 settings.add_filter(r"\b(tokens): \[[^]]*\],", "$1: _,");
510 for _ in 0..4 {
513 settings.add_filter(
514 r"(?x)
515 \b([A-Z]\w*)\(\n
516 \s*(.{1,60}),\n
517 \s*\)",
518 "$1($2)",
519 );
520 }
521 settings
522 }
523
524 #[test]
525 fn test_parse_file_pattern() {
526 let settings = insta_settings();
527 let _guard = settings.bind_to_scope();
528 let path_converter = RepoPathUiConverter::Fs {
529 cwd: PathBuf::from("/ws/cur"),
530 base: PathBuf::from("/ws"),
531 };
532 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
533
534 insta::assert_debug_snapshot!(
536 parse(".").unwrap(),
537 @r#"Pattern(PrefixPath("cur"))"#);
538 insta::assert_debug_snapshot!(
539 parse("..").unwrap(),
540 @r#"Pattern(PrefixPath(""))"#);
541 assert!(parse("../..").is_err());
542 insta::assert_debug_snapshot!(
543 parse("foo").unwrap(),
544 @r#"Pattern(PrefixPath("cur/foo"))"#);
545 insta::assert_debug_snapshot!(
546 parse("cwd:.").unwrap(),
547 @r#"Pattern(PrefixPath("cur"))"#);
548 insta::assert_debug_snapshot!(
549 parse("cwd-file:foo").unwrap(),
550 @r#"Pattern(FilePath("cur/foo"))"#);
551 insta::assert_debug_snapshot!(
552 parse("file:../foo/bar").unwrap(),
553 @r#"Pattern(FilePath("foo/bar"))"#);
554
555 insta::assert_debug_snapshot!(
557 parse("root:.").unwrap(),
558 @r#"Pattern(PrefixPath(""))"#);
559 assert!(parse("root:..").is_err());
560 insta::assert_debug_snapshot!(
561 parse("root:foo/bar").unwrap(),
562 @r#"Pattern(PrefixPath("foo/bar"))"#);
563 insta::assert_debug_snapshot!(
564 parse("root-file:bar").unwrap(),
565 @r#"Pattern(FilePath("bar"))"#);
566 }
567
568 #[test]
569 fn test_parse_glob_pattern() {
570 let settings = insta_settings();
571 let _guard = settings.bind_to_scope();
572 let path_converter = RepoPathUiConverter::Fs {
573 cwd: PathBuf::from("/ws/cur*"),
575 base: PathBuf::from("/ws"),
576 };
577 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
578
579 insta::assert_debug_snapshot!(
581 parse(r#"cwd-glob:"foo""#).unwrap(),
582 @r#"Pattern(FilePath("cur*/foo"))"#);
583 insta::assert_debug_snapshot!(
586 parse(r#"glob:"""#).unwrap(),
587 @r#"Pattern(FilePath("cur*"))"#);
588 insta::assert_debug_snapshot!(
589 parse(r#"glob:".""#).unwrap(),
590 @r#"Pattern(FilePath("cur*"))"#);
591 insta::assert_debug_snapshot!(
592 parse(r#"glob:"..""#).unwrap(),
593 @r#"Pattern(FilePath(""))"#);
594
595 insta::assert_debug_snapshot!(
597 parse(r#"glob:"*""#).unwrap(), @r#"
598 Pattern(
599 FileGlob {
600 dir: "cur*",
601 pattern: Pattern {
602 original: "*",
603 tokens: _,
604 is_recursive: false,
605 },
606 },
607 )
608 "#);
609 insta::assert_debug_snapshot!(
610 parse(r#"glob:"./*""#).unwrap(), @r#"
611 Pattern(
612 FileGlob {
613 dir: "cur*",
614 pattern: Pattern {
615 original: "*",
616 tokens: _,
617 is_recursive: false,
618 },
619 },
620 )
621 "#);
622 insta::assert_debug_snapshot!(
623 parse(r#"glob:"../*""#).unwrap(), @r#"
624 Pattern(
625 FileGlob {
626 dir: "",
627 pattern: Pattern {
628 original: "*",
629 tokens: _,
630 is_recursive: false,
631 },
632 },
633 )
634 "#);
635 insta::assert_debug_snapshot!(
637 parse(r#"glob:"**""#).unwrap(), @r#"
638 Pattern(
639 FileGlob {
640 dir: "cur*",
641 pattern: Pattern {
642 original: "**",
643 tokens: _,
644 is_recursive: true,
645 },
646 },
647 )
648 "#);
649 insta::assert_debug_snapshot!(
650 parse(r#"glob:"../foo/b?r/baz""#).unwrap(), @r#"
651 Pattern(
652 FileGlob {
653 dir: "foo",
654 pattern: Pattern {
655 original: "b?r/baz",
656 tokens: _,
657 is_recursive: false,
658 },
659 },
660 )
661 "#);
662 assert!(parse(r#"glob:"../../*""#).is_err());
663 assert!(parse(r#"glob:"/*""#).is_err());
664 assert!(parse(r#"glob:"*/..""#).is_err());
666
667 if cfg!(windows) {
669 insta::assert_debug_snapshot!(
670 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
671 Pattern(
672 FileGlob {
673 dir: "foo",
674 pattern: Pattern {
675 original: "*/bar",
676 tokens: _,
677 is_recursive: false,
678 },
679 },
680 )
681 "#);
682 } else {
683 insta::assert_debug_snapshot!(
684 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
685 Pattern(
686 FileGlob {
687 dir: "cur*",
688 pattern: Pattern {
689 original: "..\\foo\\*\\bar",
690 tokens: _,
691 is_recursive: false,
692 },
693 },
694 )
695 "#);
696 }
697
698 insta::assert_debug_snapshot!(
700 parse(r#"root-glob:"foo""#).unwrap(),
701 @r#"Pattern(FilePath("foo"))"#);
702 insta::assert_debug_snapshot!(
703 parse(r#"root-glob:"""#).unwrap(),
704 @r#"Pattern(FilePath(""))"#);
705 insta::assert_debug_snapshot!(
706 parse(r#"root-glob:".""#).unwrap(),
707 @r#"Pattern(FilePath(""))"#);
708
709 insta::assert_debug_snapshot!(
711 parse(r#"root-glob:"*""#).unwrap(), @r#"
712 Pattern(
713 FileGlob {
714 dir: "",
715 pattern: Pattern {
716 original: "*",
717 tokens: _,
718 is_recursive: false,
719 },
720 },
721 )
722 "#);
723 insta::assert_debug_snapshot!(
724 parse(r#"root-glob:"foo/bar/b[az]""#).unwrap(), @r#"
725 Pattern(
726 FileGlob {
727 dir: "foo/bar",
728 pattern: Pattern {
729 original: "b[az]",
730 tokens: _,
731 ),
732 ],
733 is_recursive: false,
734 },
735 },
736 )
737 "#);
738 assert!(parse(r#"root-glob:"../*""#).is_err());
739 assert!(parse(r#"root-glob:"/*""#).is_err());
740 }
741
742 #[test]
743 fn test_parse_function() {
744 let settings = insta_settings();
745 let _guard = settings.bind_to_scope();
746 let path_converter = RepoPathUiConverter::Fs {
747 cwd: PathBuf::from("/ws/cur"),
748 base: PathBuf::from("/ws"),
749 };
750 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
751
752 insta::assert_debug_snapshot!(parse("all()").unwrap(), @"All");
753 insta::assert_debug_snapshot!(parse("none()").unwrap(), @"None");
754 insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
755 InvalidArguments {
756 name: "all",
757 message: "Expected 0 arguments",
758 }
759 "#);
760 insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
761 NoSuchFunction {
762 name: "ale",
763 candidates: [
764 "all",
765 ],
766 }
767 "#);
768 }
769
770 #[test]
771 fn test_parse_compound_expression() {
772 let settings = insta_settings();
773 let _guard = settings.bind_to_scope();
774 let path_converter = RepoPathUiConverter::Fs {
775 cwd: PathBuf::from("/ws/cur"),
776 base: PathBuf::from("/ws"),
777 };
778 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
779
780 insta::assert_debug_snapshot!(parse("~x").unwrap(), @r#"
781 Difference(
782 All,
783 Pattern(PrefixPath("cur/x")),
784 )
785 "#);
786 insta::assert_debug_snapshot!(parse("x|y|root:z").unwrap(), @r#"
787 UnionAll(
788 [
789 Pattern(PrefixPath("cur/x")),
790 Pattern(PrefixPath("cur/y")),
791 Pattern(PrefixPath("z")),
792 ],
793 )
794 "#);
795 insta::assert_debug_snapshot!(parse("x|y&z").unwrap(), @r#"
796 UnionAll(
797 [
798 Pattern(PrefixPath("cur/x")),
799 Intersection(
800 Pattern(PrefixPath("cur/y")),
801 Pattern(PrefixPath("cur/z")),
802 ),
803 ],
804 )
805 "#);
806 }
807
808 #[test]
809 fn test_explicit_paths() {
810 let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
811 expr.explicit_paths().map(|path| path.to_owned()).collect()
812 };
813 let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
814 assert!(collect(&FilesetExpression::none()).is_empty());
815 assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
816 assert_eq!(
817 collect(&FilesetExpression::union_all(vec![
818 file_expr("a"),
819 file_expr("b"),
820 file_expr("c"),
821 ])),
822 ["a", "b", "c"].map(repo_path_buf)
823 );
824 assert_eq!(
825 collect(&FilesetExpression::intersection(
826 FilesetExpression::union_all(vec![
827 file_expr("a"),
828 FilesetExpression::none(),
829 file_expr("b"),
830 file_expr("c"),
831 ]),
832 FilesetExpression::difference(
833 file_expr("d"),
834 FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
835 )
836 )),
837 ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
838 );
839 }
840
841 #[test]
842 fn test_build_matcher_simple() {
843 let settings = insta_settings();
844 let _guard = settings.bind_to_scope();
845
846 insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
847 insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
848 insta::assert_debug_snapshot!(
849 FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
850 @r#"
851 FilesMatcher {
852 tree: Dir {
853 "foo": File {},
854 },
855 }
856 "#);
857 insta::assert_debug_snapshot!(
858 FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
859 @r#"
860 PrefixMatcher {
861 tree: Dir {
862 "foo": Prefix {},
863 },
864 }
865 "#);
866 }
867
868 #[test]
869 fn test_build_matcher_glob_pattern() {
870 let settings = insta_settings();
871 let _guard = settings.bind_to_scope();
872 let glob_expr = |dir: &str, pattern: &str| {
873 FilesetExpression::pattern(FilePattern::FileGlob {
874 dir: repo_path_buf(dir),
875 pattern: glob::Pattern::new(pattern).unwrap(),
876 })
877 };
878
879 insta::assert_debug_snapshot!(glob_expr("", "*").to_matcher(), @r#"
880 FileGlobsMatcher {
881 tree: [
882 Pattern {
883 original: "*",
884 tokens: _,
885 is_recursive: false,
886 },
887 ] {},
888 }
889 "#);
890
891 let expr =
892 FilesetExpression::union_all(vec![glob_expr("foo", "*"), glob_expr("foo/bar", "*")]);
893 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
894 FileGlobsMatcher {
895 tree: [] {
896 "foo": [
897 Pattern {
898 original: "*",
899 tokens: _,
900 is_recursive: false,
901 },
902 ] {
903 "bar": [
904 Pattern {
905 original: "*",
906 tokens: _,
907 is_recursive: false,
908 },
909 ] {},
910 },
911 },
912 }
913 "#);
914 }
915
916 #[test]
917 fn test_build_matcher_union_patterns_of_same_kind() {
918 let settings = insta_settings();
919 let _guard = settings.bind_to_scope();
920
921 let expr = FilesetExpression::union_all(vec![
922 FilesetExpression::file_path(repo_path_buf("foo")),
923 FilesetExpression::file_path(repo_path_buf("foo/bar")),
924 ]);
925 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
926 FilesMatcher {
927 tree: Dir {
928 "foo": File {
929 "bar": File {},
930 },
931 },
932 }
933 "#);
934
935 let expr = FilesetExpression::union_all(vec![
936 FilesetExpression::prefix_path(repo_path_buf("bar")),
937 FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
938 ]);
939 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
940 PrefixMatcher {
941 tree: Dir {
942 "bar": Prefix {
943 "baz": Prefix {},
944 },
945 },
946 }
947 "#);
948 }
949
950 #[test]
951 fn test_build_matcher_union_patterns_of_different_kind() {
952 let settings = insta_settings();
953 let _guard = settings.bind_to_scope();
954
955 let expr = FilesetExpression::union_all(vec![
956 FilesetExpression::file_path(repo_path_buf("foo")),
957 FilesetExpression::prefix_path(repo_path_buf("bar")),
958 ]);
959 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
960 UnionMatcher {
961 input1: FilesMatcher {
962 tree: Dir {
963 "foo": File {},
964 },
965 },
966 input2: PrefixMatcher {
967 tree: Dir {
968 "bar": Prefix {},
969 },
970 },
971 }
972 "#);
973 }
974
975 #[test]
976 fn test_build_matcher_unnormalized_union() {
977 let settings = insta_settings();
978 let _guard = settings.bind_to_scope();
979
980 let expr = FilesetExpression::UnionAll(vec![]);
981 insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
982
983 let expr =
984 FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
985 insta::assert_debug_snapshot!(expr.to_matcher(), @r"
986 UnionMatcher {
987 input1: NothingMatcher,
988 input2: EverythingMatcher,
989 }
990 ");
991 }
992
993 #[test]
994 fn test_build_matcher_combined() {
995 let settings = insta_settings();
996 let _guard = settings.bind_to_scope();
997
998 let expr = FilesetExpression::union_all(vec![
999 FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1000 FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1001 FilesetExpression::file_path(repo_path_buf("foo")),
1002 FilesetExpression::prefix_path(repo_path_buf("bar")),
1003 ]);
1004 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1005 UnionMatcher {
1006 input1: UnionMatcher {
1007 input1: IntersectionMatcher {
1008 input1: EverythingMatcher,
1009 input2: NothingMatcher,
1010 },
1011 input2: DifferenceMatcher {
1012 wanted: NothingMatcher,
1013 unwanted: EverythingMatcher,
1014 },
1015 },
1016 input2: UnionMatcher {
1017 input1: FilesMatcher {
1018 tree: Dir {
1019 "foo": File {},
1020 },
1021 },
1022 input2: PrefixMatcher {
1023 tree: Dir {
1024 "bar": Prefix {},
1025 },
1026 },
1027 },
1028 }
1029 "#);
1030 }
1031}