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
642 fn repo_path_buf(value: impl Into<String>) -> RepoPathBuf {
643 RepoPathBuf::from_internal_string(value).unwrap()
644 }
645
646 fn insta_settings() -> insta::Settings {
647 let mut settings = insta::Settings::clone_current();
648 settings.add_filter(
650 r"(?m)^(\s{12}opts):\s*GlobOptions\s*\{\n(\s{16}.*\n)*\s{12}\},",
651 "$1: _,",
652 );
653 settings.add_filter(
654 r"(?m)^(\s{12}tokens):\s*Tokens\(\n(\s{16}.*\n)*\s{12}\),",
655 "$1: _,",
656 );
657 for _ in 0..4 {
660 settings.add_filter(
661 r"(?x)
662 \b([A-Z]\w*)\(\n
663 \s*(.{1,60}),\n
664 \s*\)",
665 "$1($2)",
666 );
667 }
668 settings
669 }
670
671 #[test]
672 fn test_parse_file_pattern() {
673 let settings = insta_settings();
674 let _guard = settings.bind_to_scope();
675 let context = FilesetParseContext {
676 aliases_map: &FilesetAliasesMap::new(),
677 path_converter: &RepoPathUiConverter::Fs {
678 cwd: PathBuf::from("/ws/cur"),
679 base: PathBuf::from("/ws"),
680 },
681 };
682 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
683
684 insta::assert_debug_snapshot!(
686 parse(".").unwrap(),
687 @r#"Pattern(PrefixPath("cur"))"#);
688 insta::assert_debug_snapshot!(
689 parse("..").unwrap(),
690 @r#"Pattern(PrefixPath(""))"#);
691 assert!(parse("../..").is_err());
692 insta::assert_debug_snapshot!(
693 parse("foo").unwrap(),
694 @r#"Pattern(PrefixPath("cur/foo"))"#);
695 insta::assert_debug_snapshot!(
696 parse("*.*").unwrap(),
697 @r#"
698 Pattern(
699 PrefixGlob {
700 dir: "cur",
701 pattern: Glob {
702 glob: "*.*",
703 re: "(?-u)^[^/]*\\.[^/]*$",
704 opts: _,
705 tokens: _,
706 },
707 },
708 )
709 "#);
710 insta::assert_debug_snapshot!(
711 parse("cwd:.").unwrap(),
712 @r#"Pattern(PrefixPath("cur"))"#);
713 insta::assert_debug_snapshot!(
714 parse("cwd-file:foo").unwrap(),
715 @r#"Pattern(FilePath("cur/foo"))"#);
716 insta::assert_debug_snapshot!(
717 parse("file:../foo/bar").unwrap(),
718 @r#"Pattern(FilePath("foo/bar"))"#);
719
720 insta::assert_debug_snapshot!(
722 parse("root:.").unwrap(),
723 @r#"Pattern(PrefixPath(""))"#);
724 assert!(parse("root:..").is_err());
725 insta::assert_debug_snapshot!(
726 parse("root:foo/bar").unwrap(),
727 @r#"Pattern(PrefixPath("foo/bar"))"#);
728 insta::assert_debug_snapshot!(
729 parse("root-file:bar").unwrap(),
730 @r#"Pattern(FilePath("bar"))"#);
731
732 insta::assert_debug_snapshot!(
733 parse("file:(foo|bar)").unwrap_err().kind(),
734 @r#"Expression("Expected string")"#);
735 }
736
737 #[test]
738 fn test_parse_glob_pattern() {
739 let settings = insta_settings();
740 let _guard = settings.bind_to_scope();
741 let context = FilesetParseContext {
742 aliases_map: &FilesetAliasesMap::new(),
743 path_converter: &RepoPathUiConverter::Fs {
744 cwd: PathBuf::from("/ws/cur*"),
746 base: PathBuf::from("/ws"),
747 },
748 };
749 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
750
751 insta::assert_debug_snapshot!(
753 parse(r#"cwd-glob:"foo""#).unwrap(),
754 @r#"Pattern(FilePath("cur*/foo"))"#);
755 insta::assert_debug_snapshot!(
758 parse(r#"glob:"""#).unwrap(),
759 @r#"Pattern(FilePath("cur*"))"#);
760 insta::assert_debug_snapshot!(
761 parse(r#"glob:".""#).unwrap(),
762 @r#"Pattern(FilePath("cur*"))"#);
763 insta::assert_debug_snapshot!(
764 parse(r#"glob:"..""#).unwrap(),
765 @r#"Pattern(FilePath(""))"#);
766
767 insta::assert_debug_snapshot!(
769 parse(r#"glob:"*""#).unwrap(), @r#"
770 Pattern(
771 FileGlob {
772 dir: "cur*",
773 pattern: Glob {
774 glob: "*",
775 re: "(?-u)^[^/]*$",
776 opts: _,
777 tokens: _,
778 },
779 },
780 )
781 "#);
782 insta::assert_debug_snapshot!(
783 parse(r#"glob:"./*""#).unwrap(), @r#"
784 Pattern(
785 FileGlob {
786 dir: "cur*",
787 pattern: Glob {
788 glob: "*",
789 re: "(?-u)^[^/]*$",
790 opts: _,
791 tokens: _,
792 },
793 },
794 )
795 "#);
796 insta::assert_debug_snapshot!(
797 parse(r#"glob:"../*""#).unwrap(), @r#"
798 Pattern(
799 FileGlob {
800 dir: "",
801 pattern: Glob {
802 glob: "*",
803 re: "(?-u)^[^/]*$",
804 opts: _,
805 tokens: _,
806 },
807 },
808 )
809 "#);
810 insta::assert_debug_snapshot!(
812 parse(r#"glob:"**""#).unwrap(), @r#"
813 Pattern(
814 FileGlob {
815 dir: "cur*",
816 pattern: Glob {
817 glob: "**",
818 re: "(?-u)^.*$",
819 opts: _,
820 tokens: _,
821 },
822 },
823 )
824 "#);
825 insta::assert_debug_snapshot!(
826 parse(r#"glob:"../foo/b?r/baz""#).unwrap(), @r#"
827 Pattern(
828 FileGlob {
829 dir: "foo",
830 pattern: Glob {
831 glob: "b?r/baz",
832 re: "(?-u)^b[^/]r/baz$",
833 opts: _,
834 tokens: _,
835 },
836 },
837 )
838 "#);
839 assert!(parse(r#"glob:"../../*""#).is_err());
840 assert!(parse(r#"glob-i:"../../*""#).is_err());
841 assert!(parse(r#"glob:"/*""#).is_err());
842 assert!(parse(r#"glob-i:"/*""#).is_err());
843 assert!(parse(r#"glob:"*/..""#).is_err());
845 assert!(parse(r#"glob-i:"*/..""#).is_err());
846
847 if cfg!(windows) {
848 insta::assert_debug_snapshot!(
850 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
851 Pattern(
852 FileGlob {
853 dir: "foo",
854 pattern: Glob {
855 glob: "*/bar",
856 re: "(?-u)^[^/]*/bar$",
857 opts: _,
858 tokens: _,
859 },
860 },
861 )
862 "#);
863 } else {
864 insta::assert_debug_snapshot!(
866 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
867 Pattern(
868 FileGlob {
869 dir: "cur*",
870 pattern: Glob {
871 glob: "..\\foo\\*\\bar",
872 re: "(?-u)^\\.\\.foo\\*bar$",
873 opts: _,
874 tokens: _,
875 },
876 },
877 )
878 "#);
879 }
880
881 insta::assert_debug_snapshot!(
883 parse(r#"root-glob:"foo""#).unwrap(),
884 @r#"Pattern(FilePath("foo"))"#);
885 insta::assert_debug_snapshot!(
886 parse(r#"root-glob:"""#).unwrap(),
887 @r#"Pattern(FilePath(""))"#);
888 insta::assert_debug_snapshot!(
889 parse(r#"root-glob:".""#).unwrap(),
890 @r#"Pattern(FilePath(""))"#);
891
892 insta::assert_debug_snapshot!(
894 parse(r#"root-glob:"*""#).unwrap(), @r#"
895 Pattern(
896 FileGlob {
897 dir: "",
898 pattern: Glob {
899 glob: "*",
900 re: "(?-u)^[^/]*$",
901 opts: _,
902 tokens: _,
903 },
904 },
905 )
906 "#);
907 insta::assert_debug_snapshot!(
908 parse(r#"root-glob:"foo/bar/b[az]""#).unwrap(), @r#"
909 Pattern(
910 FileGlob {
911 dir: "foo/bar",
912 pattern: Glob {
913 glob: "b[az]",
914 re: "(?-u)^b[az]$",
915 opts: _,
916 tokens: _,
917 },
918 },
919 )
920 "#);
921 insta::assert_debug_snapshot!(
922 parse(r#"root-glob:"foo/bar/b{ar,az}""#).unwrap(), @r#"
923 Pattern(
924 FileGlob {
925 dir: "foo/bar",
926 pattern: Glob {
927 glob: "b{ar,az}",
928 re: "(?-u)^b(?:ar|az)$",
929 opts: _,
930 tokens: _,
931 },
932 },
933 )
934 "#);
935 assert!(parse(r#"root-glob:"../*""#).is_err());
936 assert!(parse(r#"root-glob-i:"../*""#).is_err());
937 assert!(parse(r#"root-glob:"/*""#).is_err());
938 assert!(parse(r#"root-glob-i:"/*""#).is_err());
939
940 if cfg!(not(windows)) {
942 insta::assert_debug_snapshot!(
943 parse(r#"root-glob:'foo/bar\baz'"#).unwrap(), @r#"
944 Pattern(
945 FileGlob {
946 dir: "foo",
947 pattern: Glob {
948 glob: "bar\\baz",
949 re: "(?-u)^barbaz$",
950 opts: _,
951 tokens: _,
952 },
953 },
954 )
955 "#);
956 }
957 }
958
959 #[test]
960 fn test_parse_glob_pattern_case_insensitive() {
961 let settings = insta_settings();
962 let _guard = settings.bind_to_scope();
963 let context = FilesetParseContext {
964 aliases_map: &FilesetAliasesMap::new(),
965 path_converter: &RepoPathUiConverter::Fs {
966 cwd: PathBuf::from("/ws/cur"),
967 base: PathBuf::from("/ws"),
968 },
969 };
970 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
971
972 insta::assert_debug_snapshot!(
974 parse(r#"glob-i:"*.TXT""#).unwrap(), @r#"
975 Pattern(
976 FileGlob {
977 dir: "cur",
978 pattern: Glob {
979 glob: "*.TXT",
980 re: "(?-u)(?i)^[^/]*\\.TXT$",
981 opts: _,
982 tokens: _,
983 },
984 },
985 )
986 "#);
987
988 insta::assert_debug_snapshot!(
990 parse(r#"cwd-glob-i:"[Ff]oo""#).unwrap(), @r#"
991 Pattern(
992 FileGlob {
993 dir: "cur",
994 pattern: Glob {
995 glob: "[Ff]oo",
996 re: "(?-u)(?i)^[Ff]oo$",
997 opts: _,
998 tokens: _,
999 },
1000 },
1001 )
1002 "#);
1003
1004 insta::assert_debug_snapshot!(
1006 parse(r#"root-glob-i:"*.Rs""#).unwrap(), @r#"
1007 Pattern(
1008 FileGlob {
1009 dir: "",
1010 pattern: Glob {
1011 glob: "*.Rs",
1012 re: "(?-u)(?i)^[^/]*\\.Rs$",
1013 opts: _,
1014 tokens: _,
1015 },
1016 },
1017 )
1018 "#);
1019
1020 insta::assert_debug_snapshot!(
1022 parse(r#"glob-i:"SubDir/*.rs""#).unwrap(), @r#"
1023 Pattern(
1024 FileGlob {
1025 dir: "cur",
1026 pattern: Glob {
1027 glob: "SubDir/*.rs",
1028 re: "(?-u)(?i)^SubDir/[^/]*\\.rs$",
1029 opts: _,
1030 tokens: _,
1031 },
1032 },
1033 )
1034 "#);
1035
1036 insta::assert_debug_snapshot!(
1038 parse(r#"glob:"SubDir/*.rs""#).unwrap(), @r#"
1039 Pattern(
1040 FileGlob {
1041 dir: "cur/SubDir",
1042 pattern: Glob {
1043 glob: "*.rs",
1044 re: "(?-u)^[^/]*\\.rs$",
1045 opts: _,
1046 tokens: _,
1047 },
1048 },
1049 )
1050 "#);
1051
1052 insta::assert_debug_snapshot!(
1054 parse(r#"glob-i:"../SomeDir/*.rs""#).unwrap(), @r#"
1055 Pattern(
1056 FileGlob {
1057 dir: "",
1058 pattern: Glob {
1059 glob: "SomeDir/*.rs",
1060 re: "(?-u)(?i)^SomeDir/[^/]*\\.rs$",
1061 opts: _,
1062 tokens: _,
1063 },
1064 },
1065 )
1066 "#);
1067
1068 insta::assert_debug_snapshot!(
1070 parse(r#"glob-i:"./SomeFile*.txt""#).unwrap(), @r#"
1071 Pattern(
1072 FileGlob {
1073 dir: "cur",
1074 pattern: Glob {
1075 glob: "SomeFile*.txt",
1076 re: "(?-u)(?i)^SomeFile[^/]*\\.txt$",
1077 opts: _,
1078 tokens: _,
1079 },
1080 },
1081 )
1082 "#);
1083 }
1084
1085 #[test]
1086 fn test_parse_prefix_glob_pattern() {
1087 let settings = insta_settings();
1088 let _guard = settings.bind_to_scope();
1089 let context = FilesetParseContext {
1090 aliases_map: &FilesetAliasesMap::new(),
1091 path_converter: &RepoPathUiConverter::Fs {
1092 cwd: PathBuf::from("/ws/cur*"),
1094 base: PathBuf::from("/ws"),
1095 },
1096 };
1097 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1098
1099 insta::assert_debug_snapshot!(
1101 parse("cwd-prefix-glob:'foo'").unwrap(),
1102 @r#"Pattern(PrefixPath("cur*/foo"))"#);
1103 insta::assert_debug_snapshot!(
1104 parse("prefix-glob:'.'").unwrap(),
1105 @r#"Pattern(PrefixPath("cur*"))"#);
1106 insta::assert_debug_snapshot!(
1107 parse("cwd-prefix-glob-i:'..'").unwrap(),
1108 @r#"Pattern(PrefixPath(""))"#);
1109 insta::assert_debug_snapshot!(
1110 parse("prefix-glob-i:'../_'").unwrap(),
1111 @r#"Pattern(PrefixPath("_"))"#);
1112
1113 insta::assert_debug_snapshot!(
1115 parse("cwd-prefix-glob:'*'").unwrap(), @r#"
1116 Pattern(
1117 PrefixGlob {
1118 dir: "cur*",
1119 pattern: Glob {
1120 glob: "*",
1121 re: "(?-u)^[^/]*$",
1122 opts: _,
1123 tokens: _,
1124 },
1125 },
1126 )
1127 "#);
1128
1129 insta::assert_debug_snapshot!(
1131 parse("cwd-prefix-glob-i:'../foo'").unwrap(), @r#"
1132 Pattern(
1133 PrefixGlob {
1134 dir: "",
1135 pattern: Glob {
1136 glob: "foo",
1137 re: "(?-u)(?i)^foo$",
1138 opts: _,
1139 tokens: _,
1140 },
1141 },
1142 )
1143 "#);
1144
1145 insta::assert_debug_snapshot!(
1147 parse("root-prefix-glob:'foo'").unwrap(),
1148 @r#"Pattern(PrefixPath("foo"))"#);
1149 insta::assert_debug_snapshot!(
1150 parse("root-prefix-glob-i:'.'").unwrap(),
1151 @r#"Pattern(PrefixPath(""))"#);
1152
1153 insta::assert_debug_snapshot!(
1155 parse("root-prefix-glob:'*'").unwrap(), @r#"
1156 Pattern(
1157 PrefixGlob {
1158 dir: "",
1159 pattern: Glob {
1160 glob: "*",
1161 re: "(?-u)^[^/]*$",
1162 opts: _,
1163 tokens: _,
1164 },
1165 },
1166 )
1167 "#);
1168
1169 insta::assert_debug_snapshot!(
1171 parse("root-prefix-glob-i:'_/foo'").unwrap(), @r#"
1172 Pattern(
1173 PrefixGlob {
1174 dir: "_",
1175 pattern: Glob {
1176 glob: "foo",
1177 re: "(?-u)(?i)^foo$",
1178 opts: _,
1179 tokens: _,
1180 },
1181 },
1182 )
1183 "#);
1184 }
1185
1186 #[test]
1187 fn test_parse_function() {
1188 let settings = insta_settings();
1189 let _guard = settings.bind_to_scope();
1190 let context = FilesetParseContext {
1191 aliases_map: &FilesetAliasesMap::new(),
1192 path_converter: &RepoPathUiConverter::Fs {
1193 cwd: PathBuf::from("/ws/cur"),
1194 base: PathBuf::from("/ws"),
1195 },
1196 };
1197 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1198
1199 insta::assert_debug_snapshot!(parse("all()").unwrap(), @"All");
1200 insta::assert_debug_snapshot!(parse("none()").unwrap(), @"None");
1201 insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
1202 InvalidArguments {
1203 name: "all",
1204 message: "Expected 0 arguments",
1205 }
1206 "#);
1207 insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
1208 NoSuchFunction {
1209 name: "ale",
1210 candidates: [
1211 "all",
1212 ],
1213 }
1214 "#);
1215 }
1216
1217 #[test]
1218 fn test_parse_compound_expression() {
1219 let settings = insta_settings();
1220 let _guard = settings.bind_to_scope();
1221 let context = FilesetParseContext {
1222 aliases_map: &FilesetAliasesMap::new(),
1223 path_converter: &RepoPathUiConverter::Fs {
1224 cwd: PathBuf::from("/ws/cur"),
1225 base: PathBuf::from("/ws"),
1226 },
1227 };
1228 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1229
1230 insta::assert_debug_snapshot!(parse("~x").unwrap(), @r#"
1231 Difference(
1232 All,
1233 Pattern(PrefixPath("cur/x")),
1234 )
1235 "#);
1236 insta::assert_debug_snapshot!(parse("x|y|root:z").unwrap(), @r#"
1237 UnionAll(
1238 [
1239 Pattern(PrefixPath("cur/x")),
1240 Pattern(PrefixPath("cur/y")),
1241 Pattern(PrefixPath("z")),
1242 ],
1243 )
1244 "#);
1245 insta::assert_debug_snapshot!(parse("x|y&z").unwrap(), @r#"
1246 UnionAll(
1247 [
1248 Pattern(PrefixPath("cur/x")),
1249 Intersection(
1250 Pattern(PrefixPath("cur/y")),
1251 Pattern(PrefixPath("cur/z")),
1252 ),
1253 ],
1254 )
1255 "#);
1256 }
1257
1258 #[test]
1259 fn test_explicit_paths() {
1260 let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
1261 expr.explicit_paths().map(|path| path.to_owned()).collect()
1262 };
1263 let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
1264 assert!(collect(&FilesetExpression::none()).is_empty());
1265 assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
1266 assert_eq!(
1267 collect(&FilesetExpression::union_all(vec![
1268 file_expr("a"),
1269 file_expr("b"),
1270 file_expr("c"),
1271 ])),
1272 ["a", "b", "c"].map(repo_path_buf)
1273 );
1274 assert_eq!(
1275 collect(&FilesetExpression::intersection(
1276 FilesetExpression::union_all(vec![
1277 file_expr("a"),
1278 FilesetExpression::none(),
1279 file_expr("b"),
1280 file_expr("c"),
1281 ]),
1282 FilesetExpression::difference(
1283 file_expr("d"),
1284 FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
1285 )
1286 )),
1287 ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
1288 );
1289 }
1290
1291 #[test]
1292 fn test_build_matcher_simple() {
1293 let settings = insta_settings();
1294 let _guard = settings.bind_to_scope();
1295
1296 insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
1297 insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
1298 insta::assert_debug_snapshot!(
1299 FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
1300 @r#"
1301 FilesMatcher {
1302 tree: Dir {
1303 "foo": File {},
1304 },
1305 }
1306 "#);
1307 insta::assert_debug_snapshot!(
1308 FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
1309 @r#"
1310 PrefixMatcher {
1311 tree: Dir {
1312 "foo": Prefix {},
1313 },
1314 }
1315 "#);
1316 }
1317
1318 #[test]
1319 fn test_build_matcher_glob_pattern() {
1320 let settings = insta_settings();
1321 let _guard = settings.bind_to_scope();
1322 let file_glob_expr = |dir: &str, pattern: &str| {
1323 FilesetExpression::pattern(FilePattern::FileGlob {
1324 dir: repo_path_buf(dir),
1325 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1326 })
1327 };
1328 let prefix_glob_expr = |dir: &str, pattern: &str| {
1329 FilesetExpression::pattern(FilePattern::PrefixGlob {
1330 dir: repo_path_buf(dir),
1331 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1332 })
1333 };
1334
1335 insta::assert_debug_snapshot!(file_glob_expr("", "*").to_matcher(), @r#"
1336 GlobsMatcher {
1337 tree: Some(RegexSet(["(?-u)^[^/]*$"])) {},
1338 matches_prefix_paths: false,
1339 }
1340 "#);
1341
1342 let expr = FilesetExpression::union_all(vec![
1343 file_glob_expr("foo", "*"),
1344 file_glob_expr("foo/bar", "*"),
1345 file_glob_expr("foo", "?"),
1346 prefix_glob_expr("foo", "ba[rz]"),
1347 prefix_glob_expr("foo", "qu*x"),
1348 ]);
1349 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1350 UnionMatcher {
1351 input1: GlobsMatcher {
1352 tree: None {
1353 "foo": Some(RegexSet(["(?-u)^[^/]*$", "(?-u)^[^/]$"])) {
1354 "bar": Some(RegexSet(["(?-u)^[^/]*$"])) {},
1355 },
1356 },
1357 matches_prefix_paths: false,
1358 },
1359 input2: GlobsMatcher {
1360 tree: None {
1361 "foo": Some(RegexSet(["(?-u)^ba[rz](?:/|$)", "(?-u)^qu[^/]*x(?:/|$)"])) {},
1362 },
1363 matches_prefix_paths: true,
1364 },
1365 }
1366 "#);
1367 }
1368
1369 #[test]
1370 fn test_build_matcher_union_patterns_of_same_kind() {
1371 let settings = insta_settings();
1372 let _guard = settings.bind_to_scope();
1373
1374 let expr = FilesetExpression::union_all(vec![
1375 FilesetExpression::file_path(repo_path_buf("foo")),
1376 FilesetExpression::file_path(repo_path_buf("foo/bar")),
1377 ]);
1378 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1379 FilesMatcher {
1380 tree: Dir {
1381 "foo": File {
1382 "bar": File {},
1383 },
1384 },
1385 }
1386 "#);
1387
1388 let expr = FilesetExpression::union_all(vec![
1389 FilesetExpression::prefix_path(repo_path_buf("bar")),
1390 FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
1391 ]);
1392 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1393 PrefixMatcher {
1394 tree: Dir {
1395 "bar": Prefix {
1396 "baz": Prefix {},
1397 },
1398 },
1399 }
1400 "#);
1401 }
1402
1403 #[test]
1404 fn test_build_matcher_union_patterns_of_different_kind() {
1405 let settings = insta_settings();
1406 let _guard = settings.bind_to_scope();
1407
1408 let expr = FilesetExpression::union_all(vec![
1409 FilesetExpression::file_path(repo_path_buf("foo")),
1410 FilesetExpression::prefix_path(repo_path_buf("bar")),
1411 ]);
1412 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1413 UnionMatcher {
1414 input1: FilesMatcher {
1415 tree: Dir {
1416 "foo": File {},
1417 },
1418 },
1419 input2: PrefixMatcher {
1420 tree: Dir {
1421 "bar": Prefix {},
1422 },
1423 },
1424 }
1425 "#);
1426 }
1427
1428 #[test]
1429 fn test_build_matcher_unnormalized_union() {
1430 let settings = insta_settings();
1431 let _guard = settings.bind_to_scope();
1432
1433 let expr = FilesetExpression::UnionAll(vec![]);
1434 insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
1435
1436 let expr =
1437 FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
1438 insta::assert_debug_snapshot!(expr.to_matcher(), @"
1439 UnionMatcher {
1440 input1: NothingMatcher,
1441 input2: EverythingMatcher,
1442 }
1443 ");
1444 }
1445
1446 #[test]
1447 fn test_build_matcher_combined() {
1448 let settings = insta_settings();
1449 let _guard = settings.bind_to_scope();
1450
1451 let expr = FilesetExpression::union_all(vec![
1452 FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1453 FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1454 FilesetExpression::file_path(repo_path_buf("foo")),
1455 FilesetExpression::prefix_path(repo_path_buf("bar")),
1456 ]);
1457 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1458 UnionMatcher {
1459 input1: UnionMatcher {
1460 input1: IntersectionMatcher {
1461 input1: EverythingMatcher,
1462 input2: NothingMatcher,
1463 },
1464 input2: DifferenceMatcher {
1465 wanted: NothingMatcher,
1466 unwanted: EverythingMatcher,
1467 },
1468 },
1469 input2: UnionMatcher {
1470 input1: FilesMatcher {
1471 tree: Dir {
1472 "foo": File {},
1473 },
1474 },
1475 input2: PrefixMatcher {
1476 tree: Dir {
1477 "bar": Prefix {},
1478 },
1479 },
1480 },
1481 }
1482 "#);
1483 }
1484}