1use std::collections::HashMap;
18use std::iter;
19use std::path;
20use std::slice;
21use std::sync::LazyLock;
22
23use itertools::Itertools as _;
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::FilesetAliasesMap;
32pub use crate::fileset_parser::FilesetDiagnostics;
33pub use crate::fileset_parser::FilesetParseError;
34pub use crate::fileset_parser::FilesetParseErrorKind;
35pub use crate::fileset_parser::FilesetParseResult;
36use crate::fileset_parser::FunctionCallNode;
37use crate::fileset_parser::UnaryOp;
38use crate::matchers::DifferenceMatcher;
39use crate::matchers::EverythingMatcher;
40use crate::matchers::FilesMatcher;
41use crate::matchers::GlobsMatcher;
42use crate::matchers::IntersectionMatcher;
43use crate::matchers::Matcher;
44use crate::matchers::NothingMatcher;
45use crate::matchers::PathGlobPattern;
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::ui_path::RepoPathUiConverter;
52use crate::ui_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<PathGlobPattern>,
84 },
85 PrefixGlob {
87 dir: RepoPathBuf,
89 pattern: Box<PathGlobPattern>,
91 },
92 }
96
97impl FilePattern {
98 pub fn from_str_kind(
100 path_converter: &RepoPathUiConverter,
101 input: &str,
102 kind: &str,
103 ) -> Result<Self, FilePatternParseError> {
104 match kind {
119 "cwd" => Self::cwd_prefix_path(path_converter, input),
120 "cwd-file" | "file" => Self::cwd_file_path(path_converter, input),
121 "cwd-glob" | "glob" => Self::cwd_file_glob(path_converter, input),
122 "cwd-glob-i" | "glob-i" => Self::cwd_file_glob_i(path_converter, input),
123 "cwd-prefix-glob" | "prefix-glob" => Self::cwd_prefix_glob(path_converter, input),
124 "cwd-prefix-glob-i" | "prefix-glob-i" => Self::cwd_prefix_glob_i(path_converter, input),
125 "root" => Self::root_prefix_path(input),
126 "root-file" => Self::root_file_path(input),
127 "root-glob" => Self::root_file_glob(input),
128 "root-glob-i" => Self::root_file_glob_i(input),
129 "root-prefix-glob" => Self::root_prefix_glob(input),
130 "root-prefix-glob-i" => Self::root_prefix_glob_i(input),
131 _ => Err(FilePatternParseError::InvalidKind(kind.to_owned())),
132 }
133 }
134
135 pub fn cwd_file_path(
137 path_converter: &RepoPathUiConverter,
138 input: impl AsRef<str>,
139 ) -> Result<Self, FilePatternParseError> {
140 let path = path_converter.parse_file_path(input.as_ref())?;
141 Ok(Self::FilePath(path))
142 }
143
144 pub fn cwd_prefix_path(
146 path_converter: &RepoPathUiConverter,
147 input: impl AsRef<str>,
148 ) -> Result<Self, FilePatternParseError> {
149 let path = path_converter.parse_file_path(input.as_ref())?;
150 Ok(Self::PrefixPath(path))
151 }
152
153 pub fn cwd_file_glob(
155 path_converter: &RepoPathUiConverter,
156 input: impl AsRef<str>,
157 ) -> Result<Self, FilePatternParseError> {
158 let (dir, pattern) = split_glob_path(input.as_ref());
159 let dir = path_converter.parse_file_path(dir)?;
160 Self::file_glob_at(dir, pattern, false)
161 }
162
163 pub fn cwd_file_glob_i(
165 path_converter: &RepoPathUiConverter,
166 input: impl AsRef<str>,
167 ) -> Result<Self, FilePatternParseError> {
168 let (dir, pattern) = split_glob_path_i(input.as_ref());
169 let dir = path_converter.parse_file_path(dir)?;
170 Self::file_glob_at(dir, pattern, true)
171 }
172
173 pub fn cwd_prefix_glob(
175 path_converter: &RepoPathUiConverter,
176 input: impl AsRef<str>,
177 ) -> Result<Self, FilePatternParseError> {
178 let (dir, pattern) = split_glob_path(input.as_ref());
179 let dir = path_converter.parse_file_path(dir)?;
180 Self::prefix_glob_at(dir, pattern, false)
181 }
182
183 pub fn cwd_prefix_glob_i(
186 path_converter: &RepoPathUiConverter,
187 input: impl AsRef<str>,
188 ) -> Result<Self, FilePatternParseError> {
189 let (dir, pattern) = split_glob_path_i(input.as_ref());
190 let dir = path_converter.parse_file_path(dir)?;
191 Self::prefix_glob_at(dir, pattern, true)
192 }
193
194 pub fn root_file_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
196 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
198 Ok(Self::FilePath(path))
199 }
200
201 pub fn root_prefix_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
203 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
204 Ok(Self::PrefixPath(path))
205 }
206
207 pub fn root_file_glob(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
209 let (dir, pattern) = split_glob_path(input.as_ref());
210 let dir = RepoPathBuf::from_relative_path(dir)?;
211 Self::file_glob_at(dir, pattern, false)
212 }
213
214 pub fn root_file_glob_i(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
217 let (dir, pattern) = split_glob_path_i(input.as_ref());
218 let dir = RepoPathBuf::from_relative_path(dir)?;
219 Self::file_glob_at(dir, pattern, true)
220 }
221
222 pub fn root_prefix_glob(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
224 let (dir, pattern) = split_glob_path(input.as_ref());
225 let dir = RepoPathBuf::from_relative_path(dir)?;
226 Self::prefix_glob_at(dir, pattern, false)
227 }
228
229 pub fn root_prefix_glob_i(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
232 let (dir, pattern) = split_glob_path_i(input.as_ref());
233 let dir = RepoPathBuf::from_relative_path(dir)?;
234 Self::prefix_glob_at(dir, pattern, true)
235 }
236
237 fn file_glob_at(
238 dir: RepoPathBuf,
239 input: &str,
240 icase: bool,
241 ) -> Result<Self, FilePatternParseError> {
242 if input.is_empty() {
243 return Ok(Self::FilePath(dir));
244 }
245 let normalized = RepoPathBuf::from_relative_path(input)?;
247 let pattern = Box::new(parse_file_glob(
248 normalized.as_internal_file_string(),
249 icase,
250 )?);
251 Ok(Self::FileGlob { dir, pattern })
252 }
253
254 fn prefix_glob_at(
255 dir: RepoPathBuf,
256 input: &str,
257 icase: bool,
258 ) -> Result<Self, FilePatternParseError> {
259 if input.is_empty() {
260 return Ok(Self::PrefixPath(dir));
261 }
262 let normalized = RepoPathBuf::from_relative_path(input)?;
264 let pattern = Box::new(parse_file_glob(
265 normalized.as_internal_file_string(),
266 icase,
267 )?);
268 Ok(Self::PrefixGlob { dir, pattern })
269 }
270
271 pub fn as_path(&self) -> Option<&RepoPath> {
274 match self {
275 Self::FilePath(path) => Some(path),
276 Self::PrefixPath(path) => Some(path),
277 Self::FileGlob { .. } | Self::PrefixGlob { .. } => None,
278 }
279 }
280}
281
282fn parse_file_glob(input: &str, icase: bool) -> Result<PathGlobPattern, globset::Error> {
283 if icase {
284 PathGlobPattern::parse_i(input)
285 } else {
286 PathGlobPattern::parse(input)
287 }
288}
289
290fn is_glob_char(c: char) -> bool {
292 const GLOB_CHARS: &[char] = if cfg!(windows) {
295 &['?', '*', '[', ']', '{', '}']
296 } else {
297 &['?', '*', '[', ']', '{', '}', '\\']
298 };
299 GLOB_CHARS.contains(&c)
300}
301
302fn split_glob_path(input: &str) -> (&str, &str) {
304 let prefix_len = input
305 .split_inclusive(path::is_separator)
306 .take_while(|component| !component.contains(is_glob_char))
307 .map(|component| component.len())
308 .sum();
309 input.split_at(prefix_len)
310}
311
312fn split_glob_path_i(input: &str) -> (&str, &str) {
315 let prefix_len = input
316 .split_inclusive(path::is_separator)
317 .take_while(|component| {
318 !component.contains(|c: char| c.is_ascii_alphabetic() || is_glob_char(c))
319 })
320 .map(|component| component.len())
321 .sum();
322 input.split_at(prefix_len)
323}
324
325#[derive(Clone, Debug)]
327pub enum FilesetExpression {
328 None,
330 All,
332 Pattern(FilePattern),
334 UnionAll(Vec<Self>),
339 Intersection(Box<Self>, Box<Self>),
341 Difference(Box<Self>, Box<Self>),
343}
344
345impl FilesetExpression {
346 pub fn none() -> Self {
348 Self::None
349 }
350
351 pub fn all() -> Self {
353 Self::All
354 }
355
356 pub fn pattern(pattern: FilePattern) -> Self {
358 Self::Pattern(pattern)
359 }
360
361 pub fn file_path(path: RepoPathBuf) -> Self {
363 Self::Pattern(FilePattern::FilePath(path))
364 }
365
366 pub fn prefix_path(path: RepoPathBuf) -> Self {
368 Self::Pattern(FilePattern::PrefixPath(path))
369 }
370
371 pub fn union_all(expressions: Vec<Self>) -> Self {
373 match expressions.len() {
374 0 => Self::none(),
375 1 => expressions.into_iter().next().unwrap(),
376 _ => Self::UnionAll(expressions),
377 }
378 }
379
380 pub fn intersection(self, other: Self) -> Self {
382 Self::Intersection(Box::new(self), Box::new(other))
383 }
384
385 pub fn difference(self, other: Self) -> Self {
387 Self::Difference(Box::new(self), Box::new(other))
388 }
389
390 fn as_union_all(&self) -> &[Self] {
392 match self {
393 Self::None => &[],
394 Self::UnionAll(exprs) => exprs,
395 _ => slice::from_ref(self),
396 }
397 }
398
399 fn dfs_pre(&self) -> impl Iterator<Item = &Self> {
400 let mut stack: Vec<&Self> = vec![self];
401 iter::from_fn(move || {
402 let expr = stack.pop()?;
403 match expr {
404 Self::None | Self::All | Self::Pattern(_) => {}
405 Self::UnionAll(exprs) => stack.extend(exprs.iter().rev()),
406 Self::Intersection(expr1, expr2) | Self::Difference(expr1, expr2) => {
407 stack.push(expr2);
408 stack.push(expr1);
409 }
410 }
411 Some(expr)
412 })
413 }
414
415 pub fn explicit_paths(&self) -> impl Iterator<Item = &RepoPath> {
420 self.dfs_pre().filter_map(|expr| match expr {
423 Self::Pattern(pattern) => pattern.as_path(),
424 _ => None,
425 })
426 }
427
428 pub fn to_matcher(&self) -> Box<dyn Matcher> {
430 build_union_matcher(self.as_union_all())
431 }
432}
433
434fn build_union_matcher(expressions: &[FilesetExpression]) -> Box<dyn Matcher> {
439 let mut file_paths = Vec::new();
440 let mut prefix_paths = Vec::new();
441 let mut file_globs = GlobsMatcher::builder().prefix_paths(false);
442 let mut prefix_globs = GlobsMatcher::builder().prefix_paths(true);
443 let mut matchers: Vec<Option<Box<dyn Matcher>>> = Vec::new();
444 for expr in expressions {
445 let matcher: Box<dyn Matcher> = match expr {
446 FilesetExpression::None => Box::new(NothingMatcher),
448 FilesetExpression::All => Box::new(EverythingMatcher),
449 FilesetExpression::Pattern(pattern) => {
450 match pattern {
451 FilePattern::FilePath(path) => file_paths.push(path),
452 FilePattern::PrefixPath(path) => prefix_paths.push(path),
453 FilePattern::FileGlob { dir, pattern } => file_globs.add(dir, pattern),
454 FilePattern::PrefixGlob { dir, pattern } => prefix_globs.add(dir, pattern),
455 }
456 continue;
457 }
458 FilesetExpression::UnionAll(exprs) => build_union_matcher(exprs),
460 FilesetExpression::Intersection(expr1, expr2) => {
461 let m1 = build_union_matcher(expr1.as_union_all());
462 let m2 = build_union_matcher(expr2.as_union_all());
463 Box::new(IntersectionMatcher::new(m1, m2))
464 }
465 FilesetExpression::Difference(expr1, expr2) => {
466 let m1 = build_union_matcher(expr1.as_union_all());
467 let m2 = build_union_matcher(expr2.as_union_all());
468 Box::new(DifferenceMatcher::new(m1, m2))
469 }
470 };
471 matchers.push(Some(matcher));
472 }
473
474 if !file_paths.is_empty() {
475 matchers.push(Some(Box::new(FilesMatcher::new(file_paths))));
476 }
477 if !prefix_paths.is_empty() {
478 matchers.push(Some(Box::new(PrefixMatcher::new(prefix_paths))));
479 }
480 if !file_globs.is_empty() {
481 matchers.push(Some(Box::new(file_globs.build())));
482 }
483 if !prefix_globs.is_empty() {
484 matchers.push(Some(Box::new(prefix_globs.build())));
485 }
486 union_all_matchers(&mut matchers)
487}
488
489fn union_all_matchers(matchers: &mut [Option<Box<dyn Matcher>>]) -> Box<dyn Matcher> {
494 match matchers {
495 [] => Box::new(NothingMatcher),
496 [matcher] => matcher.take().expect("matcher should still be available"),
497 _ => {
498 let (left, right) = matchers.split_at_mut(matchers.len() / 2);
500 let m1 = union_all_matchers(left);
501 let m2 = union_all_matchers(right);
502 Box::new(UnionMatcher::new(m1, m2))
503 }
504 }
505}
506
507type FilesetFunction = fn(
508 &mut FilesetDiagnostics,
509 &RepoPathUiConverter,
510 &FunctionCallNode,
511) -> FilesetParseResult<FilesetExpression>;
512
513static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, FilesetFunction>> = LazyLock::new(|| {
514 let mut map: HashMap<&str, FilesetFunction> = HashMap::new();
517 map.insert("none", |_diagnostics, _path_converter, function| {
518 function.expect_no_arguments()?;
519 Ok(FilesetExpression::none())
520 });
521 map.insert("all", |_diagnostics, _path_converter, function| {
522 function.expect_no_arguments()?;
523 Ok(FilesetExpression::all())
524 });
525 map
526});
527
528fn resolve_function(
529 diagnostics: &mut FilesetDiagnostics,
530 path_converter: &RepoPathUiConverter,
531 function: &FunctionCallNode,
532) -> FilesetParseResult<FilesetExpression> {
533 if let Some(func) = BUILTIN_FUNCTION_MAP.get(function.name) {
534 func(diagnostics, path_converter, function)
535 } else {
536 Err(FilesetParseError::new(
537 FilesetParseErrorKind::NoSuchFunction {
538 name: function.name.to_owned(),
539 candidates: collect_similar(function.name, BUILTIN_FUNCTION_MAP.keys()),
540 },
541 function.name_span,
542 ))
543 }
544}
545
546fn resolve_expression(
547 diagnostics: &mut FilesetDiagnostics,
548 path_converter: &RepoPathUiConverter,
549 node: &ExpressionNode,
550) -> FilesetParseResult<FilesetExpression> {
551 fileset_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
552 let wrap_pattern_error =
553 |err| FilesetParseError::expression("Invalid file pattern", node.span).with_source(err);
554 match &node.kind {
555 ExpressionKind::Identifier(name) => {
556 let pattern = FilePattern::cwd_prefix_glob(path_converter, name)
557 .map_err(wrap_pattern_error)?;
558 Ok(FilesetExpression::pattern(pattern))
559 }
560 ExpressionKind::String(name) => {
561 let pattern = FilePattern::cwd_prefix_glob(path_converter, name)
562 .map_err(wrap_pattern_error)?;
563 Ok(FilesetExpression::pattern(pattern))
564 }
565 ExpressionKind::Pattern(pattern) => {
566 let value = fileset_parser::expect_string_literal("string", &pattern.value)?;
567 let pattern = FilePattern::from_str_kind(path_converter, value, pattern.name)
568 .map_err(wrap_pattern_error)?;
569 Ok(FilesetExpression::pattern(pattern))
570 }
571 ExpressionKind::Unary(op, arg_node) => {
572 let arg = resolve_expression(diagnostics, path_converter, arg_node)?;
573 match op {
574 UnaryOp::Negate => Ok(FilesetExpression::all().difference(arg)),
575 }
576 }
577 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
578 let lhs = resolve_expression(diagnostics, path_converter, lhs_node)?;
579 let rhs = resolve_expression(diagnostics, path_converter, rhs_node)?;
580 match op {
581 BinaryOp::Intersection => Ok(lhs.intersection(rhs)),
582 BinaryOp::Difference => Ok(lhs.difference(rhs)),
583 }
584 }
585 ExpressionKind::UnionAll(nodes) => {
586 let expressions = nodes
587 .iter()
588 .map(|node| resolve_expression(diagnostics, path_converter, node))
589 .try_collect()?;
590 Ok(FilesetExpression::union_all(expressions))
591 }
592 ExpressionKind::FunctionCall(function) => {
593 resolve_function(diagnostics, path_converter, function)
594 }
595 ExpressionKind::AliasExpanded(..) => unreachable!(),
596 }
597 })
598}
599
600#[derive(Clone, Debug)]
602pub struct FilesetParseContext<'a> {
603 pub aliases_map: &'a FilesetAliasesMap,
605 pub path_converter: &'a RepoPathUiConverter,
607}
608
609pub fn parse(
611 diagnostics: &mut FilesetDiagnostics,
612 text: &str,
613 context: &FilesetParseContext,
614) -> FilesetParseResult<FilesetExpression> {
615 let node = fileset_parser::parse_program(text)?;
616 let node = fileset_parser::expand_aliases(node, context.aliases_map)?;
617 resolve_expression(diagnostics, context.path_converter, &node)
619}
620
621pub fn parse_maybe_bare(
626 diagnostics: &mut FilesetDiagnostics,
627 text: &str,
628 context: &FilesetParseContext,
629) -> FilesetParseResult<FilesetExpression> {
630 let node = fileset_parser::parse_program_or_bare_string(text)?;
631 let node = fileset_parser::expand_aliases(node, context.aliases_map)?;
632 resolve_expression(diagnostics, context.path_converter, &node)
634}
635
636#[cfg(test)]
637mod tests {
638 use std::path::PathBuf;
639
640 use super::*;
641 use crate::tests::TestResult;
642
643 fn repo_path_buf(value: impl Into<String>) -> RepoPathBuf {
644 RepoPathBuf::from_internal_string(value).unwrap()
645 }
646
647 fn insta_settings() -> insta::Settings {
648 let mut settings = insta::Settings::clone_current();
649 for _ in 0..4 {
652 settings.add_filter(
653 r"(?x)
654 \b([A-Z]\w*)\(\n
655 \s*(.{1,60}),\n
656 \s*\)",
657 "$1($2)",
658 );
659 }
660 settings
661 }
662
663 #[test]
664 fn test_parse_file_pattern() -> TestResult {
665 let settings = insta_settings();
666 let _guard = settings.bind_to_scope();
667 let context = FilesetParseContext {
668 aliases_map: &FilesetAliasesMap::new(),
669 path_converter: &RepoPathUiConverter::Fs {
670 cwd: PathBuf::from("/ws/cur"),
671 base: PathBuf::from("/ws"),
672 },
673 };
674 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
675
676 insta::assert_debug_snapshot!(
678 parse(".")?,
679 @r#"Pattern(PrefixPath("cur"))"#);
680 insta::assert_debug_snapshot!(
681 parse("..")?,
682 @r#"Pattern(PrefixPath(""))"#);
683 assert!(parse("../..").is_err());
684 insta::assert_debug_snapshot!(
685 parse("foo")?,
686 @r#"Pattern(PrefixPath("cur/foo"))"#);
687 insta::assert_debug_snapshot!(
688 parse("*.*")?,
689 @r#"
690 Pattern(
691 PrefixGlob {
692 dir: "cur",
693 pattern: PathGlobPattern {
694 glob: "*.*",
695 re: "(?-u)^[^/]*\\.[^/]*$",
696 ..
697 },
698 },
699 )
700 "#);
701 insta::assert_debug_snapshot!(
702 parse("cwd:.")?,
703 @r#"Pattern(PrefixPath("cur"))"#);
704 insta::assert_debug_snapshot!(
705 parse("cwd-file:foo")?,
706 @r#"Pattern(FilePath("cur/foo"))"#);
707 insta::assert_debug_snapshot!(
708 parse("file:../foo/bar")?,
709 @r#"Pattern(FilePath("foo/bar"))"#);
710
711 insta::assert_debug_snapshot!(
713 parse("root:.")?,
714 @r#"Pattern(PrefixPath(""))"#);
715 assert!(parse("root:..").is_err());
716 insta::assert_debug_snapshot!(
717 parse("root:foo/bar")?,
718 @r#"Pattern(PrefixPath("foo/bar"))"#);
719 insta::assert_debug_snapshot!(
720 parse("root-file:bar")?,
721 @r#"Pattern(FilePath("bar"))"#);
722
723 insta::assert_debug_snapshot!(
724 parse("file:(foo|bar)").unwrap_err().kind(),
725 @r#"Expression("Expected string")"#);
726 Ok(())
727 }
728
729 #[test]
730 fn test_parse_glob_pattern() -> TestResult {
731 let settings = insta_settings();
732 let _guard = settings.bind_to_scope();
733 let context = FilesetParseContext {
734 aliases_map: &FilesetAliasesMap::new(),
735 path_converter: &RepoPathUiConverter::Fs {
736 cwd: PathBuf::from("/ws/cur*"),
738 base: PathBuf::from("/ws"),
739 },
740 };
741 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
742
743 insta::assert_debug_snapshot!(
745 parse(r#"cwd-glob:"foo""#)?,
746 @r#"Pattern(FilePath("cur*/foo"))"#);
747 insta::assert_debug_snapshot!(
750 parse(r#"glob:"""#)?,
751 @r#"Pattern(FilePath("cur*"))"#);
752 insta::assert_debug_snapshot!(
753 parse(r#"glob:".""#)?,
754 @r#"Pattern(FilePath("cur*"))"#);
755 insta::assert_debug_snapshot!(
756 parse(r#"glob:"..""#)?,
757 @r#"Pattern(FilePath(""))"#);
758
759 insta::assert_debug_snapshot!(
761 parse(r#"glob:"*""#)?, @r#"
762 Pattern(
763 FileGlob {
764 dir: "cur*",
765 pattern: PathGlobPattern {
766 glob: "*",
767 re: "(?-u)^[^/]*$",
768 ..
769 },
770 },
771 )
772 "#);
773 insta::assert_debug_snapshot!(
774 parse(r#"glob:"./*""#)?, @r#"
775 Pattern(
776 FileGlob {
777 dir: "cur*",
778 pattern: PathGlobPattern {
779 glob: "*",
780 re: "(?-u)^[^/]*$",
781 ..
782 },
783 },
784 )
785 "#);
786 insta::assert_debug_snapshot!(
787 parse(r#"glob:"../*""#)?, @r#"
788 Pattern(
789 FileGlob {
790 dir: "",
791 pattern: PathGlobPattern {
792 glob: "*",
793 re: "(?-u)^[^/]*$",
794 ..
795 },
796 },
797 )
798 "#);
799 insta::assert_debug_snapshot!(
801 parse(r#"glob:"**""#)?, @r#"
802 Pattern(
803 FileGlob {
804 dir: "cur*",
805 pattern: PathGlobPattern {
806 glob: "**",
807 re: "(?-u)^.*$",
808 ..
809 },
810 },
811 )
812 "#);
813 insta::assert_debug_snapshot!(
814 parse(r#"glob:"../foo/b?r/baz""#)?, @r#"
815 Pattern(
816 FileGlob {
817 dir: "foo",
818 pattern: PathGlobPattern {
819 glob: "b?r/baz",
820 re: "(?-u)^b[^/]r/baz$",
821 ..
822 },
823 },
824 )
825 "#);
826 assert!(parse(r#"glob:"../../*""#).is_err());
827 assert!(parse(r#"glob-i:"../../*""#).is_err());
828 assert!(parse(r#"glob:"/*""#).is_err());
829 assert!(parse(r#"glob-i:"/*""#).is_err());
830 assert!(parse(r#"glob:"*/..""#).is_err());
832 assert!(parse(r#"glob-i:"*/..""#).is_err());
833
834 if cfg!(windows) {
835 insta::assert_debug_snapshot!(
837 parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
838 Pattern(
839 FileGlob {
840 dir: "foo",
841 pattern: PathGlobPattern {
842 glob: "*/bar",
843 re: "(?-u)^[^/]*/bar$",
844 ..
845 },
846 },
847 )
848 "#);
849 } else {
850 insta::assert_debug_snapshot!(
852 parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
853 Pattern(
854 FileGlob {
855 dir: "cur*",
856 pattern: PathGlobPattern {
857 glob: "..\\foo\\*\\bar",
858 re: "(?-u)^\\.\\.foo\\*bar$",
859 ..
860 },
861 },
862 )
863 "#);
864 }
865
866 insta::assert_debug_snapshot!(
868 parse(r#"root-glob:"foo""#)?,
869 @r#"Pattern(FilePath("foo"))"#);
870 insta::assert_debug_snapshot!(
871 parse(r#"root-glob:"""#)?,
872 @r#"Pattern(FilePath(""))"#);
873 insta::assert_debug_snapshot!(
874 parse(r#"root-glob:".""#)?,
875 @r#"Pattern(FilePath(""))"#);
876
877 insta::assert_debug_snapshot!(
879 parse(r#"root-glob:"*""#)?, @r#"
880 Pattern(
881 FileGlob {
882 dir: "",
883 pattern: PathGlobPattern {
884 glob: "*",
885 re: "(?-u)^[^/]*$",
886 ..
887 },
888 },
889 )
890 "#);
891 insta::assert_debug_snapshot!(
892 parse(r#"root-glob:"foo/bar/b[az]""#)?, @r#"
893 Pattern(
894 FileGlob {
895 dir: "foo/bar",
896 pattern: PathGlobPattern {
897 glob: "b[az]",
898 re: "(?-u)^b[az]$",
899 ..
900 },
901 },
902 )
903 "#);
904 insta::assert_debug_snapshot!(
905 parse(r#"root-glob:"foo/bar/b{ar,az}""#)?, @r#"
906 Pattern(
907 FileGlob {
908 dir: "foo/bar",
909 pattern: PathGlobPattern {
910 glob: "b{ar,az}",
911 re: "(?-u)^b(?:ar|az)$",
912 ..
913 },
914 },
915 )
916 "#);
917 assert!(parse(r#"root-glob:"../*""#).is_err());
918 assert!(parse(r#"root-glob-i:"../*""#).is_err());
919 assert!(parse(r#"root-glob:"/*""#).is_err());
920 assert!(parse(r#"root-glob-i:"/*""#).is_err());
921
922 if cfg!(not(windows)) {
924 insta::assert_debug_snapshot!(
925 parse(r#"root-glob:'foo/bar\baz'"#)?, @r#"
926 Pattern(
927 FileGlob {
928 dir: "foo",
929 pattern: PathGlobPattern {
930 glob: "bar\\baz",
931 re: "(?-u)^barbaz$",
932 ..
933 },
934 },
935 )
936 "#);
937 }
938 Ok(())
939 }
940
941 #[test]
942 fn test_parse_glob_pattern_case_insensitive() -> TestResult {
943 let settings = insta_settings();
944 let _guard = settings.bind_to_scope();
945 let context = FilesetParseContext {
946 aliases_map: &FilesetAliasesMap::new(),
947 path_converter: &RepoPathUiConverter::Fs {
948 cwd: PathBuf::from("/ws/cur"),
949 base: PathBuf::from("/ws"),
950 },
951 };
952 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
953
954 insta::assert_debug_snapshot!(
956 parse(r#"glob-i:"*.TXT""#)?, @r#"
957 Pattern(
958 FileGlob {
959 dir: "cur",
960 pattern: PathGlobPattern {
961 glob: "*.TXT",
962 re: "(?-u)(?i)^[^/]*\\.TXT$",
963 ..
964 },
965 },
966 )
967 "#);
968
969 insta::assert_debug_snapshot!(
971 parse(r#"cwd-glob-i:"[Ff]oo""#)?, @r#"
972 Pattern(
973 FileGlob {
974 dir: "cur",
975 pattern: PathGlobPattern {
976 glob: "[Ff]oo",
977 re: "(?-u)(?i)^[Ff]oo$",
978 ..
979 },
980 },
981 )
982 "#);
983
984 insta::assert_debug_snapshot!(
986 parse(r#"root-glob-i:"*.Rs""#)?, @r#"
987 Pattern(
988 FileGlob {
989 dir: "",
990 pattern: PathGlobPattern {
991 glob: "*.Rs",
992 re: "(?-u)(?i)^[^/]*\\.Rs$",
993 ..
994 },
995 },
996 )
997 "#);
998
999 insta::assert_debug_snapshot!(
1001 parse(r#"glob-i:"SubDir/*.rs""#)?, @r#"
1002 Pattern(
1003 FileGlob {
1004 dir: "cur",
1005 pattern: PathGlobPattern {
1006 glob: "SubDir/*.rs",
1007 re: "(?-u)(?i)^SubDir/[^/]*\\.rs$",
1008 ..
1009 },
1010 },
1011 )
1012 "#);
1013
1014 insta::assert_debug_snapshot!(
1016 parse(r#"glob:"SubDir/*.rs""#)?, @r#"
1017 Pattern(
1018 FileGlob {
1019 dir: "cur/SubDir",
1020 pattern: PathGlobPattern {
1021 glob: "*.rs",
1022 re: "(?-u)^[^/]*\\.rs$",
1023 ..
1024 },
1025 },
1026 )
1027 "#);
1028
1029 insta::assert_debug_snapshot!(
1031 parse(r#"glob-i:"../SomeDir/*.rs""#)?, @r#"
1032 Pattern(
1033 FileGlob {
1034 dir: "",
1035 pattern: PathGlobPattern {
1036 glob: "SomeDir/*.rs",
1037 re: "(?-u)(?i)^SomeDir/[^/]*\\.rs$",
1038 ..
1039 },
1040 },
1041 )
1042 "#);
1043
1044 insta::assert_debug_snapshot!(
1046 parse(r#"glob-i:"./SomeFile*.txt""#)?, @r#"
1047 Pattern(
1048 FileGlob {
1049 dir: "cur",
1050 pattern: PathGlobPattern {
1051 glob: "SomeFile*.txt",
1052 re: "(?-u)(?i)^SomeFile[^/]*\\.txt$",
1053 ..
1054 },
1055 },
1056 )
1057 "#);
1058 Ok(())
1059 }
1060
1061 #[test]
1062 fn test_parse_prefix_glob_pattern() -> TestResult {
1063 let settings = insta_settings();
1064 let _guard = settings.bind_to_scope();
1065 let context = FilesetParseContext {
1066 aliases_map: &FilesetAliasesMap::new(),
1067 path_converter: &RepoPathUiConverter::Fs {
1068 cwd: PathBuf::from("/ws/cur*"),
1070 base: PathBuf::from("/ws"),
1071 },
1072 };
1073 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1074
1075 insta::assert_debug_snapshot!(
1077 parse("cwd-prefix-glob:'foo'")?,
1078 @r#"Pattern(PrefixPath("cur*/foo"))"#);
1079 insta::assert_debug_snapshot!(
1080 parse("prefix-glob:'.'")?,
1081 @r#"Pattern(PrefixPath("cur*"))"#);
1082 insta::assert_debug_snapshot!(
1083 parse("cwd-prefix-glob-i:'..'")?,
1084 @r#"Pattern(PrefixPath(""))"#);
1085 insta::assert_debug_snapshot!(
1086 parse("prefix-glob-i:'../_'")?,
1087 @r#"Pattern(PrefixPath("_"))"#);
1088
1089 insta::assert_debug_snapshot!(
1091 parse("cwd-prefix-glob:'*'")?, @r#"
1092 Pattern(
1093 PrefixGlob {
1094 dir: "cur*",
1095 pattern: PathGlobPattern {
1096 glob: "*",
1097 re: "(?-u)^[^/]*$",
1098 ..
1099 },
1100 },
1101 )
1102 "#);
1103
1104 insta::assert_debug_snapshot!(
1106 parse("cwd-prefix-glob-i:'../foo'")?, @r#"
1107 Pattern(
1108 PrefixGlob {
1109 dir: "",
1110 pattern: PathGlobPattern {
1111 glob: "foo",
1112 re: "(?-u)(?i)^foo$",
1113 ..
1114 },
1115 },
1116 )
1117 "#);
1118
1119 insta::assert_debug_snapshot!(
1121 parse("root-prefix-glob:'foo'")?,
1122 @r#"Pattern(PrefixPath("foo"))"#);
1123 insta::assert_debug_snapshot!(
1124 parse("root-prefix-glob-i:'.'")?,
1125 @r#"Pattern(PrefixPath(""))"#);
1126
1127 insta::assert_debug_snapshot!(
1129 parse("root-prefix-glob:'*'")?, @r#"
1130 Pattern(
1131 PrefixGlob {
1132 dir: "",
1133 pattern: PathGlobPattern {
1134 glob: "*",
1135 re: "(?-u)^[^/]*$",
1136 ..
1137 },
1138 },
1139 )
1140 "#);
1141
1142 insta::assert_debug_snapshot!(
1144 parse("root-prefix-glob-i:'_/foo'")?, @r#"
1145 Pattern(
1146 PrefixGlob {
1147 dir: "_",
1148 pattern: PathGlobPattern {
1149 glob: "foo",
1150 re: "(?-u)(?i)^foo$",
1151 ..
1152 },
1153 },
1154 )
1155 "#);
1156 Ok(())
1157 }
1158
1159 #[test]
1160 fn test_parse_function() -> TestResult {
1161 let settings = insta_settings();
1162 let _guard = settings.bind_to_scope();
1163 let context = FilesetParseContext {
1164 aliases_map: &FilesetAliasesMap::new(),
1165 path_converter: &RepoPathUiConverter::Fs {
1166 cwd: PathBuf::from("/ws/cur"),
1167 base: PathBuf::from("/ws"),
1168 },
1169 };
1170 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1171
1172 insta::assert_debug_snapshot!(parse("all()")?, @"All");
1173 insta::assert_debug_snapshot!(parse("none()")?, @"None");
1174 insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
1175 InvalidArguments {
1176 name: "all",
1177 message: "Expected 0 arguments",
1178 }
1179 "#);
1180 insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
1181 NoSuchFunction {
1182 name: "ale",
1183 candidates: [
1184 "all",
1185 ],
1186 }
1187 "#);
1188 Ok(())
1189 }
1190
1191 #[test]
1192 fn test_parse_compound_expression() -> TestResult {
1193 let settings = insta_settings();
1194 let _guard = settings.bind_to_scope();
1195 let context = FilesetParseContext {
1196 aliases_map: &FilesetAliasesMap::new(),
1197 path_converter: &RepoPathUiConverter::Fs {
1198 cwd: PathBuf::from("/ws/cur"),
1199 base: PathBuf::from("/ws"),
1200 },
1201 };
1202 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1203
1204 insta::assert_debug_snapshot!(parse("~x")?, @r#"
1205 Difference(
1206 All,
1207 Pattern(PrefixPath("cur/x")),
1208 )
1209 "#);
1210 insta::assert_debug_snapshot!(parse("x|y|root:z")?, @r#"
1211 UnionAll(
1212 [
1213 Pattern(PrefixPath("cur/x")),
1214 Pattern(PrefixPath("cur/y")),
1215 Pattern(PrefixPath("z")),
1216 ],
1217 )
1218 "#);
1219 insta::assert_debug_snapshot!(parse("x|y&z")?, @r#"
1220 UnionAll(
1221 [
1222 Pattern(PrefixPath("cur/x")),
1223 Intersection(
1224 Pattern(PrefixPath("cur/y")),
1225 Pattern(PrefixPath("cur/z")),
1226 ),
1227 ],
1228 )
1229 "#);
1230 Ok(())
1231 }
1232
1233 #[test]
1234 fn test_explicit_paths() {
1235 let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
1236 expr.explicit_paths().map(|path| path.to_owned()).collect()
1237 };
1238 let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
1239 assert!(collect(&FilesetExpression::none()).is_empty());
1240 assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
1241 assert_eq!(
1242 collect(&FilesetExpression::union_all(vec![
1243 file_expr("a"),
1244 file_expr("b"),
1245 file_expr("c"),
1246 ])),
1247 ["a", "b", "c"].map(repo_path_buf)
1248 );
1249 assert_eq!(
1250 collect(&FilesetExpression::intersection(
1251 FilesetExpression::union_all(vec![
1252 file_expr("a"),
1253 FilesetExpression::none(),
1254 file_expr("b"),
1255 file_expr("c"),
1256 ]),
1257 FilesetExpression::difference(
1258 file_expr("d"),
1259 FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
1260 )
1261 )),
1262 ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
1263 );
1264 }
1265
1266 #[test]
1267 fn test_build_matcher_simple() {
1268 let settings = insta_settings();
1269 let _guard = settings.bind_to_scope();
1270
1271 insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
1272 insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
1273 insta::assert_debug_snapshot!(
1274 FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
1275 @r#"
1276 FilesMatcher {
1277 tree: Dir {
1278 "foo": File {},
1279 },
1280 }
1281 "#);
1282 insta::assert_debug_snapshot!(
1283 FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
1284 @r#"
1285 PrefixMatcher {
1286 tree: Dir {
1287 "foo": Prefix {},
1288 },
1289 }
1290 "#);
1291 }
1292
1293 #[test]
1294 fn test_build_matcher_glob_pattern() {
1295 let settings = insta_settings();
1296 let _guard = settings.bind_to_scope();
1297 let file_glob_expr = |dir: &str, pattern: &str| {
1298 FilesetExpression::pattern(FilePattern::FileGlob {
1299 dir: repo_path_buf(dir),
1300 pattern: Box::new(PathGlobPattern::parse(pattern).unwrap()),
1301 })
1302 };
1303 let prefix_glob_expr = |dir: &str, pattern: &str| {
1304 FilesetExpression::pattern(FilePattern::PrefixGlob {
1305 dir: repo_path_buf(dir),
1306 pattern: Box::new(PathGlobPattern::parse(pattern).unwrap()),
1307 })
1308 };
1309
1310 insta::assert_debug_snapshot!(file_glob_expr("", "*").to_matcher(), @r#"
1311 GlobsMatcher {
1312 tree: Some(RegexSet(["(?-u)^[^/]*$"])) {},
1313 matches_prefix_paths: false,
1314 }
1315 "#);
1316
1317 let expr = FilesetExpression::union_all(vec![
1318 file_glob_expr("foo", "*"),
1319 file_glob_expr("foo/bar", "*"),
1320 file_glob_expr("foo", "?"),
1321 prefix_glob_expr("foo", "ba[rz]"),
1322 prefix_glob_expr("foo", "qu*x"),
1323 ]);
1324 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1325 UnionMatcher {
1326 input1: GlobsMatcher {
1327 tree: None {
1328 "foo": Some(RegexSet(["(?-u)^[^/]*$", "(?-u)^[^/]$"])) {
1329 "bar": Some(RegexSet(["(?-u)^[^/]*$"])) {},
1330 },
1331 },
1332 matches_prefix_paths: false,
1333 },
1334 input2: GlobsMatcher {
1335 tree: None {
1336 "foo": Some(RegexSet(["(?-u)^ba[rz](?:/|$)", "(?-u)^qu[^/]*x(?:/|$)"])) {},
1337 },
1338 matches_prefix_paths: true,
1339 },
1340 }
1341 "#);
1342 }
1343
1344 #[test]
1345 fn test_build_matcher_union_patterns_of_same_kind() {
1346 let settings = insta_settings();
1347 let _guard = settings.bind_to_scope();
1348
1349 let expr = FilesetExpression::union_all(vec![
1350 FilesetExpression::file_path(repo_path_buf("foo")),
1351 FilesetExpression::file_path(repo_path_buf("foo/bar")),
1352 ]);
1353 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1354 FilesMatcher {
1355 tree: Dir {
1356 "foo": File {
1357 "bar": File {},
1358 },
1359 },
1360 }
1361 "#);
1362
1363 let expr = FilesetExpression::union_all(vec![
1364 FilesetExpression::prefix_path(repo_path_buf("bar")),
1365 FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
1366 ]);
1367 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1368 PrefixMatcher {
1369 tree: Dir {
1370 "bar": Prefix {
1371 "baz": Prefix {},
1372 },
1373 },
1374 }
1375 "#);
1376 }
1377
1378 #[test]
1379 fn test_build_matcher_union_patterns_of_different_kind() {
1380 let settings = insta_settings();
1381 let _guard = settings.bind_to_scope();
1382
1383 let expr = FilesetExpression::union_all(vec![
1384 FilesetExpression::file_path(repo_path_buf("foo")),
1385 FilesetExpression::prefix_path(repo_path_buf("bar")),
1386 ]);
1387 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1388 UnionMatcher {
1389 input1: FilesMatcher {
1390 tree: Dir {
1391 "foo": File {},
1392 },
1393 },
1394 input2: PrefixMatcher {
1395 tree: Dir {
1396 "bar": Prefix {},
1397 },
1398 },
1399 }
1400 "#);
1401 }
1402
1403 #[test]
1404 fn test_build_matcher_unnormalized_union() {
1405 let settings = insta_settings();
1406 let _guard = settings.bind_to_scope();
1407
1408 let expr = FilesetExpression::UnionAll(vec![]);
1409 insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
1410
1411 let expr =
1412 FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
1413 insta::assert_debug_snapshot!(expr.to_matcher(), @"
1414 UnionMatcher {
1415 input1: NothingMatcher,
1416 input2: EverythingMatcher,
1417 }
1418 ");
1419 }
1420
1421 #[test]
1422 fn test_build_matcher_combined() {
1423 let settings = insta_settings();
1424 let _guard = settings.bind_to_scope();
1425
1426 let expr = FilesetExpression::union_all(vec![
1427 FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1428 FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1429 FilesetExpression::file_path(repo_path_buf("foo")),
1430 FilesetExpression::prefix_path(repo_path_buf("bar")),
1431 ]);
1432 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1433 UnionMatcher {
1434 input1: UnionMatcher {
1435 input1: IntersectionMatcher {
1436 input1: EverythingMatcher,
1437 input2: NothingMatcher,
1438 },
1439 input2: DifferenceMatcher {
1440 wanted: NothingMatcher,
1441 unwanted: EverythingMatcher,
1442 },
1443 },
1444 input2: UnionMatcher {
1445 input1: FilesMatcher {
1446 tree: Dir {
1447 "foo": File {},
1448 },
1449 },
1450 input2: PrefixMatcher {
1451 tree: Dir {
1452 "bar": Prefix {},
1453 },
1454 },
1455 },
1456 }
1457 "#);
1458 }
1459}