deno_config/glob/
mod.rs

1// Copyright 2018-2025 the Deno authors. MIT license.
2
3use std::borrow::Cow;
4use std::path::Path;
5use std::path::PathBuf;
6
7use deno_error::JsError;
8use deno_path_util::normalize_path;
9use deno_path_util::url_to_file_path;
10use indexmap::IndexMap;
11use thiserror::Error;
12use url::Url;
13
14use crate::UrlToFilePathError;
15
16mod collector;
17mod gitignore;
18
19pub use collector::FileCollector;
20pub use collector::WalkEntry;
21
22#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23pub enum FilePatternsMatch {
24  /// File passes as matching, but further exclude matching (ex. .gitignore)
25  /// may be necessary.
26  Passed,
27  /// File passes matching and further exclude matching (ex. .gitignore)
28  /// should NOT be done.
29  PassedOptedOutExclude,
30  /// File was excluded.
31  Excluded,
32}
33
34#[derive(Debug, Copy, Clone, PartialEq, Eq)]
35pub enum PathKind {
36  File,
37  Directory,
38}
39
40#[derive(Clone, Debug, Eq, Hash, PartialEq)]
41pub struct FilePatterns {
42  /// Default traversal base used when calling `split_by_base()` without
43  /// any `include` patterns.
44  pub base: PathBuf,
45  pub include: Option<PathOrPatternSet>,
46  pub exclude: PathOrPatternSet,
47}
48
49impl FilePatterns {
50  pub fn new_with_base(base: PathBuf) -> Self {
51    Self {
52      base,
53      include: Default::default(),
54      exclude: Default::default(),
55    }
56  }
57
58  pub fn with_new_base(self, new_base: PathBuf) -> Self {
59    Self {
60      base: new_base,
61      ..self
62    }
63  }
64
65  pub fn matches_specifier(&self, specifier: &Url) -> bool {
66    self.matches_specifier_detail(specifier) != FilePatternsMatch::Excluded
67  }
68
69  pub fn matches_specifier_detail(&self, specifier: &Url) -> FilePatternsMatch {
70    if specifier.scheme() != "file" {
71      // can't do .gitignore on a non-file specifier
72      return FilePatternsMatch::PassedOptedOutExclude;
73    }
74    let path = match url_to_file_path(specifier) {
75      Ok(path) => path,
76      Err(_) => return FilePatternsMatch::PassedOptedOutExclude,
77    };
78    self.matches_path_detail(&path, PathKind::File) // use file matching behavior
79  }
80
81  pub fn matches_path(&self, path: &Path, path_kind: PathKind) -> bool {
82    self.matches_path_detail(path, path_kind) != FilePatternsMatch::Excluded
83  }
84
85  pub fn matches_path_detail(
86    &self,
87    path: &Path,
88    path_kind: PathKind,
89  ) -> FilePatternsMatch {
90    // if there's an include list, only include files that match it
91    // the include list is a closed set
92    if let Some(include) = &self.include {
93      match path_kind {
94        PathKind::File => {
95          if include.matches_path_detail(path) != PathOrPatternsMatch::Matched {
96            return FilePatternsMatch::Excluded;
97          }
98        }
99        PathKind::Directory => {
100          // for now ignore the include list unless there's a negated
101          // glob for the directory
102          for p in include.0.iter().rev() {
103            match p.matches_path(path) {
104              PathGlobMatch::Matched => {
105                break;
106              }
107              PathGlobMatch::MatchedNegated => {
108                return FilePatternsMatch::Excluded;
109              }
110              PathGlobMatch::NotMatched => {
111                // keep going
112              }
113            }
114          }
115        }
116      }
117    }
118
119    // the exclude list is an open set and we skip files not in the exclude list
120    match self.exclude.matches_path_detail(path) {
121      PathOrPatternsMatch::Matched => FilePatternsMatch::Excluded,
122      PathOrPatternsMatch::NotMatched => FilePatternsMatch::Passed,
123      PathOrPatternsMatch::Excluded => FilePatternsMatch::PassedOptedOutExclude,
124    }
125  }
126
127  /// Creates a collection of `FilePatterns` where the containing patterns
128  /// are only the ones applicable to the base.
129  ///
130  /// The order these are returned in is the order that the directory traversal
131  /// should occur in.
132  pub fn split_by_base(&self) -> Vec<Self> {
133    let negated_excludes = self
134      .exclude
135      .0
136      .iter()
137      .filter(|e| e.is_negated())
138      .collect::<Vec<_>>();
139    let include = match &self.include {
140      Some(include) => Cow::Borrowed(include),
141      None => {
142        if negated_excludes.is_empty() {
143          return vec![self.clone()];
144        } else {
145          Cow::Owned(PathOrPatternSet::new(vec![PathOrPattern::Path(
146            self.base.clone(),
147          )]))
148        }
149      }
150    };
151
152    let mut include_paths = Vec::with_capacity(include.0.len());
153    let mut include_patterns = Vec::with_capacity(include.0.len());
154    let mut exclude_patterns =
155      Vec::with_capacity(include.0.len() + self.exclude.0.len());
156
157    for path_or_pattern in &include.0 {
158      match path_or_pattern {
159        PathOrPattern::Path(path) => include_paths.push(path),
160        PathOrPattern::NegatedPath(path) => {
161          exclude_patterns.push(PathOrPattern::Path(path.clone()));
162        }
163        PathOrPattern::Pattern(pattern) => {
164          if pattern.is_negated() {
165            exclude_patterns.push(PathOrPattern::Pattern(pattern.as_negated()));
166          } else {
167            include_patterns.push(pattern.clone());
168          }
169        }
170        PathOrPattern::RemoteUrl(_) => {}
171      }
172    }
173
174    let capacity = include_patterns.len() + negated_excludes.len();
175    let mut include_patterns_by_base_path = include_patterns.into_iter().fold(
176      IndexMap::with_capacity(capacity),
177      |mut map: IndexMap<_, Vec<_>>, p| {
178        map.entry(p.base_path()).or_default().push(p);
179        map
180      },
181    );
182    for p in &negated_excludes {
183      if let Some(base_path) = p.base_path()
184        && !include_patterns_by_base_path.contains_key(&base_path)
185      {
186        let has_any_base_parent = include_patterns_by_base_path
187          .keys()
188          .any(|k| base_path.starts_with(k))
189          || include_paths.iter().any(|p| base_path.starts_with(p));
190        // don't include an orphaned negated pattern
191        if has_any_base_parent {
192          include_patterns_by_base_path.insert(base_path, Vec::new());
193        }
194      }
195    }
196
197    let exclude_by_base_path = exclude_patterns
198      .iter()
199      .chain(self.exclude.0.iter())
200      .filter_map(|s| Some((s.base_path()?, s)))
201      .collect::<Vec<_>>();
202    let get_applicable_excludes = |base_path: &PathBuf| -> Vec<PathOrPattern> {
203      exclude_by_base_path
204        .iter()
205        .filter_map(|(exclude_base_path, exclude)| {
206          match exclude {
207            PathOrPattern::RemoteUrl(_) => None,
208            PathOrPattern::Path(exclude_path)
209            | PathOrPattern::NegatedPath(exclude_path) => {
210              // include paths that's are sub paths or an ancestor path
211              if base_path.starts_with(exclude_path)
212                || exclude_path.starts_with(base_path)
213              {
214                Some((*exclude).clone())
215              } else {
216                None
217              }
218            }
219            PathOrPattern::Pattern(_) => {
220              // include globs that's are sub paths or an ancestor path
221              if exclude_base_path.starts_with(base_path)
222                || base_path.starts_with(exclude_base_path)
223              {
224                Some((*exclude).clone())
225              } else {
226                None
227              }
228            }
229          }
230        })
231        .collect::<Vec<_>>()
232    };
233
234    let mut result = Vec::with_capacity(
235      include_paths.len() + include_patterns_by_base_path.len(),
236    );
237    for path in include_paths {
238      let applicable_excludes = get_applicable_excludes(path);
239      result.push(Self {
240        base: path.clone(),
241        include: if self.include.is_none() {
242          None
243        } else {
244          Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
245            path.clone(),
246          )]))
247        },
248        exclude: PathOrPatternSet::new(applicable_excludes),
249      });
250    }
251
252    // todo(dsherret): This could be further optimized by not including
253    // patterns that will only ever match another base.
254    for base_path in include_patterns_by_base_path.keys() {
255      let applicable_excludes = get_applicable_excludes(base_path);
256      let mut applicable_includes = Vec::new();
257      // get all patterns that apply to the current or ancestor directories
258      for path in base_path.ancestors() {
259        if let Some(patterns) = include_patterns_by_base_path.get(path) {
260          applicable_includes.extend(
261            patterns
262              .iter()
263              .map(|p| PathOrPattern::Pattern((*p).clone())),
264          );
265        }
266      }
267      result.push(Self {
268        base: base_path.clone(),
269        include: if self.include.is_none()
270          || applicable_includes.is_empty()
271            && self
272              .include
273              .as_ref()
274              .map(|i| !i.0.is_empty())
275              .unwrap_or(false)
276        {
277          None
278        } else {
279          Some(PathOrPatternSet::new(applicable_includes))
280        },
281        exclude: PathOrPatternSet::new(applicable_excludes),
282      });
283    }
284
285    // Sort by the longest base path first. This ensures that we visit opted into
286    // nested directories first before visiting the parent directory. The directory
287    // traverser will handle not going into directories it's already been in.
288    result.sort_by(|a, b| {
289      // try looking at the parents first so that files in the same
290      // folder are kept in the same order that they're provided
291      let (a, b) =
292        if let (Some(a), Some(b)) = (a.base.parent(), b.base.parent()) {
293          (a, b)
294        } else {
295          (a.base.as_path(), b.base.as_path())
296        };
297      b.as_os_str().len().cmp(&a.as_os_str().len())
298    });
299
300    result
301  }
302}
303
304#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
305pub enum PathOrPatternsMatch {
306  Matched,
307  NotMatched,
308  Excluded,
309}
310
311#[derive(Debug, Error, JsError)]
312pub enum FromExcludeRelativePathOrPatternsError {
313  #[class(type)]
314  #[error(
315    "The negation of '{negated_entry}' is never reached due to the higher priority '{entry}' exclude. Move '{negated_entry}' after '{entry}'."
316  )]
317  HigherPriorityExclude {
318    negated_entry: String,
319    entry: String,
320  },
321  #[class(inherit)]
322  #[error("{0}")]
323  PathOrPatternParse(#[from] PathOrPatternParseError),
324}
325
326#[derive(Clone, Default, Debug, Hash, Eq, PartialEq)]
327pub struct PathOrPatternSet(Vec<PathOrPattern>);
328
329impl PathOrPatternSet {
330  pub fn new(elements: Vec<PathOrPattern>) -> Self {
331    Self(elements)
332  }
333
334  pub fn from_absolute_paths(
335    paths: &[String],
336  ) -> Result<Self, PathOrPatternParseError> {
337    Ok(Self(
338      paths
339        .iter()
340        .map(|p| PathOrPattern::new(p))
341        .collect::<Result<Vec<_>, _>>()?,
342    ))
343  }
344
345  /// Builds the set of path and patterns for an "include" list.
346  pub fn from_include_relative_path_or_patterns(
347    base: &Path,
348    entries: &[String],
349  ) -> Result<Self, PathOrPatternParseError> {
350    Ok(Self(
351      entries
352        .iter()
353        .map(|p| PathOrPattern::from_relative(base, p))
354        .collect::<Result<Vec<_>, _>>()?,
355    ))
356  }
357
358  /// Builds the set and ensures no negations are overruled by
359  /// higher priority entries.
360  pub fn from_exclude_relative_path_or_patterns(
361    base: &Path,
362    entries: &[String],
363  ) -> Result<Self, FromExcludeRelativePathOrPatternsError> {
364    // error when someone does something like:
365    // exclude: ["!./a/b", "./a"] as it should be the opposite
366    fn validate_entry(
367      found_negated_paths: &Vec<(&str, PathBuf)>,
368      entry: &str,
369      entry_path: &Path,
370    ) -> Result<(), FromExcludeRelativePathOrPatternsError> {
371      for (negated_entry, negated_path) in found_negated_paths {
372        if negated_path.starts_with(entry_path) {
373          return Err(
374            FromExcludeRelativePathOrPatternsError::HigherPriorityExclude {
375              negated_entry: negated_entry.to_string(),
376              entry: entry.to_string(),
377            },
378          );
379        }
380      }
381      Ok(())
382    }
383
384    let mut found_negated_paths: Vec<(&str, PathBuf)> =
385      Vec::with_capacity(entries.len());
386    let mut result = Vec::with_capacity(entries.len());
387    for entry in entries {
388      let p = PathOrPattern::from_relative(base, entry)?;
389      match &p {
390        PathOrPattern::Path(p) => {
391          validate_entry(&found_negated_paths, entry, p)?;
392        }
393        PathOrPattern::NegatedPath(p) => {
394          found_negated_paths.push((entry.as_str(), p.clone()));
395        }
396        PathOrPattern::RemoteUrl(_) => {
397          // ignore
398        }
399        PathOrPattern::Pattern(p) => {
400          if p.is_negated() {
401            let base_path = p.base_path();
402            found_negated_paths.push((entry.as_str(), base_path));
403          }
404        }
405      }
406      result.push(p);
407    }
408    Ok(Self(result))
409  }
410
411  pub fn inner(&self) -> &Vec<PathOrPattern> {
412    &self.0
413  }
414
415  pub fn inner_mut(&mut self) -> &mut Vec<PathOrPattern> {
416    &mut self.0
417  }
418
419  pub fn into_path_or_patterns(self) -> Vec<PathOrPattern> {
420    self.0
421  }
422
423  pub fn matches_path(&self, path: &Path) -> bool {
424    self.matches_path_detail(path) == PathOrPatternsMatch::Matched
425  }
426
427  pub fn matches_path_detail(&self, path: &Path) -> PathOrPatternsMatch {
428    for p in self.0.iter().rev() {
429      match p.matches_path(path) {
430        PathGlobMatch::Matched => return PathOrPatternsMatch::Matched,
431        PathGlobMatch::MatchedNegated => return PathOrPatternsMatch::Excluded,
432        PathGlobMatch::NotMatched => {
433          // ignore
434        }
435      }
436    }
437    PathOrPatternsMatch::NotMatched
438  }
439
440  pub fn base_paths(&self) -> Vec<PathBuf> {
441    let mut result = Vec::with_capacity(self.0.len());
442    for element in &self.0 {
443      match element {
444        PathOrPattern::Path(path) | PathOrPattern::NegatedPath(path) => {
445          result.push(path.to_path_buf());
446        }
447        PathOrPattern::RemoteUrl(_) => {
448          // ignore
449        }
450        PathOrPattern::Pattern(pattern) => {
451          result.push(pattern.base_path());
452        }
453      }
454    }
455    result
456  }
457
458  pub fn push(&mut self, item: PathOrPattern) {
459    self.0.push(item);
460  }
461
462  pub fn append(&mut self, items: impl Iterator<Item = PathOrPattern>) {
463    self.0.extend(items)
464  }
465}
466
467#[derive(Debug, Error, JsError, Clone)]
468#[class(inherit)]
469#[error("Invalid URL '{}'", url)]
470pub struct UrlParseError {
471  url: String,
472  #[source]
473  #[inherit]
474  source: url::ParseError,
475}
476
477#[derive(Debug, Error, JsError)]
478pub enum PathOrPatternParseError {
479  #[class(inherit)]
480  #[error(transparent)]
481  UrlParse(#[from] UrlParseError),
482  #[class(inherit)]
483  #[error(transparent)]
484  UrlToFilePathError(#[from] UrlToFilePathError),
485  #[class(inherit)]
486  #[error(transparent)]
487  GlobParse(#[from] GlobPatternParseError),
488}
489
490#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
491pub enum PathOrPattern {
492  Path(PathBuf),
493  NegatedPath(PathBuf),
494  RemoteUrl(Url),
495  Pattern(GlobPattern),
496}
497
498impl PathOrPattern {
499  pub fn new(path: &str) -> Result<Self, PathOrPatternParseError> {
500    if has_url_prefix(path) {
501      let url = Url::parse(path).map_err(|err| UrlParseError {
502        url: path.to_string(),
503        source: err,
504      })?;
505      if url.scheme() == "file" {
506        let path = url_to_file_path(&url)?;
507        return Ok(Self::Path(path));
508      } else {
509        return Ok(Self::RemoteUrl(url));
510      }
511    }
512
513    GlobPattern::new_if_pattern(path)
514      .map(|maybe_pattern| {
515        maybe_pattern
516          .map(PathOrPattern::Pattern)
517          .unwrap_or_else(|| {
518            PathOrPattern::Path(
519              normalize_path(Cow::Borrowed(Path::new(path))).into_owned(),
520            )
521          })
522      })
523      .map_err(|err| err.into())
524  }
525
526  pub fn from_relative(
527    base: &Path,
528    p: &str,
529  ) -> Result<PathOrPattern, PathOrPatternParseError> {
530    if is_glob_pattern(p) {
531      GlobPattern::from_relative(base, p)
532        .map(PathOrPattern::Pattern)
533        .map_err(|err| err.into())
534    } else if has_url_prefix(p) {
535      PathOrPattern::new(p)
536    } else if let Some(path) = p.strip_prefix('!') {
537      Ok(PathOrPattern::NegatedPath(
538        normalize_path(Cow::Owned(base.join(path))).into_owned(),
539      ))
540    } else {
541      Ok(PathOrPattern::Path(
542        normalize_path(Cow::Owned(base.join(p))).into_owned(),
543      ))
544    }
545  }
546
547  pub fn matches_path(&self, path: &Path) -> PathGlobMatch {
548    match self {
549      PathOrPattern::Path(p) => {
550        if path.starts_with(p) {
551          PathGlobMatch::Matched
552        } else {
553          PathGlobMatch::NotMatched
554        }
555      }
556      PathOrPattern::NegatedPath(p) => {
557        if path.starts_with(p) {
558          PathGlobMatch::MatchedNegated
559        } else {
560          PathGlobMatch::NotMatched
561        }
562      }
563      PathOrPattern::RemoteUrl(_) => PathGlobMatch::NotMatched,
564      PathOrPattern::Pattern(p) => p.matches_path(path),
565    }
566  }
567
568  /// Returns the base path of the pattern if it's not a remote url pattern.
569  pub fn base_path(&self) -> Option<PathBuf> {
570    match self {
571      PathOrPattern::Path(p) | PathOrPattern::NegatedPath(p) => Some(p.clone()),
572      PathOrPattern::RemoteUrl(_) => None,
573      PathOrPattern::Pattern(p) => Some(p.base_path()),
574    }
575  }
576
577  /// If this is a negated pattern.
578  pub fn is_negated(&self) -> bool {
579    match self {
580      PathOrPattern::Path(_) => false,
581      PathOrPattern::NegatedPath(_) => true,
582      PathOrPattern::RemoteUrl(_) => false,
583      PathOrPattern::Pattern(p) => p.is_negated(),
584    }
585  }
586}
587
588#[derive(Debug, Clone, Copy, PartialEq, Eq)]
589pub enum PathGlobMatch {
590  Matched,
591  MatchedNegated,
592  NotMatched,
593}
594
595#[derive(Debug, Error, JsError)]
596#[class(type)]
597#[error("Failed to expand glob: \"{pattern}\"")]
598pub struct GlobPatternParseError {
599  pattern: String,
600  #[source]
601  source: glob::PatternError,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
605pub struct GlobPattern {
606  is_negated: bool,
607  pattern: glob::Pattern,
608}
609
610impl GlobPattern {
611  pub fn new_if_pattern(
612    pattern: &str,
613  ) -> Result<Option<Self>, GlobPatternParseError> {
614    if !is_glob_pattern(pattern) {
615      return Ok(None);
616    }
617    Self::new(pattern).map(Some)
618  }
619
620  pub fn new(pattern: &str) -> Result<Self, GlobPatternParseError> {
621    let (is_negated, pattern) = match pattern.strip_prefix('!') {
622      Some(pattern) => (true, pattern),
623      None => (false, pattern),
624    };
625    let pattern = escape_brackets(pattern).replace('\\', "/");
626    let pattern =
627      glob::Pattern::new(&pattern).map_err(|source| GlobPatternParseError {
628        pattern: pattern.to_string(),
629        source,
630      })?;
631    Ok(Self {
632      is_negated,
633      pattern,
634    })
635  }
636
637  pub fn from_relative(
638    base: &Path,
639    p: &str,
640  ) -> Result<Self, GlobPatternParseError> {
641    let (is_negated, p) = match p.strip_prefix('!') {
642      Some(p) => (true, p),
643      None => (false, p),
644    };
645    let base_str = base.to_string_lossy().replace('\\', "/");
646    let p = p.strip_prefix("./").unwrap_or(p);
647    let p = p.strip_suffix('/').unwrap_or(p);
648    let pattern = capacity_builder::StringBuilder::<String>::build(|builder| {
649      if is_negated {
650        builder.append('!');
651      }
652      builder.append(&base_str);
653      if !base_str.ends_with('/') {
654        builder.append('/');
655      }
656      builder.append(p);
657    })
658    .unwrap();
659    GlobPattern::new(&pattern)
660  }
661
662  pub fn as_str(&self) -> Cow<'_, str> {
663    if self.is_negated {
664      Cow::Owned(format!("!{}", self.pattern.as_str()))
665    } else {
666      Cow::Borrowed(self.pattern.as_str())
667    }
668  }
669
670  pub fn matches_path(&self, path: &Path) -> PathGlobMatch {
671    if self.pattern.matches_path_with(path, match_options()) {
672      if self.is_negated {
673        PathGlobMatch::MatchedNegated
674      } else {
675        PathGlobMatch::Matched
676      }
677    } else {
678      PathGlobMatch::NotMatched
679    }
680  }
681
682  pub fn base_path(&self) -> PathBuf {
683    let base_path = self
684      .pattern
685      .as_str()
686      .split('/')
687      .take_while(|c| !has_glob_chars(c))
688      .collect::<Vec<_>>()
689      .join(std::path::MAIN_SEPARATOR_STR);
690    PathBuf::from(base_path)
691  }
692
693  pub fn is_negated(&self) -> bool {
694    self.is_negated
695  }
696
697  fn as_negated(&self) -> GlobPattern {
698    Self {
699      is_negated: !self.is_negated,
700      pattern: self.pattern.clone(),
701    }
702  }
703}
704
705pub fn is_glob_pattern(path: &str) -> bool {
706  !has_url_prefix(path) && has_glob_chars(path)
707}
708
709fn has_url_prefix(pattern: &str) -> bool {
710  pattern.starts_with("http://")
711    || pattern.starts_with("https://")
712    || pattern.starts_with("file://")
713    || pattern.starts_with("npm:")
714    || pattern.starts_with("jsr:")
715}
716
717fn has_glob_chars(pattern: &str) -> bool {
718  // we don't support [ and ]
719  pattern.chars().any(|c| matches!(c, '*' | '?'))
720}
721
722fn escape_brackets(pattern: &str) -> String {
723  // Escape brackets - we currently don't support them, because with introduction
724  // of glob expansion paths like "pages/[id].ts" would suddenly start giving
725  // wrong results. We might want to revisit that in the future.
726  pattern.replace('[', "[[]").replace(']', "[]]")
727}
728
729fn match_options() -> glob::MatchOptions {
730  // Matches what `deno_task_shell` does
731  glob::MatchOptions {
732    // false because it should work the same way on case insensitive file systems
733    case_sensitive: false,
734    // true because it copies what sh does
735    require_literal_separator: true,
736    // true because it copies with sh does—these files are considered "hidden"
737    require_literal_leading_dot: true,
738  }
739}
740
741#[cfg(test)]
742mod test {
743  use std::error::Error;
744
745  use deno_path_util::url_from_directory_path;
746  use pretty_assertions::assert_eq;
747  use tempfile::TempDir;
748
749  use super::*;
750
751  // For easier comparisons in tests.
752  #[derive(Debug, PartialEq, Eq)]
753  struct ComparableFilePatterns {
754    base: String,
755    include: Option<Vec<String>>,
756    exclude: Vec<String>,
757  }
758
759  impl ComparableFilePatterns {
760    pub fn new(root: &Path, file_patterns: &FilePatterns) -> Self {
761      fn path_to_string(root: &Path, path: &Path) -> String {
762        path
763          .strip_prefix(root)
764          .unwrap()
765          .to_string_lossy()
766          .replace('\\', "/")
767      }
768
769      fn path_or_pattern_to_string(
770        root: &Path,
771        p: &PathOrPattern,
772      ) -> Option<String> {
773        match p {
774          PathOrPattern::RemoteUrl(_) => None,
775          PathOrPattern::Path(p) => Some(path_to_string(root, p)),
776          PathOrPattern::NegatedPath(p) => {
777            Some(format!("!{}", path_to_string(root, p)))
778          }
779          PathOrPattern::Pattern(p) => {
780            let was_negated = p.is_negated();
781            let p = if was_negated {
782              p.as_negated()
783            } else {
784              p.clone()
785            };
786            let text = p
787              .as_str()
788              .strip_prefix(&format!(
789                "{}/",
790                root.to_string_lossy().replace('\\', "/")
791              ))
792              .unwrap_or_else(|| panic!("pattern: {:?}, root: {:?}", p, root))
793              .to_string();
794            Some(if was_negated {
795              format!("!{}", text)
796            } else {
797              text
798            })
799          }
800        }
801      }
802
803      Self {
804        base: path_to_string(root, &file_patterns.base),
805        include: file_patterns.include.as_ref().map(|p| {
806          p.0
807            .iter()
808            .filter_map(|p| path_or_pattern_to_string(root, p))
809            .collect()
810        }),
811        exclude: file_patterns
812          .exclude
813          .0
814          .iter()
815          .filter_map(|p| path_or_pattern_to_string(root, p))
816          .collect(),
817      }
818    }
819
820    pub fn from_split(
821      root: &Path,
822      patterns_by_base: &[FilePatterns],
823    ) -> Vec<ComparableFilePatterns> {
824      patterns_by_base
825        .iter()
826        .map(|file_patterns| ComparableFilePatterns::new(root, file_patterns))
827        .collect()
828    }
829  }
830
831  #[test]
832  fn file_patterns_split_by_base_dir() {
833    let temp_dir = TempDir::new().unwrap();
834    let patterns = FilePatterns {
835      base: temp_dir.path().to_path_buf(),
836      include: Some(PathOrPatternSet::new(vec![
837        PathOrPattern::Pattern(
838          GlobPattern::new(&format!(
839            "{}/inner/**/*.ts",
840            temp_dir.path().to_string_lossy().replace('\\', "/")
841          ))
842          .unwrap(),
843        ),
844        PathOrPattern::Pattern(
845          GlobPattern::new(&format!(
846            "{}/inner/sub/deeper/**/*.js",
847            temp_dir.path().to_string_lossy().replace('\\', "/")
848          ))
849          .unwrap(),
850        ),
851        PathOrPattern::Pattern(
852          GlobPattern::new(&format!(
853            "{}/other/**/*.js",
854            temp_dir.path().to_string_lossy().replace('\\', "/")
855          ))
856          .unwrap(),
857        ),
858        PathOrPattern::from_relative(temp_dir.path(), "!./other/**/*.ts")
859          .unwrap(),
860        PathOrPattern::from_relative(temp_dir.path(), "sub/file.ts").unwrap(),
861      ])),
862      exclude: PathOrPatternSet::new(vec![
863        PathOrPattern::Pattern(
864          GlobPattern::new(&format!(
865            "{}/inner/other/**/*.ts",
866            temp_dir.path().to_string_lossy().replace('\\', "/")
867          ))
868          .unwrap(),
869        ),
870        PathOrPattern::Path(
871          temp_dir
872            .path()
873            .join("inner/sub/deeper/file.js")
874            .to_path_buf(),
875        ),
876      ]),
877    };
878    let split = ComparableFilePatterns::from_split(
879      temp_dir.path(),
880      &patterns.split_by_base(),
881    );
882    assert_eq!(
883      split,
884      vec![
885        ComparableFilePatterns {
886          base: "inner/sub/deeper".to_string(),
887          include: Some(vec![
888            "inner/sub/deeper/**/*.js".to_string(),
889            "inner/**/*.ts".to_string(),
890          ]),
891          exclude: vec!["inner/sub/deeper/file.js".to_string()],
892        },
893        ComparableFilePatterns {
894          base: "sub/file.ts".to_string(),
895          include: Some(vec!["sub/file.ts".to_string()]),
896          exclude: vec![],
897        },
898        ComparableFilePatterns {
899          base: "inner".to_string(),
900          include: Some(vec!["inner/**/*.ts".to_string()]),
901          exclude: vec![
902            "inner/other/**/*.ts".to_string(),
903            "inner/sub/deeper/file.js".to_string(),
904          ],
905        },
906        ComparableFilePatterns {
907          base: "other".to_string(),
908          include: Some(vec!["other/**/*.js".to_string()]),
909          exclude: vec!["other/**/*.ts".to_string()],
910        }
911      ]
912    );
913  }
914
915  #[test]
916  fn file_patterns_split_by_base_dir_unexcluded() {
917    let temp_dir = TempDir::new().unwrap();
918    let patterns = FilePatterns {
919      base: temp_dir.path().to_path_buf(),
920      include: None,
921      exclude: PathOrPatternSet::new(vec![
922        PathOrPattern::from_relative(temp_dir.path(), "./ignored").unwrap(),
923        PathOrPattern::from_relative(temp_dir.path(), "!./ignored/unexcluded")
924          .unwrap(),
925        PathOrPattern::from_relative(temp_dir.path(), "!./ignored/test/**")
926          .unwrap(),
927      ]),
928    };
929    let split = ComparableFilePatterns::from_split(
930      temp_dir.path(),
931      &patterns.split_by_base(),
932    );
933    assert_eq!(
934      split,
935      vec![
936        ComparableFilePatterns {
937          base: "ignored/unexcluded".to_string(),
938          include: None,
939          exclude: vec![
940            // still keeps the higher level exclude for cases
941            // where these two are accidentally swapped
942            "ignored".to_string(),
943            // keep the glob for the current dir because it
944            // could be used to override the .gitignore
945            "!ignored/unexcluded".to_string(),
946          ],
947        },
948        ComparableFilePatterns {
949          base: "ignored/test".to_string(),
950          include: None,
951          exclude: vec!["ignored".to_string(), "!ignored/test/**".to_string(),],
952        },
953        ComparableFilePatterns {
954          base: "".to_string(),
955          include: None,
956          exclude: vec![
957            "ignored".to_string(),
958            "!ignored/unexcluded".to_string(),
959            "!ignored/test/**".to_string(),
960          ],
961        },
962      ]
963    );
964  }
965
966  #[test]
967  fn file_patterns_split_by_base_dir_unexcluded_with_path_includes() {
968    let temp_dir = TempDir::new().unwrap();
969    let patterns = FilePatterns {
970      base: temp_dir.path().to_path_buf(),
971      include: Some(PathOrPatternSet::new(vec![
972        PathOrPattern::from_relative(temp_dir.path(), "./sub").unwrap(),
973      ])),
974      exclude: PathOrPatternSet::new(vec![
975        PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
976        PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**")
977          .unwrap(),
978        PathOrPattern::from_relative(temp_dir.path(), "./orphan").unwrap(),
979        PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**")
980          .unwrap(),
981      ]),
982    };
983    let split = ComparableFilePatterns::from_split(
984      temp_dir.path(),
985      &patterns.split_by_base(),
986    );
987    assert_eq!(
988      split,
989      vec![
990        ComparableFilePatterns {
991          base: "sub/ignored/test".to_string(),
992          include: None,
993          exclude: vec![
994            "sub/ignored".to_string(),
995            "!sub/ignored/test/**".to_string(),
996          ],
997        },
998        ComparableFilePatterns {
999          base: "sub".to_string(),
1000          include: Some(vec!["sub".to_string()]),
1001          exclude: vec![
1002            "sub/ignored".to_string(),
1003            "!sub/ignored/test/**".to_string(),
1004          ],
1005        },
1006      ]
1007    );
1008  }
1009
1010  #[test]
1011  fn file_patterns_split_by_base_dir_unexcluded_with_glob_includes() {
1012    let temp_dir = TempDir::new().unwrap();
1013    let patterns = FilePatterns {
1014      base: temp_dir.path().to_path_buf(),
1015      include: Some(PathOrPatternSet::new(vec![
1016        PathOrPattern::from_relative(temp_dir.path(), "./sub/**").unwrap(),
1017      ])),
1018      exclude: PathOrPatternSet::new(vec![
1019        PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
1020        PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/**")
1021          .unwrap(),
1022        PathOrPattern::from_relative(temp_dir.path(), "!./orphan/test/**")
1023          .unwrap(),
1024        PathOrPattern::from_relative(temp_dir.path(), "!orphan/other").unwrap(),
1025      ]),
1026    };
1027    let split = ComparableFilePatterns::from_split(
1028      temp_dir.path(),
1029      &patterns.split_by_base(),
1030    );
1031    assert_eq!(
1032      split,
1033      vec![
1034        ComparableFilePatterns {
1035          base: "sub/ignored/test".to_string(),
1036          include: Some(vec!["sub/**".to_string()]),
1037          exclude: vec![
1038            "sub/ignored".to_string(),
1039            "!sub/ignored/test/**".to_string()
1040          ],
1041        },
1042        ComparableFilePatterns {
1043          base: "sub".to_string(),
1044          include: Some(vec!["sub/**".to_string()]),
1045          exclude: vec![
1046            "sub/ignored".to_string(),
1047            "!sub/ignored/test/**".to_string(),
1048          ],
1049        }
1050      ]
1051    );
1052  }
1053
1054  #[test]
1055  fn file_patterns_split_by_base_dir_opposite_exclude() {
1056    let temp_dir = TempDir::new().unwrap();
1057    let patterns = FilePatterns {
1058      base: temp_dir.path().to_path_buf(),
1059      include: None,
1060      // this will actually error before it gets here in integration,
1061      // but it's best to ensure it's handled anyway
1062      exclude: PathOrPatternSet::new(vec![
1063        // this won't be unexcluded because it's lower priority than the entry below
1064        PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/")
1065          .unwrap(),
1066        // this is higher priority
1067        PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
1068      ]),
1069    };
1070    let split = ComparableFilePatterns::from_split(
1071      temp_dir.path(),
1072      &patterns.split_by_base(),
1073    );
1074    assert_eq!(
1075      split,
1076      vec![
1077        ComparableFilePatterns {
1078          base: "sub/ignored/test".to_string(),
1079          include: None,
1080          exclude: vec![
1081            "!sub/ignored/test".to_string(),
1082            "sub/ignored".to_string(),
1083          ],
1084        },
1085        ComparableFilePatterns {
1086          base: "".to_string(),
1087          include: None,
1088          exclude: vec![
1089            "!sub/ignored/test".to_string(),
1090            "sub/ignored".to_string(),
1091          ],
1092        },
1093      ]
1094    );
1095  }
1096
1097  #[test]
1098  fn file_patterns_split_by_base_dir_exclude_unexcluded_and_glob() {
1099    let temp_dir = TempDir::new().unwrap();
1100    let patterns = FilePatterns {
1101      base: temp_dir.path().to_path_buf(),
1102      include: None,
1103      exclude: PathOrPatternSet::new(vec![
1104        PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored").unwrap(),
1105        PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/")
1106          .unwrap(),
1107        PathOrPattern::from_relative(temp_dir.path(), "./sub/ignored/**/*.ts")
1108          .unwrap(),
1109      ]),
1110    };
1111    let split = ComparableFilePatterns::from_split(
1112      temp_dir.path(),
1113      &patterns.split_by_base(),
1114    );
1115    assert_eq!(
1116      split,
1117      vec![
1118        ComparableFilePatterns {
1119          base: "sub/ignored/test".to_string(),
1120          include: None,
1121          exclude: vec![
1122            "sub/ignored".to_string(),
1123            "!sub/ignored/test".to_string(),
1124            "sub/ignored/**/*.ts".to_string()
1125          ],
1126        },
1127        ComparableFilePatterns {
1128          base: "".to_string(),
1129          include: None,
1130          exclude: vec![
1131            "sub/ignored".to_string(),
1132            "!sub/ignored/test".to_string(),
1133            "sub/ignored/**/*.ts".to_string(),
1134          ],
1135        },
1136      ]
1137    );
1138  }
1139
1140  #[track_caller]
1141  fn run_file_patterns_match_test(
1142    file_patterns: &FilePatterns,
1143    path: &Path,
1144    kind: PathKind,
1145    expected: FilePatternsMatch,
1146  ) {
1147    assert_eq!(
1148      file_patterns.matches_path_detail(path, kind),
1149      expected,
1150      "path: {:?}, kind: {:?}",
1151      path,
1152      kind
1153    );
1154    assert_eq!(
1155      file_patterns.matches_path(path, kind),
1156      match expected {
1157        FilePatternsMatch::Passed
1158        | FilePatternsMatch::PassedOptedOutExclude => true,
1159        FilePatternsMatch::Excluded => false,
1160      }
1161    )
1162  }
1163
1164  #[test]
1165  fn file_patterns_include() {
1166    let cwd = current_dir();
1167    // include is a closed set
1168    let file_patterns = FilePatterns {
1169      base: cwd.clone(),
1170      include: Some(PathOrPatternSet(vec![
1171        PathOrPattern::from_relative(&cwd, "target").unwrap(),
1172        PathOrPattern::from_relative(&cwd, "other/**/*.ts").unwrap(),
1173      ])),
1174      exclude: PathOrPatternSet(vec![]),
1175    };
1176    let run_test =
1177      |path: &Path, kind: PathKind, expected: FilePatternsMatch| {
1178        run_file_patterns_match_test(&file_patterns, path, kind, expected);
1179      };
1180    run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed);
1181    run_test(
1182      &cwd.join("other"),
1183      PathKind::Directory,
1184      FilePatternsMatch::Passed,
1185    );
1186    run_test(
1187      &cwd.join("other/sub_dir"),
1188      PathKind::Directory,
1189      FilePatternsMatch::Passed,
1190    );
1191    run_test(
1192      &cwd.join("not_matched"),
1193      PathKind::File,
1194      FilePatternsMatch::Excluded,
1195    );
1196    run_test(
1197      &cwd.join("other/test.ts"),
1198      PathKind::File,
1199      FilePatternsMatch::Passed,
1200    );
1201    run_test(
1202      &cwd.join("other/test.js"),
1203      PathKind::File,
1204      FilePatternsMatch::Excluded,
1205    );
1206  }
1207
1208  #[test]
1209  fn file_patterns_exclude() {
1210    let cwd = current_dir();
1211    let file_patterns = FilePatterns {
1212      base: cwd.clone(),
1213      include: None,
1214      exclude: PathOrPatternSet(vec![
1215        PathOrPattern::from_relative(&cwd, "target").unwrap(),
1216        PathOrPattern::from_relative(&cwd, "!not_excluded").unwrap(),
1217        // lower items take priority
1218        PathOrPattern::from_relative(&cwd, "excluded_then_not_excluded")
1219          .unwrap(),
1220        PathOrPattern::from_relative(&cwd, "!excluded_then_not_excluded")
1221          .unwrap(),
1222        PathOrPattern::from_relative(&cwd, "!not_excluded_then_excluded")
1223          .unwrap(),
1224        PathOrPattern::from_relative(&cwd, "not_excluded_then_excluded")
1225          .unwrap(),
1226      ]),
1227    };
1228    let run_test =
1229      |path: &Path, kind: PathKind, expected: FilePatternsMatch| {
1230        run_file_patterns_match_test(&file_patterns, path, kind, expected);
1231      };
1232    run_test(&cwd, PathKind::Directory, FilePatternsMatch::Passed);
1233    run_test(
1234      &cwd.join("target"),
1235      PathKind::File,
1236      FilePatternsMatch::Excluded,
1237    );
1238    run_test(
1239      &cwd.join("not_excluded"),
1240      PathKind::File,
1241      FilePatternsMatch::PassedOptedOutExclude,
1242    );
1243    run_test(
1244      &cwd.join("excluded_then_not_excluded"),
1245      PathKind::File,
1246      FilePatternsMatch::PassedOptedOutExclude,
1247    );
1248    run_test(
1249      &cwd.join("not_excluded_then_excluded"),
1250      PathKind::File,
1251      FilePatternsMatch::Excluded,
1252    );
1253  }
1254
1255  #[test]
1256  fn file_patterns_include_exclude() {
1257    let cwd = current_dir();
1258    let file_patterns = FilePatterns {
1259      base: cwd.clone(),
1260      include: Some(PathOrPatternSet(vec![
1261        PathOrPattern::from_relative(&cwd, "other").unwrap(),
1262        PathOrPattern::from_relative(&cwd, "target").unwrap(),
1263        PathOrPattern::from_relative(&cwd, "**/*.js").unwrap(),
1264        PathOrPattern::from_relative(&cwd, "**/file.ts").unwrap(),
1265      ])),
1266      exclude: PathOrPatternSet(vec![
1267        PathOrPattern::from_relative(&cwd, "target").unwrap(),
1268        PathOrPattern::from_relative(&cwd, "!target/unexcluded/").unwrap(),
1269        PathOrPattern::from_relative(&cwd, "!target/other/**").unwrap(),
1270        PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap(),
1271        PathOrPattern::from_relative(&cwd, "!**/file.ts").unwrap(),
1272      ]),
1273    };
1274    let run_test =
1275      |path: &Path, kind: PathKind, expected: FilePatternsMatch| {
1276        run_file_patterns_match_test(&file_patterns, path, kind, expected);
1277      };
1278    // matches other
1279    run_test(
1280      &cwd.join("other/test.txt"),
1281      PathKind::File,
1282      FilePatternsMatch::Passed,
1283    );
1284    // matches **/*.js
1285    run_test(
1286      &cwd.join("sub_dir/test.js"),
1287      PathKind::File,
1288      FilePatternsMatch::Passed,
1289    );
1290    // not in include set
1291    run_test(
1292      &cwd.join("sub_dir/test.txt"),
1293      PathKind::File,
1294      FilePatternsMatch::Excluded,
1295    );
1296    // .ts extension not matched
1297    run_test(
1298      &cwd.join("other/test.ts"),
1299      PathKind::File,
1300      FilePatternsMatch::Excluded,
1301    );
1302    // file.ts excluded from excludes
1303    run_test(
1304      &cwd.join("other/file.ts"),
1305      PathKind::File,
1306      FilePatternsMatch::PassedOptedOutExclude,
1307    );
1308    // not allowed target dir
1309    run_test(
1310      &cwd.join("target/test.txt"),
1311      PathKind::File,
1312      FilePatternsMatch::Excluded,
1313    );
1314    run_test(
1315      &cwd.join("target/sub_dir/test.txt"),
1316      PathKind::File,
1317      FilePatternsMatch::Excluded,
1318    );
1319    // but allowed target/other dir
1320    run_test(
1321      &cwd.join("target/other/test.txt"),
1322      PathKind::File,
1323      FilePatternsMatch::PassedOptedOutExclude,
1324    );
1325    run_test(
1326      &cwd.join("target/other/sub/dir/test.txt"),
1327      PathKind::File,
1328      FilePatternsMatch::PassedOptedOutExclude,
1329    );
1330    // and in target/unexcluded
1331    run_test(
1332      &cwd.join("target/unexcluded/test.txt"),
1333      PathKind::File,
1334      FilePatternsMatch::PassedOptedOutExclude,
1335    );
1336  }
1337
1338  #[test]
1339  fn file_patterns_include_excluded() {
1340    let cwd = current_dir();
1341    let file_patterns = FilePatterns {
1342      base: cwd.clone(),
1343      include: None,
1344      exclude: PathOrPatternSet(vec![
1345        PathOrPattern::from_relative(&cwd, "js/").unwrap(),
1346        PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(),
1347      ]),
1348    };
1349    let run_test =
1350      |path: &Path, kind: PathKind, expected: FilePatternsMatch| {
1351        run_file_patterns_match_test(&file_patterns, path, kind, expected);
1352      };
1353    run_test(
1354      &cwd.join("js/test.txt"),
1355      PathKind::File,
1356      FilePatternsMatch::Excluded,
1357    );
1358    run_test(
1359      &cwd.join("js/sub_dir/test.txt"),
1360      PathKind::File,
1361      FilePatternsMatch::PassedOptedOutExclude,
1362    );
1363  }
1364
1365  #[test]
1366  fn file_patterns_opposite_incorrect_excluded_include() {
1367    let cwd = current_dir();
1368    let file_patterns = FilePatterns {
1369      base: cwd.clone(),
1370      include: None,
1371      exclude: PathOrPatternSet(vec![
1372        // this is lower priority
1373        PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(),
1374        // this wins because it's higher priority
1375        PathOrPattern::from_relative(&cwd, "js/").unwrap(),
1376      ]),
1377    };
1378    let run_test =
1379      |path: &Path, kind: PathKind, expected: FilePatternsMatch| {
1380        run_file_patterns_match_test(&file_patterns, path, kind, expected);
1381      };
1382    run_test(
1383      &cwd.join("js/test.txt"),
1384      PathKind::File,
1385      FilePatternsMatch::Excluded,
1386    );
1387    run_test(
1388      &cwd.join("js/sub_dir/test.txt"),
1389      PathKind::File,
1390      FilePatternsMatch::Excluded,
1391    );
1392  }
1393
1394  #[test]
1395  fn from_relative() {
1396    let cwd = current_dir();
1397    // leading dot slash
1398    {
1399      let pattern = PathOrPattern::from_relative(&cwd, "./**/*.ts").unwrap();
1400      assert_eq!(
1401        pattern.matches_path(&cwd.join("foo.ts")),
1402        PathGlobMatch::Matched
1403      );
1404      assert_eq!(
1405        pattern.matches_path(&cwd.join("dir/foo.ts")),
1406        PathGlobMatch::Matched
1407      );
1408      assert_eq!(
1409        pattern.matches_path(&cwd.join("foo.js")),
1410        PathGlobMatch::NotMatched
1411      );
1412      assert_eq!(
1413        pattern.matches_path(&cwd.join("dir/foo.js")),
1414        PathGlobMatch::NotMatched
1415      );
1416    }
1417    // no leading dot slash
1418    {
1419      let pattern = PathOrPattern::from_relative(&cwd, "**/*.ts").unwrap();
1420      assert_eq!(
1421        pattern.matches_path(&cwd.join("foo.ts")),
1422        PathGlobMatch::Matched
1423      );
1424      assert_eq!(
1425        pattern.matches_path(&cwd.join("dir/foo.ts")),
1426        PathGlobMatch::Matched
1427      );
1428      assert_eq!(
1429        pattern.matches_path(&cwd.join("foo.js")),
1430        PathGlobMatch::NotMatched
1431      );
1432      assert_eq!(
1433        pattern.matches_path(&cwd.join("dir/foo.js")),
1434        PathGlobMatch::NotMatched
1435      );
1436    }
1437    // exact file, leading dot slash
1438    {
1439      let pattern = PathOrPattern::from_relative(&cwd, "./foo.ts").unwrap();
1440      assert_eq!(
1441        pattern.matches_path(&cwd.join("foo.ts")),
1442        PathGlobMatch::Matched
1443      );
1444      assert_eq!(
1445        pattern.matches_path(&cwd.join("dir/foo.ts")),
1446        PathGlobMatch::NotMatched
1447      );
1448      assert_eq!(
1449        pattern.matches_path(&cwd.join("foo.js")),
1450        PathGlobMatch::NotMatched
1451      );
1452    }
1453    // exact file, no leading dot slash
1454    {
1455      let pattern = PathOrPattern::from_relative(&cwd, "foo.ts").unwrap();
1456      assert_eq!(
1457        pattern.matches_path(&cwd.join("foo.ts")),
1458        PathGlobMatch::Matched
1459      );
1460      assert_eq!(
1461        pattern.matches_path(&cwd.join("dir/foo.ts")),
1462        PathGlobMatch::NotMatched
1463      );
1464      assert_eq!(
1465        pattern.matches_path(&cwd.join("foo.js")),
1466        PathGlobMatch::NotMatched
1467      );
1468    }
1469    // error for invalid url
1470    {
1471      let err = PathOrPattern::from_relative(&cwd, "https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts").unwrap_err();
1472      assert_eq!(
1473        format!("{:#}", err),
1474        "Invalid URL 'https://raw.githubusercontent.com%2Fdyedgreen%2Fdeno-sqlite%2Frework_api%2Fmod.ts'"
1475      );
1476      assert_eq!(
1477        format!("{:#}", err.source().unwrap()),
1478        "invalid international domain name"
1479      );
1480    }
1481    // sibling dir
1482    {
1483      let pattern = PathOrPattern::from_relative(&cwd, "../sibling").unwrap();
1484      let parent_dir = cwd.parent().unwrap();
1485      assert_eq!(pattern.base_path().unwrap(), parent_dir.join("sibling"));
1486      assert_eq!(
1487        pattern.matches_path(&parent_dir.join("sibling/foo.ts")),
1488        PathGlobMatch::Matched
1489      );
1490      assert_eq!(
1491        pattern.matches_path(&parent_dir.join("./other/foo.js")),
1492        PathGlobMatch::NotMatched
1493      );
1494    }
1495  }
1496
1497  #[test]
1498  fn from_relative_dot_slash() {
1499    let cwd = current_dir();
1500    let pattern = PathOrPattern::from_relative(&cwd, "./").unwrap();
1501    match pattern {
1502      PathOrPattern::Path(p) => assert_eq!(p, cwd),
1503      _ => unreachable!(),
1504    }
1505  }
1506
1507  #[test]
1508  fn new_ctor() {
1509    let cwd = current_dir();
1510    for scheme in &["http", "https"] {
1511      let url = format!("{}://deno.land/x/test", scheme);
1512      let pattern = PathOrPattern::new(&url).unwrap();
1513      match pattern {
1514        PathOrPattern::RemoteUrl(p) => {
1515          assert_eq!(p.as_str(), url)
1516        }
1517        _ => unreachable!(),
1518      }
1519    }
1520    for scheme in &["npm", "jsr"] {
1521      let url = format!("{}:@denotest/basic", scheme);
1522      let pattern = PathOrPattern::new(&url).unwrap();
1523      match pattern {
1524        PathOrPattern::RemoteUrl(p) => {
1525          assert_eq!(p.as_str(), url)
1526        }
1527        _ => unreachable!(),
1528      }
1529    }
1530    {
1531      let file_specifier = url_from_directory_path(&cwd).unwrap();
1532      let pattern = PathOrPattern::new(file_specifier.as_str()).unwrap();
1533      match pattern {
1534        PathOrPattern::Path(p) => {
1535          assert_eq!(p, cwd);
1536        }
1537        _ => {
1538          unreachable!()
1539        }
1540      }
1541    }
1542  }
1543
1544  #[test]
1545  fn from_relative_specifier() {
1546    let cwd = current_dir();
1547    for scheme in &["http", "https"] {
1548      let url = format!("{}://deno.land/x/test", scheme);
1549      let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap();
1550      match pattern {
1551        PathOrPattern::RemoteUrl(p) => {
1552          assert_eq!(p.as_str(), url)
1553        }
1554        _ => unreachable!(),
1555      }
1556    }
1557    for scheme in &["npm", "jsr"] {
1558      let url = format!("{}:@denotest/basic", scheme);
1559      let pattern = PathOrPattern::from_relative(&cwd, &url).unwrap();
1560      match pattern {
1561        PathOrPattern::RemoteUrl(p) => {
1562          assert_eq!(p.as_str(), url)
1563        }
1564        _ => unreachable!(),
1565      }
1566    }
1567    {
1568      let file_specifier = url_from_directory_path(&cwd).unwrap();
1569      let pattern =
1570        PathOrPattern::from_relative(&cwd, file_specifier.as_str()).unwrap();
1571      match pattern {
1572        PathOrPattern::Path(p) => {
1573          assert_eq!(p, cwd);
1574        }
1575        _ => {
1576          unreachable!()
1577        }
1578      }
1579    }
1580  }
1581
1582  #[test]
1583  fn negated_globs() {
1584    #[allow(clippy::disallowed_methods)]
1585    let cwd = current_dir();
1586    {
1587      let pattern = GlobPattern::from_relative(&cwd, "!./**/*.ts").unwrap();
1588      assert!(pattern.is_negated());
1589      assert_eq!(pattern.base_path(), cwd);
1590      assert!(pattern.as_str().starts_with('!'));
1591      assert_eq!(
1592        pattern.matches_path(&cwd.join("foo.ts")),
1593        PathGlobMatch::MatchedNegated
1594      );
1595      assert_eq!(
1596        pattern.matches_path(&cwd.join("foo.js")),
1597        PathGlobMatch::NotMatched
1598      );
1599      let pattern = pattern.as_negated();
1600      assert!(!pattern.is_negated());
1601      assert_eq!(pattern.base_path(), cwd);
1602      assert!(!pattern.as_str().starts_with('!'));
1603      assert_eq!(
1604        pattern.matches_path(&cwd.join("foo.ts")),
1605        PathGlobMatch::Matched
1606      );
1607      let pattern = pattern.as_negated();
1608      assert!(pattern.is_negated());
1609      assert_eq!(pattern.base_path(), cwd);
1610      assert!(pattern.as_str().starts_with('!'));
1611      assert_eq!(
1612        pattern.matches_path(&cwd.join("foo.ts")),
1613        PathGlobMatch::MatchedNegated
1614      );
1615    }
1616  }
1617
1618  #[test]
1619  fn test_is_glob_pattern() {
1620    assert!(!is_glob_pattern("npm:@scope/pkg@*"));
1621    assert!(!is_glob_pattern("jsr:@scope/pkg@*"));
1622    assert!(!is_glob_pattern("https://deno.land/x/?"));
1623    assert!(!is_glob_pattern("http://deno.land/x/?"));
1624    assert!(!is_glob_pattern("file:///deno.land/x/?"));
1625    assert!(is_glob_pattern("**/*.ts"));
1626    assert!(is_glob_pattern("test/?"));
1627    assert!(!is_glob_pattern("test/test"));
1628  }
1629
1630  fn current_dir() -> PathBuf {
1631    // ok because this is test code
1632    #[allow(clippy::disallowed_methods)]
1633    std::env::current_dir().unwrap()
1634  }
1635}