Skip to main content

jj_lib/
fileset.rs

1// Copyright 2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Functional language for selecting a set of paths.
16
17use 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/// Error occurred during file pattern parsing.
56#[derive(Debug, Error)]
57pub enum FilePatternParseError {
58    /// Unknown pattern kind is specified.
59    #[error("Invalid file pattern kind `{0}:`")]
60    InvalidKind(String),
61    /// Failed to parse input UI path.
62    #[error(transparent)]
63    UiPath(#[from] UiPathParseError),
64    /// Failed to parse input workspace-relative path.
65    #[error(transparent)]
66    RelativePath(#[from] RelativePathParseError),
67    /// Failed to parse glob pattern.
68    #[error(transparent)]
69    GlobPattern(#[from] globset::Error),
70}
71
72/// Basic pattern to match `RepoPath`.
73#[derive(Clone, Debug)]
74pub enum FilePattern {
75    /// Matches file (or exact) path.
76    FilePath(RepoPathBuf),
77    /// Matches path prefix.
78    PrefixPath(RepoPathBuf),
79    /// Matches file (or exact) path with glob pattern.
80    FileGlob {
81        /// Prefix directory path where the `pattern` will be evaluated.
82        dir: RepoPathBuf,
83        /// Glob pattern relative to `dir`.
84        pattern: Box<Glob>,
85    },
86    /// Matches path prefix with glob pattern.
87    PrefixGlob {
88        /// Prefix directory path where the `pattern` will be evaluated.
89        dir: RepoPathBuf,
90        /// Glob pattern relative to `dir`.
91        pattern: Box<Glob>,
92    },
93    // TODO: add more patterns:
94    // - FilesInPath: files in directory, non-recursively?
95    // - NameGlob or SuffixGlob: file name with glob?
96}
97
98impl FilePattern {
99    /// Parses the given `input` string as pattern of the specified `kind`.
100    pub fn from_str_kind(
101        path_converter: &RepoPathUiConverter,
102        input: &str,
103        kind: &str,
104    ) -> Result<Self, FilePatternParseError> {
105        // Naming convention:
106        // * path normalization
107        //   * cwd: cwd-relative path (default)
108        //   * root: workspace-relative path
109        // * where to anchor
110        //   * file: exact file path
111        //   * prefix: path prefix (files under directory recursively)
112        //   * files-in: files in directory non-recursively
113        //   * name: file name component (or suffix match?)
114        //   * substring: substring match?
115        // * string pattern syntax (+ case sensitivity?)
116        //   * path: literal path (default) (default anchor: prefix)
117        //   * glob: glob pattern (default anchor: file)
118        //   * regex?
119        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    /// Pattern that matches cwd-relative file (or exact) path.
137    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    /// Pattern that matches cwd-relative path prefix.
146    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    /// Pattern that matches cwd-relative file path glob.
155    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    /// Pattern that matches cwd-relative file path glob (case-insensitive).
165    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    /// Pattern that matches cwd-relative path prefix by glob.
175    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    /// Pattern that matches cwd-relative path prefix by glob
185    /// (case-insensitive).
186    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    /// Pattern that matches workspace-relative file (or exact) path.
196    pub fn root_file_path(input: impl AsRef<str>) -> Result<Self, FilePatternParseError> {
197        // TODO: Let caller pass in converter for root-relative paths too
198        let path = RepoPathBuf::from_relative_path(input.as_ref())?;
199        Ok(Self::FilePath(path))
200    }
201
202    /// Pattern that matches workspace-relative path prefix.
203    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    /// Pattern that matches workspace-relative file path glob.
209    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    /// Pattern that matches workspace-relative file path glob
216    /// (case-insensitive).
217    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    /// Pattern that matches workspace-relative path prefix by glob.
224    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    /// Pattern that matches workspace-relative path prefix by glob
231    /// (case-insensitive).
232    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        // Normalize separator to '/', reject ".." which will never match
247        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        // Normalize separator to '/', reject ".." which will never match
264        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    /// Returns path if this pattern represents a literal path in a workspace.
273    /// Returns `None` if this is a glob pattern for example.
274    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
290/// Checks if a character is a glob metacharacter.
291fn is_glob_char(c: char) -> bool {
292    // See globset::escape(). In addition to that, backslash is parsed as an
293    // escape sequence on Unix.
294    const GLOB_CHARS: &[char] = if cfg!(windows) {
295        &['?', '*', '[', ']', '{', '}']
296    } else {
297        &['?', '*', '[', ']', '{', '}', '\\']
298    };
299    GLOB_CHARS.contains(&c)
300}
301
302/// Splits `input` path into literal directory path and glob pattern.
303fn 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
312/// Splits `input` path into literal directory path and glob pattern, for
313/// case-insensitive patterns.
314fn 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/// AST-level representation of the fileset expression.
326#[derive(Clone, Debug)]
327pub enum FilesetExpression {
328    /// Matches nothing.
329    None,
330    /// Matches everything.
331    All,
332    /// Matches basic pattern.
333    Pattern(FilePattern),
334    /// Matches any of the expressions.
335    ///
336    /// Use `FilesetExpression::union_all()` to construct a union expression.
337    /// It will normalize 0-ary or 1-ary union.
338    UnionAll(Vec<Self>),
339    /// Matches both expressions.
340    Intersection(Box<Self>, Box<Self>),
341    /// Matches the first expression, but not the second expression.
342    Difference(Box<Self>, Box<Self>),
343}
344
345impl FilesetExpression {
346    /// Expression that matches nothing.
347    pub fn none() -> Self {
348        Self::None
349    }
350
351    /// Expression that matches everything.
352    pub fn all() -> Self {
353        Self::All
354    }
355
356    /// Expression that matches the given `pattern`.
357    pub fn pattern(pattern: FilePattern) -> Self {
358        Self::Pattern(pattern)
359    }
360
361    /// Expression that matches file (or exact) path.
362    pub fn file_path(path: RepoPathBuf) -> Self {
363        Self::Pattern(FilePattern::FilePath(path))
364    }
365
366    /// Expression that matches path prefix.
367    pub fn prefix_path(path: RepoPathBuf) -> Self {
368        Self::Pattern(FilePattern::PrefixPath(path))
369    }
370
371    /// Expression that matches any of the given `expressions`.
372    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    /// Expression that matches both `self` and `other`.
381    pub fn intersection(self, other: Self) -> Self {
382        Self::Intersection(Box::new(self), Box::new(other))
383    }
384
385    /// Expression that matches `self` but not `other`.
386    pub fn difference(self, other: Self) -> Self {
387        Self::Difference(Box::new(self), Box::new(other))
388    }
389
390    /// Flattens union expression at most one level.
391    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    /// Iterates literal paths recursively from this expression.
416    ///
417    /// For example, `"a", "b", "c"` will be yielded in that order for
418    /// expression `"a" | all() & "b" | ~"c"`.
419    pub fn explicit_paths(&self) -> impl Iterator<Item = &RepoPath> {
420        // pre/post-ordering doesn't matter so long as children are visited from
421        // left to right.
422        self.dfs_pre().filter_map(|expr| match expr {
423            Self::Pattern(pattern) => pattern.as_path(),
424            _ => None,
425        })
426    }
427
428    /// Transforms the expression tree to `Matcher` object.
429    pub fn to_matcher(&self) -> Box<dyn Matcher> {
430        build_union_matcher(self.as_union_all())
431    }
432}
433
434/// Transforms the union `expressions` to `Matcher` object.
435///
436/// Since `Matcher` typically accepts a set of patterns to be OR-ed, this
437/// function takes a list of union `expressions` as input.
438fn 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            // None and All are supposed to be simplified by caller.
447            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            // UnionAll is supposed to be flattened by caller.
459            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
489/// Concatenates all `matchers` as union.
490///
491/// Each matcher element must be wrapped in `Some` so the matchers can be moved
492/// in arbitrary order.
493fn 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            // Build balanced tree to minimize the recursion depth.
499            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    // Not using maplit::hashmap!{} or custom declarative macro here because
515    // code completion inside macro is quite restricted.
516    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/// Information needed to parse fileset expression.
601#[derive(Clone, Debug)]
602pub struct FilesetParseContext<'a> {
603    /// Aliases to be expanded.
604    pub aliases_map: &'a FilesetAliasesMap,
605    /// Context to resolve cwd-relative paths.
606    pub path_converter: &'a RepoPathUiConverter,
607}
608
609/// Parses text into `FilesetExpression` without bare string fallback.
610pub 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    // TODO: add basic tree substitution pass to eliminate redundant expressions
618    resolve_expression(diagnostics, context.path_converter, &node)
619}
620
621/// Parses text into `FilesetExpression` with bare string fallback.
622///
623/// If the text can't be parsed as a fileset expression, and if it doesn't
624/// contain any operator-like characters, it will be parsed as a file path.
625pub 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    // TODO: add basic tree substitution pass to eliminate redundant expressions
633    resolve_expression(diagnostics, context.path_converter, &node)
634}
635
636#[cfg(test)]
637mod tests {
638    use std::path::PathBuf;
639
640    use super::*;
641    use crate::tests::TestResult;
642
643    fn repo_path_buf(value: impl Into<String>) -> RepoPathBuf {
644        RepoPathBuf::from_internal_string(value).unwrap()
645    }
646
647    fn insta_settings() -> insta::Settings {
648        let mut settings = insta::Settings::clone_current();
649        // Elide parsed glob options and tokens, which aren't interesting.
650        settings.add_filter(
651            r"(?m)^(\s{12}opts):\s*GlobOptions\s*\{\n(\s{16}.*\n)*\s{12}\},",
652            "$1: _,",
653        );
654        settings.add_filter(
655            r"(?m)^(\s{12}tokens):\s*Tokens\(\n(\s{16}.*\n)*\s{12}\),",
656            "$1: _,",
657        );
658        // Collapse short "Thing(_,)" repeatedly to save vertical space and make
659        // the output more readable.
660        for _ in 0..4 {
661            settings.add_filter(
662                r"(?x)
663                \b([A-Z]\w*)\(\n
664                    \s*(.{1,60}),\n
665                \s*\)",
666                "$1($2)",
667            );
668        }
669        settings
670    }
671
672    #[test]
673    fn test_parse_file_pattern() -> TestResult {
674        let settings = insta_settings();
675        let _guard = settings.bind_to_scope();
676        let context = FilesetParseContext {
677            aliases_map: &FilesetAliasesMap::new(),
678            path_converter: &RepoPathUiConverter::Fs {
679                cwd: PathBuf::from("/ws/cur"),
680                base: PathBuf::from("/ws"),
681            },
682        };
683        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
684
685        // cwd-relative patterns
686        insta::assert_debug_snapshot!(
687            parse(".")?,
688            @r#"Pattern(PrefixPath("cur"))"#);
689        insta::assert_debug_snapshot!(
690            parse("..")?,
691            @r#"Pattern(PrefixPath(""))"#);
692        assert!(parse("../..").is_err());
693        insta::assert_debug_snapshot!(
694            parse("foo")?,
695            @r#"Pattern(PrefixPath("cur/foo"))"#);
696        insta::assert_debug_snapshot!(
697            parse("*.*")?,
698            @r#"
699        Pattern(
700            PrefixGlob {
701                dir: "cur",
702                pattern: Glob {
703                    glob: "*.*",
704                    re: "(?-u)^[^/]*\\.[^/]*$",
705                    opts: _,
706                    tokens: _,
707                },
708            },
709        )
710        "#);
711        insta::assert_debug_snapshot!(
712            parse("cwd:.")?,
713            @r#"Pattern(PrefixPath("cur"))"#);
714        insta::assert_debug_snapshot!(
715            parse("cwd-file:foo")?,
716            @r#"Pattern(FilePath("cur/foo"))"#);
717        insta::assert_debug_snapshot!(
718            parse("file:../foo/bar")?,
719            @r#"Pattern(FilePath("foo/bar"))"#);
720
721        // workspace-relative patterns
722        insta::assert_debug_snapshot!(
723            parse("root:.")?,
724            @r#"Pattern(PrefixPath(""))"#);
725        assert!(parse("root:..").is_err());
726        insta::assert_debug_snapshot!(
727            parse("root:foo/bar")?,
728            @r#"Pattern(PrefixPath("foo/bar"))"#);
729        insta::assert_debug_snapshot!(
730            parse("root-file:bar")?,
731            @r#"Pattern(FilePath("bar"))"#);
732
733        insta::assert_debug_snapshot!(
734            parse("file:(foo|bar)").unwrap_err().kind(),
735            @r#"Expression("Expected string")"#);
736        Ok(())
737    }
738
739    #[test]
740    fn test_parse_glob_pattern() -> TestResult {
741        let settings = insta_settings();
742        let _guard = settings.bind_to_scope();
743        let context = FilesetParseContext {
744            aliases_map: &FilesetAliasesMap::new(),
745            path_converter: &RepoPathUiConverter::Fs {
746                // meta character in cwd path shouldn't be expanded
747                cwd: PathBuf::from("/ws/cur*"),
748                base: PathBuf::from("/ws"),
749            },
750        };
751        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
752
753        // cwd-relative, without meta characters
754        insta::assert_debug_snapshot!(
755            parse(r#"cwd-glob:"foo""#)?,
756            @r#"Pattern(FilePath("cur*/foo"))"#);
757        // Strictly speaking, glob:"" shouldn't match a file named <cwd>, but
758        // file pattern doesn't distinguish "foo/" from "foo".
759        insta::assert_debug_snapshot!(
760            parse(r#"glob:"""#)?,
761            @r#"Pattern(FilePath("cur*"))"#);
762        insta::assert_debug_snapshot!(
763            parse(r#"glob:".""#)?,
764            @r#"Pattern(FilePath("cur*"))"#);
765        insta::assert_debug_snapshot!(
766            parse(r#"glob:"..""#)?,
767            @r#"Pattern(FilePath(""))"#);
768
769        // cwd-relative, with meta characters
770        insta::assert_debug_snapshot!(
771            parse(r#"glob:"*""#)?, @r#"
772        Pattern(
773            FileGlob {
774                dir: "cur*",
775                pattern: Glob {
776                    glob: "*",
777                    re: "(?-u)^[^/]*$",
778                    opts: _,
779                    tokens: _,
780                },
781            },
782        )
783        "#);
784        insta::assert_debug_snapshot!(
785            parse(r#"glob:"./*""#)?, @r#"
786        Pattern(
787            FileGlob {
788                dir: "cur*",
789                pattern: Glob {
790                    glob: "*",
791                    re: "(?-u)^[^/]*$",
792                    opts: _,
793                    tokens: _,
794                },
795            },
796        )
797        "#);
798        insta::assert_debug_snapshot!(
799            parse(r#"glob:"../*""#)?, @r#"
800        Pattern(
801            FileGlob {
802                dir: "",
803                pattern: Glob {
804                    glob: "*",
805                    re: "(?-u)^[^/]*$",
806                    opts: _,
807                    tokens: _,
808                },
809            },
810        )
811        "#);
812        // glob:"**" is equivalent to root-glob:"<cwd>/**", not root-glob:"**"
813        insta::assert_debug_snapshot!(
814            parse(r#"glob:"**""#)?, @r#"
815        Pattern(
816            FileGlob {
817                dir: "cur*",
818                pattern: Glob {
819                    glob: "**",
820                    re: "(?-u)^.*$",
821                    opts: _,
822                    tokens: _,
823                },
824            },
825        )
826        "#);
827        insta::assert_debug_snapshot!(
828            parse(r#"glob:"../foo/b?r/baz""#)?, @r#"
829        Pattern(
830            FileGlob {
831                dir: "foo",
832                pattern: Glob {
833                    glob: "b?r/baz",
834                    re: "(?-u)^b[^/]r/baz$",
835                    opts: _,
836                    tokens: _,
837                },
838            },
839        )
840        "#);
841        assert!(parse(r#"glob:"../../*""#).is_err());
842        assert!(parse(r#"glob-i:"../../*""#).is_err());
843        assert!(parse(r#"glob:"/*""#).is_err());
844        assert!(parse(r#"glob-i:"/*""#).is_err());
845        // no support for relative path component after glob meta character
846        assert!(parse(r#"glob:"*/..""#).is_err());
847        assert!(parse(r#"glob-i:"*/..""#).is_err());
848
849        if cfg!(windows) {
850            // cwd-relative, with Windows path separators
851            insta::assert_debug_snapshot!(
852                parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
853            Pattern(
854                FileGlob {
855                    dir: "foo",
856                    pattern: Glob {
857                        glob: "*/bar",
858                        re: "(?-u)^[^/]*/bar$",
859                        opts: _,
860                        tokens: _,
861                    },
862                },
863            )
864            "#);
865        } else {
866            // backslash is an escape character on Unix
867            insta::assert_debug_snapshot!(
868                parse(r#"glob:"..\\foo\\*\\bar""#)?, @r#"
869            Pattern(
870                FileGlob {
871                    dir: "cur*",
872                    pattern: Glob {
873                        glob: "..\\foo\\*\\bar",
874                        re: "(?-u)^\\.\\.foo\\*bar$",
875                        opts: _,
876                        tokens: _,
877                    },
878                },
879            )
880            "#);
881        }
882
883        // workspace-relative, without meta characters
884        insta::assert_debug_snapshot!(
885            parse(r#"root-glob:"foo""#)?,
886            @r#"Pattern(FilePath("foo"))"#);
887        insta::assert_debug_snapshot!(
888            parse(r#"root-glob:"""#)?,
889            @r#"Pattern(FilePath(""))"#);
890        insta::assert_debug_snapshot!(
891            parse(r#"root-glob:".""#)?,
892            @r#"Pattern(FilePath(""))"#);
893
894        // workspace-relative, with meta characters
895        insta::assert_debug_snapshot!(
896            parse(r#"root-glob:"*""#)?, @r#"
897        Pattern(
898            FileGlob {
899                dir: "",
900                pattern: Glob {
901                    glob: "*",
902                    re: "(?-u)^[^/]*$",
903                    opts: _,
904                    tokens: _,
905                },
906            },
907        )
908        "#);
909        insta::assert_debug_snapshot!(
910            parse(r#"root-glob:"foo/bar/b[az]""#)?, @r#"
911        Pattern(
912            FileGlob {
913                dir: "foo/bar",
914                pattern: Glob {
915                    glob: "b[az]",
916                    re: "(?-u)^b[az]$",
917                    opts: _,
918                    tokens: _,
919                },
920            },
921        )
922        "#);
923        insta::assert_debug_snapshot!(
924            parse(r#"root-glob:"foo/bar/b{ar,az}""#)?, @r#"
925        Pattern(
926            FileGlob {
927                dir: "foo/bar",
928                pattern: Glob {
929                    glob: "b{ar,az}",
930                    re: "(?-u)^b(?:ar|az)$",
931                    opts: _,
932                    tokens: _,
933                },
934            },
935        )
936        "#);
937        assert!(parse(r#"root-glob:"../*""#).is_err());
938        assert!(parse(r#"root-glob-i:"../*""#).is_err());
939        assert!(parse(r#"root-glob:"/*""#).is_err());
940        assert!(parse(r#"root-glob-i:"/*""#).is_err());
941
942        // workspace-relative, backslash escape without meta characters
943        if cfg!(not(windows)) {
944            insta::assert_debug_snapshot!(
945                parse(r#"root-glob:'foo/bar\baz'"#)?, @r#"
946            Pattern(
947                FileGlob {
948                    dir: "foo",
949                    pattern: Glob {
950                        glob: "bar\\baz",
951                        re: "(?-u)^barbaz$",
952                        opts: _,
953                        tokens: _,
954                    },
955                },
956            )
957            "#);
958        }
959        Ok(())
960    }
961
962    #[test]
963    fn test_parse_glob_pattern_case_insensitive() -> TestResult {
964        let settings = insta_settings();
965        let _guard = settings.bind_to_scope();
966        let context = FilesetParseContext {
967            aliases_map: &FilesetAliasesMap::new(),
968            path_converter: &RepoPathUiConverter::Fs {
969                cwd: PathBuf::from("/ws/cur"),
970                base: PathBuf::from("/ws"),
971            },
972        };
973        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
974
975        // cwd-relative case-insensitive glob
976        insta::assert_debug_snapshot!(
977            parse(r#"glob-i:"*.TXT""#)?, @r#"
978        Pattern(
979            FileGlob {
980                dir: "cur",
981                pattern: Glob {
982                    glob: "*.TXT",
983                    re: "(?-u)(?i)^[^/]*\\.TXT$",
984                    opts: _,
985                    tokens: _,
986                },
987            },
988        )
989        "#);
990
991        // cwd-relative case-insensitive glob with more specific pattern
992        insta::assert_debug_snapshot!(
993            parse(r#"cwd-glob-i:"[Ff]oo""#)?, @r#"
994        Pattern(
995            FileGlob {
996                dir: "cur",
997                pattern: Glob {
998                    glob: "[Ff]oo",
999                    re: "(?-u)(?i)^[Ff]oo$",
1000                    opts: _,
1001                    tokens: _,
1002                },
1003            },
1004        )
1005        "#);
1006
1007        // workspace-relative case-insensitive glob
1008        insta::assert_debug_snapshot!(
1009            parse(r#"root-glob-i:"*.Rs""#)?, @r#"
1010        Pattern(
1011            FileGlob {
1012                dir: "",
1013                pattern: Glob {
1014                    glob: "*.Rs",
1015                    re: "(?-u)(?i)^[^/]*\\.Rs$",
1016                    opts: _,
1017                    tokens: _,
1018                },
1019            },
1020        )
1021        "#);
1022
1023        // case-insensitive pattern with directory component (should not split the path)
1024        insta::assert_debug_snapshot!(
1025            parse(r#"glob-i:"SubDir/*.rs""#)?, @r#"
1026        Pattern(
1027            FileGlob {
1028                dir: "cur",
1029                pattern: Glob {
1030                    glob: "SubDir/*.rs",
1031                    re: "(?-u)(?i)^SubDir/[^/]*\\.rs$",
1032                    opts: _,
1033                    tokens: _,
1034                },
1035            },
1036        )
1037        "#);
1038
1039        // case-sensitive pattern with directory component (should split the path)
1040        insta::assert_debug_snapshot!(
1041            parse(r#"glob:"SubDir/*.rs""#)?, @r#"
1042        Pattern(
1043            FileGlob {
1044                dir: "cur/SubDir",
1045                pattern: Glob {
1046                    glob: "*.rs",
1047                    re: "(?-u)^[^/]*\\.rs$",
1048                    opts: _,
1049                    tokens: _,
1050                },
1051            },
1052        )
1053        "#);
1054
1055        // case-insensitive pattern with leading dots (should split dots but not dirs)
1056        insta::assert_debug_snapshot!(
1057            parse(r#"glob-i:"../SomeDir/*.rs""#)?, @r#"
1058        Pattern(
1059            FileGlob {
1060                dir: "",
1061                pattern: Glob {
1062                    glob: "SomeDir/*.rs",
1063                    re: "(?-u)(?i)^SomeDir/[^/]*\\.rs$",
1064                    opts: _,
1065                    tokens: _,
1066                },
1067            },
1068        )
1069        "#);
1070
1071        // case-insensitive pattern with single leading dot
1072        insta::assert_debug_snapshot!(
1073            parse(r#"glob-i:"./SomeFile*.txt""#)?, @r#"
1074        Pattern(
1075            FileGlob {
1076                dir: "cur",
1077                pattern: Glob {
1078                    glob: "SomeFile*.txt",
1079                    re: "(?-u)(?i)^SomeFile[^/]*\\.txt$",
1080                    opts: _,
1081                    tokens: _,
1082                },
1083            },
1084        )
1085        "#);
1086        Ok(())
1087    }
1088
1089    #[test]
1090    fn test_parse_prefix_glob_pattern() -> TestResult {
1091        let settings = insta_settings();
1092        let _guard = settings.bind_to_scope();
1093        let context = FilesetParseContext {
1094            aliases_map: &FilesetAliasesMap::new(),
1095            path_converter: &RepoPathUiConverter::Fs {
1096                // meta character in cwd path shouldn't be expanded
1097                cwd: PathBuf::from("/ws/cur*"),
1098                base: PathBuf::from("/ws"),
1099            },
1100        };
1101        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1102
1103        // cwd-relative, without meta/case-insensitive characters
1104        insta::assert_debug_snapshot!(
1105            parse("cwd-prefix-glob:'foo'")?,
1106            @r#"Pattern(PrefixPath("cur*/foo"))"#);
1107        insta::assert_debug_snapshot!(
1108            parse("prefix-glob:'.'")?,
1109            @r#"Pattern(PrefixPath("cur*"))"#);
1110        insta::assert_debug_snapshot!(
1111            parse("cwd-prefix-glob-i:'..'")?,
1112            @r#"Pattern(PrefixPath(""))"#);
1113        insta::assert_debug_snapshot!(
1114            parse("prefix-glob-i:'../_'")?,
1115            @r#"Pattern(PrefixPath("_"))"#);
1116
1117        // cwd-relative, with meta characters
1118        insta::assert_debug_snapshot!(
1119            parse("cwd-prefix-glob:'*'")?, @r#"
1120        Pattern(
1121            PrefixGlob {
1122                dir: "cur*",
1123                pattern: Glob {
1124                    glob: "*",
1125                    re: "(?-u)^[^/]*$",
1126                    opts: _,
1127                    tokens: _,
1128                },
1129            },
1130        )
1131        "#);
1132
1133        // cwd-relative, with case-insensitive characters
1134        insta::assert_debug_snapshot!(
1135            parse("cwd-prefix-glob-i:'../foo'")?, @r#"
1136        Pattern(
1137            PrefixGlob {
1138                dir: "",
1139                pattern: Glob {
1140                    glob: "foo",
1141                    re: "(?-u)(?i)^foo$",
1142                    opts: _,
1143                    tokens: _,
1144                },
1145            },
1146        )
1147        "#);
1148
1149        // workspace-relative, without meta/case-insensitive characters
1150        insta::assert_debug_snapshot!(
1151            parse("root-prefix-glob:'foo'")?,
1152            @r#"Pattern(PrefixPath("foo"))"#);
1153        insta::assert_debug_snapshot!(
1154            parse("root-prefix-glob-i:'.'")?,
1155            @r#"Pattern(PrefixPath(""))"#);
1156
1157        // workspace-relative, with meta characters
1158        insta::assert_debug_snapshot!(
1159            parse("root-prefix-glob:'*'")?, @r#"
1160        Pattern(
1161            PrefixGlob {
1162                dir: "",
1163                pattern: Glob {
1164                    glob: "*",
1165                    re: "(?-u)^[^/]*$",
1166                    opts: _,
1167                    tokens: _,
1168                },
1169            },
1170        )
1171        "#);
1172
1173        // workspace-relative, with case-insensitive characters
1174        insta::assert_debug_snapshot!(
1175            parse("root-prefix-glob-i:'_/foo'")?, @r#"
1176        Pattern(
1177            PrefixGlob {
1178                dir: "_",
1179                pattern: Glob {
1180                    glob: "foo",
1181                    re: "(?-u)(?i)^foo$",
1182                    opts: _,
1183                    tokens: _,
1184                },
1185            },
1186        )
1187        "#);
1188        Ok(())
1189    }
1190
1191    #[test]
1192    fn test_parse_function() -> TestResult {
1193        let settings = insta_settings();
1194        let _guard = settings.bind_to_scope();
1195        let context = FilesetParseContext {
1196            aliases_map: &FilesetAliasesMap::new(),
1197            path_converter: &RepoPathUiConverter::Fs {
1198                cwd: PathBuf::from("/ws/cur"),
1199                base: PathBuf::from("/ws"),
1200            },
1201        };
1202        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1203
1204        insta::assert_debug_snapshot!(parse("all()")?, @"All");
1205        insta::assert_debug_snapshot!(parse("none()")?, @"None");
1206        insta::assert_debug_snapshot!(parse("all(x)").unwrap_err().kind(), @r#"
1207        InvalidArguments {
1208            name: "all",
1209            message: "Expected 0 arguments",
1210        }
1211        "#);
1212        insta::assert_debug_snapshot!(parse("ale()").unwrap_err().kind(), @r#"
1213        NoSuchFunction {
1214            name: "ale",
1215            candidates: [
1216                "all",
1217            ],
1218        }
1219        "#);
1220        Ok(())
1221    }
1222
1223    #[test]
1224    fn test_parse_compound_expression() -> TestResult {
1225        let settings = insta_settings();
1226        let _guard = settings.bind_to_scope();
1227        let context = FilesetParseContext {
1228            aliases_map: &FilesetAliasesMap::new(),
1229            path_converter: &RepoPathUiConverter::Fs {
1230                cwd: PathBuf::from("/ws/cur"),
1231                base: PathBuf::from("/ws"),
1232            },
1233        };
1234        let parse = |text| parse_maybe_bare(&mut FilesetDiagnostics::new(), text, &context);
1235
1236        insta::assert_debug_snapshot!(parse("~x")?, @r#"
1237        Difference(
1238            All,
1239            Pattern(PrefixPath("cur/x")),
1240        )
1241        "#);
1242        insta::assert_debug_snapshot!(parse("x|y|root:z")?, @r#"
1243        UnionAll(
1244            [
1245                Pattern(PrefixPath("cur/x")),
1246                Pattern(PrefixPath("cur/y")),
1247                Pattern(PrefixPath("z")),
1248            ],
1249        )
1250        "#);
1251        insta::assert_debug_snapshot!(parse("x|y&z")?, @r#"
1252        UnionAll(
1253            [
1254                Pattern(PrefixPath("cur/x")),
1255                Intersection(
1256                    Pattern(PrefixPath("cur/y")),
1257                    Pattern(PrefixPath("cur/z")),
1258                ),
1259            ],
1260        )
1261        "#);
1262        Ok(())
1263    }
1264
1265    #[test]
1266    fn test_explicit_paths() {
1267        let collect = |expr: &FilesetExpression| -> Vec<RepoPathBuf> {
1268            expr.explicit_paths().map(|path| path.to_owned()).collect()
1269        };
1270        let file_expr = |path: &str| FilesetExpression::file_path(repo_path_buf(path));
1271        assert!(collect(&FilesetExpression::none()).is_empty());
1272        assert_eq!(collect(&file_expr("a")), ["a"].map(repo_path_buf));
1273        assert_eq!(
1274            collect(&FilesetExpression::union_all(vec![
1275                file_expr("a"),
1276                file_expr("b"),
1277                file_expr("c"),
1278            ])),
1279            ["a", "b", "c"].map(repo_path_buf)
1280        );
1281        assert_eq!(
1282            collect(&FilesetExpression::intersection(
1283                FilesetExpression::union_all(vec![
1284                    file_expr("a"),
1285                    FilesetExpression::none(),
1286                    file_expr("b"),
1287                    file_expr("c"),
1288                ]),
1289                FilesetExpression::difference(
1290                    file_expr("d"),
1291                    FilesetExpression::union_all(vec![file_expr("e"), file_expr("f")])
1292                )
1293            )),
1294            ["a", "b", "c", "d", "e", "f"].map(repo_path_buf)
1295        );
1296    }
1297
1298    #[test]
1299    fn test_build_matcher_simple() {
1300        let settings = insta_settings();
1301        let _guard = settings.bind_to_scope();
1302
1303        insta::assert_debug_snapshot!(FilesetExpression::none().to_matcher(), @"NothingMatcher");
1304        insta::assert_debug_snapshot!(FilesetExpression::all().to_matcher(), @"EverythingMatcher");
1305        insta::assert_debug_snapshot!(
1306            FilesetExpression::file_path(repo_path_buf("foo")).to_matcher(),
1307            @r#"
1308        FilesMatcher {
1309            tree: Dir {
1310                "foo": File {},
1311            },
1312        }
1313        "#);
1314        insta::assert_debug_snapshot!(
1315            FilesetExpression::prefix_path(repo_path_buf("foo")).to_matcher(),
1316            @r#"
1317        PrefixMatcher {
1318            tree: Dir {
1319                "foo": Prefix {},
1320            },
1321        }
1322        "#);
1323    }
1324
1325    #[test]
1326    fn test_build_matcher_glob_pattern() {
1327        let settings = insta_settings();
1328        let _guard = settings.bind_to_scope();
1329        let file_glob_expr = |dir: &str, pattern: &str| {
1330            FilesetExpression::pattern(FilePattern::FileGlob {
1331                dir: repo_path_buf(dir),
1332                pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1333            })
1334        };
1335        let prefix_glob_expr = |dir: &str, pattern: &str| {
1336            FilesetExpression::pattern(FilePattern::PrefixGlob {
1337                dir: repo_path_buf(dir),
1338                pattern: Box::new(parse_file_glob(pattern, false).unwrap()),
1339            })
1340        };
1341
1342        insta::assert_debug_snapshot!(file_glob_expr("", "*").to_matcher(), @r#"
1343        GlobsMatcher {
1344            tree: Some(RegexSet(["(?-u)^[^/]*$"])) {},
1345            matches_prefix_paths: false,
1346        }
1347        "#);
1348
1349        let expr = FilesetExpression::union_all(vec![
1350            file_glob_expr("foo", "*"),
1351            file_glob_expr("foo/bar", "*"),
1352            file_glob_expr("foo", "?"),
1353            prefix_glob_expr("foo", "ba[rz]"),
1354            prefix_glob_expr("foo", "qu*x"),
1355        ]);
1356        insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1357        UnionMatcher {
1358            input1: GlobsMatcher {
1359                tree: None {
1360                    "foo": Some(RegexSet(["(?-u)^[^/]*$", "(?-u)^[^/]$"])) {
1361                        "bar": Some(RegexSet(["(?-u)^[^/]*$"])) {},
1362                    },
1363                },
1364                matches_prefix_paths: false,
1365            },
1366            input2: GlobsMatcher {
1367                tree: None {
1368                    "foo": Some(RegexSet(["(?-u)^ba[rz](?:/|$)", "(?-u)^qu[^/]*x(?:/|$)"])) {},
1369                },
1370                matches_prefix_paths: true,
1371            },
1372        }
1373        "#);
1374    }
1375
1376    #[test]
1377    fn test_build_matcher_union_patterns_of_same_kind() {
1378        let settings = insta_settings();
1379        let _guard = settings.bind_to_scope();
1380
1381        let expr = FilesetExpression::union_all(vec![
1382            FilesetExpression::file_path(repo_path_buf("foo")),
1383            FilesetExpression::file_path(repo_path_buf("foo/bar")),
1384        ]);
1385        insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1386        FilesMatcher {
1387            tree: Dir {
1388                "foo": File {
1389                    "bar": File {},
1390                },
1391            },
1392        }
1393        "#);
1394
1395        let expr = FilesetExpression::union_all(vec![
1396            FilesetExpression::prefix_path(repo_path_buf("bar")),
1397            FilesetExpression::prefix_path(repo_path_buf("bar/baz")),
1398        ]);
1399        insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1400        PrefixMatcher {
1401            tree: Dir {
1402                "bar": Prefix {
1403                    "baz": Prefix {},
1404                },
1405            },
1406        }
1407        "#);
1408    }
1409
1410    #[test]
1411    fn test_build_matcher_union_patterns_of_different_kind() {
1412        let settings = insta_settings();
1413        let _guard = settings.bind_to_scope();
1414
1415        let expr = FilesetExpression::union_all(vec![
1416            FilesetExpression::file_path(repo_path_buf("foo")),
1417            FilesetExpression::prefix_path(repo_path_buf("bar")),
1418        ]);
1419        insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1420        UnionMatcher {
1421            input1: FilesMatcher {
1422                tree: Dir {
1423                    "foo": File {},
1424                },
1425            },
1426            input2: PrefixMatcher {
1427                tree: Dir {
1428                    "bar": Prefix {},
1429                },
1430            },
1431        }
1432        "#);
1433    }
1434
1435    #[test]
1436    fn test_build_matcher_unnormalized_union() {
1437        let settings = insta_settings();
1438        let _guard = settings.bind_to_scope();
1439
1440        let expr = FilesetExpression::UnionAll(vec![]);
1441        insta::assert_debug_snapshot!(expr.to_matcher(), @"NothingMatcher");
1442
1443        let expr =
1444            FilesetExpression::UnionAll(vec![FilesetExpression::None, FilesetExpression::All]);
1445        insta::assert_debug_snapshot!(expr.to_matcher(), @"
1446        UnionMatcher {
1447            input1: NothingMatcher,
1448            input2: EverythingMatcher,
1449        }
1450        ");
1451    }
1452
1453    #[test]
1454    fn test_build_matcher_combined() {
1455        let settings = insta_settings();
1456        let _guard = settings.bind_to_scope();
1457
1458        let expr = FilesetExpression::union_all(vec![
1459            FilesetExpression::intersection(FilesetExpression::all(), FilesetExpression::none()),
1460            FilesetExpression::difference(FilesetExpression::none(), FilesetExpression::all()),
1461            FilesetExpression::file_path(repo_path_buf("foo")),
1462            FilesetExpression::prefix_path(repo_path_buf("bar")),
1463        ]);
1464        insta::assert_debug_snapshot!(expr.to_matcher(), @r#"
1465        UnionMatcher {
1466            input1: UnionMatcher {
1467                input1: IntersectionMatcher {
1468                    input1: EverythingMatcher,
1469                    input2: NothingMatcher,
1470                },
1471                input2: DifferenceMatcher {
1472                    wanted: NothingMatcher,
1473                    unwanted: EverythingMatcher,
1474                },
1475            },
1476            input2: UnionMatcher {
1477                input1: FilesMatcher {
1478                    tree: Dir {
1479                        "foo": File {},
1480                    },
1481                },
1482                input2: PrefixMatcher {
1483                    tree: Dir {
1484                        "bar": Prefix {},
1485                    },
1486                },
1487            },
1488        }
1489        "#);
1490    }
1491}