1use 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 Passed,
27 PassedOptedOutExclude,
30 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 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 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) }
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 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 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 }
113 }
114 }
115 }
116 }
117 }
118
119 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 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 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 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 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 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 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 result.sort_by(|a, b| {
289 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 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 pub fn from_exclude_relative_path_or_patterns(
361 base: &Path,
362 entries: &[String],
363 ) -> Result<Self, FromExcludeRelativePathOrPatternsError> {
364 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 }
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 }
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 }
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 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 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 pattern.chars().any(|c| matches!(c, '*' | '?'))
720}
721
722fn escape_brackets(pattern: &str) -> String {
723 pattern.replace('[', "[[]").replace(']', "[]]")
727}
728
729fn match_options() -> glob::MatchOptions {
730 glob::MatchOptions {
732 case_sensitive: false,
734 require_literal_separator: true,
736 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 #[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 "ignored".to_string(),
943 "!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 exclude: PathOrPatternSet::new(vec![
1063 PathOrPattern::from_relative(temp_dir.path(), "!./sub/ignored/test/")
1065 .unwrap(),
1066 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 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 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 run_test(
1280 &cwd.join("other/test.txt"),
1281 PathKind::File,
1282 FilePatternsMatch::Passed,
1283 );
1284 run_test(
1286 &cwd.join("sub_dir/test.js"),
1287 PathKind::File,
1288 FilePatternsMatch::Passed,
1289 );
1290 run_test(
1292 &cwd.join("sub_dir/test.txt"),
1293 PathKind::File,
1294 FilePatternsMatch::Excluded,
1295 );
1296 run_test(
1298 &cwd.join("other/test.ts"),
1299 PathKind::File,
1300 FilePatternsMatch::Excluded,
1301 );
1302 run_test(
1304 &cwd.join("other/file.ts"),
1305 PathKind::File,
1306 FilePatternsMatch::PassedOptedOutExclude,
1307 );
1308 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 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 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 PathOrPattern::from_relative(&cwd, "!js/sub_dir/").unwrap(),
1374 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 {
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 {
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 {
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 {
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 {
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 {
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 #[allow(clippy::disallowed_methods)]
1633 std::env::current_dir().unwrap()
1634 }
1635}