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::FilesetAliasesMap;
34pub use crate::fileset_parser::FilesetDiagnostics;
35pub use crate::fileset_parser::FilesetParseError;
36pub use crate::fileset_parser::FilesetParseErrorKind;
37pub use crate::fileset_parser::FilesetParseResult;
38use crate::fileset_parser::FunctionCallNode;
39use crate::fileset_parser::UnaryOp;
40use crate::matchers::DifferenceMatcher;
41use crate::matchers::EverythingMatcher;
42use crate::matchers::FilesMatcher;
43use crate::matchers::GlobsMatcher;
44use crate::matchers::IntersectionMatcher;
45use crate::matchers::Matcher;
46use crate::matchers::NothingMatcher;
47use crate::matchers::PrefixMatcher;
48use crate::matchers::UnionMatcher;
49use crate::repo_path::RelativePathParseError;
50use crate::repo_path::RepoPath;
51use crate::repo_path::RepoPathBuf;
52use crate::repo_path::RepoPathUiConverter;
53use crate::repo_path::UiPathParseError;
54
55#[derive(Debug, Error)]
57pub enum FilePatternParseError {
58 #[error("Invalid file pattern kind `{0}:`")]
60 InvalidKind(String),
61 #[error(transparent)]
63 UiPath(#[from] UiPathParseError),
64 #[error(transparent)]
66 RelativePath(#[from] RelativePathParseError),
67 #[error(transparent)]
69 GlobPattern(#[from] globset::Error),
70}
71
72#[derive(Clone, Debug)]
74pub enum FilePattern {
75 FilePath(RepoPathBuf),
77 PrefixPath(RepoPathBuf),
79 FileGlob {
81 dir: RepoPathBuf,
83 pattern: Box<Glob>,
85 },
86 PrefixGlob {
88 dir: RepoPathBuf,
90 pattern: Box<Glob>,
92 },
93 }
97
98impl FilePattern {
99 pub fn from_str_kind(
101 path_converter: &RepoPathUiConverter,
102 input: &str,
103 kind: &str,
104 ) -> Result<Self, FilePatternParseError> {
105 match kind {
120 "cwd" => Self::cwd_prefix_path(path_converter, input),
121 "cwd-file" | "file" => Self::cwd_file_path(path_converter, input),
122 "cwd-glob" | "glob" => Self::cwd_file_glob(path_converter, input),
123 "cwd-glob-i" | "glob-i" => Self::cwd_file_glob_i(path_converter, input),
124 "cwd-prefix-glob" | "prefix-glob" => Self::cwd_prefix_glob(path_converter, input),
125 "cwd-prefix-glob-i" | "prefix-glob-i" => Self::cwd_prefix_glob_i(path_converter, input),
126 "root" => Self::root_prefix_path(input),
127 "root-file" => Self::root_file_path(input),
128 "root-glob" => Self::root_file_glob(input),
129 "root-glob-i" => Self::root_file_glob_i(input),
130 "root-prefix-glob" => Self::root_prefix_glob(input),
131 "root-prefix-glob-i" => Self::root_prefix_glob_i(input),
132 _ => Err(FilePatternParseError::InvalidKind(kind.to_owned())),
133 }
134 }
135
136 pub fn cwd_file_path(
138 path_converter: &RepoPathUiConverter,
139 input: impl AsRef<str>,
140 ) -> Result<Self, FilePatternParseError> {
141 let path = path_converter.parse_file_path(input.as_ref())?;
142 Ok(Self::FilePath(path))
143 }
144
145 pub fn cwd_prefix_path(
147 path_converter: &RepoPathUiConverter,
148 input: impl AsRef<str>,
149 ) -> Result<Self, FilePatternParseError> {
150 let path = path_converter.parse_file_path(input.as_ref())?;
151 Ok(Self::PrefixPath(path))
152 }
153
154 pub fn cwd_file_glob(
156 path_converter: &RepoPathUiConverter,
157 input: impl AsRef<str>,
158 ) -> Result<Self, FilePatternParseError> {
159 let (dir, pattern) = split_glob_path(input.as_ref());
160 let dir = path_converter.parse_file_path(dir)?;
161 Self::file_glob_at(dir, pattern, false)
162 }
163
164 pub fn cwd_file_glob_i(
166 path_converter: &RepoPathUiConverter,
167 input: impl AsRef<str>,
168 ) -> Result<Self, FilePatternParseError> {
169 let (dir, pattern) = split_glob_path_i(input.as_ref());
170 let dir = path_converter.parse_file_path(dir)?;
171 Self::file_glob_at(dir, pattern, true)
172 }
173
174 pub fn cwd_prefix_glob(
176 path_converter: &RepoPathUiConverter,
177 input: impl AsRef<str>,
178 ) -> Result<Self, FilePatternParseError> {
179 let (dir, pattern) = split_glob_path(input.as_ref());
180 let dir = path_converter.parse_file_path(dir)?;
181 Self::prefix_glob_at(dir, pattern, false)
182 }
183
184 pub fn cwd_prefix_glob_i(
187 path_converter: &RepoPathUiConverter,
188 input: impl AsRef<str>,
189 ) -> Result<Self, FilePatternParseError> {
190 let (dir, pattern) = split_glob_path_i(input.as_ref());
191 let dir = path_converter.parse_file_path(dir)?;
192 Self::prefix_glob_at(dir, pattern, true)
193 }
194
195 pub fn root_file_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
197 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
199 Ok(Self::FilePath(path))
200 }
201
202 pub fn root_prefix_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
204 let path = RepoPathBuf::from_relative_path(input.as_ref())?;
205 Ok(Self::PrefixPath(path))
206 }
207
208 pub fn root_file_glob(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
210 let (dir, pattern) = split_glob_path(input.as_ref());
211 let dir = RepoPathBuf::from_relative_path(dir)?;
212 Self::file_glob_at(dir, pattern, false)
213 }
214
215 pub fn root_file_glob_i(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
218 let (dir, pattern) = split_glob_path_i(input.as_ref());
219 let dir = RepoPathBuf::from_relative_path(dir)?;
220 Self::file_glob_at(dir, pattern, true)
221 }
222
223 pub fn root_prefix_glob(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
225 let (dir, pattern) = split_glob_path(input.as_ref());
226 let dir = RepoPathBuf::from_relative_path(dir)?;
227 Self::prefix_glob_at(dir, pattern, false)
228 }
229
230 pub fn root_prefix_glob_i(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
233 let (dir, pattern) = split_glob_path_i(input.as_ref());
234 let dir = RepoPathBuf::from_relative_path(dir)?;
235 Self::prefix_glob_at(dir, pattern, true)
236 }
237
238 fn file_glob_at(
239 dir: RepoPathBuf,
240 input: &str,
241 icase: bool,
242 ) -> Result<Self, FilePatternParseError> {
243 if input.is_empty() {
244 return Ok(Self::FilePath(dir));
245 }
246 let normalized = RepoPathBuf::from_relative_path(input)?;
248 let pattern = Box::new(parse_file_glob(
249 normalized.as_internal_file_string(),
250 icase,
251 )?);
252 Ok(Self::FileGlob { dir, pattern })
253 }
254
255 fn prefix_glob_at(
256 dir: RepoPathBuf,
257 input: &str,
258 icase: bool,
259 ) -> Result<Self, FilePatternParseError> {
260 if input.is_empty() {
261 return Ok(Self::PrefixPath(dir));
262 }
263 let normalized = RepoPathBuf::from_relative_path(input)?;
265 let pattern = Box::new(parse_file_glob(
266 normalized.as_internal_file_string(),
267 icase,
268 )?);
269 Ok(Self::PrefixGlob { dir, pattern })
270 }
271
272 pub fn as_path(&self) -> Option<&RepoPath> {
275 match self {
276 Self::FilePath(path) => Some(path),
277 Self::PrefixPath(path) => Some(path),
278 Self::FileGlob { .. } | Self::PrefixGlob { .. } => None,
279 }
280 }
281}
282
283pub(super) fn parse_file_glob(input: &str, icase: bool) -> Result<Glob, globset::Error> {
284 GlobBuilder::new(input)
285 .literal_separator(true)
286 .case_insensitive(icase)
287 .build()
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 settings.add_filter(
651 r"(?m)^(\s{12}opts):\s*GlobOptions\s*\{\n(\s{16}.*\n)*\s{12}\},",
652 "$1: _,",
653 );
654 settings.add_filter(
655 r"(?m)^(\s{12}tokens):\s*Tokens\(\n(\s{16}.*\n)*\s{12}\),",
656 "$1: _,",
657 );
658 for _ in 0..4 {
661 settings.add_filter(
662 r"(?x)
663 \b([A-Z]\w*)\(\n
664 \s*(.{1,60}),\n
665 \s*\)",
666 "$1($2)",
667 );
668 }
669 settings
670 }
671
672 #[test]
673 fn test_parse_file_pattern() -> TestResult {
674 let settings = insta_settings();
675 let _guard = settings.bind_to_scope();
676 let context = FilesetParseContext {
677 aliases_map: &FilesetAliasesMap::new(),
678 path_converter: &RepoPathUiConverter::Fs {
679 cwd: PathBuf::from("/ws/cur"),
680 base: PathBuf::from("/ws"),
681 },
682 };
683 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
684
685 insta::assert_debug_snapshot!(
687 parse(".")?,
688 @r#"Pattern(PrefixPath("cur"))"#);
689 insta::assert_debug_snapshot!(
690 parse("..")?,
691 @r#"Pattern(PrefixPath(""))"#);
692 assert!(parse("../..").is_err());
693 insta::assert_debug_snapshot!(
694 parse("foo")?,
695 @r#"Pattern(PrefixPath("cur/foo"))"#);
696 insta::assert_debug_snapshot!(
697 parse("*.*")?,
698 @r#"
699 Pattern(
700 PrefixGlob {
701 dir: "cur",
702 pattern: Glob {
703 glob: "*.*",
704 re: "(?-u)^[^/]*\\.[^/]*$",
705 opts: _,
706 tokens: _,
707 },
708 },
709 )
710 "#);
711 insta::assert_debug_snapshot!(
712 parse("cwd:.")?,
713 @r#"Pattern(PrefixPath("cur"))"#);
714 insta::assert_debug_snapshot!(
715 parse("cwd-file:foo")?,
716 @r#"Pattern(FilePath("cur/foo"))"#);
717 insta::assert_debug_snapshot!(
718 parse("file:../foo/bar")?,
719 @r#"Pattern(FilePath("foo/bar"))"#);
720
721 insta::assert_debug_snapshot!(
723 parse("root:.")?,
724 @r#"Pattern(PrefixPath(""))"#);
725 assert!(parse("root:..").is_err());
726 insta::assert_debug_snapshot!(
727 parse("root:foo/bar")?,
728 @r#"Pattern(PrefixPath("foo/bar"))"#);
729 insta::assert_debug_snapshot!(
730 parse("root-file:bar")?,
731 @r#"Pattern(FilePath("bar"))"#);
732
733 insta::assert_debug_snapshot!(
734 parse("file:(foo|bar)").unwrap_err().kind(),
735 @r#"Expression("Expected string")"#);
736 Ok(())
737 }
738
739 #[test]
740 fn test_parse_glob_pattern() -> TestResult {
741 let settings = insta_settings();
742 let _guard = settings.bind_to_scope();
743 let context = FilesetParseContext {
744 aliases_map: &FilesetAliasesMap::new(),
745 path_converter: &RepoPathUiConverter::Fs {
746 cwd: PathBuf::from("/ws/cur*"),
748 base: PathBuf::from("/ws"),
749 },
750 };
751 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
752
753 insta::assert_debug_snapshot!(
755 parse(r#"cwd-glob:"foo""#)?,
756 @r#"Pattern(FilePath("cur*/foo"))"#);
757 insta::assert_debug_snapshot!(
760 parse(r#"glob:"""#)?,
761 @r#"Pattern(FilePath("cur*"))"#);
762 insta::assert_debug_snapshot!(
763 parse(r#"glob:".""#)?,
764 @r#"Pattern(FilePath("cur*"))"#);
765 insta::assert_debug_snapshot!(
766 parse(r#"glob:"..""#)?,
767 @r#"Pattern(FilePath(""))"#);
768
769 insta::assert_debug_snapshot!(
771 parse(r#"glob:"*""#)?, @r#"
772 Pattern(
773 FileGlob {
774 dir: "cur*",
775 pattern: Glob {
776 glob: "*",
777 re: "(?-u)^[^/]*$",
778 opts: _,
779 tokens: _,
780 },
781 },
782 )
783 "#);
784 insta::assert_debug_snapshot!(
785 parse(r#"glob:"./*""#)?, @r#"
786 Pattern(
787 FileGlob {
788 dir: "cur*",
789 pattern: Glob {
790 glob: "*",
791 re: "(?-u)^[^/]*$",
792 opts: _,
793 tokens: _,
794 },
795 },
796 )
797 "#);
798 insta::assert_debug_snapshot!(
799 parse(r#"glob:"../*""#)?, @r#"
800 Pattern(
801 FileGlob {
802 dir: "",
803 pattern: Glob {
804 glob: "*",
805 re: "(?-u)^[^/]*$",
806 opts: _,
807 tokens: _,
808 },
809 },
810 )
811 "#);
812 insta::assert_debug_snapshot!(
814 parse(r#"glob:"**""#)?, @r#"
815 Pattern(
816 FileGlob {
817 dir: "cur*",
818 pattern: Glob {
819 glob: "**",
820 re: "(?-u)^.*$",
821 opts: _,
822 tokens: _,
823 },
824 },
825 )
826 "#);
827 insta::assert_debug_snapshot!(
828 parse(r#"glob:"../foo/b?r/baz""#)?, @r#"
829 Pattern(
830 FileGlob {
831 dir: "foo",
832 pattern: Glob {
833 glob: "b?r/baz",
834 re: "(?-u)^b[^/]r/baz$",
835 opts: _,
836 tokens: _,
837 },
838 },
839 )
840 "#);
841 assert!(parse(r#"glob:"../../*""#).is_err());
842 assert!(parse(r#"glob-i:"../../*""#).is_err());
843 assert!(parse(r#"glob:"/*""#).is_err());
844 assert!(parse(r#"glob-i:"/*""#).is_err());
845 assert!(parse(r#"glob:"*/..""#).is_err());
847 assert!(parse(r#"glob-i:"*/..""#).is_err());
848
849 if cfg!(windows) {
850 insta::assert_debug_snapshot!(
852 parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
853 Pattern(
854 FileGlob {
855 dir: "foo",
856 pattern: Glob {
857 glob: "*/bar",
858 re: "(?-u)^[^/]*/bar$",
859 opts: _,
860 tokens: _,
861 },
862 },
863 )
864 "#);
865 } else {
866 insta::assert_debug_snapshot!(
868 parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
869 Pattern(
870 FileGlob {
871 dir: "cur*",
872 pattern: Glob {
873 glob: "..\\foo\\*\\bar",
874 re: "(?-u)^\\.\\.foo\\*bar$",
875 opts: _,
876 tokens: _,
877 },
878 },
879 )
880 "#);
881 }
882
883 insta::assert_debug_snapshot!(
885 parse(r#"root-glob:"foo""#)?,
886 @r#"Pattern(FilePath("foo"))"#);
887 insta::assert_debug_snapshot!(
888 parse(r#"root-glob:"""#)?,
889 @r#"Pattern(FilePath(""))"#);
890 insta::assert_debug_snapshot!(
891 parse(r#"root-glob:".""#)?,
892 @r#"Pattern(FilePath(""))"#);
893
894 insta::assert_debug_snapshot!(
896 parse(r#"root-glob:"*""#)?, @r#"
897 Pattern(
898 FileGlob {
899 dir: "",
900 pattern: Glob {
901 glob: "*",
902 re: "(?-u)^[^/]*$",
903 opts: _,
904 tokens: _,
905 },
906 },
907 )
908 "#);
909 insta::assert_debug_snapshot!(
910 parse(r#"root-glob:"foo/bar/b[az]""#)?, @r#"
911 Pattern(
912 FileGlob {
913 dir: "foo/bar",
914 pattern: Glob {
915 glob: "b[az]",
916 re: "(?-u)^b[az]$",
917 opts: _,
918 tokens: _,
919 },
920 },
921 )
922 "#);
923 insta::assert_debug_snapshot!(
924 parse(r#"root-glob:"foo/bar/b{ar,az}""#)?, @r#"
925 Pattern(
926 FileGlob {
927 dir: "foo/bar",
928 pattern: Glob {
929 glob: "b{ar,az}",
930 re: "(?-u)^b(?:ar|az)$",
931 opts: _,
932 tokens: _,
933 },
934 },
935 )
936 "#);
937 assert!(parse(r#"root-glob:"../*""#).is_err());
938 assert!(parse(r#"root-glob-i:"../*""#).is_err());
939 assert!(parse(r#"root-glob:"/*""#).is_err());
940 assert!(parse(r#"root-glob-i:"/*""#).is_err());
941
942 if cfg!(not(windows)) {
944 insta::assert_debug_snapshot!(
945 parse(r#"root-glob:'foo/bar\baz'"#)?, @r#"
946 Pattern(
947 FileGlob {
948 dir: "foo",
949 pattern: Glob {
950 glob: "bar\\baz",
951 re: "(?-u)^barbaz$",
952 opts: _,
953 tokens: _,
954 },
955 },
956 )
957 "#);
958 }
959 Ok(())
960 }
961
962 #[test]
963 fn test_parse_glob_pattern_case_insensitive() -> TestResult {
964 let settings = insta_settings();
965 let _guard = settings.bind_to_scope();
966 let context = FilesetParseContext {
967 aliases_map: &FilesetAliasesMap::new(),
968 path_converter: &RepoPathUiConverter::Fs {
969 cwd: PathBuf::from("/ws/cur"),
970 base: PathBuf::from("/ws"),
971 },
972 };
973 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
974
975 insta::assert_debug_snapshot!(
977 parse(r#"glob-i:"*.TXT""#)?, @r#"
978 Pattern(
979 FileGlob {
980 dir: "cur",
981 pattern: Glob {
982 glob: "*.TXT",
983 re: "(?-u)(?i)^[^/]*\\.TXT$",
984 opts: _,
985 tokens: _,
986 },
987 },
988 )
989 "#);
990
991 insta::assert_debug_snapshot!(
993 parse(r#"cwd-glob-i:"[Ff]oo""#)?, @r#"
994 Pattern(
995 FileGlob {
996 dir: "cur",
997 pattern: Glob {
998 glob: "[Ff]oo",
999 re: "(?-u)(?i)^[Ff]oo$",
1000 opts: _,
1001 tokens: _,
1002 },
1003 },
1004 )
1005 "#);
1006
1007 insta::assert_debug_snapshot!(
1009 parse(r#"root-glob-i:"*.Rs""#)?, @r#"
1010 Pattern(
1011 FileGlob {
1012 dir: "",
1013 pattern: Glob {
1014 glob: "*.Rs",
1015 re: "(?-u)(?i)^[^/]*\\.Rs$",
1016 opts: _,
1017 tokens: _,
1018 },
1019 },
1020 )
1021 "#);
1022
1023 insta::assert_debug_snapshot!(
1025 parse(r#"glob-i:"SubDir/*.rs""#)?, @r#"
1026 Pattern(
1027 FileGlob {
1028 dir: "cur",
1029 pattern: Glob {
1030 glob: "SubDir/*.rs",
1031 re: "(?-u)(?i)^SubDir/[^/]*\\.rs$",
1032 opts: _,
1033 tokens: _,
1034 },
1035 },
1036 )
1037 "#);
1038
1039 insta::assert_debug_snapshot!(
1041 parse(r#"glob:"SubDir/*.rs""#)?, @r#"
1042 Pattern(
1043 FileGlob {
1044 dir: "cur/SubDir",
1045 pattern: Glob {
1046 glob: "*.rs",
1047 re: "(?-u)^[^/]*\\.rs$",
1048 opts: _,
1049 tokens: _,
1050 },
1051 },
1052 )
1053 "#);
1054
1055 insta::assert_debug_snapshot!(
1057 parse(r#"glob-i:"../SomeDir/*.rs""#)?, @r#"
1058 Pattern(
1059 FileGlob {
1060 dir: "",
1061 pattern: Glob {
1062 glob: "SomeDir/*.rs",
1063 re: "(?-u)(?i)^SomeDir/[^/]*\\.rs$",
1064 opts: _,
1065 tokens: _,
1066 },
1067 },
1068 )
1069 "#);
1070
1071 insta::assert_debug_snapshot!(
1073 parse(r#"glob-i:"./SomeFile*.txt""#)?, @r#"
1074 Pattern(
1075 FileGlob {
1076 dir: "cur",
1077 pattern: Glob {
1078 glob: "SomeFile*.txt",
1079 re: "(?-u)(?i)^SomeFile[^/]*\\.txt$",
1080 opts: _,
1081 tokens: _,
1082 },
1083 },
1084 )
1085 "#);
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn test_parse_prefix_glob_pattern() -> TestResult {
1091 let settings = insta_settings();
1092 let _guard = settings.bind_to_scope();
1093 let context = FilesetParseContext {
1094 aliases_map: &FilesetAliasesMap::new(),
1095 path_converter: &RepoPathUiConverter::Fs {
1096 cwd: PathBuf::from("/ws/cur*"),
1098 base: PathBuf::from("/ws"),
1099 },
1100 };
1101 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1102
1103 insta::assert_debug_snapshot!(
1105 parse("cwd-prefix-glob:'foo'")?,
1106 @r#"Pattern(PrefixPath("cur*/foo"))"#);
1107 insta::assert_debug_snapshot!(
1108 parse("prefix-glob:'.'")?,
1109 @r#"Pattern(PrefixPath("cur*"))"#);
1110 insta::assert_debug_snapshot!(
1111 parse("cwd-prefix-glob-i:'..'")?,
1112 @r#"Pattern(PrefixPath(""))"#);
1113 insta::assert_debug_snapshot!(
1114 parse("prefix-glob-i:'../_'")?,
1115 @r#"Pattern(PrefixPath("_"))"#);
1116
1117 insta::assert_debug_snapshot!(
1119 parse("cwd-prefix-glob:'*'")?, @r#"
1120 Pattern(
1121 PrefixGlob {
1122 dir: "cur*",
1123 pattern: Glob {
1124 glob: "*",
1125 re: "(?-u)^[^/]*$",
1126 opts: _,
1127 tokens: _,
1128 },
1129 },
1130 )
1131 "#);
1132
1133 insta::assert_debug_snapshot!(
1135 parse("cwd-prefix-glob-i:'../foo'")?, @r#"
1136 Pattern(
1137 PrefixGlob {
1138 dir: "",
1139 pattern: Glob {
1140 glob: "foo",
1141 re: "(?-u)(?i)^foo$",
1142 opts: _,
1143 tokens: _,
1144 },
1145 },
1146 )
1147 "#);
1148
1149 insta::assert_debug_snapshot!(
1151 parse("root-prefix-glob:'foo'")?,
1152 @r#"Pattern(PrefixPath("foo"))"#);
1153 insta::assert_debug_snapshot!(
1154 parse("root-prefix-glob-i:'.'")?,
1155 @r#"Pattern(PrefixPath(""))"#);
1156
1157 insta::assert_debug_snapshot!(
1159 parse("root-prefix-glob:'*'")?, @r#"
1160 Pattern(
1161 PrefixGlob {
1162 dir: "",
1163 pattern: Glob {
1164 glob: "*",
1165 re: "(?-u)^[^/]*$",
1166 opts: _,
1167 tokens: _,
1168 },
1169 },
1170 )
1171 "#);
1172
1173 insta::assert_debug_snapshot!(
1175 parse("root-prefix-glob-i:'_/foo'")?, @r#"
1176 Pattern(
1177 PrefixGlob {
1178 dir: "_",
1179 pattern: Glob {
1180 glob: "foo",
1181 re: "(?-u)(?i)^foo$",
1182 opts: _,
1183 tokens: _,
1184 },
1185 },
1186 )
1187 "#);
1188 Ok(())
1189 }
1190
1191 #[test]
1192 fn test_parse_function() -> 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("all()")?, @"All");
1205 insta::assert_debug_snapshot!(parse("none()")?, @"None");
1206 insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
1207 InvalidArguments {
1208 name: "all",
1209 message: "Expected 0 arguments",
1210 }
1211 "#);
1212 insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
1213 NoSuchFunction {
1214 name: "ale",
1215 candidates: [
1216 "all",
1217 ],
1218 }
1219 "#);
1220 Ok(())
1221 }
1222
1223 #[test]
1224 fn test_parse_compound_expression() -> TestResult {
1225 let settings = insta_settings();
1226 let _guard = settings.bind_to_scope();
1227 let context = FilesetParseContext {
1228 aliases_map: &FilesetAliasesMap::new(),
1229 path_converter: &RepoPathUiConverter::Fs {
1230 cwd: PathBuf::from("/ws/cur"),
1231 base: PathBuf::from("/ws"),
1232 },
1233 };
1234 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1235
1236 insta::assert_debug_snapshot!(parse("~x")?, @r#"
1237 Difference(
1238 All,
1239 Pattern(PrefixPath("cur/x")),
1240 )
1241 "#);
1242 insta::assert_debug_snapshot!(parse("x|y|root:z")?, @r#"
1243 UnionAll(
1244 [
1245 Pattern(PrefixPath("cur/x")),
1246 Pattern(PrefixPath("cur/y")),
1247 Pattern(PrefixPath("z")),
1248 ],
1249 )
1250 "#);
1251 insta::assert_debug_snapshot!(parse("x|y&z")?, @r#"
1252 UnionAll(
1253 [
1254 Pattern(PrefixPath("cur/x")),
1255 Intersection(
1256 Pattern(PrefixPath("cur/y")),
1257 Pattern(PrefixPath("cur/z")),
1258 ),
1259 ],
1260 )
1261 "#);
1262 Ok(())
1263 }
1264
1265 #[test]
1266 fn test_explicit_paths() {
1267 let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
1268 expr.explicit_paths().map(|path| path.to_owned()).collect()
1269 };
1270 let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
1271 assert!(collect(&FilesetExpression::none()).is_empty());
1272 assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
1273 assert_eq!(
1274 collect(&FilesetExpression::union_all(vec![
1275 file_expr("a"),
1276 file_expr("b"),
1277 file_expr("c"),
1278 ])),
1279 ["a", "b", "c"].map(repo_path_buf)
1280 );
1281 assert_eq!(
1282 collect(&FilesetExpression::intersection(
1283 FilesetExpression::union_all(vec![
1284 file_expr("a"),
1285 FilesetExpression::none(),
1286 file_expr("b"),
1287 file_expr("c"),
1288 ]),
1289 FilesetExpression::difference(
1290 file_expr("d"),
1291 FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
1292 )
1293 )),
1294 ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
1295 );
1296 }
1297
1298 #[test]
1299 fn test_build_matcher_simple() {
1300 let settings = insta_settings();
1301 let _guard = settings.bind_to_scope();
1302
1303 insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
1304 insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
1305 insta::assert_debug_snapshot!(
1306 FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
1307 @r#"
1308 FilesMatcher {
1309 tree: Dir {
1310 "foo": File {},
1311 },
1312 }
1313 "#);
1314 insta::assert_debug_snapshot!(
1315 FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
1316 @r#"
1317 PrefixMatcher {
1318 tree: Dir {
1319 "foo": Prefix {},
1320 },
1321 }
1322 "#);
1323 }
1324
1325 #[test]
1326 fn test_build_matcher_glob_pattern() {
1327 let settings = insta_settings();
1328 let _guard = settings.bind_to_scope();
1329 let file_glob_expr = |dir: &str, pattern: &str| {
1330 FilesetExpression::pattern(FilePattern::FileGlob {
1331 dir: repo_path_buf(dir),
1332 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1333 })
1334 };
1335 let prefix_glob_expr = |dir: &str, pattern: &str| {
1336 FilesetExpression::pattern(FilePattern::PrefixGlob {
1337 dir: repo_path_buf(dir),
1338 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1339 })
1340 };
1341
1342 insta::assert_debug_snapshot!(file_glob_expr("", "*").to_matcher(), @r#"
1343 GlobsMatcher {
1344 tree: Some(RegexSet(["(?-u)^[^/]*$"])) {},
1345 matches_prefix_paths: false,
1346 }
1347 "#);
1348
1349 let expr = FilesetExpression::union_all(vec![
1350 file_glob_expr("foo", "*"),
1351 file_glob_expr("foo/bar", "*"),
1352 file_glob_expr("foo", "?"),
1353 prefix_glob_expr("foo", "ba[rz]"),
1354 prefix_glob_expr("foo", "qu*x"),
1355 ]);
1356 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1357 UnionMatcher {
1358 input1: GlobsMatcher {
1359 tree: None {
1360 "foo": Some(RegexSet(["(?-u)^[^/]*$", "(?-u)^[^/]$"])) {
1361 "bar": Some(RegexSet(["(?-u)^[^/]*$"])) {},
1362 },
1363 },
1364 matches_prefix_paths: false,
1365 },
1366 input2: GlobsMatcher {
1367 tree: None {
1368 "foo": Some(RegexSet(["(?-u)^ba[rz](?:/|$)", "(?-u)^qu[^/]*x(?:/|$)"])) {},
1369 },
1370 matches_prefix_paths: true,
1371 },
1372 }
1373 "#);
1374 }
1375
1376 #[test]
1377 fn test_build_matcher_union_patterns_of_same_kind() {
1378 let settings = insta_settings();
1379 let _guard = settings.bind_to_scope();
1380
1381 let expr = FilesetExpression::union_all(vec![
1382 FilesetExpression::file_path(repo_path_buf("foo")),
1383 FilesetExpression::file_path(repo_path_buf("foo/bar")),
1384 ]);
1385 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1386 FilesMatcher {
1387 tree: Dir {
1388 "foo": File {
1389 "bar": File {},
1390 },
1391 },
1392 }
1393 "#);
1394
1395 let expr = FilesetExpression::union_all(vec![
1396 FilesetExpression::prefix_path(repo_path_buf("bar")),
1397 FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
1398 ]);
1399 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1400 PrefixMatcher {
1401 tree: Dir {
1402 "bar": Prefix {
1403 "baz": Prefix {},
1404 },
1405 },
1406 }
1407 "#);
1408 }
1409
1410 #[test]
1411 fn test_build_matcher_union_patterns_of_different_kind() {
1412 let settings = insta_settings();
1413 let _guard = settings.bind_to_scope();
1414
1415 let expr = FilesetExpression::union_all(vec![
1416 FilesetExpression::file_path(repo_path_buf("foo")),
1417 FilesetExpression::prefix_path(repo_path_buf("bar")),
1418 ]);
1419 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1420 UnionMatcher {
1421 input1: FilesMatcher {
1422 tree: Dir {
1423 "foo": File {},
1424 },
1425 },
1426 input2: PrefixMatcher {
1427 tree: Dir {
1428 "bar": Prefix {},
1429 },
1430 },
1431 }
1432 "#);
1433 }
1434
1435 #[test]
1436 fn test_build_matcher_unnormalized_union() {
1437 let settings = insta_settings();
1438 let _guard = settings.bind_to_scope();
1439
1440 let expr = FilesetExpression::UnionAll(vec![]);
1441 insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
1442
1443 let expr =
1444 FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
1445 insta::assert_debug_snapshot!(expr.to_matcher(), @"
1446 UnionMatcher {
1447 input1: NothingMatcher,
1448 input2: EverythingMatcher,
1449 }
1450 ");
1451 }
1452
1453 #[test]
1454 fn test_build_matcher_combined() {
1455 let settings = insta_settings();
1456 let _guard = settings.bind_to_scope();
1457
1458 let expr = FilesetExpression::union_all(vec![
1459 FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1460 FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1461 FilesetExpression::file_path(repo_path_buf("foo")),
1462 FilesetExpression::prefix_path(repo_path_buf("bar")),
1463 ]);
1464 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1465 UnionMatcher {
1466 input1: UnionMatcher {
1467 input1: IntersectionMatcher {
1468 input1: EverythingMatcher,
1469 input2: NothingMatcher,
1470 },
1471 input2: DifferenceMatcher {
1472 wanted: NothingMatcher,
1473 unwanted: EverythingMatcher,
1474 },
1475 },
1476 input2: UnionMatcher {
1477 input1: FilesMatcher {
1478 tree: Dir {
1479 "foo": File {},
1480 },
1481 },
1482 input2: PrefixMatcher {
1483 tree: Dir {
1484 "bar": Prefix {},
1485 },
1486 },
1487 },
1488 }
1489 "#);
1490 }
1491}