1use std::collections::HashMap;
18use std::iter;
19use std::path;
20use std::slice;
21use std::sync::LazyLock;
22
23use globset::Glob;
24use globset::GlobBuilder;
25use itertools::Itertools as _;
26use thiserror::Error;
27
28use crate::dsl_util::collect_similar;
29use crate::fileset_parser;
30use crate::fileset_parser::BinaryOp;
31use crate::fileset_parser::ExpressionKind;
32use crate::fileset_parser::ExpressionNode;
33pub use crate::fileset_parser::FilesetDiagnostics;
34pub use crate::fileset_parser::FilesetParseError;
35pub use crate::fileset_parser::FilesetParseErrorKind;
36pub use crate::fileset_parser::FilesetParseResult;
37use crate::fileset_parser::FunctionCallNode;
38use crate::fileset_parser::UnaryOp;
39use crate::matchers::DifferenceMatcher;
40use crate::matchers::EverythingMatcher;
41use crate::matchers::FilesMatcher;
42use crate::matchers::GlobsMatcher;
43use crate::matchers::IntersectionMatcher;
44use crate::matchers::Matcher;
45use crate::matchers::NothingMatcher;
46use crate::matchers::PrefixMatcher;
47use crate::matchers::UnionMatcher;
48use crate::repo_path::RelativePathParseError;
49use crate::repo_path::RepoPath;
50use crate::repo_path::RepoPathBuf;
51use crate::repo_path::RepoPathUiConverter;
52use crate::repo_path::UiPathParseError;
53
54#[derive(Debug, Error)]
56pub enum FilePatternParseError {
57 #[error("Invalid file pattern kind `{0}:`")]
59 InvalidKind(String),
60 #[error(transparent)]
62 UiPath(#[from] UiPathParseError),
63 #[error(transparent)]
65 RelativePath(#[from] RelativePathParseError),
66 #[error(transparent)]
68 GlobPattern(#[from] globset::Error),
69}
70
71#[derive(Clone, Debug)]
73pub enum FilePattern {
74 FilePath(RepoPathBuf),
76 PrefixPath(RepoPathBuf),
78 FileGlob {
80 dir: RepoPathBuf,
82 pattern: Box<Glob>,
84 },
85 PrefixGlob {
87 dir: RepoPathBuf,
89 pattern: Box<Glob>,
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
282pub(super) fn parse_file_glob(input: &str, icase: bool) -> Result<Glob, globset::Error> {
283 GlobBuilder::new(input)
284 .literal_separator(true)
285 .case_insensitive(icase)
286 .build()
287}
288
289fn is_glob_char(c: char) -> bool {
291 const GLOB_CHARS: &[char] = if cfg!(windows) {
294 &['?', '*', '[', ']', '{', '}']
295 } else {
296 &['?', '*', '[', ']', '{', '}', '\\']
297 };
298 GLOB_CHARS.contains(&c)
299}
300
301fn split_glob_path(input: &str) -> (&str, &str) {
303 let prefix_len = input
304 .split_inclusive(path::is_separator)
305 .take_while(|component| !component.contains(is_glob_char))
306 .map(|component| component.len())
307 .sum();
308 input.split_at(prefix_len)
309}
310
311fn split_glob_path_i(input: &str) -> (&str, &str) {
314 let prefix_len = input
315 .split_inclusive(path::is_separator)
316 .take_while(|component| {
317 !component.contains(|c: char| c.is_ascii_alphabetic() || is_glob_char(c))
318 })
319 .map(|component| component.len())
320 .sum();
321 input.split_at(prefix_len)
322}
323
324#[derive(Clone, Debug)]
326pub enum FilesetExpression {
327 None,
329 All,
331 Pattern(FilePattern),
333 UnionAll(Vec<Self>),
338 Intersection(Box<Self>, Box<Self>),
340 Difference(Box<Self>, Box<Self>),
342}
343
344impl FilesetExpression {
345 pub fn none() -> Self {
347 Self::None
348 }
349
350 pub fn all() -> Self {
352 Self::All
353 }
354
355 pub fn pattern(pattern: FilePattern) -> Self {
357 Self::Pattern(pattern)
358 }
359
360 pub fn file_path(path: RepoPathBuf) -> Self {
362 Self::Pattern(FilePattern::FilePath(path))
363 }
364
365 pub fn prefix_path(path: RepoPathBuf) -> Self {
367 Self::Pattern(FilePattern::PrefixPath(path))
368 }
369
370 pub fn union_all(expressions: Vec<Self>) -> Self {
372 match expressions.len() {
373 0 => Self::none(),
374 1 => expressions.into_iter().next().unwrap(),
375 _ => Self::UnionAll(expressions),
376 }
377 }
378
379 pub fn intersection(self, other: Self) -> Self {
381 Self::Intersection(Box::new(self), Box::new(other))
382 }
383
384 pub fn difference(self, other: Self) -> Self {
386 Self::Difference(Box::new(self), Box::new(other))
387 }
388
389 fn as_union_all(&self) -> &[Self] {
391 match self {
392 Self::None => &[],
393 Self::UnionAll(exprs) => exprs,
394 _ => slice::from_ref(self),
395 }
396 }
397
398 fn dfs_pre(&self) -> impl Iterator<Item = &Self> {
399 let mut stack: Vec<&Self> = vec![self];
400 iter::from_fn(move || {
401 let expr = stack.pop()?;
402 match expr {
403 Self::None | Self::All | Self::Pattern(_) => {}
404 Self::UnionAll(exprs) => stack.extend(exprs.iter().rev()),
405 Self::Intersection(expr1, expr2) | Self::Difference(expr1, expr2) => {
406 stack.push(expr2);
407 stack.push(expr1);
408 }
409 }
410 Some(expr)
411 })
412 }
413
414 pub fn explicit_paths(&self) -> impl Iterator<Item = &RepoPath> {
419 self.dfs_pre().filter_map(|expr| match expr {
422 Self::Pattern(pattern) => pattern.as_path(),
423 _ => None,
424 })
425 }
426
427 pub fn to_matcher(&self) -> Box<dyn Matcher> {
429 build_union_matcher(self.as_union_all())
430 }
431}
432
433fn build_union_matcher(expressions: &[FilesetExpression]) -> Box<dyn Matcher> {
438 let mut file_paths = Vec::new();
439 let mut prefix_paths = Vec::new();
440 let mut file_globs = GlobsMatcher::builder().prefix_paths(false);
441 let mut prefix_globs = GlobsMatcher::builder().prefix_paths(true);
442 let mut matchers: Vec<Option<Box<dyn Matcher>>> = Vec::new();
443 for expr in expressions {
444 let matcher: Box<dyn Matcher> = match expr {
445 FilesetExpression::None => Box::new(NothingMatcher),
447 FilesetExpression::All => Box::new(EverythingMatcher),
448 FilesetExpression::Pattern(pattern) => {
449 match pattern {
450 FilePattern::FilePath(path) => file_paths.push(path),
451 FilePattern::PrefixPath(path) => prefix_paths.push(path),
452 FilePattern::FileGlob { dir, pattern } => file_globs.add(dir, pattern),
453 FilePattern::PrefixGlob { dir, pattern } => prefix_globs.add(dir, pattern),
454 }
455 continue;
456 }
457 FilesetExpression::UnionAll(exprs) => build_union_matcher(exprs),
459 FilesetExpression::Intersection(expr1, expr2) => {
460 let m1 = build_union_matcher(expr1.as_union_all());
461 let m2 = build_union_matcher(expr2.as_union_all());
462 Box::new(IntersectionMatcher::new(m1, m2))
463 }
464 FilesetExpression::Difference(expr1, expr2) => {
465 let m1 = build_union_matcher(expr1.as_union_all());
466 let m2 = build_union_matcher(expr2.as_union_all());
467 Box::new(DifferenceMatcher::new(m1, m2))
468 }
469 };
470 matchers.push(Some(matcher));
471 }
472
473 if !file_paths.is_empty() {
474 matchers.push(Some(Box::new(FilesMatcher::new(file_paths))));
475 }
476 if !prefix_paths.is_empty() {
477 matchers.push(Some(Box::new(PrefixMatcher::new(prefix_paths))));
478 }
479 if !file_globs.is_empty() {
480 matchers.push(Some(Box::new(file_globs.build())));
481 }
482 if !prefix_globs.is_empty() {
483 matchers.push(Some(Box::new(prefix_globs.build())));
484 }
485 union_all_matchers(&mut matchers)
486}
487
488fn union_all_matchers(matchers: &mut [Option<Box<dyn Matcher>>]) -> Box<dyn Matcher> {
493 match matchers {
494 [] => Box::new(NothingMatcher),
495 [matcher] => matcher.take().expect("matcher should still be available"),
496 _ => {
497 let (left, right) = matchers.split_at_mut(matchers.len() / 2);
499 let m1 = union_all_matchers(left);
500 let m2 = union_all_matchers(right);
501 Box::new(UnionMatcher::new(m1, m2))
502 }
503 }
504}
505
506type FilesetFunction = fn(
507 &mut FilesetDiagnostics,
508 &RepoPathUiConverter,
509 &FunctionCallNode,
510) -> FilesetParseResult<FilesetExpression>;
511
512static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, FilesetFunction>> = LazyLock::new(|| {
513 let mut map: HashMap<&str, FilesetFunction> = HashMap::new();
516 map.insert("none", |_diagnostics, _path_converter, function| {
517 function.expect_no_arguments()?;
518 Ok(FilesetExpression::none())
519 });
520 map.insert("all", |_diagnostics, _path_converter, function| {
521 function.expect_no_arguments()?;
522 Ok(FilesetExpression::all())
523 });
524 map
525});
526
527fn resolve_function(
528 diagnostics: &mut FilesetDiagnostics,
529 path_converter: &RepoPathUiConverter,
530 function: &FunctionCallNode,
531) -> FilesetParseResult<FilesetExpression> {
532 if let Some(func) = BUILTIN_FUNCTION_MAP.get(function.name) {
533 func(diagnostics, path_converter, function)
534 } else {
535 Err(FilesetParseError::new(
536 FilesetParseErrorKind::NoSuchFunction {
537 name: function.name.to_owned(),
538 candidates: collect_similar(function.name, BUILTIN_FUNCTION_MAP.keys()),
539 },
540 function.name_span,
541 ))
542 }
543}
544
545fn resolve_expression(
546 diagnostics: &mut FilesetDiagnostics,
547 path_converter: &RepoPathUiConverter,
548 node: &ExpressionNode,
549) -> FilesetParseResult<FilesetExpression> {
550 let wrap_pattern_error =
551 |err| FilesetParseError::expression("Invalid file pattern", node.span).with_source(err);
552 match &node.kind {
553 ExpressionKind::Identifier(name) => {
554 let pattern =
555 FilePattern::cwd_prefix_glob(path_converter, name).map_err(wrap_pattern_error)?;
556 Ok(FilesetExpression::pattern(pattern))
557 }
558 ExpressionKind::String(name) => {
559 let pattern =
560 FilePattern::cwd_prefix_glob(path_converter, name).map_err(wrap_pattern_error)?;
561 Ok(FilesetExpression::pattern(pattern))
562 }
563 ExpressionKind::Pattern { kind, value } => {
564 let value = fileset_parser::expect_string_literal("string", value)?;
565 let pattern = FilePattern::from_str_kind(path_converter, value, kind)
566 .map_err(wrap_pattern_error)?;
567 Ok(FilesetExpression::pattern(pattern))
568 }
569 ExpressionKind::Unary(op, arg_node) => {
570 let arg = resolve_expression(diagnostics, path_converter, arg_node)?;
571 match op {
572 UnaryOp::Negate => Ok(FilesetExpression::all().difference(arg)),
573 }
574 }
575 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
576 let lhs = resolve_expression(diagnostics, path_converter, lhs_node)?;
577 let rhs = resolve_expression(diagnostics, path_converter, rhs_node)?;
578 match op {
579 BinaryOp::Intersection => Ok(lhs.intersection(rhs)),
580 BinaryOp::Difference => Ok(lhs.difference(rhs)),
581 }
582 }
583 ExpressionKind::UnionAll(nodes) => {
584 let expressions = nodes
585 .iter()
586 .map(|node| resolve_expression(diagnostics, path_converter, node))
587 .try_collect()?;
588 Ok(FilesetExpression::union_all(expressions))
589 }
590 ExpressionKind::FunctionCall(function) => {
591 resolve_function(diagnostics, path_converter, function)
592 }
593 }
594}
595
596pub fn parse(
598 diagnostics: &mut FilesetDiagnostics,
599 text: &str,
600 path_converter: &RepoPathUiConverter,
601) -> FilesetParseResult<FilesetExpression> {
602 let node = fileset_parser::parse_program(text)?;
603 resolve_expression(diagnostics, path_converter, &node)
605}
606
607pub fn parse_maybe_bare(
612 diagnostics: &mut FilesetDiagnostics,
613 text: &str,
614 path_converter: &RepoPathUiConverter,
615) -> FilesetParseResult<FilesetExpression> {
616 let node = fileset_parser::parse_program_or_bare_string(text)?;
617 resolve_expression(diagnostics, path_converter, &node)
619}
620
621#[cfg(test)]
622mod tests {
623 use std::path::PathBuf;
624
625 use super::*;
626
627 fn repo_path_buf(value: impl Into<String>) -> RepoPathBuf {
628 RepoPathBuf::from_internal_string(value).unwrap()
629 }
630
631 fn insta_settings() -> insta::Settings {
632 let mut settings = insta::Settings::clone_current();
633 settings.add_filter(
635 r"(?m)^(\s{12}opts):\s*GlobOptions\s*\{\n(\s{16}.*\n)*\s{12}\},",
636 "$1: _,",
637 );
638 settings.add_filter(
639 r"(?m)^(\s{12}tokens):\s*Tokens\(\n(\s{16}.*\n)*\s{12}\),",
640 "$1: _,",
641 );
642 for _ in 0..4 {
645 settings.add_filter(
646 r"(?x)
647 \b([A-Z]\w*)\(\n
648 \s*(.{1,60}),\n
649 \s*\)",
650 "$1($2)",
651 );
652 }
653 settings
654 }
655
656 #[test]
657 fn test_parse_file_pattern() {
658 let settings = insta_settings();
659 let _guard = settings.bind_to_scope();
660 let path_converter = RepoPathUiConverter::Fs {
661 cwd: PathBuf::from("/ws/cur"),
662 base: PathBuf::from("/ws"),
663 };
664 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
665
666 insta::assert_debug_snapshot!(
668 parse(".").unwrap(),
669 @r#"Pattern(PrefixPath("cur"))"#);
670 insta::assert_debug_snapshot!(
671 parse("..").unwrap(),
672 @r#"Pattern(PrefixPath(""))"#);
673 assert!(parse("../..").is_err());
674 insta::assert_debug_snapshot!(
675 parse("foo").unwrap(),
676 @r#"Pattern(PrefixPath("cur/foo"))"#);
677 insta::assert_debug_snapshot!(
678 parse("*.*").unwrap(),
679 @r#"
680 Pattern(
681 PrefixGlob {
682 dir: "cur",
683 pattern: Glob {
684 glob: "*.*",
685 re: "(?-u)^[^/]*\\.[^/]*$",
686 opts: _,
687 tokens: _,
688 },
689 },
690 )
691 "#);
692 insta::assert_debug_snapshot!(
693 parse("cwd:.").unwrap(),
694 @r#"Pattern(PrefixPath("cur"))"#);
695 insta::assert_debug_snapshot!(
696 parse("cwd-file:foo").unwrap(),
697 @r#"Pattern(FilePath("cur/foo"))"#);
698 insta::assert_debug_snapshot!(
699 parse("file:../foo/bar").unwrap(),
700 @r#"Pattern(FilePath("foo/bar"))"#);
701
702 insta::assert_debug_snapshot!(
704 parse("root:.").unwrap(),
705 @r#"Pattern(PrefixPath(""))"#);
706 assert!(parse("root:..").is_err());
707 insta::assert_debug_snapshot!(
708 parse("root:foo/bar").unwrap(),
709 @r#"Pattern(PrefixPath("foo/bar"))"#);
710 insta::assert_debug_snapshot!(
711 parse("root-file:bar").unwrap(),
712 @r#"Pattern(FilePath("bar"))"#);
713 }
714
715 #[test]
716 fn test_parse_glob_pattern() {
717 let settings = insta_settings();
718 let _guard = settings.bind_to_scope();
719 let path_converter = RepoPathUiConverter::Fs {
720 cwd: PathBuf::from("/ws/cur*"),
722 base: PathBuf::from("/ws"),
723 };
724 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
725
726 insta::assert_debug_snapshot!(
728 parse(r#"cwd-glob:"foo""#).unwrap(),
729 @r#"Pattern(FilePath("cur*/foo"))"#);
730 insta::assert_debug_snapshot!(
733 parse(r#"glob:"""#).unwrap(),
734 @r#"Pattern(FilePath("cur*"))"#);
735 insta::assert_debug_snapshot!(
736 parse(r#"glob:".""#).unwrap(),
737 @r#"Pattern(FilePath("cur*"))"#);
738 insta::assert_debug_snapshot!(
739 parse(r#"glob:"..""#).unwrap(),
740 @r#"Pattern(FilePath(""))"#);
741
742 insta::assert_debug_snapshot!(
744 parse(r#"glob:"*""#).unwrap(), @r#"
745 Pattern(
746 FileGlob {
747 dir: "cur*",
748 pattern: Glob {
749 glob: "*",
750 re: "(?-u)^[^/]*$",
751 opts: _,
752 tokens: _,
753 },
754 },
755 )
756 "#);
757 insta::assert_debug_snapshot!(
758 parse(r#"glob:"./*""#).unwrap(), @r#"
759 Pattern(
760 FileGlob {
761 dir: "cur*",
762 pattern: Glob {
763 glob: "*",
764 re: "(?-u)^[^/]*$",
765 opts: _,
766 tokens: _,
767 },
768 },
769 )
770 "#);
771 insta::assert_debug_snapshot!(
772 parse(r#"glob:"../*""#).unwrap(), @r#"
773 Pattern(
774 FileGlob {
775 dir: "",
776 pattern: Glob {
777 glob: "*",
778 re: "(?-u)^[^/]*$",
779 opts: _,
780 tokens: _,
781 },
782 },
783 )
784 "#);
785 insta::assert_debug_snapshot!(
787 parse(r#"glob:"**""#).unwrap(), @r#"
788 Pattern(
789 FileGlob {
790 dir: "cur*",
791 pattern: Glob {
792 glob: "**",
793 re: "(?-u)^.*$",
794 opts: _,
795 tokens: _,
796 },
797 },
798 )
799 "#);
800 insta::assert_debug_snapshot!(
801 parse(r#"glob:"../foo/b?r/baz""#).unwrap(), @r#"
802 Pattern(
803 FileGlob {
804 dir: "foo",
805 pattern: Glob {
806 glob: "b?r/baz",
807 re: "(?-u)^b[^/]r/baz$",
808 opts: _,
809 tokens: _,
810 },
811 },
812 )
813 "#);
814 assert!(parse(r#"glob:"../../*""#).is_err());
815 assert!(parse(r#"glob-i:"../../*""#).is_err());
816 assert!(parse(r#"glob:"/*""#).is_err());
817 assert!(parse(r#"glob-i:"/*""#).is_err());
818 assert!(parse(r#"glob:"*/..""#).is_err());
820 assert!(parse(r#"glob-i:"*/..""#).is_err());
821
822 if cfg!(windows) {
823 insta::assert_debug_snapshot!(
825 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
826 Pattern(
827 FileGlob {
828 dir: "foo",
829 pattern: Glob {
830 glob: "*/bar",
831 re: "(?-u)^[^/]*/bar$",
832 opts: _,
833 tokens: _,
834 },
835 },
836 )
837 "#);
838 } else {
839 insta::assert_debug_snapshot!(
841 parse(r#"glob:"..\\foo\\*\\bar""#).unwrap(), @r#"
842 Pattern(
843 FileGlob {
844 dir: "cur*",
845 pattern: Glob {
846 glob: "..\\foo\\*\\bar",
847 re: "(?-u)^\\.\\.foo\\*bar$",
848 opts: _,
849 tokens: _,
850 },
851 },
852 )
853 "#);
854 }
855
856 insta::assert_debug_snapshot!(
858 parse(r#"root-glob:"foo""#).unwrap(),
859 @r#"Pattern(FilePath("foo"))"#);
860 insta::assert_debug_snapshot!(
861 parse(r#"root-glob:"""#).unwrap(),
862 @r#"Pattern(FilePath(""))"#);
863 insta::assert_debug_snapshot!(
864 parse(r#"root-glob:".""#).unwrap(),
865 @r#"Pattern(FilePath(""))"#);
866
867 insta::assert_debug_snapshot!(
869 parse(r#"root-glob:"*""#).unwrap(), @r#"
870 Pattern(
871 FileGlob {
872 dir: "",
873 pattern: Glob {
874 glob: "*",
875 re: "(?-u)^[^/]*$",
876 opts: _,
877 tokens: _,
878 },
879 },
880 )
881 "#);
882 insta::assert_debug_snapshot!(
883 parse(r#"root-glob:"foo/bar/b[az]""#).unwrap(), @r#"
884 Pattern(
885 FileGlob {
886 dir: "foo/bar",
887 pattern: Glob {
888 glob: "b[az]",
889 re: "(?-u)^b[az]$",
890 opts: _,
891 tokens: _,
892 },
893 },
894 )
895 "#);
896 insta::assert_debug_snapshot!(
897 parse(r#"root-glob:"foo/bar/b{ar,az}""#).unwrap(), @r#"
898 Pattern(
899 FileGlob {
900 dir: "foo/bar",
901 pattern: Glob {
902 glob: "b{ar,az}",
903 re: "(?-u)^b(?:ar|az)$",
904 opts: _,
905 tokens: _,
906 },
907 },
908 )
909 "#);
910 assert!(parse(r#"root-glob:"../*""#).is_err());
911 assert!(parse(r#"root-glob-i:"../*""#).is_err());
912 assert!(parse(r#"root-glob:"/*""#).is_err());
913 assert!(parse(r#"root-glob-i:"/*""#).is_err());
914
915 if cfg!(not(windows)) {
917 insta::assert_debug_snapshot!(
918 parse(r#"root-glob:'foo/bar\baz'"#).unwrap(), @r#"
919 Pattern(
920 FileGlob {
921 dir: "foo",
922 pattern: Glob {
923 glob: "bar\\baz",
924 re: "(?-u)^barbaz$",
925 opts: _,
926 tokens: _,
927 },
928 },
929 )
930 "#);
931 }
932 }
933
934 #[test]
935 fn test_parse_glob_pattern_case_insensitive() {
936 let settings = insta_settings();
937 let _guard = settings.bind_to_scope();
938 let path_converter = RepoPathUiConverter::Fs {
939 cwd: PathBuf::from("/ws/cur"),
940 base: PathBuf::from("/ws"),
941 };
942 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
943
944 insta::assert_debug_snapshot!(
946 parse(r#"glob-i:"*.TXT""#).unwrap(), @r#"
947 Pattern(
948 FileGlob {
949 dir: "cur",
950 pattern: Glob {
951 glob: "*.TXT",
952 re: "(?-u)(?i)^[^/]*\\.TXT$",
953 opts: _,
954 tokens: _,
955 },
956 },
957 )
958 "#);
959
960 insta::assert_debug_snapshot!(
962 parse(r#"cwd-glob-i:"[Ff]oo""#).unwrap(), @r#"
963 Pattern(
964 FileGlob {
965 dir: "cur",
966 pattern: Glob {
967 glob: "[Ff]oo",
968 re: "(?-u)(?i)^[Ff]oo$",
969 opts: _,
970 tokens: _,
971 },
972 },
973 )
974 "#);
975
976 insta::assert_debug_snapshot!(
978 parse(r#"root-glob-i:"*.Rs""#).unwrap(), @r#"
979 Pattern(
980 FileGlob {
981 dir: "",
982 pattern: Glob {
983 glob: "*.Rs",
984 re: "(?-u)(?i)^[^/]*\\.Rs$",
985 opts: _,
986 tokens: _,
987 },
988 },
989 )
990 "#);
991
992 insta::assert_debug_snapshot!(
994 parse(r#"glob-i:"SubDir/*.rs""#).unwrap(), @r#"
995 Pattern(
996 FileGlob {
997 dir: "cur",
998 pattern: Glob {
999 glob: "SubDir/*.rs",
1000 re: "(?-u)(?i)^SubDir/[^/]*\\.rs$",
1001 opts: _,
1002 tokens: _,
1003 },
1004 },
1005 )
1006 "#);
1007
1008 insta::assert_debug_snapshot!(
1010 parse(r#"glob:"SubDir/*.rs""#).unwrap(), @r#"
1011 Pattern(
1012 FileGlob {
1013 dir: "cur/SubDir",
1014 pattern: Glob {
1015 glob: "*.rs",
1016 re: "(?-u)^[^/]*\\.rs$",
1017 opts: _,
1018 tokens: _,
1019 },
1020 },
1021 )
1022 "#);
1023
1024 insta::assert_debug_snapshot!(
1026 parse(r#"glob-i:"../SomeDir/*.rs""#).unwrap(), @r#"
1027 Pattern(
1028 FileGlob {
1029 dir: "",
1030 pattern: Glob {
1031 glob: "SomeDir/*.rs",
1032 re: "(?-u)(?i)^SomeDir/[^/]*\\.rs$",
1033 opts: _,
1034 tokens: _,
1035 },
1036 },
1037 )
1038 "#);
1039
1040 insta::assert_debug_snapshot!(
1042 parse(r#"glob-i:"./SomeFile*.txt""#).unwrap(), @r#"
1043 Pattern(
1044 FileGlob {
1045 dir: "cur",
1046 pattern: Glob {
1047 glob: "SomeFile*.txt",
1048 re: "(?-u)(?i)^SomeFile[^/]*\\.txt$",
1049 opts: _,
1050 tokens: _,
1051 },
1052 },
1053 )
1054 "#);
1055 }
1056
1057 #[test]
1058 fn test_parse_prefix_glob_pattern() {
1059 let settings = insta_settings();
1060 let _guard = settings.bind_to_scope();
1061 let path_converter = RepoPathUiConverter::Fs {
1062 cwd: PathBuf::from("/ws/cur*"),
1064 base: PathBuf::from("/ws"),
1065 };
1066 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
1067
1068 insta::assert_debug_snapshot!(
1070 parse("cwd-prefix-glob:'foo'").unwrap(),
1071 @r#"Pattern(PrefixPath("cur*/foo"))"#);
1072 insta::assert_debug_snapshot!(
1073 parse("prefix-glob:'.'").unwrap(),
1074 @r#"Pattern(PrefixPath("cur*"))"#);
1075 insta::assert_debug_snapshot!(
1076 parse("cwd-prefix-glob-i:'..'").unwrap(),
1077 @r#"Pattern(PrefixPath(""))"#);
1078 insta::assert_debug_snapshot!(
1079 parse("prefix-glob-i:'../_'").unwrap(),
1080 @r#"Pattern(PrefixPath("_"))"#);
1081
1082 insta::assert_debug_snapshot!(
1084 parse("cwd-prefix-glob:'*'").unwrap(), @r#"
1085 Pattern(
1086 PrefixGlob {
1087 dir: "cur*",
1088 pattern: Glob {
1089 glob: "*",
1090 re: "(?-u)^[^/]*$",
1091 opts: _,
1092 tokens: _,
1093 },
1094 },
1095 )
1096 "#);
1097
1098 insta::assert_debug_snapshot!(
1100 parse("cwd-prefix-glob-i:'../foo'").unwrap(), @r#"
1101 Pattern(
1102 PrefixGlob {
1103 dir: "",
1104 pattern: Glob {
1105 glob: "foo",
1106 re: "(?-u)(?i)^foo$",
1107 opts: _,
1108 tokens: _,
1109 },
1110 },
1111 )
1112 "#);
1113
1114 insta::assert_debug_snapshot!(
1116 parse("root-prefix-glob:'foo'").unwrap(),
1117 @r#"Pattern(PrefixPath("foo"))"#);
1118 insta::assert_debug_snapshot!(
1119 parse("root-prefix-glob-i:'.'").unwrap(),
1120 @r#"Pattern(PrefixPath(""))"#);
1121
1122 insta::assert_debug_snapshot!(
1124 parse("root-prefix-glob:'*'").unwrap(), @r#"
1125 Pattern(
1126 PrefixGlob {
1127 dir: "",
1128 pattern: Glob {
1129 glob: "*",
1130 re: "(?-u)^[^/]*$",
1131 opts: _,
1132 tokens: _,
1133 },
1134 },
1135 )
1136 "#);
1137
1138 insta::assert_debug_snapshot!(
1140 parse("root-prefix-glob-i:'_/foo'").unwrap(), @r#"
1141 Pattern(
1142 PrefixGlob {
1143 dir: "_",
1144 pattern: Glob {
1145 glob: "foo",
1146 re: "(?-u)(?i)^foo$",
1147 opts: _,
1148 tokens: _,
1149 },
1150 },
1151 )
1152 "#);
1153 }
1154
1155 #[test]
1156 fn test_parse_function() {
1157 let settings = insta_settings();
1158 let _guard = settings.bind_to_scope();
1159 let path_converter = RepoPathUiConverter::Fs {
1160 cwd: PathBuf::from("/ws/cur"),
1161 base: PathBuf::from("/ws"),
1162 };
1163 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
1164
1165 insta::assert_debug_snapshot!(parse("all()").unwrap(), @"All");
1166 insta::assert_debug_snapshot!(parse("none()").unwrap(), @"None");
1167 insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
1168 InvalidArguments {
1169 name: "all",
1170 message: "Expected 0 arguments",
1171 }
1172 "#);
1173 insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
1174 NoSuchFunction {
1175 name: "ale",
1176 candidates: [
1177 "all",
1178 ],
1179 }
1180 "#);
1181 }
1182
1183 #[test]
1184 fn test_parse_compound_expression() {
1185 let settings = insta_settings();
1186 let _guard = settings.bind_to_scope();
1187 let path_converter = RepoPathUiConverter::Fs {
1188 cwd: PathBuf::from("/ws/cur"),
1189 base: PathBuf::from("/ws"),
1190 };
1191 let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &path_converter);
1192
1193 insta::assert_debug_snapshot!(parse("~x").unwrap(), @r#"
1194 Difference(
1195 All,
1196 Pattern(PrefixPath("cur/x")),
1197 )
1198 "#);
1199 insta::assert_debug_snapshot!(parse("x|y|root:z").unwrap(), @r#"
1200 UnionAll(
1201 [
1202 Pattern(PrefixPath("cur/x")),
1203 Pattern(PrefixPath("cur/y")),
1204 Pattern(PrefixPath("z")),
1205 ],
1206 )
1207 "#);
1208 insta::assert_debug_snapshot!(parse("x|y&z").unwrap(), @r#"
1209 UnionAll(
1210 [
1211 Pattern(PrefixPath("cur/x")),
1212 Intersection(
1213 Pattern(PrefixPath("cur/y")),
1214 Pattern(PrefixPath("cur/z")),
1215 ),
1216 ],
1217 )
1218 "#);
1219 }
1220
1221 #[test]
1222 fn test_explicit_paths() {
1223 let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
1224 expr.explicit_paths().map(|path| path.to_owned()).collect()
1225 };
1226 let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
1227 assert!(collect(&FilesetExpression::none()).is_empty());
1228 assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
1229 assert_eq!(
1230 collect(&FilesetExpression::union_all(vec![
1231 file_expr("a"),
1232 file_expr("b"),
1233 file_expr("c"),
1234 ])),
1235 ["a", "b", "c"].map(repo_path_buf)
1236 );
1237 assert_eq!(
1238 collect(&FilesetExpression::intersection(
1239 FilesetExpression::union_all(vec![
1240 file_expr("a"),
1241 FilesetExpression::none(),
1242 file_expr("b"),
1243 file_expr("c"),
1244 ]),
1245 FilesetExpression::difference(
1246 file_expr("d"),
1247 FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
1248 )
1249 )),
1250 ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
1251 );
1252 }
1253
1254 #[test]
1255 fn test_build_matcher_simple() {
1256 let settings = insta_settings();
1257 let _guard = settings.bind_to_scope();
1258
1259 insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
1260 insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
1261 insta::assert_debug_snapshot!(
1262 FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
1263 @r#"
1264 FilesMatcher {
1265 tree: Dir {
1266 "foo": File {},
1267 },
1268 }
1269 "#);
1270 insta::assert_debug_snapshot!(
1271 FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
1272 @r#"
1273 PrefixMatcher {
1274 tree: Dir {
1275 "foo": Prefix {},
1276 },
1277 }
1278 "#);
1279 }
1280
1281 #[test]
1282 fn test_build_matcher_glob_pattern() {
1283 let settings = insta_settings();
1284 let _guard = settings.bind_to_scope();
1285 let file_glob_expr = |dir: &str, pattern: &str| {
1286 FilesetExpression::pattern(FilePattern::FileGlob {
1287 dir: repo_path_buf(dir),
1288 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1289 })
1290 };
1291 let prefix_glob_expr = |dir: &str, pattern: &str| {
1292 FilesetExpression::pattern(FilePattern::PrefixGlob {
1293 dir: repo_path_buf(dir),
1294 pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1295 })
1296 };
1297
1298 insta::assert_debug_snapshot!(file_glob_expr("", "*").to_matcher(), @r#"
1299 GlobsMatcher {
1300 tree: Some(RegexSet(["(?-u)^[^/]*$"])) {},
1301 matches_prefix_paths: false,
1302 }
1303 "#);
1304
1305 let expr = FilesetExpression::union_all(vec![
1306 file_glob_expr("foo", "*"),
1307 file_glob_expr("foo/bar", "*"),
1308 file_glob_expr("foo", "?"),
1309 prefix_glob_expr("foo", "ba[rz]"),
1310 prefix_glob_expr("foo", "qu*x"),
1311 ]);
1312 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1313 UnionMatcher {
1314 input1: GlobsMatcher {
1315 tree: None {
1316 "foo": Some(RegexSet(["(?-u)^[^/]*$", "(?-u)^[^/]$"])) {
1317 "bar": Some(RegexSet(["(?-u)^[^/]*$"])) {},
1318 },
1319 },
1320 matches_prefix_paths: false,
1321 },
1322 input2: GlobsMatcher {
1323 tree: None {
1324 "foo": Some(RegexSet(["(?-u)^ba[rz](?:/|$)", "(?-u)^qu[^/]*x(?:/|$)"])) {},
1325 },
1326 matches_prefix_paths: true,
1327 },
1328 }
1329 "#);
1330 }
1331
1332 #[test]
1333 fn test_build_matcher_union_patterns_of_same_kind() {
1334 let settings = insta_settings();
1335 let _guard = settings.bind_to_scope();
1336
1337 let expr = FilesetExpression::union_all(vec![
1338 FilesetExpression::file_path(repo_path_buf("foo")),
1339 FilesetExpression::file_path(repo_path_buf("foo/bar")),
1340 ]);
1341 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1342 FilesMatcher {
1343 tree: Dir {
1344 "foo": File {
1345 "bar": File {},
1346 },
1347 },
1348 }
1349 "#);
1350
1351 let expr = FilesetExpression::union_all(vec![
1352 FilesetExpression::prefix_path(repo_path_buf("bar")),
1353 FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
1354 ]);
1355 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1356 PrefixMatcher {
1357 tree: Dir {
1358 "bar": Prefix {
1359 "baz": Prefix {},
1360 },
1361 },
1362 }
1363 "#);
1364 }
1365
1366 #[test]
1367 fn test_build_matcher_union_patterns_of_different_kind() {
1368 let settings = insta_settings();
1369 let _guard = settings.bind_to_scope();
1370
1371 let expr = FilesetExpression::union_all(vec![
1372 FilesetExpression::file_path(repo_path_buf("foo")),
1373 FilesetExpression::prefix_path(repo_path_buf("bar")),
1374 ]);
1375 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1376 UnionMatcher {
1377 input1: FilesMatcher {
1378 tree: Dir {
1379 "foo": File {},
1380 },
1381 },
1382 input2: PrefixMatcher {
1383 tree: Dir {
1384 "bar": Prefix {},
1385 },
1386 },
1387 }
1388 "#);
1389 }
1390
1391 #[test]
1392 fn test_build_matcher_unnormalized_union() {
1393 let settings = insta_settings();
1394 let _guard = settings.bind_to_scope();
1395
1396 let expr = FilesetExpression::UnionAll(vec![]);
1397 insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
1398
1399 let expr =
1400 FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
1401 insta::assert_debug_snapshot!(expr.to_matcher(), @r"
1402 UnionMatcher {
1403 input1: NothingMatcher,
1404 input2: EverythingMatcher,
1405 }
1406 ");
1407 }
1408
1409 #[test]
1410 fn test_build_matcher_combined() {
1411 let settings = insta_settings();
1412 let _guard = settings.bind_to_scope();
1413
1414 let expr = FilesetExpression::union_all(vec![
1415 FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1416 FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1417 FilesetExpression::file_path(repo_path_buf("foo")),
1418 FilesetExpression::prefix_path(repo_path_buf("bar")),
1419 ]);
1420 insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1421 UnionMatcher {
1422 input1: UnionMatcher {
1423 input1: IntersectionMatcher {
1424 input1: EverythingMatcher,
1425 input2: NothingMatcher,
1426 },
1427 input2: DifferenceMatcher {
1428 wanted: NothingMatcher,
1429 unwanted: EverythingMatcher,
1430 },
1431 },
1432 input2: UnionMatcher {
1433 input1: FilesMatcher {
1434 tree: Dir {
1435 "foo": File {},
1436 },
1437 },
1438 input2: PrefixMatcher {
1439 tree: Dir {
1440 "bar": Prefix {},
1441 },
1442 },
1443 },
1444 }
1445 "#);
1446 }
1447}