1use std::{
2 collections::{VecDeque, hash_map::Entry},
3 mem::take,
4 sync::LazyLock,
5};
6
7use anyhow::{Result, bail};
8use bincode::{Decode, Encode};
9use regex::Regex;
10use rustc_hash::{FxHashMap, FxHashSet};
11use tracing::Instrument;
12use turbo_rcstr::{RcStr, rcstr};
13use turbo_tasks::{
14 NonLocalValue, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs,
15};
16use turbo_tasks_fs::{
17 FileSystemPath, LinkContent, LinkType, RawDirectoryContent, RawDirectoryEntry,
18};
19use turbo_unix_path::normalize_path;
20
21#[turbo_tasks::value]
22#[derive(Hash, Clone, Debug, Default, ValueToString)]
23#[value_to_string(self.describe_as_string())]
24pub enum Pattern {
25 Constant(RcStr),
26 #[default]
27 Dynamic,
28 DynamicNoSlash,
29 Alternatives(Vec<Pattern>),
30 Concatenation(Vec<Pattern>),
31}
32
33impl TaskInput for Pattern {
36 fn is_transient(&self) -> bool {
37 false
39 }
40}
41
42fn concatenation_push_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
43 if let Pattern::Constant(ref s) = pat
44 && let Some(Pattern::Constant(last)) = list.last_mut()
45 {
46 let mut buf = last.to_string();
47 buf.push_str(s);
48 *last = buf.into();
49 return;
50 }
51 list.push(pat);
52}
53
54fn concatenation_push_front_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
55 if let Pattern::Constant(s) = pat {
56 if let Some(Pattern::Constant(first)) = list.iter_mut().next() {
57 let mut buf = s.into_owned();
58 buf.push_str(first);
59
60 *first = buf.into();
61 return;
62 }
63 list.insert(0, Pattern::Constant(s));
64 } else {
65 list.insert(0, pat);
66 }
67}
68
69fn concatenation_extend_or_merge_items(
70 list: &mut Vec<Pattern>,
71 mut iter: impl Iterator<Item = Pattern>,
72) {
73 if let Some(first) = iter.next() {
74 concatenation_push_or_merge_item(list, first);
75 list.extend(iter);
76 }
77}
78
79fn longest_common_prefix<'a>(strings: &[&'a str]) -> &'a str {
80 if strings.is_empty() {
81 return "";
82 }
83 if let [single] = strings {
84 return single;
85 }
86 let first = strings[0];
87 let mut len = first.len();
88 for str in &strings[1..] {
89 len = std::cmp::min(
90 len,
91 str.chars()
93 .zip(first.chars())
94 .take_while(|&(a, b)| a == b)
95 .count(),
96 );
97 }
98 &first[..len]
99}
100
101fn longest_common_suffix<'a>(strings: &[&'a str]) -> &'a str {
102 if strings.is_empty() {
103 return "";
104 }
105 let first = strings[0];
106 let mut len = first.len();
107 for str in &strings[1..] {
108 len = std::cmp::min(
109 len,
110 str.chars()
112 .rev()
113 .zip(first.chars().rev())
114 .take_while(|&(a, b)| a == b)
115 .count(),
116 );
117 }
118 &first[(first.len() - len)..]
119}
120
121impl Pattern {
122 pub fn as_constant_string(&self) -> Option<&RcStr> {
124 match self {
125 Pattern::Constant(str) => Some(str),
126 _ => None,
127 }
128 }
129
130 pub fn has_constant_parts(&self) -> bool {
133 match self {
134 Pattern::Constant(str) => str != "/",
135 Pattern::Dynamic | Pattern::DynamicNoSlash => false,
136 Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
137 list.iter().any(|p| p.has_constant_parts())
138 }
139 }
140 }
141
142 pub fn has_dynamic_parts(&self) -> bool {
143 match self {
144 Pattern::Constant(_) => false,
145 Pattern::Dynamic | Pattern::DynamicNoSlash => true,
146 Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
147 list.iter().any(|p| p.has_dynamic_parts())
148 }
149 }
150 }
151
152 pub fn constant_prefix(&self) -> &str {
153 if let Pattern::Constant(c) = self {
158 return c;
159 }
160
161 fn collect_constant_prefix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
162 match pattern {
163 Pattern::Constant(c) => {
164 result.push(c.as_str());
165 }
166 Pattern::Concatenation(list) => {
167 if let Some(Pattern::Constant(first)) = list.first() {
168 result.push(first.as_str());
169 }
170 }
171 Pattern::Alternatives(_) => {
172 panic!("for constant_prefix a Pattern must be normalized");
173 }
174 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
175 }
176 }
177
178 let mut strings: Vec<&str> = vec![];
179 match self {
180 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
181 collect_constant_prefix(c, &mut strings);
182 }
183 Pattern::Alternatives(list) => {
184 for c in list {
185 collect_constant_prefix(c, &mut strings);
186 }
187 }
188 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
189 }
190 longest_common_prefix(&strings)
191 }
192
193 pub fn constant_suffix(&self) -> &str {
194 fn collect_constant_suffix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
199 match pattern {
200 Pattern::Constant(c) => {
201 result.push(c.as_str());
202 }
203 Pattern::Concatenation(list) => {
204 if let Some(Pattern::Constant(first)) = list.last() {
205 result.push(first.as_str());
206 }
207 }
208 Pattern::Alternatives(_) => {
209 panic!("for constant_suffix a Pattern must be normalized");
210 }
211 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
212 }
213 }
214
215 let mut strings: Vec<&str> = vec![];
216 match self {
217 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
218 collect_constant_suffix(c, &mut strings);
219 }
220 Pattern::Alternatives(list) => {
221 for c in list {
222 collect_constant_suffix(c, &mut strings);
223 }
224 }
225 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
226 }
227 longest_common_suffix(&strings)
228 }
229
230 pub fn strip_prefix(&self, prefix: &str) -> Result<Option<Self>> {
231 if self.must_match(prefix) {
232 let mut pat = self.clone();
233 pat.strip_prefix_len(prefix.len())?;
234 Ok(Some(pat))
235 } else {
236 Ok(None)
237 }
238 }
239
240 pub fn strip_prefix_len(&mut self, len: usize) -> Result<()> {
241 fn strip_prefix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) -> Result<()> {
242 match pattern {
243 Pattern::Constant(c) => {
244 let c_len = c.len();
245 if *chars_to_strip >= c_len {
246 *c = rcstr!("");
247 } else {
248 *c = (&c[*chars_to_strip..]).into();
249 }
250 *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
251 }
252 Pattern::Concatenation(list) => {
253 for c in list {
254 if *chars_to_strip > 0 {
255 strip_prefix_internal(c, chars_to_strip)?;
256 }
257 }
258 }
259 Pattern::Alternatives(_) => {
260 bail!("strip_prefix pattern must be normalized");
261 }
262 Pattern::Dynamic | Pattern::DynamicNoSlash => {
263 bail!("strip_prefix prefix is too long");
264 }
265 }
266 Ok(())
267 }
268
269 match &mut *self {
270 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
271 let mut len_local = len;
272 strip_prefix_internal(c, &mut len_local)?;
273 }
274 Pattern::Alternatives(list) => {
275 for c in list {
276 let mut len_local = len;
277 strip_prefix_internal(c, &mut len_local)?;
278 }
279 }
280 Pattern::Dynamic | Pattern::DynamicNoSlash => {
281 if len > 0 {
282 bail!(
283 "strip_prefix prefix ({}) is too long: {}",
284 len,
285 self.describe_as_string()
286 );
287 }
288 }
289 };
290
291 self.normalize();
292
293 Ok(())
294 }
295
296 pub fn strip_suffix_len(&mut self, len: usize) {
297 fn strip_suffix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) {
298 match pattern {
299 Pattern::Constant(c) => {
300 let c_len = c.len();
301 if *chars_to_strip >= c_len {
302 *c = rcstr!("");
303 } else {
304 *c = (&c[..(c_len - *chars_to_strip)]).into();
305 }
306 *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
307 }
308 Pattern::Concatenation(list) => {
309 for c in list.iter_mut().rev() {
310 if *chars_to_strip > 0 {
311 strip_suffix_internal(c, chars_to_strip);
312 }
313 }
314 }
315 Pattern::Alternatives(_) => {
316 panic!("for strip_suffix a Pattern must be normalized");
317 }
318 Pattern::Dynamic | Pattern::DynamicNoSlash => {
319 panic!("strip_suffix suffix is too long");
320 }
321 }
322 }
323
324 match &mut *self {
325 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
326 let mut len_local = len;
327 strip_suffix_internal(c, &mut len_local);
328 }
329 Pattern::Alternatives(list) => {
330 for c in list {
331 let mut len_local = len;
332 strip_suffix_internal(c, &mut len_local);
333 }
334 }
335 Pattern::Dynamic | Pattern::DynamicNoSlash => {
336 if len > 0 {
337 panic!("strip_suffix suffix is too long");
338 }
339 }
340 };
341
342 self.normalize()
343 }
344
345 pub fn spread_into_star(&self, template: &str) -> Pattern {
350 if template.contains("*") {
351 let alternatives: Box<dyn Iterator<Item = &Pattern>> = match self {
352 Pattern::Alternatives(list) => Box::new(list.iter()),
353 c => Box::new(std::iter::once(c)),
354 };
355
356 let mut result = Pattern::alternatives(alternatives.map(|pat| {
357 let mut split = template.split("*");
358 let mut concatenation: Vec<Pattern> = Vec::with_capacity(3);
359
360 concatenation.push(Pattern::Constant(split.next().unwrap().into()));
362
363 for part in split {
364 concatenation.push(pat.clone());
365 if !part.is_empty() {
366 concatenation.push(Pattern::Constant(part.into()));
367 }
368 }
369 Pattern::Concatenation(concatenation)
370 }));
371
372 result.normalize();
373 result
374 } else {
375 Pattern::Constant(template.into())
376 }
377 }
378
379 pub fn extend(&mut self, concatenated: impl Iterator<Item = Self>) {
381 if let Pattern::Concatenation(list) = self {
382 concatenation_extend_or_merge_items(list, concatenated);
383 } else {
384 let mut vec = vec![take(self)];
385 for item in concatenated {
386 if let Pattern::Concatenation(more) = item {
387 concatenation_extend_or_merge_items(&mut vec, more.into_iter());
388 } else {
389 concatenation_push_or_merge_item(&mut vec, item);
390 }
391 }
392 *self = Pattern::Concatenation(vec);
393 }
394 }
395
396 pub fn push(&mut self, pat: Pattern) {
398 if let Pattern::Constant(this) = &*self
399 && this.is_empty()
400 {
401 *self = pat;
403 return;
404 }
405 if let Pattern::Constant(pat) = &pat
406 && pat.is_empty()
407 {
408 return;
410 }
411
412 match (self, pat) {
413 (Pattern::Concatenation(list), Pattern::Concatenation(more)) => {
414 concatenation_extend_or_merge_items(list, more.into_iter());
415 }
416 (Pattern::Concatenation(list), pat) => {
417 concatenation_push_or_merge_item(list, pat);
418 }
419 (this, Pattern::Concatenation(mut list)) => {
420 concatenation_push_front_or_merge_item(&mut list, take(this));
421 *this = Pattern::Concatenation(list);
422 }
423 (Pattern::Constant(str), Pattern::Constant(other)) => {
424 let mut buf = str.to_string();
425 buf.push_str(&other);
426 *str = buf.into();
427 }
428 (this, pat) => {
429 *this = Pattern::Concatenation(vec![take(this), pat]);
430 }
431 }
432 }
433
434 pub fn push_front(&mut self, pat: Pattern) {
436 match (self, pat) {
437 (Pattern::Concatenation(list), Pattern::Concatenation(mut more)) => {
438 concatenation_extend_or_merge_items(&mut more, take(list).into_iter());
439 *list = more;
440 }
441 (Pattern::Concatenation(list), pat) => {
442 concatenation_push_front_or_merge_item(list, pat);
443 }
444 (this, Pattern::Concatenation(mut list)) => {
445 concatenation_push_or_merge_item(&mut list, take(this));
446 *this = Pattern::Concatenation(list);
447 }
448 (Pattern::Constant(str), Pattern::Constant(other)) => {
449 let mut buf = other.into_owned();
450
451 buf.push_str(str);
452 *str = buf.into();
453 }
454 (this, pat) => {
455 *this = Pattern::Concatenation(vec![pat, take(this)]);
456 }
457 }
458 }
459
460 pub fn alternatives(alts: impl IntoIterator<Item = Pattern>) -> Self {
461 let mut list = Vec::new();
462 for alt in alts {
463 if let Pattern::Alternatives(inner) = alt {
464 list.extend(inner);
465 } else {
466 list.push(alt)
467 }
468 }
469 Self::Alternatives(list)
470 }
471
472 pub fn concat(items: impl IntoIterator<Item = Pattern>) -> Self {
473 let mut items = items.into_iter();
474 let mut current = items.next().unwrap_or_default();
475 for item in items {
476 current.push(item);
477 }
478 current
479 }
480
481 pub fn with_normalized_path(&self) -> Option<Pattern> {
488 let mut new = self.clone();
489
490 #[derive(Debug)]
491 enum PathElement {
492 Segment(Pattern),
493 Separator,
494 }
495
496 fn normalize_path_internal(pattern: &mut Pattern) -> Option<()> {
497 match pattern {
498 Pattern::Constant(c) => {
499 let normalized = c.replace('\\', "/");
500 *c = RcStr::from(normalize_path(normalized.as_str())?);
501 Some(())
502 }
503 Pattern::Dynamic | Pattern::DynamicNoSlash => Some(()),
504 Pattern::Concatenation(list) => {
505 let mut segments = Vec::new();
506 for segment in list.iter() {
507 match segment {
508 Pattern::Constant(str) => {
509 let mut iter = str.split('/').peekable();
510 while let Some(segment) = iter.next() {
511 match segment {
512 "." | "" => {
513 continue;
515 }
516 ".." => {
517 if segments.is_empty() {
518 return None;
520 }
521
522 if let Some(PathElement::Separator) = segments.last()
523 && let Some(PathElement::Segment(
524 Pattern::Constant(_),
525 )) = segments.get(segments.len() - 2)
526 {
527 segments.truncate(segments.len() - 2);
529 continue;
530 }
531
532 segments.push(PathElement::Segment(Pattern::Constant(
534 rcstr!(".."),
535 )));
536 }
537 segment => {
538 segments.push(PathElement::Segment(Pattern::Constant(
539 segment.into(),
540 )));
541 }
542 }
543
544 if iter.peek().is_some() {
545 segments.push(PathElement::Separator);
547 }
548 }
549 }
550 Pattern::Dynamic | Pattern::DynamicNoSlash => {
551 segments.push(PathElement::Segment(segment.clone()));
552 }
553 Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
554 panic!("for with_normalized_path the Pattern must be normalized");
555 }
556 }
557 }
558 let separator = rcstr!("/");
559 *list = segments
560 .into_iter()
561 .map(|c| match c {
562 PathElement::Segment(p) => p,
563 PathElement::Separator => Pattern::Constant(separator.clone()),
564 })
565 .collect();
566 Some(())
567 }
568 Pattern::Alternatives(_) => {
569 panic!("for with_normalized_path the Pattern must be normalized");
570 }
571 }
572 }
573
574 match &mut new {
575 c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
576 normalize_path_internal(c)?;
577 }
578 Pattern::Alternatives(list) => {
579 for c in list {
580 normalize_path_internal(c)?;
581 }
582 }
583 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
584 }
585
586 new.normalize();
587 Some(new)
588 }
589
590 pub fn normalize(&mut self) {
593 match self {
594 Pattern::Dynamic | Pattern::DynamicNoSlash | Pattern::Constant(_) => {
595 }
597 Pattern::Alternatives(list) => {
598 for alt in list.iter_mut() {
599 alt.normalize();
600 }
601 let mut new_alternatives = Vec::new();
602 let mut has_dynamic = false;
603 for alt in list.drain(..) {
604 if let Pattern::Alternatives(inner) = alt {
605 for alt in inner {
606 if alt == Pattern::Dynamic {
607 if !has_dynamic {
608 has_dynamic = true;
609 new_alternatives.push(alt);
610 }
611 } else {
612 new_alternatives.push(alt);
613 }
614 }
615 } else if alt == Pattern::Dynamic {
616 if !has_dynamic {
617 has_dynamic = true;
618 new_alternatives.push(alt);
619 }
620 } else {
621 new_alternatives.push(alt);
622 }
623 }
624 if new_alternatives.len() == 1 {
625 *self = new_alternatives.into_iter().next().unwrap();
626 } else {
627 *list = new_alternatives;
628 }
629 }
630 Pattern::Concatenation(list) => {
631 let mut has_alternatives = false;
632 for part in list.iter_mut() {
633 part.normalize();
634 if let Pattern::Alternatives(_) = part {
635 has_alternatives = true;
636 }
637 }
638 if has_alternatives {
639 let mut new_alternatives: Vec<Vec<Pattern>> = vec![Vec::new()];
643 for part in list.drain(..) {
644 if let Pattern::Alternatives(list) = part {
645 let mut combined = Vec::new();
647 for alt2 in list.iter() {
648 for mut alt in new_alternatives.clone() {
649 if let Pattern::Concatenation(parts) = alt2 {
650 alt.extend(parts.clone());
651 } else {
652 alt.push(alt2.clone());
653 }
654 combined.push(alt)
655 }
656 }
657 new_alternatives = combined;
658 } else {
659 for alt in new_alternatives.iter_mut() {
661 if let Pattern::Concatenation(ref parts) = part {
662 alt.extend(parts.clone());
663 } else {
664 alt.push(part.clone());
665 }
666 }
667 }
668 }
669 *self = Pattern::Alternatives(
672 new_alternatives
673 .into_iter()
674 .map(|parts| {
675 if parts.len() == 1 {
676 parts.into_iter().next().unwrap()
677 } else {
678 Pattern::Concatenation(parts)
679 }
680 })
681 .collect(),
682 );
683 self.normalize();
685 } else {
686 let mut new_parts = Vec::new();
687 for part in list.drain(..) {
688 fn add_part(part: Pattern, new_parts: &mut Vec<Pattern>) {
689 match part {
690 Pattern::Constant(c) => {
691 if !c.is_empty() {
692 if let Some(Pattern::Constant(last)) = new_parts.last_mut()
693 {
694 let mut buf = last.to_string();
695 buf.push_str(&c);
696 *last = buf.into();
697 } else {
698 new_parts.push(Pattern::Constant(c));
699 }
700 }
701 }
702 Pattern::Dynamic => {
703 if let Some(Pattern::Dynamic | Pattern::DynamicNoSlash) =
704 new_parts.last()
705 {
706 } else {
708 new_parts.push(Pattern::Dynamic);
709 }
710 }
711 Pattern::DynamicNoSlash => {
712 if let Some(Pattern::DynamicNoSlash) = new_parts.last() {
713 } else {
715 new_parts.push(Pattern::DynamicNoSlash);
716 }
717 }
718 Pattern::Concatenation(parts) => {
719 for part in parts {
720 add_part(part, new_parts);
721 }
722 }
723 Pattern::Alternatives(_) => unreachable!(),
724 }
725 }
726
727 add_part(part, &mut new_parts);
728 }
729 if new_parts.len() == 1 {
730 *self = new_parts.into_iter().next().unwrap();
731 } else {
732 *list = new_parts;
733 }
734 }
735 }
736 }
737 }
738
739 pub fn is_empty(&self) -> bool {
740 match self {
741 Pattern::Constant(s) => s.is_empty(),
742 Pattern::Dynamic | Pattern::DynamicNoSlash => false,
743 Pattern::Concatenation(parts) => parts.iter().all(|p| p.is_empty()),
744 Pattern::Alternatives(parts) => parts.iter().all(|p| p.is_empty()),
745 }
746 }
747
748 pub fn filter_could_match(&self, value: &str) -> Option<Pattern> {
749 if let Pattern::Alternatives(list) = self {
750 let new_list = list
751 .iter()
752 .filter(|alt| alt.could_match(value))
753 .cloned()
754 .collect::<Vec<_>>();
755 if new_list.is_empty() {
756 None
757 } else {
758 Some(Pattern::Alternatives(new_list))
759 }
760 } else if self.could_match(value) {
761 Some(self.clone())
762 } else {
763 None
764 }
765 }
766
767 pub fn filter_could_not_match(&self, value: &str) -> Option<Pattern> {
768 if let Pattern::Alternatives(list) = self {
769 let new_list = list
770 .iter()
771 .filter(|alt| !alt.could_match(value))
772 .cloned()
773 .collect::<Vec<_>>();
774 if new_list.is_empty() {
775 None
776 } else {
777 Some(Pattern::Alternatives(new_list))
778 }
779 } else if self.could_match(value) {
780 None
781 } else {
782 Some(self.clone())
783 }
784 }
785
786 pub fn split_could_match(&self, value: &str) -> (Option<Pattern>, Option<Pattern>) {
787 if let Pattern::Alternatives(list) = self {
788 let mut could_match_list = Vec::new();
789 let mut could_not_match_list = Vec::new();
790 for alt in list.iter() {
791 if alt.could_match(value) {
792 could_match_list.push(alt.clone());
793 } else {
794 could_not_match_list.push(alt.clone());
795 }
796 }
797 (
798 if could_match_list.is_empty() {
799 None
800 } else if could_match_list.len() == 1 {
801 Some(could_match_list.into_iter().next().unwrap())
802 } else {
803 Some(Pattern::Alternatives(could_match_list))
804 },
805 if could_not_match_list.is_empty() {
806 None
807 } else if could_not_match_list.len() == 1 {
808 Some(could_not_match_list.into_iter().next().unwrap())
809 } else {
810 Some(Pattern::Alternatives(could_not_match_list))
811 },
812 )
813 } else if self.could_match(value) {
814 (Some(self.clone()), None)
815 } else {
816 (None, Some(self.clone()))
817 }
818 }
819
820 pub fn is_match(&self, value: &str) -> bool {
821 if let Pattern::Alternatives(list) = self {
822 list.iter().any(|alt| {
823 alt.match_internal(value, None, InNodeModules::False, false)
824 .is_match()
825 })
826 } else {
827 self.match_internal(value, None, InNodeModules::False, false)
828 .is_match()
829 }
830 }
831
832 pub fn is_match_ignore_dynamic(&self, value: &str) -> bool {
835 if let Pattern::Alternatives(list) = self {
836 list.iter().any(|alt| {
837 alt.match_internal(value, None, InNodeModules::False, true)
838 .is_match()
839 })
840 } else {
841 self.match_internal(value, None, InNodeModules::False, true)
842 .is_match()
843 }
844 }
845
846 pub fn match_position(&self, value: &str) -> Option<usize> {
847 if let Pattern::Alternatives(list) = self {
848 list.iter().position(|alt| {
849 alt.match_internal(value, None, InNodeModules::False, false)
850 .is_match()
851 })
852 } else {
853 self.match_internal(value, None, InNodeModules::False, false)
854 .is_match()
855 .then_some(0)
856 }
857 }
858
859 pub fn could_match_others(&self, value: &str) -> bool {
860 if let Pattern::Alternatives(list) = self {
861 list.iter().any(|alt| {
862 alt.match_internal(value, None, InNodeModules::False, false)
863 .could_match_others()
864 })
865 } else {
866 self.match_internal(value, None, InNodeModules::False, false)
867 .could_match_others()
868 }
869 }
870
871 pub fn must_match(&self, value: &str) -> bool {
873 if let Pattern::Alternatives(list) = self {
874 list.iter().all(|alt| {
875 alt.match_internal(value, None, InNodeModules::False, false)
876 .could_match()
877 })
878 } else {
879 self.match_internal(value, None, InNodeModules::False, false)
880 .could_match()
881 }
882 }
883
884 pub fn could_match(&self, value: &str) -> bool {
886 if let Pattern::Alternatives(list) = self {
887 list.iter().any(|alt| {
888 alt.match_internal(value, None, InNodeModules::False, false)
889 .could_match()
890 })
891 } else {
892 self.match_internal(value, None, InNodeModules::False, false)
893 .could_match()
894 }
895 }
896
897 pub fn could_match_position(&self, value: &str) -> Option<usize> {
898 if let Pattern::Alternatives(list) = self {
899 list.iter().position(|alt| {
900 alt.match_internal(value, None, InNodeModules::False, false)
901 .could_match()
902 })
903 } else {
904 self.match_internal(value, None, InNodeModules::False, false)
905 .could_match()
906 .then_some(0)
907 }
908 }
909 fn match_internal<'a>(
910 &self,
911 mut value: &'a str,
912 mut any_offset: Option<usize>,
913 mut in_node_modules: InNodeModules,
914 ignore_dynamic: bool,
915 ) -> MatchResult<'a> {
916 match self {
917 Pattern::Constant(c) => {
918 if let Some(offset) = any_offset {
919 if let Some(index) = value.find(&**c) {
920 if index <= offset {
921 MatchResult::Consumed {
922 remaining: &value[index + c.len()..],
923 any_offset: None,
924 in_node_modules: InNodeModules::check(c),
925 }
926 } else {
927 MatchResult::None
928 }
929 } else if offset >= value.len() {
930 MatchResult::Partial
931 } else {
932 MatchResult::None
933 }
934 } else if value.starts_with(&**c) {
935 MatchResult::Consumed {
936 remaining: &value[c.len()..],
937 any_offset: None,
938 in_node_modules: InNodeModules::check(c),
939 }
940 } else if c.starts_with(value) {
941 MatchResult::Partial
942 } else {
943 MatchResult::None
944 }
945 }
946 Pattern::Dynamic | Pattern::DynamicNoSlash => {
947 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
948 Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
949 });
950 static FORBIDDEN_MATCH: LazyLock<Regex> =
951 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
952 if in_node_modules == InNodeModules::FolderSlashMatched
953 || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
954 {
955 MatchResult::None
956 } else if let Some(m) = FORBIDDEN.find(value) {
957 MatchResult::Consumed {
958 remaining: value,
959 any_offset: Some(m.start()),
960 in_node_modules: InNodeModules::False,
961 }
962 } else if FORBIDDEN_MATCH.find(value).is_some() {
963 MatchResult::Partial
964 } else if ignore_dynamic {
965 MatchResult::None
966 } else {
967 let match_length = matches!(self, Pattern::DynamicNoSlash)
968 .then(|| value.find("/"))
969 .flatten()
970 .unwrap_or(value.len());
971 MatchResult::Consumed {
972 remaining: value,
973 any_offset: Some(match_length),
974 in_node_modules: InNodeModules::False,
975 }
976 }
977 }
978 Pattern::Alternatives(_) => {
979 panic!("for matching a Pattern must be normalized {self:?}")
980 }
981 Pattern::Concatenation(list) => {
982 for part in list {
983 match part.match_internal(value, any_offset, in_node_modules, ignore_dynamic) {
984 MatchResult::None => return MatchResult::None,
985 MatchResult::Partial => return MatchResult::Partial,
986 MatchResult::Consumed {
987 remaining: new_value,
988 any_offset: new_any_offset,
989 in_node_modules: new_in_node_modules,
990 } => {
991 value = new_value;
992 any_offset = new_any_offset;
993 in_node_modules = new_in_node_modules
994 }
995 }
996 }
997 MatchResult::Consumed {
998 remaining: value,
999 any_offset,
1000 in_node_modules,
1001 }
1002 }
1003 }
1004 }
1005
1006 fn match_collect_internal<'a>(
1009 &self,
1010 mut value: &'a str,
1011 mut any_offset: Option<usize>,
1012 mut in_node_modules: InNodeModules,
1013 dynamics: &mut VecDeque<&'a str>,
1014 ) -> MatchResult<'a> {
1015 match self {
1016 Pattern::Constant(c) => {
1017 if let Some(offset) = any_offset {
1018 if let Some(index) = value.find(&**c) {
1019 if index <= offset {
1020 if index > 0 {
1021 dynamics.push_back(&value[..index]);
1022 }
1023 MatchResult::Consumed {
1024 remaining: &value[index + c.len()..],
1025 any_offset: None,
1026 in_node_modules: InNodeModules::check(c),
1027 }
1028 } else {
1029 MatchResult::None
1030 }
1031 } else if offset >= value.len() {
1032 MatchResult::Partial
1033 } else {
1034 MatchResult::None
1035 }
1036 } else if value.starts_with(&**c) {
1037 MatchResult::Consumed {
1038 remaining: &value[c.len()..],
1039 any_offset: None,
1040 in_node_modules: InNodeModules::check(c),
1041 }
1042 } else if c.starts_with(value) {
1043 MatchResult::Partial
1044 } else {
1045 MatchResult::None
1046 }
1047 }
1048 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1049 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
1050 Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
1051 });
1052 static FORBIDDEN_MATCH: LazyLock<Regex> =
1053 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
1054 if in_node_modules == InNodeModules::FolderSlashMatched
1055 || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
1056 {
1057 MatchResult::None
1058 } else if let Some(m) = FORBIDDEN.find(value) {
1059 MatchResult::Consumed {
1060 remaining: value,
1061 any_offset: Some(m.start()),
1062 in_node_modules: InNodeModules::False,
1063 }
1064 } else if FORBIDDEN_MATCH.find(value).is_some() {
1065 MatchResult::Partial
1066 } else {
1067 let match_length = matches!(self, Pattern::DynamicNoSlash)
1068 .then(|| value.find("/"))
1069 .flatten()
1070 .unwrap_or(value.len());
1071 MatchResult::Consumed {
1072 remaining: value,
1073 any_offset: Some(match_length),
1074 in_node_modules: InNodeModules::False,
1075 }
1076 }
1077 }
1078 Pattern::Alternatives(_) => {
1079 panic!("for matching a Pattern must be normalized {self:?}")
1080 }
1081 Pattern::Concatenation(list) => {
1082 for part in list {
1083 match part.match_collect_internal(value, any_offset, in_node_modules, dynamics)
1084 {
1085 MatchResult::None => return MatchResult::None,
1086 MatchResult::Partial => return MatchResult::Partial,
1087 MatchResult::Consumed {
1088 remaining: new_value,
1089 any_offset: new_any_offset,
1090 in_node_modules: new_in_node_modules,
1091 } => {
1092 value = new_value;
1093 any_offset = new_any_offset;
1094 in_node_modules = new_in_node_modules
1095 }
1096 }
1097 }
1098 if let Some(offset) = any_offset
1099 && offset == value.len()
1100 {
1101 dynamics.push_back(value);
1102 }
1103 MatchResult::Consumed {
1104 remaining: value,
1105 any_offset,
1106 in_node_modules,
1107 }
1108 }
1109 }
1110 }
1111
1112 pub fn next_constants<'a>(&'a self, value: &str) -> Option<Vec<(&'a str, bool)>> {
1113 if let Pattern::Alternatives(list) = self {
1114 let mut results = Vec::new();
1115 for alt in list.iter() {
1116 match alt.next_constants_internal(value, None) {
1117 NextConstantUntilResult::NoMatch => {}
1118 NextConstantUntilResult::PartialDynamic => {
1119 return None;
1120 }
1121 NextConstantUntilResult::Partial(s, end) => {
1122 results.push((s, end));
1123 }
1124 NextConstantUntilResult::Consumed(rem, None) => {
1125 if rem.is_empty() {
1126 results.push(("", true));
1127 }
1128 }
1129 NextConstantUntilResult::Consumed(rem, Some(any)) => {
1130 if any == rem.len() {
1131 return None;
1134 }
1135 }
1136 }
1137 }
1138 Some(results)
1139 } else {
1140 match self.next_constants_internal(value, None) {
1141 NextConstantUntilResult::NoMatch => None,
1142 NextConstantUntilResult::PartialDynamic => None,
1143 NextConstantUntilResult::Partial(s, e) => Some(vec![(s, e)]),
1144 NextConstantUntilResult::Consumed(_, _) => None,
1145 }
1146 }
1147 }
1148
1149 fn next_constants_internal<'a, 'b>(
1150 &'a self,
1151 mut value: &'b str,
1152 mut any_offset: Option<usize>,
1153 ) -> NextConstantUntilResult<'a, 'b> {
1154 match self {
1155 Pattern::Constant(c) => {
1156 if let Some(offset) = any_offset {
1157 if let Some(index) = value.find(&**c) {
1158 if index <= offset {
1159 NextConstantUntilResult::Consumed(&value[index + c.len()..], None)
1160 } else {
1161 NextConstantUntilResult::NoMatch
1162 }
1163 } else if offset >= value.len() {
1164 NextConstantUntilResult::PartialDynamic
1165 } else {
1166 NextConstantUntilResult::NoMatch
1167 }
1168 } else if let Some(stripped) = value.strip_prefix(&**c) {
1169 NextConstantUntilResult::Consumed(stripped, None)
1170 } else if let Some(stripped) = c.strip_prefix(value) {
1171 NextConstantUntilResult::Partial(stripped, true)
1172 } else {
1173 NextConstantUntilResult::NoMatch
1174 }
1175 }
1176 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1177 static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
1178 Regex::new(r"(/|^)(\.|(node_modules|__tests?__)(/|$))").unwrap()
1179 });
1180 static FORBIDDEN_MATCH: LazyLock<Regex> =
1181 LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
1182 if let Some(m) = FORBIDDEN.find(value) {
1183 NextConstantUntilResult::Consumed(value, Some(m.start()))
1184 } else if FORBIDDEN_MATCH.find(value).is_some() {
1185 NextConstantUntilResult::PartialDynamic
1186 } else {
1187 NextConstantUntilResult::Consumed(value, Some(value.len()))
1188 }
1189 }
1190 Pattern::Alternatives(_) => {
1191 panic!("for next_constants() the Pattern must be normalized");
1192 }
1193 Pattern::Concatenation(list) => {
1194 let mut iter = list.iter();
1195 while let Some(part) = iter.next() {
1196 match part.next_constants_internal(value, any_offset) {
1197 NextConstantUntilResult::NoMatch => {
1198 return NextConstantUntilResult::NoMatch;
1199 }
1200 NextConstantUntilResult::PartialDynamic => {
1201 return NextConstantUntilResult::PartialDynamic;
1202 }
1203 NextConstantUntilResult::Partial(r, end) => {
1204 return NextConstantUntilResult::Partial(
1205 r,
1206 end && iter.next().is_none(),
1207 );
1208 }
1209 NextConstantUntilResult::Consumed(new_value, new_any_offset) => {
1210 value = new_value;
1211 any_offset = new_any_offset;
1212 }
1213 }
1214 }
1215 NextConstantUntilResult::Consumed(value, any_offset)
1216 }
1217 }
1218 }
1219
1220 pub fn or_any_nested_file(&self) -> Self {
1221 let mut new = self.clone();
1222 new.push(Pattern::Constant(rcstr!("/")));
1223 new.push(Pattern::Dynamic);
1224 new.normalize();
1225 Pattern::alternatives([self.clone(), new])
1226 }
1227
1228 pub fn replace_final_constants(
1232 &mut self,
1233 cb: &mut impl FnMut(&RcStr) -> Option<Pattern>,
1234 ) -> bool {
1235 let mut replaced = false;
1236 match self {
1237 Pattern::Constant(c) => {
1238 if let Some(replacement) = cb(c) {
1239 *self = replacement;
1240 replaced = true;
1241 }
1242 }
1243 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1244 Pattern::Alternatives(list) => {
1245 for i in list {
1246 replaced = i.replace_final_constants(cb) || replaced;
1247 }
1248 }
1249 Pattern::Concatenation(list) => {
1250 if let Some(i) = list.last_mut() {
1251 replaced = i.replace_final_constants(cb) || replaced;
1252 }
1253 }
1254 }
1255 replaced
1256 }
1257
1258 pub fn replace_constants(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
1261 let mut replaced = false;
1262 match self {
1263 Pattern::Constant(c) => {
1264 if let Some(replacement) = cb(c) {
1265 *self = replacement;
1266 replaced = true;
1267 }
1268 }
1269 Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1270 Pattern::Concatenation(list) | Pattern::Alternatives(list) => {
1271 for i in list {
1272 replaced = i.replace_constants(cb) || replaced;
1273 }
1274 }
1275 }
1276 replaced
1277 }
1278
1279 pub fn match_apply_template(&self, value: &str, target: &Pattern) -> Option<String> {
1284 let match_idx = self.match_position(value)?;
1285 let source = match self {
1286 Pattern::Alternatives(list) => list.get(match_idx),
1287 Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1288 if match_idx == 0 =>
1289 {
1290 Some(self)
1291 }
1292 _ => None,
1293 }?;
1294 let target = match target {
1295 Pattern::Alternatives(list) => list.get(match_idx),
1296 Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1297 if match_idx == 0 =>
1298 {
1299 Some(target)
1300 }
1301 _ => None,
1302 }?;
1303
1304 let mut dynamics = VecDeque::new();
1305 source.match_collect_internal(value, None, InNodeModules::False, &mut dynamics);
1307
1308 let mut result = "".to_string();
1309 match target {
1310 Pattern::Constant(c) => result.push_str(c),
1311 Pattern::Dynamic | Pattern::DynamicNoSlash => result.push_str(dynamics.pop_front()?),
1312 Pattern::Concatenation(list) => {
1313 for c in list {
1314 match c {
1315 Pattern::Constant(c) => result.push_str(c),
1316 Pattern::Dynamic | Pattern::DynamicNoSlash => {
1317 result.push_str(dynamics.pop_front()?)
1318 }
1319 Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
1320 panic!("Pattern must be normalized")
1321 }
1322 }
1323 }
1324 }
1325 Pattern::Alternatives(_) => panic!("Pattern must be normalized"),
1326 }
1327 if !dynamics.is_empty() {
1328 return None;
1329 }
1330
1331 Some(result)
1332 }
1333}
1334
1335impl Pattern {
1336 pub fn new(mut pattern: Pattern) -> Vc<Self> {
1337 pattern.normalize();
1338 Pattern::new_internal(pattern)
1339 }
1340}
1341
1342#[turbo_tasks::value_impl]
1343impl Pattern {
1344 #[turbo_tasks::function]
1345 fn new_internal(pattern: Pattern) -> Vc<Self> {
1346 Self::cell(pattern)
1347 }
1348}
1349
1350#[derive(PartialEq, Debug)]
1351enum InNodeModules {
1352 False,
1353 FolderMatched,
1355 FolderSlashMatched,
1357}
1358impl InNodeModules {
1359 fn check(value: &str) -> Self {
1360 if value.ends_with("node_modules/") {
1361 InNodeModules::FolderSlashMatched
1362 } else if value.ends_with("node_modules") {
1363 InNodeModules::FolderMatched
1364 } else {
1365 InNodeModules::False
1366 }
1367 }
1368}
1369
1370#[derive(PartialEq, Debug)]
1371enum MatchResult<'a> {
1372 None,
1374 Partial,
1376 Consumed {
1378 remaining: &'a str,
1380 any_offset: Option<usize>,
1383 in_node_modules: InNodeModules,
1386 },
1387}
1388
1389impl MatchResult<'_> {
1390 fn is_match(&self) -> bool {
1392 match self {
1393 MatchResult::None => false,
1394 MatchResult::Partial => false,
1395 MatchResult::Consumed {
1396 remaining: rem,
1397 any_offset,
1398 in_node_modules: _,
1399 } => {
1400 if let Some(offset) = any_offset {
1401 *offset == rem.len()
1402 } else {
1403 rem.is_empty()
1404 }
1405 }
1406 }
1407 }
1408
1409 fn could_match_others(&self) -> bool {
1412 match self {
1413 MatchResult::None => false,
1414 MatchResult::Partial => true,
1415 MatchResult::Consumed {
1416 remaining: rem,
1417 any_offset,
1418 in_node_modules: _,
1419 } => {
1420 if let Some(offset) = any_offset {
1421 *offset == rem.len()
1422 } else {
1423 false
1424 }
1425 }
1426 }
1427 }
1428
1429 fn could_match(&self) -> bool {
1432 match self {
1433 MatchResult::None => false,
1434 MatchResult::Partial => true,
1435 MatchResult::Consumed {
1436 remaining: rem,
1437 any_offset,
1438 in_node_modules: _,
1439 } => {
1440 if let Some(offset) = any_offset {
1441 *offset == rem.len()
1442 } else {
1443 rem.is_empty()
1444 }
1445 }
1446 }
1447 }
1448}
1449
1450#[derive(PartialEq, Debug)]
1451enum NextConstantUntilResult<'a, 'b> {
1452 NoMatch,
1453 PartialDynamic,
1454 Partial(&'a str, bool),
1455 Consumed(&'b str, Option<usize>),
1456}
1457
1458impl From<RcStr> for Pattern {
1459 fn from(s: RcStr) -> Self {
1460 Pattern::Constant(s)
1461 }
1462}
1463
1464impl Pattern {
1465 pub fn describe_as_string(&self) -> String {
1466 match self {
1467 Pattern::Constant(c) => format!("'{c}'"),
1468 Pattern::Dynamic => "<dynamic>".to_string(),
1469 Pattern::DynamicNoSlash => "<dynamic no slash>".to_string(),
1470 Pattern::Alternatives(list) => format!(
1471 "({})",
1472 list.iter()
1473 .map(|i| i.describe_as_string())
1474 .collect::<Vec<_>>()
1475 .join(" | ")
1476 ),
1477 Pattern::Concatenation(list) => list
1478 .iter()
1479 .map(|i| i.describe_as_string())
1480 .collect::<Vec<_>>()
1481 .join(" "),
1482 }
1483 }
1484}
1485
1486#[derive(
1487 Debug, PartialEq, Eq, Clone, TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode,
1488)]
1489pub enum PatternMatch {
1490 File(RcStr, FileSystemPath),
1491 Directory(RcStr, FileSystemPath),
1492}
1493
1494impl PatternMatch {
1495 pub fn path(&self) -> Vc<FileSystemPath> {
1496 match self {
1497 PatternMatch::File(_, path) | PatternMatch::Directory(_, path) => path.clone().cell(),
1498 }
1499 }
1500
1501 pub fn name(&self) -> &str {
1502 match self {
1503 PatternMatch::File(name, _) | PatternMatch::Directory(name, _) => name.as_str(),
1504 }
1505 }
1506}
1507
1508#[turbo_tasks::value(transparent)]
1511#[derive(Debug)]
1512pub struct PatternMatches(Vec<PatternMatch>);
1513
1514#[turbo_tasks::function]
1523pub async fn read_matches(
1524 lookup_dir: FileSystemPath,
1525 prefix: RcStr,
1526 force_in_lookup_dir: bool,
1527 pattern: Vc<Pattern>,
1528) -> Result<Vc<PatternMatches>> {
1529 let mut prefix = prefix.to_string();
1530 let pat = pattern.await?;
1531 let mut results = Vec::new();
1532 let mut nested = Vec::new();
1533 let slow_path = if let Some(constants) = pat.next_constants(&prefix) {
1534 if constants
1535 .iter()
1536 .all(|(str, until_end)| *until_end || str.contains('/'))
1537 {
1538 let mut handled = FxHashSet::default();
1542 let mut read_dir_results = FxHashMap::default();
1543 for (index, (str, until_end)) in constants.into_iter().enumerate() {
1544 if until_end {
1545 if !handled.insert(str) {
1546 continue;
1547 }
1548 let (parent_path, last_segment) = split_last_segment(str);
1549 if last_segment.is_empty() {
1550 let joined = if force_in_lookup_dir {
1552 lookup_dir.try_join_inside(parent_path)
1553 } else {
1554 lookup_dir.try_join(parent_path)
1555 };
1556 let Some(fs_path) = joined else {
1557 continue;
1558 };
1559 results.push((
1560 index,
1561 PatternMatch::Directory(concat(&prefix, str).into(), fs_path),
1562 ));
1563 continue;
1564 }
1565 let entry = read_dir_results.entry(parent_path);
1566 let read_dir = match entry {
1567 Entry::Occupied(e) => Some(e.into_mut()),
1568 Entry::Vacant(e) => {
1569 let path_option = if force_in_lookup_dir {
1570 lookup_dir.try_join_inside(parent_path)
1571 } else {
1572 lookup_dir.try_join(parent_path)
1573 };
1574 if let Some(path) = path_option {
1575 Some(e.insert((path.raw_read_dir().await?, path)))
1576 } else {
1577 None
1578 }
1579 }
1580 };
1581 let Some((read_dir, parent_fs_path)) = read_dir else {
1582 continue;
1583 };
1584 let RawDirectoryContent::Entries(entries) = &**read_dir else {
1585 continue;
1586 };
1587 let Some(entry) = entries.get(last_segment) else {
1588 continue;
1589 };
1590 match *entry {
1591 RawDirectoryEntry::File => {
1592 results.push((
1593 index,
1594 PatternMatch::File(
1595 concat(&prefix, str).into(),
1596 parent_fs_path.join(last_segment)?,
1597 ),
1598 ));
1599 }
1600 RawDirectoryEntry::Directory => results.push((
1601 index,
1602 PatternMatch::Directory(
1603 concat(&prefix, str).into(),
1604 parent_fs_path.join(last_segment)?,
1605 ),
1606 )),
1607 RawDirectoryEntry::Symlink => {
1608 let fs_path = parent_fs_path.join(last_segment)?;
1609 let LinkContent::Link { link_type, .. } = &*fs_path.read_link().await?
1610 else {
1611 continue;
1612 };
1613 let path = concat(&prefix, str).into();
1614 if link_type.contains(LinkType::DIRECTORY) {
1615 results.push((index, PatternMatch::Directory(path, fs_path)));
1616 } else {
1617 results.push((index, PatternMatch::File(path, fs_path)))
1618 }
1619 }
1620 _ => {}
1621 }
1622 } else {
1623 let subpath = &str[..=str.rfind('/').unwrap()];
1624 if handled.insert(subpath) {
1625 let joined = if force_in_lookup_dir {
1626 lookup_dir.try_join_inside(subpath)
1627 } else {
1628 lookup_dir.try_join(subpath)
1629 };
1630 let Some(fs_path) = joined else {
1631 continue;
1632 };
1633 nested.push((
1634 index,
1635 read_matches(
1636 fs_path.clone(),
1637 concat(&prefix, subpath).into(),
1638 force_in_lookup_dir,
1639 pattern,
1640 ),
1641 ));
1642 }
1643 }
1644 }
1645 false
1646 } else {
1647 true
1648 }
1649 } else {
1650 true
1651 };
1652
1653 if slow_path {
1654 async {
1655 if !force_in_lookup_dir {
1658 prefix.push_str("..");
1660 if let Some(pos) = pat.match_position(&prefix) {
1661 results.push((
1662 pos,
1663 PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1664 ));
1665 }
1666
1667 prefix.push('/');
1669 if let Some(pos) = pat.match_position(&prefix) {
1670 results.push((
1671 pos,
1672 PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1673 ));
1674 }
1675 if let Some(pos) = pat.could_match_position(&prefix) {
1676 nested.push((
1677 pos,
1678 read_matches(lookup_dir.parent(), prefix.clone().into(), false, pattern),
1679 ));
1680 }
1681 prefix.pop();
1682 prefix.pop();
1683 prefix.pop();
1684 }
1685 {
1686 prefix.push('.');
1687 if let Some(pos) = pat.match_position(&prefix) {
1689 results.push((
1690 pos,
1691 PatternMatch::Directory(prefix.clone().into(), lookup_dir.clone()),
1692 ));
1693 }
1694 prefix.pop();
1695 }
1696 if prefix.is_empty() {
1697 if let Some(pos) = pat.match_position("./") {
1698 results.push((
1699 pos,
1700 PatternMatch::Directory(rcstr!("./"), lookup_dir.clone()),
1701 ));
1702 }
1703 if let Some(pos) = pat.could_match_position("./") {
1704 nested.push((
1705 pos,
1706 read_matches(lookup_dir.clone(), rcstr!("./"), false, pattern),
1707 ));
1708 }
1709 } else {
1710 prefix.push('/');
1711 if let Some(pos) = pat.could_match_position(&prefix) {
1713 nested.push((
1714 pos,
1715 read_matches(
1716 lookup_dir.clone(),
1717 prefix.to_string().into(),
1718 false,
1719 pattern,
1720 ),
1721 ));
1722 }
1723 prefix.pop();
1724 prefix.push_str("./");
1725 if let Some(pos) = pat.could_match_position(&prefix) {
1727 nested.push((
1728 pos,
1729 read_matches(
1730 lookup_dir.clone(),
1731 prefix.to_string().into(),
1732 false,
1733 pattern,
1734 ),
1735 ));
1736 }
1737 prefix.pop();
1738 prefix.pop();
1739 }
1740 match &*lookup_dir.raw_read_dir().await? {
1741 RawDirectoryContent::Entries(map) => {
1742 for (key, entry) in map.iter() {
1743 match entry {
1744 RawDirectoryEntry::File => {
1745 let len = prefix.len();
1746 prefix.push_str(key);
1747 if let Some(pos) = pat.match_position(&prefix) {
1749 let path = lookup_dir.join(key)?;
1750 results.push((
1751 pos,
1752 PatternMatch::File(prefix.clone().into(), path),
1753 ));
1754 }
1755 prefix.truncate(len)
1756 }
1757 RawDirectoryEntry::Directory => {
1758 let len = prefix.len();
1759 prefix.push_str(key);
1760 if prefix.ends_with('/') {
1762 prefix.pop();
1763 }
1764 if let Some(pos) = pat.match_position(&prefix) {
1765 let path = lookup_dir.join(key)?;
1766 results.push((
1767 pos,
1768 PatternMatch::Directory(prefix.clone().into(), path),
1769 ));
1770 }
1771 prefix.push('/');
1772 if let Some(pos) = pat.match_position(&prefix) {
1774 let path = lookup_dir.join(key)?;
1775 results.push((
1776 pos,
1777 PatternMatch::Directory(prefix.clone().into(), path),
1778 ));
1779 }
1780 if let Some(pos) = pat.could_match_position(&prefix) {
1781 let path = lookup_dir.join(key)?;
1782 nested.push((
1783 pos,
1784 read_matches(path, prefix.clone().into(), true, pattern),
1785 ));
1786 }
1787 prefix.truncate(len)
1788 }
1789 RawDirectoryEntry::Symlink => {
1790 let len = prefix.len();
1791 prefix.push_str(key);
1792 if prefix.ends_with('/') {
1794 prefix.pop();
1795 }
1796 if let Some(pos) = pat.match_position(&prefix) {
1797 let fs_path = lookup_dir.join(key)?;
1798 if let LinkContent::Link { link_type, .. } =
1799 &*fs_path.read_link().await?
1800 {
1801 if link_type.contains(LinkType::DIRECTORY) {
1802 results.push((
1803 pos,
1804 PatternMatch::Directory(
1805 prefix.clone().into(),
1806 fs_path,
1807 ),
1808 ));
1809 } else {
1810 results.push((
1811 pos,
1812 PatternMatch::File(prefix.clone().into(), fs_path),
1813 ));
1814 }
1815 }
1816 }
1817 prefix.push('/');
1818 if let Some(pos) = pat.match_position(&prefix) {
1819 let fs_path = lookup_dir.join(key)?;
1820 if let LinkContent::Link { link_type, .. } =
1821 &*fs_path.read_link().await?
1822 && link_type.contains(LinkType::DIRECTORY)
1823 {
1824 results.push((
1825 pos,
1826 PatternMatch::Directory(prefix.clone().into(), fs_path),
1827 ));
1828 }
1829 }
1830 if let Some(pos) = pat.could_match_position(&prefix) {
1831 let fs_path = lookup_dir.join(key)?;
1832 if let LinkContent::Link { link_type, .. } =
1833 &*fs_path.read_link().await?
1834 && link_type.contains(LinkType::DIRECTORY)
1835 {
1836 results.push((
1837 pos,
1838 PatternMatch::Directory(prefix.clone().into(), fs_path),
1839 ));
1840 }
1841 }
1842 prefix.truncate(len)
1843 }
1844 RawDirectoryEntry::Other => {}
1845 }
1846 }
1847 }
1848 RawDirectoryContent::NotFound => {}
1849 };
1850 anyhow::Ok(())
1851 }
1852 .instrument(tracing::trace_span!("read_matches slow_path"))
1853 .await?;
1854 }
1855 if results.is_empty() && nested.len() == 1 {
1856 Ok(nested.into_iter().next().unwrap().1)
1857 } else {
1858 for (pos, nested) in nested.into_iter() {
1859 results.extend(nested.await?.iter().cloned().map(|p| (pos, p)));
1860 }
1861 results.sort_by(|(a, am), (b, bm)| (*a).cmp(b).then_with(|| am.name().cmp(bm.name())));
1862 Ok(Vc::cell(
1863 results.into_iter().map(|(_, p)| p).collect::<Vec<_>>(),
1864 ))
1865 }
1866}
1867
1868fn concat(a: &str, b: &str) -> String {
1869 let mut result = String::with_capacity(a.len() + b.len());
1870 result.push_str(a);
1871 result.push_str(b);
1872 result
1873}
1874
1875fn split_last_segment(path: &str) -> (&str, &str) {
1878 if let Some((remaining_path, last_segment)) = path.rsplit_once('/') {
1879 match last_segment {
1880 "" => split_last_segment(remaining_path),
1881 "." => split_last_segment(remaining_path),
1882 ".." => match split_last_segment(remaining_path) {
1883 (_, "") => (path, ""),
1884 (parent_path, _) => split_last_segment(parent_path),
1885 },
1886 _ => (remaining_path, last_segment),
1887 }
1888 } else {
1889 match path {
1890 "" => ("", ""),
1891 "." => ("", ""),
1892 ".." => ("..", ""),
1893 _ => ("", path),
1894 }
1895 }
1896}
1897
1898#[cfg(test)]
1899mod tests {
1900 use std::path::Path;
1901
1902 use rstest::*;
1903 use turbo_rcstr::{RcStr, rcstr};
1904 use turbo_tasks::Vc;
1905 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1906 use turbo_tasks_fs::{DiskFileSystem, FileSystem};
1907
1908 use super::{
1909 Pattern, longest_common_prefix, longest_common_suffix, read_matches, split_last_segment,
1910 };
1911
1912 #[test]
1913 fn longest_common_prefix_test() {
1914 assert_eq!(longest_common_prefix(&["ab"]), "ab");
1915 assert_eq!(longest_common_prefix(&["ab", "cd", "ef"]), "");
1916 assert_eq!(longest_common_prefix(&["ab1", "ab23", "ab456"]), "ab");
1917 assert_eq!(longest_common_prefix(&["abc", "abc", "abc"]), "abc");
1918 assert_eq!(longest_common_prefix(&["abc", "a", "abc"]), "a");
1919 }
1920
1921 #[test]
1922 fn longest_common_suffix_test() {
1923 assert_eq!(longest_common_suffix(&["ab"]), "ab");
1924 assert_eq!(longest_common_suffix(&["ab", "cd", "ef"]), "");
1925 assert_eq!(longest_common_suffix(&["1ab", "23ab", "456ab"]), "ab");
1926 assert_eq!(longest_common_suffix(&["abc", "abc", "abc"]), "abc");
1927 assert_eq!(longest_common_suffix(&["abc", "c", "abc"]), "c");
1928 }
1929
1930 #[test]
1931 fn normalize() {
1932 let a = Pattern::Constant(rcstr!("a"));
1933 let b = Pattern::Constant(rcstr!("b"));
1934 let c = Pattern::Constant(rcstr!("c"));
1935 let s = Pattern::Constant(rcstr!("/"));
1936 let d = Pattern::Dynamic;
1937 {
1938 let mut p = Pattern::Concatenation(vec![
1939 Pattern::Alternatives(vec![a.clone(), b.clone()]),
1940 s.clone(),
1941 c.clone(),
1942 ]);
1943 p.normalize();
1944 assert_eq!(
1945 p,
1946 Pattern::Alternatives(vec![
1947 Pattern::Constant(rcstr!("a/c")),
1948 Pattern::Constant(rcstr!("b/c")),
1949 ])
1950 );
1951 }
1952
1953 #[allow(clippy::redundant_clone)] {
1955 let mut p = Pattern::Concatenation(vec![
1956 Pattern::Alternatives(vec![a.clone(), b.clone(), d.clone()]),
1957 s.clone(),
1958 Pattern::Alternatives(vec![b.clone(), c.clone(), d.clone()]),
1959 ]);
1960 p.normalize();
1961
1962 assert_eq!(
1963 p,
1964 Pattern::Alternatives(vec![
1965 Pattern::Constant(rcstr!("a/b")),
1966 Pattern::Constant(rcstr!("b/b")),
1967 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/b"))]),
1968 Pattern::Constant(rcstr!("a/c")),
1969 Pattern::Constant(rcstr!("b/c")),
1970 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/c"))]),
1971 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
1972 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
1973 Pattern::Concatenation(vec![
1974 Pattern::Dynamic,
1975 Pattern::Constant(rcstr!("/")),
1976 Pattern::Dynamic
1977 ]),
1978 ])
1979 );
1980 }
1981
1982 #[allow(clippy::redundant_clone)] {
1984 let mut p = Pattern::Alternatives(vec![a.clone()]);
1985 p.normalize();
1986
1987 assert_eq!(p, a);
1988 }
1989
1990 #[allow(clippy::redundant_clone)] {
1992 let mut p = Pattern::Alternatives(vec![Pattern::Dynamic, Pattern::Dynamic]);
1993 p.normalize();
1994
1995 assert_eq!(p, Pattern::Dynamic);
1996 }
1997 }
1998
1999 #[test]
2000 fn with_normalized_path() {
2001 assert!(
2002 Pattern::Constant(rcstr!("a/../.."))
2003 .with_normalized_path()
2004 .is_none()
2005 );
2006 assert_eq!(
2007 Pattern::Constant(rcstr!("a/b/../c"))
2008 .with_normalized_path()
2009 .unwrap(),
2010 Pattern::Constant(rcstr!("a/c"))
2011 );
2012 assert_eq!(
2013 Pattern::Alternatives(vec![
2014 Pattern::Constant(rcstr!("a/b/../c")),
2015 Pattern::Constant(rcstr!("a/b/../c/d"))
2016 ])
2017 .with_normalized_path()
2018 .unwrap(),
2019 Pattern::Alternatives(vec![
2020 Pattern::Constant(rcstr!("a/c")),
2021 Pattern::Constant(rcstr!("a/c/d"))
2022 ])
2023 );
2024 assert_eq!(
2025 Pattern::Constant(rcstr!("a/b/"))
2026 .with_normalized_path()
2027 .unwrap(),
2028 Pattern::Constant(rcstr!("a/b"))
2029 );
2030
2031 assert_eq!(
2033 Pattern::Concatenation(vec![
2034 Pattern::Constant(rcstr!("a/b/")),
2035 Pattern::Dynamic,
2036 Pattern::Constant(rcstr!("../c"))
2037 ])
2038 .with_normalized_path()
2039 .unwrap(),
2040 Pattern::Concatenation(vec![
2041 Pattern::Constant(rcstr!("a/b/")),
2042 Pattern::Dynamic,
2043 Pattern::Constant(rcstr!("../c"))
2044 ])
2045 );
2046
2047 assert_eq!(
2049 Pattern::Concatenation(vec![
2050 Pattern::Constant(rcstr!("a/b")),
2051 Pattern::Dynamic,
2052 Pattern::Constant(rcstr!("../c"))
2053 ])
2054 .with_normalized_path()
2055 .unwrap(),
2056 Pattern::Concatenation(vec![
2057 Pattern::Constant(rcstr!("a/b")),
2058 Pattern::Dynamic,
2059 Pattern::Constant(rcstr!("../c"))
2060 ])
2061 );
2062 assert_eq!(
2063 Pattern::Concatenation(vec![
2064 Pattern::Constant(rcstr!("src/")),
2065 Pattern::Dynamic,
2066 Pattern::Constant(rcstr!(".js"))
2067 ])
2068 .with_normalized_path()
2069 .unwrap(),
2070 Pattern::Concatenation(vec![
2071 Pattern::Constant(rcstr!("src/")),
2072 Pattern::Dynamic,
2073 Pattern::Constant(rcstr!(".js"))
2074 ])
2075 );
2076 }
2077
2078 #[test]
2079 fn is_match() {
2080 let pat = Pattern::Concatenation(vec![
2081 Pattern::Constant(rcstr!(".")),
2082 Pattern::Constant(rcstr!("/")),
2083 Pattern::Dynamic,
2084 Pattern::Constant(rcstr!(".js")),
2085 ]);
2086 assert!(pat.could_match(""));
2087 assert!(pat.could_match("./"));
2088 assert!(!pat.is_match("./"));
2089 assert!(pat.is_match("./index.js"));
2090 assert!(!pat.is_match("./index"));
2091 assert!(pat.is_match("./foo/index.js"));
2092 assert!(pat.is_match("./foo/bar/index.js"));
2093
2094 assert!(!pat.is_match("./../index.js"));
2096 assert!(!pat.is_match("././index.js"));
2097 assert!(!pat.is_match("./.git/index.js"));
2098 assert!(!pat.is_match("./inner/../index.js"));
2099 assert!(!pat.is_match("./inner/./index.js"));
2100 assert!(!pat.is_match("./inner/.git/index.js"));
2101 assert!(!pat.could_match("./../"));
2102 assert!(!pat.could_match("././"));
2103 assert!(!pat.could_match("./.git/"));
2104 assert!(!pat.could_match("./inner/../"));
2105 assert!(!pat.could_match("./inner/./"));
2106 assert!(!pat.could_match("./inner/.git/"));
2107 }
2108
2109 #[test]
2110 fn is_match_dynamic_no_slash() {
2111 let pat = Pattern::Concatenation(vec![
2112 Pattern::Constant(rcstr!(".")),
2113 Pattern::Constant(rcstr!("/")),
2114 Pattern::DynamicNoSlash,
2115 Pattern::Constant(rcstr!(".js")),
2116 ]);
2117 assert!(pat.could_match(""));
2118 assert!(pat.could_match("./"));
2119 assert!(!pat.is_match("./"));
2120 assert!(pat.is_match("./index.js"));
2121 assert!(!pat.is_match("./index"));
2122 assert!(!pat.is_match("./foo/index.js"));
2123 assert!(!pat.is_match("./foo/bar/index.js"));
2124 }
2125
2126 #[test]
2127 fn constant_prefix() {
2128 assert_eq!(
2129 Pattern::Constant(rcstr!("a/b/c.js")).constant_prefix(),
2130 "a/b/c.js",
2131 );
2132
2133 let pat = Pattern::Alternatives(vec![
2134 Pattern::Constant(rcstr!("a/b/x")),
2135 Pattern::Constant(rcstr!("a/b/y")),
2136 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]),
2137 ]);
2138 assert_eq!(pat.constant_prefix(), "a/b/");
2139 }
2140
2141 #[test]
2142 fn constant_suffix() {
2143 assert_eq!(
2144 Pattern::Constant(rcstr!("a/b/c.js")).constant_suffix(),
2145 "a/b/c.js",
2146 );
2147
2148 let pat = Pattern::Alternatives(vec![
2149 Pattern::Constant(rcstr!("a/b/x.js")),
2150 Pattern::Constant(rcstr!("a/b/y.js")),
2151 Pattern::Concatenation(vec![
2152 Pattern::Constant(rcstr!("a/b/c/")),
2153 Pattern::Dynamic,
2154 Pattern::Constant(rcstr!(".js")),
2155 ]),
2156 ]);
2157 assert_eq!(pat.constant_suffix(), ".js");
2158 }
2159
2160 #[test]
2161 fn strip_prefix() {
2162 fn strip(mut pat: Pattern, n: usize) -> Pattern {
2163 pat.strip_prefix_len(n).unwrap();
2164 pat
2165 }
2166
2167 assert_eq!(
2168 strip(Pattern::Constant(rcstr!("a/b")), 0),
2169 Pattern::Constant(rcstr!("a/b"))
2170 );
2171
2172 assert_eq!(
2173 strip(
2174 Pattern::Alternatives(vec![
2175 Pattern::Constant(rcstr!("a/b/x")),
2176 Pattern::Constant(rcstr!("a/b/y")),
2177 ]),
2178 2
2179 ),
2180 Pattern::Alternatives(vec![
2181 Pattern::Constant(rcstr!("b/x")),
2182 Pattern::Constant(rcstr!("b/y")),
2183 ])
2184 );
2185
2186 assert_eq!(
2187 strip(
2188 Pattern::Concatenation(vec![
2189 Pattern::Constant(rcstr!("a/")),
2190 Pattern::Constant(rcstr!("b")),
2191 Pattern::Constant(rcstr!("/")),
2192 Pattern::Constant(rcstr!("y/")),
2193 Pattern::Dynamic
2194 ]),
2195 4
2196 ),
2197 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("y/")), Pattern::Dynamic]),
2198 );
2199 }
2200
2201 #[test]
2202 fn strip_suffix() {
2203 fn strip(mut pat: Pattern, n: usize) -> Pattern {
2204 pat.strip_suffix_len(n);
2205 pat
2206 }
2207
2208 assert_eq!(
2209 strip(Pattern::Constant(rcstr!("a/b")), 0),
2210 Pattern::Constant(rcstr!("a/b"))
2211 );
2212
2213 assert_eq!(
2214 strip(
2215 Pattern::Alternatives(vec![
2216 Pattern::Constant(rcstr!("x/b/a")),
2217 Pattern::Constant(rcstr!("y/b/a")),
2218 ]),
2219 2
2220 ),
2221 Pattern::Alternatives(vec![
2222 Pattern::Constant(rcstr!("x/b")),
2223 Pattern::Constant(rcstr!("y/b")),
2224 ])
2225 );
2226
2227 assert_eq!(
2228 strip(
2229 Pattern::Concatenation(vec![
2230 Pattern::Dynamic,
2231 Pattern::Constant(rcstr!("/a/")),
2232 Pattern::Constant(rcstr!("b")),
2233 Pattern::Constant(rcstr!("/")),
2234 Pattern::Constant(rcstr!("y/")),
2235 ]),
2236 4
2237 ),
2238 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/a/")),]),
2239 );
2240 }
2241
2242 #[test]
2243 fn spread_into_star() {
2244 let pat = Pattern::Constant(rcstr!("xyz"));
2245 assert_eq!(
2246 pat.spread_into_star("before/after"),
2247 Pattern::Constant(rcstr!("before/after")),
2248 );
2249
2250 let pat =
2251 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]);
2252 assert_eq!(
2253 pat.spread_into_star("before/*/after"),
2254 Pattern::Concatenation(vec![
2255 Pattern::Constant(rcstr!("before/a/b/c/")),
2256 Pattern::Dynamic,
2257 Pattern::Constant(rcstr!("/after"))
2258 ])
2259 );
2260
2261 let pat = Pattern::Alternatives(vec![
2262 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
2263 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
2264 ]);
2265 assert_eq!(
2266 pat.spread_into_star("before/*/after"),
2267 Pattern::Alternatives(vec![
2268 Pattern::Concatenation(vec![
2269 Pattern::Constant(rcstr!("before/a/")),
2270 Pattern::Dynamic,
2271 Pattern::Constant(rcstr!("/after"))
2272 ]),
2273 Pattern::Concatenation(vec![
2274 Pattern::Constant(rcstr!("before/b/")),
2275 Pattern::Dynamic,
2276 Pattern::Constant(rcstr!("/after"))
2277 ]),
2278 ])
2279 );
2280
2281 let pat = Pattern::Alternatives(vec![
2282 Pattern::Constant(rcstr!("a")),
2283 Pattern::Constant(rcstr!("b")),
2284 ]);
2285 assert_eq!(
2286 pat.spread_into_star("before/*/*"),
2287 Pattern::Alternatives(vec![
2288 Pattern::Constant(rcstr!("before/a/a")),
2289 Pattern::Constant(rcstr!("before/b/b")),
2290 ])
2291 );
2292
2293 let pat = Pattern::Dynamic;
2294 assert_eq!(
2295 pat.spread_into_star("before/*/*"),
2296 Pattern::Concatenation(vec![
2297 Pattern::Constant(rcstr!("before/")),
2299 Pattern::Dynamic,
2300 Pattern::Constant(rcstr!("/")),
2301 Pattern::Dynamic
2302 ])
2303 );
2304 }
2305
2306 #[rstest]
2307 #[case::dynamic(Pattern::Dynamic)]
2308 #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2309 fn dynamic_match(#[case] pat: Pattern) {
2310 assert!(pat.could_match(""));
2311 assert!(pat.is_match("index.js"));
2312
2313 assert!(!pat.could_match("./"));
2315 assert!(!pat.is_match("./"));
2316 assert!(!pat.could_match("."));
2317 assert!(!pat.is_match("."));
2318 assert!(!pat.could_match("../"));
2319 assert!(!pat.is_match("../"));
2320 assert!(!pat.could_match(".."));
2321 assert!(!pat.is_match(".."));
2322 assert!(!pat.is_match("./../index.js"));
2323 assert!(!pat.is_match("././index.js"));
2324 assert!(!pat.is_match("./.git/index.js"));
2325 assert!(!pat.is_match("./inner/../index.js"));
2326 assert!(!pat.is_match("./inner/./index.js"));
2327 assert!(!pat.is_match("./inner/.git/index.js"));
2328 assert!(!pat.could_match("./../"));
2329 assert!(!pat.could_match("././"));
2330 assert!(!pat.could_match("./.git/"));
2331 assert!(!pat.could_match("./inner/../"));
2332 assert!(!pat.could_match("./inner/./"));
2333 assert!(!pat.could_match("./inner/.git/"));
2334 assert!(!pat.could_match("dir//"));
2335 assert!(!pat.could_match("dir//dir"));
2336 assert!(!pat.could_match("dir///dir"));
2337 assert!(!pat.could_match("/"));
2338 assert!(!pat.could_match("//"));
2339 assert!(!pat.could_match("/ROOT/"));
2340
2341 assert!(!pat.could_match("node_modules"));
2342 assert!(!pat.could_match("node_modules/package"));
2343 assert!(!pat.could_match("nested/node_modules"));
2344 assert!(!pat.could_match("nested/node_modules/package"));
2345
2346 assert!(pat.could_match("file.map"));
2348 assert!(!pat.is_match("file.map"));
2349 assert!(pat.is_match("file.map/file.js"));
2350 assert!(!pat.is_match("file.d.ts"));
2351 assert!(!pat.is_match("file.d.ts.map"));
2352 assert!(!pat.is_match("file.d.ts.map"));
2353 assert!(!pat.is_match("dir/file.d.ts.map"));
2354 assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2355 assert!(pat.could_match("dir/inner/file.d.ts.map"));
2356 }
2357
2358 #[rstest]
2359 #[case::slash(Pattern::Concatenation(vec![Pattern::Constant(rcstr!("node_modules/")),Pattern::Dynamic]))]
2360 #[case::nested(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file())]
2361 fn dynamic_match_node_modules(#[case] pat: Pattern) {
2362 assert!(!pat.is_match("node_modules/package"));
2363 assert!(!pat.could_match("node_modules/package"));
2364 assert!(!pat.is_match("node_modules/package/index.js"));
2365 assert!(!pat.could_match("node_modules/package/index.js"));
2366 }
2367
2368 #[rstest]
2369 fn dynamic_match2() {
2370 let pat = Pattern::Concatenation(vec![
2371 Pattern::Dynamic,
2372 Pattern::Constant(rcstr!("/")),
2373 Pattern::Dynamic,
2374 ]);
2375 assert!(pat.could_match("dir"));
2376 assert!(pat.could_match("dir/"));
2377 assert!(pat.is_match("dir/index.js"));
2378
2379 assert!(!pat.could_match("./"));
2381 assert!(!pat.is_match("./"));
2382 assert!(!pat.could_match("."));
2383 assert!(!pat.is_match("."));
2384 assert!(!pat.could_match("../"));
2385 assert!(!pat.is_match("../"));
2386 assert!(!pat.could_match(".."));
2387 assert!(!pat.is_match(".."));
2388 assert!(!pat.is_match("./../index.js"));
2389 assert!(!pat.is_match("././index.js"));
2390 assert!(!pat.is_match("./.git/index.js"));
2391 assert!(!pat.is_match("./inner/../index.js"));
2392 assert!(!pat.is_match("./inner/./index.js"));
2393 assert!(!pat.is_match("./inner/.git/index.js"));
2394 assert!(!pat.could_match("./../"));
2395 assert!(!pat.could_match("././"));
2396 assert!(!pat.could_match("./.git/"));
2397 assert!(!pat.could_match("./inner/../"));
2398 assert!(!pat.could_match("./inner/./"));
2399 assert!(!pat.could_match("./inner/.git/"));
2400 assert!(!pat.could_match("dir//"));
2401 assert!(!pat.could_match("dir//dir"));
2402 assert!(!pat.could_match("dir///dir"));
2403 assert!(!pat.could_match("/ROOT/"));
2404
2405 assert!(!pat.could_match("node_modules"));
2406 assert!(!pat.could_match("node_modules/package"));
2407 assert!(!pat.could_match("nested/node_modules"));
2408 assert!(!pat.could_match("nested/node_modules/package"));
2409
2410 assert!(pat.could_match("dir/file.map"));
2412 assert!(!pat.is_match("dir/file.map"));
2413 assert!(pat.is_match("file.map/file.js"));
2414 assert!(!pat.is_match("dir/file.d.ts"));
2415 assert!(!pat.is_match("dir/file.d.ts.map"));
2416 assert!(!pat.is_match("dir/file.d.ts.map"));
2417 assert!(!pat.is_match("dir/file.d.ts.map"));
2418 assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2419 assert!(pat.could_match("dir/inner/file.d.ts.map"));
2420 }
2421
2422 #[rstest]
2423 #[case::dynamic(Pattern::Dynamic)]
2424 #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2425 #[case::dynamic_concat2(Pattern::Concatenation(vec![
2426 Pattern::Dynamic,
2427 Pattern::Constant(rcstr!("/")),
2428 Pattern::Dynamic,
2429 ]))]
2430 #[case::dynamic_alt_concat(Pattern::alternatives(vec![
2431 Pattern::Concatenation(vec![
2432 Pattern::Dynamic,
2433 Pattern::Constant(rcstr!("/")),
2434 Pattern::Dynamic,
2435 ]),
2436 Pattern::Dynamic,
2437 ]))]
2438 fn split_could_match(#[case] pat: Pattern) {
2439 let (abs, rel) = pat.split_could_match("/ROOT/");
2440 assert!(abs.is_none());
2441 assert!(rel.is_some());
2442 }
2443
2444 #[rstest]
2445 #[case::dynamic(Pattern::Dynamic, "feijf", None)]
2446 #[case::dynamic_concat(
2447 Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]),
2448 "hello.", None
2449 )]
2450 #[case::constant(Pattern::Constant(rcstr!("Hello World")), "Hello ", Some(vec![("World", true)]))]
2451 #[case::alternatives(
2452 Pattern::Alternatives(vec![
2453 Pattern::Constant(rcstr!("Hello World")),
2454 Pattern::Constant(rcstr!("Hello All"))
2455 ]), "Hello ", Some(vec![("World", true), ("All", true)])
2456 )]
2457 #[case::alternatives_non_end(
2458 Pattern::Alternatives(vec![
2459 Pattern::Constant(rcstr!("Hello World")),
2460 Pattern::Constant(rcstr!("Hello All")),
2461 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("Hello more")), Pattern::Dynamic])
2462 ]), "Hello ", Some(vec![("World", true), ("All", true), ("more", false)])
2463 )]
2464 #[case::request_with_extensions(
2465 Pattern::Alternatives(vec![
2466 Pattern::Constant(rcstr!("./file.js")),
2467 Pattern::Constant(rcstr!("./file.ts")),
2468 Pattern::Constant(rcstr!("./file.cjs")),
2469 ]), "./", Some(vec![("file.js", true), ("file.ts", true), ("file.cjs", true)])
2470 )]
2471 fn next_constants(
2472 #[case] pat: Pattern,
2473 #[case] value: &str,
2474 #[case] expected: Option<Vec<(&str, bool)>>,
2475 ) {
2476 assert_eq!(pat.next_constants(value), expected);
2477 }
2478
2479 #[test]
2480 fn replace_final_constants() {
2481 fn f(mut p: Pattern, cb: &mut impl FnMut(&RcStr) -> Option<Pattern>) -> Pattern {
2482 p.replace_final_constants(cb);
2483 p
2484 }
2485
2486 let mut js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
2487 c.strip_suffix(".js").map(|rest| {
2488 let new_ending = Pattern::Alternatives(vec![
2489 Pattern::Constant(rcstr!(".ts")),
2490 Pattern::Constant(rcstr!(".tsx")),
2491 Pattern::Constant(rcstr!(".js")),
2492 ]);
2493 if !rest.is_empty() {
2494 Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
2495 } else {
2496 new_ending
2497 }
2498 })
2499 };
2500
2501 assert_eq!(
2502 f(
2503 Pattern::Concatenation(vec![
2504 Pattern::Constant(rcstr!(".")),
2505 Pattern::Constant(rcstr!("/")),
2506 Pattern::Dynamic,
2507 Pattern::Alternatives(vec![
2508 Pattern::Constant(rcstr!(".js")),
2509 Pattern::Constant(rcstr!(".node")),
2510 ])
2511 ]),
2512 &mut js_to_ts_tsx
2513 ),
2514 Pattern::Concatenation(vec![
2515 Pattern::Constant(rcstr!(".")),
2516 Pattern::Constant(rcstr!("/")),
2517 Pattern::Dynamic,
2518 Pattern::Alternatives(vec![
2519 Pattern::Alternatives(vec![
2520 Pattern::Constant(rcstr!(".ts")),
2521 Pattern::Constant(rcstr!(".tsx")),
2522 Pattern::Constant(rcstr!(".js")),
2523 ]),
2524 Pattern::Constant(rcstr!(".node")),
2525 ])
2526 ]),
2527 );
2528 assert_eq!(
2529 f(
2530 Pattern::Concatenation(vec![
2531 Pattern::Constant(rcstr!(".")),
2532 Pattern::Constant(rcstr!("/")),
2533 Pattern::Constant(rcstr!("abc.js")),
2534 ]),
2535 &mut js_to_ts_tsx
2536 ),
2537 Pattern::Concatenation(vec![
2538 Pattern::Constant(rcstr!(".")),
2539 Pattern::Constant(rcstr!("/")),
2540 Pattern::Concatenation(vec![
2541 Pattern::Constant(rcstr!("abc")),
2542 Pattern::Alternatives(vec![
2543 Pattern::Constant(rcstr!(".ts")),
2544 Pattern::Constant(rcstr!(".tsx")),
2545 Pattern::Constant(rcstr!(".js")),
2546 ])
2547 ]),
2548 ])
2549 );
2550 }
2551
2552 #[test]
2553 fn match_apply_template() {
2554 assert_eq!(
2555 Pattern::Concatenation(vec![
2556 Pattern::Constant(rcstr!("a/b/")),
2557 Pattern::Dynamic,
2558 Pattern::Constant(rcstr!(".ts")),
2559 ])
2560 .match_apply_template(
2561 "a/b/foo.ts",
2562 &Pattern::Concatenation(vec![
2563 Pattern::Constant(rcstr!("@/a/b/")),
2564 Pattern::Dynamic,
2565 Pattern::Constant(rcstr!(".js")),
2566 ])
2567 )
2568 .as_deref(),
2569 Some("@/a/b/foo.js")
2570 );
2571 assert_eq!(
2572 Pattern::Concatenation(vec![
2573 Pattern::Constant(rcstr!("b/")),
2574 Pattern::Dynamic,
2575 Pattern::Constant(rcstr!(".ts")),
2576 ])
2577 .match_apply_template(
2578 "a/b/foo.ts",
2579 &Pattern::Concatenation(vec![
2580 Pattern::Constant(rcstr!("@/a/b/")),
2581 Pattern::Dynamic,
2582 Pattern::Constant(rcstr!(".js")),
2583 ])
2584 )
2585 .as_deref(),
2586 None,
2587 );
2588 assert_eq!(
2589 Pattern::Concatenation(vec![
2590 Pattern::Constant(rcstr!("a/b/")),
2591 Pattern::Dynamic,
2592 Pattern::Constant(rcstr!(".ts")),
2593 ])
2594 .match_apply_template(
2595 "a/b/foo.ts",
2596 &Pattern::Concatenation(vec![
2597 Pattern::Constant(rcstr!("@/a/b/x")),
2598 Pattern::Constant(rcstr!(".js")),
2599 ])
2600 )
2601 .as_deref(),
2602 None,
2603 );
2604 assert_eq!(
2605 Pattern::Concatenation(vec![Pattern::Constant(rcstr!("./sub/")), Pattern::Dynamic])
2606 .match_apply_template(
2607 "./sub/file1",
2608 &Pattern::Concatenation(vec![
2609 Pattern::Constant(rcstr!("@/sub/")),
2610 Pattern::Dynamic
2611 ])
2612 )
2613 .as_deref(),
2614 Some("@/sub/file1"),
2615 );
2616 }
2617
2618 #[test]
2619 fn test_split_last_segment() {
2620 assert_eq!(split_last_segment(""), ("", ""));
2621 assert_eq!(split_last_segment("a"), ("", "a"));
2622 assert_eq!(split_last_segment("a/"), ("", "a"));
2623 assert_eq!(split_last_segment("a/b"), ("a", "b"));
2624 assert_eq!(split_last_segment("a/b/"), ("a", "b"));
2625 assert_eq!(split_last_segment("a/b/c"), ("a/b", "c"));
2626 assert_eq!(split_last_segment("a/b/."), ("a", "b"));
2627 assert_eq!(split_last_segment("a/b/.."), ("", "a"));
2628 assert_eq!(split_last_segment("a/b/c/.."), ("a", "b"));
2629 assert_eq!(split_last_segment("a/b/c/../.."), ("", "a"));
2630 assert_eq!(split_last_segment("a/b/c/d/../.."), ("a", "b"));
2631 assert_eq!(split_last_segment("a/b/c/../d/.."), ("a", "b"));
2632 assert_eq!(split_last_segment("a/b/../c/d/.."), ("a/b/..", "c"));
2633 assert_eq!(split_last_segment("."), ("", ""));
2634 assert_eq!(split_last_segment("./"), ("", ""));
2635 assert_eq!(split_last_segment(".."), ("..", ""));
2636 assert_eq!(split_last_segment("../"), ("..", ""));
2637 assert_eq!(split_last_segment("./../"), ("./..", ""));
2638 assert_eq!(split_last_segment("../../"), ("../..", ""));
2639 assert_eq!(split_last_segment("../../."), ("../..", ""));
2640 assert_eq!(split_last_segment("../.././"), ("../..", ""));
2641 assert_eq!(split_last_segment("a/.."), ("", ""));
2642 assert_eq!(split_last_segment("a/../"), ("", ""));
2643 assert_eq!(split_last_segment("a/../.."), ("a/../..", ""));
2644 assert_eq!(split_last_segment("a/../../"), ("a/../..", ""));
2645 assert_eq!(split_last_segment("a/././../"), ("", ""));
2646 assert_eq!(split_last_segment("../a"), ("..", "a"));
2647 assert_eq!(split_last_segment("../a/"), ("..", "a"));
2648 assert_eq!(split_last_segment("../../a"), ("../..", "a"));
2649 assert_eq!(split_last_segment("../../a/"), ("../..", "a"));
2650 }
2651
2652 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2653 async fn test_read_matches() {
2654 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2655 BackendOptions::default(),
2656 noop_backing_storage(),
2657 ));
2658 tt.run_once(async {
2659 #[turbo_tasks::value]
2660 struct ReadMatchesOutput {
2661 dynamic: Vec<String>,
2662 dynamic_file_suffix: Vec<String>,
2663 node_modules_dynamic: Vec<String>,
2664 extension_ordering: Vec<String>,
2665 subpath_ordering: Vec<String>,
2666 }
2667
2668 #[turbo_tasks::function(operation, root)]
2669 async fn read_matches_operation() -> anyhow::Result<Vc<ReadMatchesOutput>> {
2670 let root = DiskFileSystem::new(
2671 rcstr!("test"),
2672 Vc::cell(
2673 Path::new(env!("CARGO_MANIFEST_DIR"))
2674 .join("tests/pattern/read_matches")
2675 .to_str()
2676 .unwrap()
2677 .into(),
2678 ),
2679 )
2680 .root()
2681 .owned()
2682 .await?;
2683
2684 let dynamic = read_matches(
2685 root.clone(),
2686 rcstr!(""),
2687 false,
2688 Pattern::new(Pattern::Dynamic),
2689 )
2690 .await?
2691 .into_iter()
2692 .map(|m| m.name().to_string())
2693 .collect::<Vec<_>>();
2694
2695 let dynamic_file_suffix = read_matches(
2696 root.clone(),
2697 rcstr!(""),
2698 false,
2699 Pattern::new(Pattern::concat([
2700 Pattern::Constant(rcstr!("sub/foo")),
2701 Pattern::Dynamic,
2702 ])),
2703 )
2704 .await?
2705 .into_iter()
2706 .map(|m| m.name().to_string())
2707 .collect::<Vec<_>>();
2708
2709 let node_modules_dynamic = read_matches(
2710 root.clone(),
2711 rcstr!(""),
2712 false,
2713 Pattern::new(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file()),
2714 )
2715 .await?
2716 .into_iter()
2717 .map(|m| m.name().to_string())
2718 .collect::<Vec<_>>();
2719
2720 let extension_ordering = read_matches(
2724 root.clone(),
2725 rcstr!(""),
2726 false,
2727 Pattern::new(Pattern::Alternatives(vec![
2728 Pattern::Constant(rcstr!("extensions/Component")),
2729 Pattern::Constant(rcstr!("extensions/Component.web.tsx")),
2730 Pattern::Constant(rcstr!("extensions/Component.tsx")),
2731 ])),
2732 )
2733 .await?
2734 .into_iter()
2735 .map(|m| m.name().to_string())
2736 .collect::<Vec<_>>();
2737
2738 let subpath_ordering = read_matches(
2742 root.clone(),
2743 rcstr!(""),
2744 false,
2745 Pattern::new({
2746 let mut p = Pattern::Alternatives(vec![
2747 Pattern::Concatenation(vec![
2748 Pattern::Constant(rcstr!("prio/a/")),
2749 Pattern::Dynamic,
2750 ]),
2751 Pattern::Concatenation(vec![
2752 Pattern::Constant(rcstr!("prio/b/")),
2753 Pattern::Dynamic,
2754 ]),
2755 ]);
2756 p.normalize();
2757 p
2758 }),
2759 )
2760 .await?
2761 .into_iter()
2762 .map(|m| m.name().to_string())
2763 .collect::<Vec<_>>();
2764
2765 Ok(ReadMatchesOutput {
2766 dynamic,
2767 dynamic_file_suffix,
2768 node_modules_dynamic,
2769 extension_ordering,
2770 subpath_ordering,
2771 }
2772 .cell())
2773 }
2774
2775 let matches = read_matches_operation().read_strongly_consistent().await?;
2776
2777 assert_eq!(
2779 matches.dynamic,
2780 &[
2781 "extensions",
2782 "extensions/",
2783 "extensions/Component.tsx",
2784 "extensions/Component.web.tsx",
2785 "index.js",
2786 "prio",
2787 "prio/",
2788 "prio/a",
2789 "prio/a/",
2790 "prio/a/Component.tsx",
2791 "prio/b",
2792 "prio/b/",
2793 "prio/b/Component.tsx",
2794 "sub",
2795 "sub/",
2796 "sub/foo-a.js",
2797 "sub/foo-b.js",
2798 ]
2799 );
2800
2801 assert_eq!(
2803 matches.dynamic_file_suffix,
2804 &["sub/foo-a.js", "sub/foo-b.js"]
2805 );
2806
2807 assert_eq!(matches.node_modules_dynamic, &["node_modules"]);
2810
2811 assert_eq!(
2813 matches.extension_ordering,
2814 &["extensions/Component.web.tsx", "extensions/Component.tsx",]
2815 );
2816
2817 assert!(
2821 matches
2822 .subpath_ordering
2823 .iter()
2824 .position(|s| s.starts_with("prio/a/"))
2825 .unwrap()
2826 < matches
2827 .subpath_ordering
2828 .iter()
2829 .position(|s| s.starts_with("prio/b/"))
2830 .unwrap(),
2831 "Expected prio/a/ results before prio/b/ results, got: {:?}",
2832 matches.subpath_ordering
2833 );
2834
2835 Ok(())
2836 })
2837 .await
2838 .unwrap();
2839 }
2840}