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