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