turbopack_core/resolve/
pattern.rs

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)]
23pub enum Pattern {
24    Constant(RcStr),
25    #[default]
26    Dynamic,
27    DynamicNoSlash,
28    Alternatives(Vec<Pattern>),
29    Concatenation(Vec<Pattern>),
30}
31
32/// manually implement TaskInput to avoid recursion in the implementation of `resolve_input` in the
33/// derived implementation.  We can instead use the default implementation since `Pattern` contains
34/// no VCs.
35impl TaskInput for Pattern {
36    fn is_transient(&self) -> bool {
37        // We contain no vcs so they cannot be transient.
38        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            // TODO these are Unicode Scalar Values, not graphemes
92            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            // TODO these are Unicode Scalar Values, not graphemes
111            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    // TODO this should be removed in favor of pattern resolving
123    pub fn as_constant_string(&self) -> Option<&RcStr> {
124        match self {
125            Pattern::Constant(str) => Some(str),
126            _ => None,
127        }
128    }
129
130    /// Whether the pattern has any significant constant parts (everything except `/`).
131    /// E.g. `<dynamic>/<dynamic>` doesn't really have constant parts
132    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        // The normalized pattern is an Alternative of maximally merged
154        // Concatenations, so extracting the first/only Concatenation child
155        // elements is enough.
156
157        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        // The normalized pattern is an Alternative of maximally merged
195        // Concatenations, so extracting the first/only Concatenation child
196        // elements is enough.
197
198        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    /// Replace all `*`s in `template` with self.
346    ///
347    /// Handle top-level alternatives separately so that multiple star placeholders
348    /// match the same pattern instead of the whole alternative.
349    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                // There are at least two elements in the iterator
361                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    /// Appends something to end the pattern.
380    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    /// Appends something to end the pattern.
397    pub fn push(&mut self, pat: Pattern) {
398        if let Pattern::Constant(this) = &*self
399            && this.is_empty()
400        {
401            // Short-circuit to replace empty constants with the appended pattern
402            *self = pat;
403            return;
404        }
405        if let Pattern::Constant(pat) = &pat
406            && pat.is_empty()
407        {
408            // Short-circuit to ignore when trying to append an empty string.
409            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    /// Prepends something to front of the pattern.
435    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    /// Normalizes paths by
482    /// - processing path segments: `.` and `..`
483    /// - normalizing windows filepaths by replacing `\` with `/`
484    ///
485    /// The Pattern must have already been processed by [Self::normalize].
486    /// Returns [Option::None] if any of the patterns attempt to navigate out of the root.
487    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                                            // Ignore empty segments
514                                            continue;
515                                        }
516                                        ".." => {
517                                            if segments.is_empty() {
518                                                // Leaving root
519                                                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                                                // Resolve `foo/..`
528                                                segments.truncate(segments.len() - 2);
529                                                continue;
530                                            }
531
532                                            // Keep it, can't pop non-constant segment.
533                                            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                                        // If not last, add separator
546                                        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    /// Order into Alternatives -> Concatenation -> Constant/Dynamic
591    /// Merge when possible
592    pub fn normalize(&mut self) {
593        match self {
594            Pattern::Dynamic | Pattern::DynamicNoSlash | Pattern::Constant(_) => {
595                // already normalized
596            }
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                    // list has items that are one of these
640                    // * Alternatives -> [Concatenation] -> ...
641                    // * [Concatenation] -> ...
642                    let mut new_alternatives: Vec<Vec<Pattern>> = vec![Vec::new()];
643                    for part in list.drain(..) {
644                        if let Pattern::Alternatives(list) = part {
645                            // list is [Concatenation] -> ...
646                            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                            // part is [Concatenation] -> ...
660                            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                    // new_alternatives has items in that form:
670                    // * [Concatenation] -> ...
671                    *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                    // The recursive call will deduplicate the alternatives after simplifying them
684                    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                                        // do nothing
707                                    } else {
708                                        new_parts.push(Pattern::Dynamic);
709                                    }
710                                }
711                                Pattern::DynamicNoSlash => {
712                                    if let Some(Pattern::DynamicNoSlash) = new_parts.last() {
713                                        // do nothing
714                                    } 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    /// Like [`Pattern::is_match`], but does not consider any dynamic
833    /// pattern matching
834    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    /// Returns true if all matches of the pattern start with `value`.
872    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    /// Returns true the pattern could match something that starts with `value`.
885    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    /// Same as `match_internal`, but additionally pushing matched dynamic elements into the given
1007    /// result list.
1008    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                            // can match anything
1132                            // we don't have constant only matches
1133                            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    /// Calls `cb` on all constants that are at the end of the pattern and
1229    /// replaces the given final constant with the returned pattern. Returns
1230    /// true if replacements were performed.
1231    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    /// Calls `cb` on all constants and replaces the them with the returned pattern. Returns true if
1259    /// replacements were performed.
1260    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    /// Matches the given string against self, and applies the match onto the target pattern.
1280    ///
1281    /// The two patterns should have a similar structure (same number of alternatives and dynamics)
1282    /// and only differ in the constant contents.
1283    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        // This is definitely a match, because it matched above in `self.match_position(value)`
1306        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    // Inside of a match ending in `node_modules`
1354    FolderMatched,
1355    // Inside of a match ending in `node_modules/`
1356    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    /// No match
1373    None,
1374    /// Matches only a part of the pattern before reaching the end of the string
1375    Partial,
1376    /// Matches the whole pattern (but maybe not the whole string)
1377    Consumed {
1378        /// Part of the string remaining after matching the whole pattern
1379        remaining: &'a str,
1380        /// Set when the pattern ends with a dynamic part. The dynamic part
1381        /// could match n bytes more of the string.
1382        any_offset: Option<usize>,
1383        /// Set when the pattern ends with `node_modules` or `node_modules/` (and a following
1384        /// Pattern::Dynamic would thus match all existing packages)
1385        in_node_modules: InNodeModules,
1386    },
1387}
1388
1389impl MatchResult<'_> {
1390    /// Returns true if the whole pattern matches the whole string
1391    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    /// Returns true if (at least a part of) the pattern matches the whole
1410    /// string and can also match more bytes in the string
1411    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    /// Returns true if (at least a part of) the pattern matches the whole
1430    /// string
1431    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#[turbo_tasks::value_impl]
1487impl ValueToString for Pattern {
1488    #[turbo_tasks::function]
1489    fn to_string(&self) -> Vc<RcStr> {
1490        Vc::cell(self.describe_as_string().into())
1491    }
1492}
1493
1494#[derive(
1495    Debug, PartialEq, Eq, Clone, TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode,
1496)]
1497pub enum PatternMatch {
1498    File(RcStr, FileSystemPath),
1499    Directory(RcStr, FileSystemPath),
1500}
1501
1502impl PatternMatch {
1503    pub fn path(&self) -> Vc<FileSystemPath> {
1504        match self {
1505            PatternMatch::File(_, path) | PatternMatch::Directory(_, path) => path.clone().cell(),
1506        }
1507    }
1508
1509    pub fn name(&self) -> &str {
1510        match self {
1511            PatternMatch::File(name, _) | PatternMatch::Directory(name, _) => name.as_str(),
1512        }
1513    }
1514}
1515
1516// TODO this isn't super efficient
1517// avoid storing a large list of matches
1518#[turbo_tasks::value(transparent)]
1519#[derive(Debug)]
1520pub struct PatternMatches(Vec<PatternMatch>);
1521
1522/// Find all files or directories that match the provided `pattern` with the
1523/// specified `lookup_dir` directory. `prefix` is the already matched part of
1524/// the pattern that leads to the `lookup_dir` directory. When
1525/// `force_in_lookup_dir` is set, leaving the `lookup_dir` directory by
1526/// matching `..` is not allowed.
1527///
1528/// Symlinks will not be resolved. It's expected that the caller resolves
1529/// symlinks when they are interested in that.
1530#[turbo_tasks::function]
1531pub async fn read_matches(
1532    lookup_dir: FileSystemPath,
1533    prefix: RcStr,
1534    force_in_lookup_dir: bool,
1535    pattern: Vc<Pattern>,
1536) -> Result<Vc<PatternMatches>> {
1537    let mut prefix = prefix.to_string();
1538    let pat = pattern.await?;
1539    let mut results = Vec::new();
1540    let mut nested = Vec::new();
1541    let slow_path = if let Some(constants) = pat.next_constants(&prefix) {
1542        if constants
1543            .iter()
1544            .all(|(str, until_end)| *until_end || str.contains('/'))
1545        {
1546            // Fast path: There is a finite list of possible strings that include at least
1547            // one path segment We will enumerate the list instead of the
1548            // directory
1549            let mut handled = FxHashSet::default();
1550            let mut read_dir_results = FxHashMap::default();
1551            for (index, (str, until_end)) in constants.into_iter().enumerate() {
1552                if until_end {
1553                    if !handled.insert(str) {
1554                        continue;
1555                    }
1556                    let (parent_path, last_segment) = split_last_segment(str);
1557                    if last_segment.is_empty() {
1558                        // This means we don't have a last segment, so we just have a directory
1559                        let joined = if force_in_lookup_dir {
1560                            lookup_dir.try_join_inside(parent_path)
1561                        } else {
1562                            lookup_dir.try_join(parent_path)
1563                        };
1564                        let Some(fs_path) = joined else {
1565                            continue;
1566                        };
1567                        results.push((
1568                            index,
1569                            PatternMatch::Directory(concat(&prefix, str).into(), fs_path),
1570                        ));
1571                        continue;
1572                    }
1573                    let entry = read_dir_results.entry(parent_path);
1574                    let read_dir = match entry {
1575                        Entry::Occupied(e) => Some(e.into_mut()),
1576                        Entry::Vacant(e) => {
1577                            let path_option = if force_in_lookup_dir {
1578                                lookup_dir.try_join_inside(parent_path)
1579                            } else {
1580                                lookup_dir.try_join(parent_path)
1581                            };
1582                            if let Some(path) = path_option {
1583                                Some(e.insert((path.raw_read_dir().await?, path)))
1584                            } else {
1585                                None
1586                            }
1587                        }
1588                    };
1589                    let Some((read_dir, parent_fs_path)) = read_dir else {
1590                        continue;
1591                    };
1592                    let RawDirectoryContent::Entries(entries) = &**read_dir else {
1593                        continue;
1594                    };
1595                    let Some(entry) = entries.get(last_segment) else {
1596                        continue;
1597                    };
1598                    match *entry {
1599                        RawDirectoryEntry::File => {
1600                            results.push((
1601                                index,
1602                                PatternMatch::File(
1603                                    concat(&prefix, str).into(),
1604                                    parent_fs_path.join(last_segment)?,
1605                                ),
1606                            ));
1607                        }
1608                        RawDirectoryEntry::Directory => results.push((
1609                            index,
1610                            PatternMatch::Directory(
1611                                concat(&prefix, str).into(),
1612                                parent_fs_path.join(last_segment)?,
1613                            ),
1614                        )),
1615                        RawDirectoryEntry::Symlink => {
1616                            let fs_path = parent_fs_path.join(last_segment)?;
1617                            let LinkContent::Link { link_type, .. } = &*fs_path.read_link().await?
1618                            else {
1619                                continue;
1620                            };
1621                            let path = concat(&prefix, str).into();
1622                            if link_type.contains(LinkType::DIRECTORY) {
1623                                results.push((index, PatternMatch::Directory(path, fs_path)));
1624                            } else {
1625                                results.push((index, PatternMatch::File(path, fs_path)))
1626                            }
1627                        }
1628                        _ => {}
1629                    }
1630                } else {
1631                    let subpath = &str[..=str.rfind('/').unwrap()];
1632                    if handled.insert(subpath) {
1633                        let joined = if force_in_lookup_dir {
1634                            lookup_dir.try_join_inside(subpath)
1635                        } else {
1636                            lookup_dir.try_join(subpath)
1637                        };
1638                        let Some(fs_path) = joined else {
1639                            continue;
1640                        };
1641                        nested.push((
1642                            0,
1643                            read_matches(
1644                                fs_path.clone(),
1645                                concat(&prefix, subpath).into(),
1646                                force_in_lookup_dir,
1647                                pattern,
1648                            ),
1649                        ));
1650                    }
1651                }
1652            }
1653            false
1654        } else {
1655            true
1656        }
1657    } else {
1658        true
1659    };
1660
1661    if slow_path {
1662        async {
1663            // Slow path: There are infinite matches for the pattern
1664            // We will enumerate the filesystem to find matches
1665            if !force_in_lookup_dir {
1666                // {prefix}..
1667                prefix.push_str("..");
1668                if let Some(pos) = pat.match_position(&prefix) {
1669                    results.push((
1670                        pos,
1671                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1672                    ));
1673                }
1674
1675                // {prefix}../
1676                prefix.push('/');
1677                if let Some(pos) = pat.match_position(&prefix) {
1678                    results.push((
1679                        pos,
1680                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1681                    ));
1682                }
1683                if let Some(pos) = pat.could_match_position(&prefix) {
1684                    nested.push((
1685                        pos,
1686                        read_matches(lookup_dir.parent(), prefix.clone().into(), false, pattern),
1687                    ));
1688                }
1689                prefix.pop();
1690                prefix.pop();
1691                prefix.pop();
1692            }
1693            {
1694                prefix.push('.');
1695                // {prefix}.
1696                if let Some(pos) = pat.match_position(&prefix) {
1697                    results.push((
1698                        pos,
1699                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.clone()),
1700                    ));
1701                }
1702                prefix.pop();
1703            }
1704            if prefix.is_empty() {
1705                if let Some(pos) = pat.match_position("./") {
1706                    results.push((
1707                        pos,
1708                        PatternMatch::Directory(rcstr!("./"), lookup_dir.clone()),
1709                    ));
1710                }
1711                if let Some(pos) = pat.could_match_position("./") {
1712                    nested.push((
1713                        pos,
1714                        read_matches(lookup_dir.clone(), rcstr!("./"), false, pattern),
1715                    ));
1716                }
1717            } else {
1718                prefix.push('/');
1719                // {prefix}/
1720                if let Some(pos) = pat.could_match_position(&prefix) {
1721                    nested.push((
1722                        pos,
1723                        read_matches(
1724                            lookup_dir.clone(),
1725                            prefix.to_string().into(),
1726                            false,
1727                            pattern,
1728                        ),
1729                    ));
1730                }
1731                prefix.pop();
1732                prefix.push_str("./");
1733                // {prefix}./
1734                if let Some(pos) = pat.could_match_position(&prefix) {
1735                    nested.push((
1736                        pos,
1737                        read_matches(
1738                            lookup_dir.clone(),
1739                            prefix.to_string().into(),
1740                            false,
1741                            pattern,
1742                        ),
1743                    ));
1744                }
1745                prefix.pop();
1746                prefix.pop();
1747            }
1748            match &*lookup_dir.raw_read_dir().await? {
1749                RawDirectoryContent::Entries(map) => {
1750                    for (key, entry) in map.iter() {
1751                        match entry {
1752                            RawDirectoryEntry::File => {
1753                                let len = prefix.len();
1754                                prefix.push_str(key);
1755                                // {prefix}{key}
1756                                if let Some(pos) = pat.match_position(&prefix) {
1757                                    let path = lookup_dir.join(key)?;
1758                                    results.push((
1759                                        pos,
1760                                        PatternMatch::File(prefix.clone().into(), path),
1761                                    ));
1762                                }
1763                                prefix.truncate(len)
1764                            }
1765                            RawDirectoryEntry::Directory => {
1766                                let len = prefix.len();
1767                                prefix.push_str(key);
1768                                // {prefix}{key}
1769                                if prefix.ends_with('/') {
1770                                    prefix.pop();
1771                                }
1772                                if let Some(pos) = pat.match_position(&prefix) {
1773                                    let path = lookup_dir.join(key)?;
1774                                    results.push((
1775                                        pos,
1776                                        PatternMatch::Directory(prefix.clone().into(), path),
1777                                    ));
1778                                }
1779                                prefix.push('/');
1780                                // {prefix}{key}/
1781                                if let Some(pos) = pat.match_position(&prefix) {
1782                                    let path = lookup_dir.join(key)?;
1783                                    results.push((
1784                                        pos,
1785                                        PatternMatch::Directory(prefix.clone().into(), path),
1786                                    ));
1787                                }
1788                                if let Some(pos) = pat.could_match_position(&prefix) {
1789                                    let path = lookup_dir.join(key)?;
1790                                    nested.push((
1791                                        pos,
1792                                        read_matches(path, prefix.clone().into(), true, pattern),
1793                                    ));
1794                                }
1795                                prefix.truncate(len)
1796                            }
1797                            RawDirectoryEntry::Symlink => {
1798                                let len = prefix.len();
1799                                prefix.push_str(key);
1800                                // {prefix}{key}
1801                                if prefix.ends_with('/') {
1802                                    prefix.pop();
1803                                }
1804                                if let Some(pos) = pat.match_position(&prefix) {
1805                                    let fs_path = lookup_dir.join(key)?;
1806                                    if let LinkContent::Link { link_type, .. } =
1807                                        &*fs_path.read_link().await?
1808                                    {
1809                                        if link_type.contains(LinkType::DIRECTORY) {
1810                                            results.push((
1811                                                pos,
1812                                                PatternMatch::Directory(
1813                                                    prefix.clone().into(),
1814                                                    fs_path,
1815                                                ),
1816                                            ));
1817                                        } else {
1818                                            results.push((
1819                                                pos,
1820                                                PatternMatch::File(prefix.clone().into(), fs_path),
1821                                            ));
1822                                        }
1823                                    }
1824                                }
1825                                prefix.push('/');
1826                                if let Some(pos) = pat.match_position(&prefix) {
1827                                    let fs_path = lookup_dir.join(key)?;
1828                                    if let LinkContent::Link { link_type, .. } =
1829                                        &*fs_path.read_link().await?
1830                                        && link_type.contains(LinkType::DIRECTORY)
1831                                    {
1832                                        results.push((
1833                                            pos,
1834                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
1835                                        ));
1836                                    }
1837                                }
1838                                if let Some(pos) = pat.could_match_position(&prefix) {
1839                                    let fs_path = lookup_dir.join(key)?;
1840                                    if let LinkContent::Link { link_type, .. } =
1841                                        &*fs_path.read_link().await?
1842                                        && link_type.contains(LinkType::DIRECTORY)
1843                                    {
1844                                        results.push((
1845                                            pos,
1846                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
1847                                        ));
1848                                    }
1849                                }
1850                                prefix.truncate(len)
1851                            }
1852                            RawDirectoryEntry::Other => {}
1853                        }
1854                    }
1855                }
1856                RawDirectoryContent::NotFound => {}
1857            };
1858            anyhow::Ok(())
1859        }
1860        .instrument(tracing::trace_span!("read_matches slow_path"))
1861        .await?;
1862    }
1863    if results.is_empty() && nested.len() == 1 {
1864        Ok(nested.into_iter().next().unwrap().1)
1865    } else {
1866        for (pos, nested) in nested.into_iter() {
1867            results.extend(nested.await?.iter().cloned().map(|p| (pos, p)));
1868        }
1869        results.sort_by(|(a, am), (b, bm)| (*a).cmp(b).then_with(|| am.name().cmp(bm.name())));
1870        Ok(Vc::cell(
1871            results.into_iter().map(|(_, p)| p).collect::<Vec<_>>(),
1872        ))
1873    }
1874}
1875
1876fn concat(a: &str, b: &str) -> String {
1877    let mut result = String::with_capacity(a.len() + b.len());
1878    result.push_str(a);
1879    result.push_str(b);
1880    result
1881}
1882
1883/// Returns the parent folder and the last segment of the path. When the last segment is unknown (e.
1884/// g. when using `../`) it returns the full path and an empty string.
1885fn split_last_segment(path: &str) -> (&str, &str) {
1886    if let Some((remaining_path, last_segment)) = path.rsplit_once('/') {
1887        match last_segment {
1888            "" => split_last_segment(remaining_path),
1889            "." => split_last_segment(remaining_path),
1890            ".." => match split_last_segment(remaining_path) {
1891                (_, "") => (path, ""),
1892                (parent_path, _) => split_last_segment(parent_path),
1893            },
1894            _ => (remaining_path, last_segment),
1895        }
1896    } else {
1897        match path {
1898            "" => ("", ""),
1899            "." => ("", ""),
1900            ".." => ("..", ""),
1901            _ => ("", path),
1902        }
1903    }
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908    use std::path::Path;
1909
1910    use rstest::*;
1911    use turbo_rcstr::{RcStr, rcstr};
1912    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1913    use turbo_tasks_fs::{DiskFileSystem, FileSystem};
1914
1915    use super::{
1916        Pattern, longest_common_prefix, longest_common_suffix, read_matches, split_last_segment,
1917    };
1918
1919    #[test]
1920    fn longest_common_prefix_test() {
1921        assert_eq!(longest_common_prefix(&["ab"]), "ab");
1922        assert_eq!(longest_common_prefix(&["ab", "cd", "ef"]), "");
1923        assert_eq!(longest_common_prefix(&["ab1", "ab23", "ab456"]), "ab");
1924        assert_eq!(longest_common_prefix(&["abc", "abc", "abc"]), "abc");
1925        assert_eq!(longest_common_prefix(&["abc", "a", "abc"]), "a");
1926    }
1927
1928    #[test]
1929    fn longest_common_suffix_test() {
1930        assert_eq!(longest_common_suffix(&["ab"]), "ab");
1931        assert_eq!(longest_common_suffix(&["ab", "cd", "ef"]), "");
1932        assert_eq!(longest_common_suffix(&["1ab", "23ab", "456ab"]), "ab");
1933        assert_eq!(longest_common_suffix(&["abc", "abc", "abc"]), "abc");
1934        assert_eq!(longest_common_suffix(&["abc", "c", "abc"]), "c");
1935    }
1936
1937    #[test]
1938    fn normalize() {
1939        let a = Pattern::Constant(rcstr!("a"));
1940        let b = Pattern::Constant(rcstr!("b"));
1941        let c = Pattern::Constant(rcstr!("c"));
1942        let s = Pattern::Constant(rcstr!("/"));
1943        let d = Pattern::Dynamic;
1944        {
1945            let mut p = Pattern::Concatenation(vec![
1946                Pattern::Alternatives(vec![a.clone(), b.clone()]),
1947                s.clone(),
1948                c.clone(),
1949            ]);
1950            p.normalize();
1951            assert_eq!(
1952                p,
1953                Pattern::Alternatives(vec![
1954                    Pattern::Constant(rcstr!("a/c")),
1955                    Pattern::Constant(rcstr!("b/c")),
1956                ])
1957            );
1958        }
1959
1960        #[allow(clippy::redundant_clone)] // alignment
1961        {
1962            let mut p = Pattern::Concatenation(vec![
1963                Pattern::Alternatives(vec![a.clone(), b.clone(), d.clone()]),
1964                s.clone(),
1965                Pattern::Alternatives(vec![b.clone(), c.clone(), d.clone()]),
1966            ]);
1967            p.normalize();
1968
1969            assert_eq!(
1970                p,
1971                Pattern::Alternatives(vec![
1972                    Pattern::Constant(rcstr!("a/b")),
1973                    Pattern::Constant(rcstr!("b/b")),
1974                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/b"))]),
1975                    Pattern::Constant(rcstr!("a/c")),
1976                    Pattern::Constant(rcstr!("b/c")),
1977                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/c"))]),
1978                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
1979                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
1980                    Pattern::Concatenation(vec![
1981                        Pattern::Dynamic,
1982                        Pattern::Constant(rcstr!("/")),
1983                        Pattern::Dynamic
1984                    ]),
1985                ])
1986            );
1987        }
1988
1989        #[allow(clippy::redundant_clone)] // alignment
1990        {
1991            let mut p = Pattern::Alternatives(vec![a.clone()]);
1992            p.normalize();
1993
1994            assert_eq!(p, a);
1995        }
1996
1997        #[allow(clippy::redundant_clone)] // alignment
1998        {
1999            let mut p = Pattern::Alternatives(vec![Pattern::Dynamic, Pattern::Dynamic]);
2000            p.normalize();
2001
2002            assert_eq!(p, Pattern::Dynamic);
2003        }
2004    }
2005
2006    #[test]
2007    fn with_normalized_path() {
2008        assert!(
2009            Pattern::Constant(rcstr!("a/../.."))
2010                .with_normalized_path()
2011                .is_none()
2012        );
2013        assert_eq!(
2014            Pattern::Constant(rcstr!("a/b/../c"))
2015                .with_normalized_path()
2016                .unwrap(),
2017            Pattern::Constant(rcstr!("a/c"))
2018        );
2019        assert_eq!(
2020            Pattern::Alternatives(vec![
2021                Pattern::Constant(rcstr!("a/b/../c")),
2022                Pattern::Constant(rcstr!("a/b/../c/d"))
2023            ])
2024            .with_normalized_path()
2025            .unwrap(),
2026            Pattern::Alternatives(vec![
2027                Pattern::Constant(rcstr!("a/c")),
2028                Pattern::Constant(rcstr!("a/c/d"))
2029            ])
2030        );
2031        assert_eq!(
2032            Pattern::Constant(rcstr!("a/b/"))
2033                .with_normalized_path()
2034                .unwrap(),
2035            Pattern::Constant(rcstr!("a/b"))
2036        );
2037
2038        // Dynamic is a segment itself
2039        assert_eq!(
2040            Pattern::Concatenation(vec![
2041                Pattern::Constant(rcstr!("a/b/")),
2042                Pattern::Dynamic,
2043                Pattern::Constant(rcstr!("../c"))
2044            ])
2045            .with_normalized_path()
2046            .unwrap(),
2047            Pattern::Concatenation(vec![
2048                Pattern::Constant(rcstr!("a/b/")),
2049                Pattern::Dynamic,
2050                Pattern::Constant(rcstr!("../c"))
2051            ])
2052        );
2053
2054        // Dynamic is part of a segment
2055        assert_eq!(
2056            Pattern::Concatenation(vec![
2057                Pattern::Constant(rcstr!("a/b")),
2058                Pattern::Dynamic,
2059                Pattern::Constant(rcstr!("../c"))
2060            ])
2061            .with_normalized_path()
2062            .unwrap(),
2063            Pattern::Concatenation(vec![
2064                Pattern::Constant(rcstr!("a/b")),
2065                Pattern::Dynamic,
2066                Pattern::Constant(rcstr!("../c"))
2067            ])
2068        );
2069        assert_eq!(
2070            Pattern::Concatenation(vec![
2071                Pattern::Constant(rcstr!("src/")),
2072                Pattern::Dynamic,
2073                Pattern::Constant(rcstr!(".js"))
2074            ])
2075            .with_normalized_path()
2076            .unwrap(),
2077            Pattern::Concatenation(vec![
2078                Pattern::Constant(rcstr!("src/")),
2079                Pattern::Dynamic,
2080                Pattern::Constant(rcstr!(".js"))
2081            ])
2082        );
2083    }
2084
2085    #[test]
2086    fn is_match() {
2087        let pat = Pattern::Concatenation(vec![
2088            Pattern::Constant(rcstr!(".")),
2089            Pattern::Constant(rcstr!("/")),
2090            Pattern::Dynamic,
2091            Pattern::Constant(rcstr!(".js")),
2092        ]);
2093        assert!(pat.could_match(""));
2094        assert!(pat.could_match("./"));
2095        assert!(!pat.is_match("./"));
2096        assert!(pat.is_match("./index.js"));
2097        assert!(!pat.is_match("./index"));
2098        assert!(pat.is_match("./foo/index.js"));
2099        assert!(pat.is_match("./foo/bar/index.js"));
2100
2101        // forbidden:
2102        assert!(!pat.is_match("./../index.js"));
2103        assert!(!pat.is_match("././index.js"));
2104        assert!(!pat.is_match("./.git/index.js"));
2105        assert!(!pat.is_match("./inner/../index.js"));
2106        assert!(!pat.is_match("./inner/./index.js"));
2107        assert!(!pat.is_match("./inner/.git/index.js"));
2108        assert!(!pat.could_match("./../"));
2109        assert!(!pat.could_match("././"));
2110        assert!(!pat.could_match("./.git/"));
2111        assert!(!pat.could_match("./inner/../"));
2112        assert!(!pat.could_match("./inner/./"));
2113        assert!(!pat.could_match("./inner/.git/"));
2114    }
2115
2116    #[test]
2117    fn is_match_dynamic_no_slash() {
2118        let pat = Pattern::Concatenation(vec![
2119            Pattern::Constant(rcstr!(".")),
2120            Pattern::Constant(rcstr!("/")),
2121            Pattern::DynamicNoSlash,
2122            Pattern::Constant(rcstr!(".js")),
2123        ]);
2124        assert!(pat.could_match(""));
2125        assert!(pat.could_match("./"));
2126        assert!(!pat.is_match("./"));
2127        assert!(pat.is_match("./index.js"));
2128        assert!(!pat.is_match("./index"));
2129        assert!(!pat.is_match("./foo/index.js"));
2130        assert!(!pat.is_match("./foo/bar/index.js"));
2131    }
2132
2133    #[test]
2134    fn constant_prefix() {
2135        assert_eq!(
2136            Pattern::Constant(rcstr!("a/b/c.js")).constant_prefix(),
2137            "a/b/c.js",
2138        );
2139
2140        let pat = Pattern::Alternatives(vec![
2141            Pattern::Constant(rcstr!("a/b/x")),
2142            Pattern::Constant(rcstr!("a/b/y")),
2143            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]),
2144        ]);
2145        assert_eq!(pat.constant_prefix(), "a/b/");
2146    }
2147
2148    #[test]
2149    fn constant_suffix() {
2150        assert_eq!(
2151            Pattern::Constant(rcstr!("a/b/c.js")).constant_suffix(),
2152            "a/b/c.js",
2153        );
2154
2155        let pat = Pattern::Alternatives(vec![
2156            Pattern::Constant(rcstr!("a/b/x.js")),
2157            Pattern::Constant(rcstr!("a/b/y.js")),
2158            Pattern::Concatenation(vec![
2159                Pattern::Constant(rcstr!("a/b/c/")),
2160                Pattern::Dynamic,
2161                Pattern::Constant(rcstr!(".js")),
2162            ]),
2163        ]);
2164        assert_eq!(pat.constant_suffix(), ".js");
2165    }
2166
2167    #[test]
2168    fn strip_prefix() {
2169        fn strip(mut pat: Pattern, n: usize) -> Pattern {
2170            pat.strip_prefix_len(n).unwrap();
2171            pat
2172        }
2173
2174        assert_eq!(
2175            strip(Pattern::Constant(rcstr!("a/b")), 0),
2176            Pattern::Constant(rcstr!("a/b"))
2177        );
2178
2179        assert_eq!(
2180            strip(
2181                Pattern::Alternatives(vec![
2182                    Pattern::Constant(rcstr!("a/b/x")),
2183                    Pattern::Constant(rcstr!("a/b/y")),
2184                ]),
2185                2
2186            ),
2187            Pattern::Alternatives(vec![
2188                Pattern::Constant(rcstr!("b/x")),
2189                Pattern::Constant(rcstr!("b/y")),
2190            ])
2191        );
2192
2193        assert_eq!(
2194            strip(
2195                Pattern::Concatenation(vec![
2196                    Pattern::Constant(rcstr!("a/")),
2197                    Pattern::Constant(rcstr!("b")),
2198                    Pattern::Constant(rcstr!("/")),
2199                    Pattern::Constant(rcstr!("y/")),
2200                    Pattern::Dynamic
2201                ]),
2202                4
2203            ),
2204            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("y/")), Pattern::Dynamic]),
2205        );
2206    }
2207
2208    #[test]
2209    fn strip_suffix() {
2210        fn strip(mut pat: Pattern, n: usize) -> Pattern {
2211            pat.strip_suffix_len(n);
2212            pat
2213        }
2214
2215        assert_eq!(
2216            strip(Pattern::Constant(rcstr!("a/b")), 0),
2217            Pattern::Constant(rcstr!("a/b"))
2218        );
2219
2220        assert_eq!(
2221            strip(
2222                Pattern::Alternatives(vec![
2223                    Pattern::Constant(rcstr!("x/b/a")),
2224                    Pattern::Constant(rcstr!("y/b/a")),
2225                ]),
2226                2
2227            ),
2228            Pattern::Alternatives(vec![
2229                Pattern::Constant(rcstr!("x/b")),
2230                Pattern::Constant(rcstr!("y/b")),
2231            ])
2232        );
2233
2234        assert_eq!(
2235            strip(
2236                Pattern::Concatenation(vec![
2237                    Pattern::Dynamic,
2238                    Pattern::Constant(rcstr!("/a/")),
2239                    Pattern::Constant(rcstr!("b")),
2240                    Pattern::Constant(rcstr!("/")),
2241                    Pattern::Constant(rcstr!("y/")),
2242                ]),
2243                4
2244            ),
2245            Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/a/")),]),
2246        );
2247    }
2248
2249    #[test]
2250    fn spread_into_star() {
2251        let pat = Pattern::Constant(rcstr!("xyz"));
2252        assert_eq!(
2253            pat.spread_into_star("before/after"),
2254            Pattern::Constant(rcstr!("before/after")),
2255        );
2256
2257        let pat =
2258            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]);
2259        assert_eq!(
2260            pat.spread_into_star("before/*/after"),
2261            Pattern::Concatenation(vec![
2262                Pattern::Constant(rcstr!("before/a/b/c/")),
2263                Pattern::Dynamic,
2264                Pattern::Constant(rcstr!("/after"))
2265            ])
2266        );
2267
2268        let pat = Pattern::Alternatives(vec![
2269            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
2270            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
2271        ]);
2272        assert_eq!(
2273            pat.spread_into_star("before/*/after"),
2274            Pattern::Alternatives(vec![
2275                Pattern::Concatenation(vec![
2276                    Pattern::Constant(rcstr!("before/a/")),
2277                    Pattern::Dynamic,
2278                    Pattern::Constant(rcstr!("/after"))
2279                ]),
2280                Pattern::Concatenation(vec![
2281                    Pattern::Constant(rcstr!("before/b/")),
2282                    Pattern::Dynamic,
2283                    Pattern::Constant(rcstr!("/after"))
2284                ]),
2285            ])
2286        );
2287
2288        let pat = Pattern::Alternatives(vec![
2289            Pattern::Constant(rcstr!("a")),
2290            Pattern::Constant(rcstr!("b")),
2291        ]);
2292        assert_eq!(
2293            pat.spread_into_star("before/*/*"),
2294            Pattern::Alternatives(vec![
2295                Pattern::Constant(rcstr!("before/a/a")),
2296                Pattern::Constant(rcstr!("before/b/b")),
2297            ])
2298        );
2299
2300        let pat = Pattern::Dynamic;
2301        assert_eq!(
2302            pat.spread_into_star("before/*/*"),
2303            Pattern::Concatenation(vec![
2304                // TODO currently nothing ensures that both Dynamic parts are equal
2305                Pattern::Constant(rcstr!("before/")),
2306                Pattern::Dynamic,
2307                Pattern::Constant(rcstr!("/")),
2308                Pattern::Dynamic
2309            ])
2310        );
2311    }
2312
2313    #[rstest]
2314    #[case::dynamic(Pattern::Dynamic)]
2315    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2316    fn dynamic_match(#[case] pat: Pattern) {
2317        assert!(pat.could_match(""));
2318        assert!(pat.is_match("index.js"));
2319
2320        // forbidden:
2321        assert!(!pat.could_match("./"));
2322        assert!(!pat.is_match("./"));
2323        assert!(!pat.could_match("."));
2324        assert!(!pat.is_match("."));
2325        assert!(!pat.could_match("../"));
2326        assert!(!pat.is_match("../"));
2327        assert!(!pat.could_match(".."));
2328        assert!(!pat.is_match(".."));
2329        assert!(!pat.is_match("./../index.js"));
2330        assert!(!pat.is_match("././index.js"));
2331        assert!(!pat.is_match("./.git/index.js"));
2332        assert!(!pat.is_match("./inner/../index.js"));
2333        assert!(!pat.is_match("./inner/./index.js"));
2334        assert!(!pat.is_match("./inner/.git/index.js"));
2335        assert!(!pat.could_match("./../"));
2336        assert!(!pat.could_match("././"));
2337        assert!(!pat.could_match("./.git/"));
2338        assert!(!pat.could_match("./inner/../"));
2339        assert!(!pat.could_match("./inner/./"));
2340        assert!(!pat.could_match("./inner/.git/"));
2341        assert!(!pat.could_match("dir//"));
2342        assert!(!pat.could_match("dir//dir"));
2343        assert!(!pat.could_match("dir///dir"));
2344        assert!(!pat.could_match("/"));
2345        assert!(!pat.could_match("//"));
2346        assert!(!pat.could_match("/ROOT/"));
2347
2348        assert!(!pat.could_match("node_modules"));
2349        assert!(!pat.could_match("node_modules/package"));
2350        assert!(!pat.could_match("nested/node_modules"));
2351        assert!(!pat.could_match("nested/node_modules/package"));
2352
2353        // forbidden match
2354        assert!(pat.could_match("file.map"));
2355        assert!(!pat.is_match("file.map"));
2356        assert!(pat.is_match("file.map/file.js"));
2357        assert!(!pat.is_match("file.d.ts"));
2358        assert!(!pat.is_match("file.d.ts.map"));
2359        assert!(!pat.is_match("file.d.ts.map"));
2360        assert!(!pat.is_match("dir/file.d.ts.map"));
2361        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2362        assert!(pat.could_match("dir/inner/file.d.ts.map"));
2363    }
2364
2365    #[rstest]
2366    #[case::slash(Pattern::Concatenation(vec![Pattern::Constant(rcstr!("node_modules/")),Pattern::Dynamic]))]
2367    #[case::nested(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file())]
2368    fn dynamic_match_node_modules(#[case] pat: Pattern) {
2369        assert!(!pat.is_match("node_modules/package"));
2370        assert!(!pat.could_match("node_modules/package"));
2371        assert!(!pat.is_match("node_modules/package/index.js"));
2372        assert!(!pat.could_match("node_modules/package/index.js"));
2373    }
2374
2375    #[rstest]
2376    fn dynamic_match2() {
2377        let pat = Pattern::Concatenation(vec![
2378            Pattern::Dynamic,
2379            Pattern::Constant(rcstr!("/")),
2380            Pattern::Dynamic,
2381        ]);
2382        assert!(pat.could_match("dir"));
2383        assert!(pat.could_match("dir/"));
2384        assert!(pat.is_match("dir/index.js"));
2385
2386        // forbidden:
2387        assert!(!pat.could_match("./"));
2388        assert!(!pat.is_match("./"));
2389        assert!(!pat.could_match("."));
2390        assert!(!pat.is_match("."));
2391        assert!(!pat.could_match("../"));
2392        assert!(!pat.is_match("../"));
2393        assert!(!pat.could_match(".."));
2394        assert!(!pat.is_match(".."));
2395        assert!(!pat.is_match("./../index.js"));
2396        assert!(!pat.is_match("././index.js"));
2397        assert!(!pat.is_match("./.git/index.js"));
2398        assert!(!pat.is_match("./inner/../index.js"));
2399        assert!(!pat.is_match("./inner/./index.js"));
2400        assert!(!pat.is_match("./inner/.git/index.js"));
2401        assert!(!pat.could_match("./../"));
2402        assert!(!pat.could_match("././"));
2403        assert!(!pat.could_match("./.git/"));
2404        assert!(!pat.could_match("./inner/../"));
2405        assert!(!pat.could_match("./inner/./"));
2406        assert!(!pat.could_match("./inner/.git/"));
2407        assert!(!pat.could_match("dir//"));
2408        assert!(!pat.could_match("dir//dir"));
2409        assert!(!pat.could_match("dir///dir"));
2410        assert!(!pat.could_match("/ROOT/"));
2411
2412        assert!(!pat.could_match("node_modules"));
2413        assert!(!pat.could_match("node_modules/package"));
2414        assert!(!pat.could_match("nested/node_modules"));
2415        assert!(!pat.could_match("nested/node_modules/package"));
2416
2417        // forbidden match
2418        assert!(pat.could_match("dir/file.map"));
2419        assert!(!pat.is_match("dir/file.map"));
2420        assert!(pat.is_match("file.map/file.js"));
2421        assert!(!pat.is_match("dir/file.d.ts"));
2422        assert!(!pat.is_match("dir/file.d.ts.map"));
2423        assert!(!pat.is_match("dir/file.d.ts.map"));
2424        assert!(!pat.is_match("dir/file.d.ts.map"));
2425        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2426        assert!(pat.could_match("dir/inner/file.d.ts.map"));
2427    }
2428
2429    #[rstest]
2430    #[case::dynamic(Pattern::Dynamic)]
2431    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2432    #[case::dynamic_concat2(Pattern::Concatenation(vec![
2433        Pattern::Dynamic,
2434        Pattern::Constant(rcstr!("/")),
2435        Pattern::Dynamic,
2436    ]))]
2437    #[case::dynamic_alt_concat(Pattern::alternatives(vec![
2438        Pattern::Concatenation(vec![
2439            Pattern::Dynamic,
2440            Pattern::Constant(rcstr!("/")),
2441            Pattern::Dynamic,
2442        ]),
2443        Pattern::Dynamic,
2444    ]))]
2445    fn split_could_match(#[case] pat: Pattern) {
2446        let (abs, rel) = pat.split_could_match("/ROOT/");
2447        assert!(abs.is_none());
2448        assert!(rel.is_some());
2449    }
2450
2451    #[rstest]
2452    #[case::dynamic(Pattern::Dynamic, "feijf", None)]
2453    #[case::dynamic_concat(
2454        Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]),
2455        "hello.", None
2456    )]
2457    #[case::constant(Pattern::Constant(rcstr!("Hello World")), "Hello ", Some(vec![("World", true)]))]
2458    #[case::alternatives(
2459        Pattern::Alternatives(vec![
2460            Pattern::Constant(rcstr!("Hello World")),
2461            Pattern::Constant(rcstr!("Hello All"))
2462        ]), "Hello ", Some(vec![("World", true), ("All", true)])
2463    )]
2464    #[case::alternatives_non_end(
2465        Pattern::Alternatives(vec![
2466            Pattern::Constant(rcstr!("Hello World")),
2467            Pattern::Constant(rcstr!("Hello All")),
2468            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("Hello more")), Pattern::Dynamic])
2469        ]), "Hello ", Some(vec![("World", true), ("All", true), ("more", false)])
2470    )]
2471    #[case::request_with_extensions(
2472        Pattern::Alternatives(vec![
2473            Pattern::Constant(rcstr!("./file.js")),
2474            Pattern::Constant(rcstr!("./file.ts")),
2475            Pattern::Constant(rcstr!("./file.cjs")),
2476        ]), "./", Some(vec![("file.js", true), ("file.ts", true), ("file.cjs", true)])
2477    )]
2478    fn next_constants(
2479        #[case] pat: Pattern,
2480        #[case] value: &str,
2481        #[case] expected: Option<Vec<(&str, bool)>>,
2482    ) {
2483        assert_eq!(pat.next_constants(value), expected);
2484    }
2485
2486    #[test]
2487    fn replace_final_constants() {
2488        fn f(mut p: Pattern, cb: &mut impl FnMut(&RcStr) -> Option<Pattern>) -> Pattern {
2489            p.replace_final_constants(cb);
2490            p
2491        }
2492
2493        let mut js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
2494            c.strip_suffix(".js").map(|rest| {
2495                let new_ending = Pattern::Alternatives(vec![
2496                    Pattern::Constant(rcstr!(".ts")),
2497                    Pattern::Constant(rcstr!(".tsx")),
2498                    Pattern::Constant(rcstr!(".js")),
2499                ]);
2500                if !rest.is_empty() {
2501                    Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
2502                } else {
2503                    new_ending
2504                }
2505            })
2506        };
2507
2508        assert_eq!(
2509            f(
2510                Pattern::Concatenation(vec![
2511                    Pattern::Constant(rcstr!(".")),
2512                    Pattern::Constant(rcstr!("/")),
2513                    Pattern::Dynamic,
2514                    Pattern::Alternatives(vec![
2515                        Pattern::Constant(rcstr!(".js")),
2516                        Pattern::Constant(rcstr!(".node")),
2517                    ])
2518                ]),
2519                &mut js_to_ts_tsx
2520            ),
2521            Pattern::Concatenation(vec![
2522                Pattern::Constant(rcstr!(".")),
2523                Pattern::Constant(rcstr!("/")),
2524                Pattern::Dynamic,
2525                Pattern::Alternatives(vec![
2526                    Pattern::Alternatives(vec![
2527                        Pattern::Constant(rcstr!(".ts")),
2528                        Pattern::Constant(rcstr!(".tsx")),
2529                        Pattern::Constant(rcstr!(".js")),
2530                    ]),
2531                    Pattern::Constant(rcstr!(".node")),
2532                ])
2533            ]),
2534        );
2535        assert_eq!(
2536            f(
2537                Pattern::Concatenation(vec![
2538                    Pattern::Constant(rcstr!(".")),
2539                    Pattern::Constant(rcstr!("/")),
2540                    Pattern::Constant(rcstr!("abc.js")),
2541                ]),
2542                &mut js_to_ts_tsx
2543            ),
2544            Pattern::Concatenation(vec![
2545                Pattern::Constant(rcstr!(".")),
2546                Pattern::Constant(rcstr!("/")),
2547                Pattern::Concatenation(vec![
2548                    Pattern::Constant(rcstr!("abc")),
2549                    Pattern::Alternatives(vec![
2550                        Pattern::Constant(rcstr!(".ts")),
2551                        Pattern::Constant(rcstr!(".tsx")),
2552                        Pattern::Constant(rcstr!(".js")),
2553                    ])
2554                ]),
2555            ])
2556        );
2557    }
2558
2559    #[test]
2560    fn match_apply_template() {
2561        assert_eq!(
2562            Pattern::Concatenation(vec![
2563                Pattern::Constant(rcstr!("a/b/")),
2564                Pattern::Dynamic,
2565                Pattern::Constant(rcstr!(".ts")),
2566            ])
2567            .match_apply_template(
2568                "a/b/foo.ts",
2569                &Pattern::Concatenation(vec![
2570                    Pattern::Constant(rcstr!("@/a/b/")),
2571                    Pattern::Dynamic,
2572                    Pattern::Constant(rcstr!(".js")),
2573                ])
2574            )
2575            .as_deref(),
2576            Some("@/a/b/foo.js")
2577        );
2578        assert_eq!(
2579            Pattern::Concatenation(vec![
2580                Pattern::Constant(rcstr!("b/")),
2581                Pattern::Dynamic,
2582                Pattern::Constant(rcstr!(".ts")),
2583            ])
2584            .match_apply_template(
2585                "a/b/foo.ts",
2586                &Pattern::Concatenation(vec![
2587                    Pattern::Constant(rcstr!("@/a/b/")),
2588                    Pattern::Dynamic,
2589                    Pattern::Constant(rcstr!(".js")),
2590                ])
2591            )
2592            .as_deref(),
2593            None,
2594        );
2595        assert_eq!(
2596            Pattern::Concatenation(vec![
2597                Pattern::Constant(rcstr!("a/b/")),
2598                Pattern::Dynamic,
2599                Pattern::Constant(rcstr!(".ts")),
2600            ])
2601            .match_apply_template(
2602                "a/b/foo.ts",
2603                &Pattern::Concatenation(vec![
2604                    Pattern::Constant(rcstr!("@/a/b/x")),
2605                    Pattern::Constant(rcstr!(".js")),
2606                ])
2607            )
2608            .as_deref(),
2609            None,
2610        );
2611        assert_eq!(
2612            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("./sub/")), Pattern::Dynamic])
2613                .match_apply_template(
2614                    "./sub/file1",
2615                    &Pattern::Concatenation(vec![
2616                        Pattern::Constant(rcstr!("@/sub/")),
2617                        Pattern::Dynamic
2618                    ])
2619                )
2620                .as_deref(),
2621            Some("@/sub/file1"),
2622        );
2623    }
2624
2625    #[test]
2626    fn test_split_last_segment() {
2627        assert_eq!(split_last_segment(""), ("", ""));
2628        assert_eq!(split_last_segment("a"), ("", "a"));
2629        assert_eq!(split_last_segment("a/"), ("", "a"));
2630        assert_eq!(split_last_segment("a/b"), ("a", "b"));
2631        assert_eq!(split_last_segment("a/b/"), ("a", "b"));
2632        assert_eq!(split_last_segment("a/b/c"), ("a/b", "c"));
2633        assert_eq!(split_last_segment("a/b/."), ("a", "b"));
2634        assert_eq!(split_last_segment("a/b/.."), ("", "a"));
2635        assert_eq!(split_last_segment("a/b/c/.."), ("a", "b"));
2636        assert_eq!(split_last_segment("a/b/c/../.."), ("", "a"));
2637        assert_eq!(split_last_segment("a/b/c/d/../.."), ("a", "b"));
2638        assert_eq!(split_last_segment("a/b/c/../d/.."), ("a", "b"));
2639        assert_eq!(split_last_segment("a/b/../c/d/.."), ("a/b/..", "c"));
2640        assert_eq!(split_last_segment("."), ("", ""));
2641        assert_eq!(split_last_segment("./"), ("", ""));
2642        assert_eq!(split_last_segment(".."), ("..", ""));
2643        assert_eq!(split_last_segment("../"), ("..", ""));
2644        assert_eq!(split_last_segment("./../"), ("./..", ""));
2645        assert_eq!(split_last_segment("../../"), ("../..", ""));
2646        assert_eq!(split_last_segment("../../."), ("../..", ""));
2647        assert_eq!(split_last_segment("../.././"), ("../..", ""));
2648        assert_eq!(split_last_segment("a/.."), ("", ""));
2649        assert_eq!(split_last_segment("a/../"), ("", ""));
2650        assert_eq!(split_last_segment("a/../.."), ("a/../..", ""));
2651        assert_eq!(split_last_segment("a/../../"), ("a/../..", ""));
2652        assert_eq!(split_last_segment("a/././../"), ("", ""));
2653        assert_eq!(split_last_segment("../a"), ("..", "a"));
2654        assert_eq!(split_last_segment("../a/"), ("..", "a"));
2655        assert_eq!(split_last_segment("../../a"), ("../..", "a"));
2656        assert_eq!(split_last_segment("../../a/"), ("../..", "a"));
2657    }
2658
2659    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2660    async fn test_read_matches() {
2661        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2662            BackendOptions::default(),
2663            noop_backing_storage(),
2664        ));
2665        tt.run_once(async {
2666            let root = DiskFileSystem::new(
2667                rcstr!("test"),
2668                Path::new(env!("CARGO_MANIFEST_DIR"))
2669                    .join("tests/pattern/read_matches")
2670                    .to_str()
2671                    .unwrap()
2672                    .into(),
2673            )
2674            .root()
2675            .owned()
2676            .await?;
2677
2678            // node_modules shouldn't be matched by Dynamic here
2679            assert_eq!(
2680                vec!["index.js", "sub", "sub/", "sub/foo-a.js", "sub/foo-b.js"],
2681                read_matches(
2682                    root.clone(),
2683                    rcstr!(""),
2684                    false,
2685                    Pattern::new(Pattern::Dynamic),
2686                )
2687                .await?
2688                .into_iter()
2689                .map(|m| m.name())
2690                .collect::<Vec<_>>()
2691            );
2692
2693            // basic dynamic file suffix
2694            assert_eq!(
2695                vec!["sub/foo-a.js", "sub/foo-b.js"],
2696                read_matches(
2697                    root.clone(),
2698                    rcstr!(""),
2699                    false,
2700                    Pattern::new(Pattern::concat([
2701                        Pattern::Constant(rcstr!("sub/foo")),
2702                        Pattern::Dynamic,
2703                    ])),
2704                )
2705                .await?
2706                .into_iter()
2707                .map(|m| m.name())
2708                .collect::<Vec<_>>()
2709            );
2710
2711            // read_matches "node_modules/<dynamic>" should not return anything inside. We never
2712            // want to enumerate the list of packages here.
2713            assert_eq!(
2714                vec!["node_modules"] as Vec<&str>,
2715                read_matches(
2716                    root.clone(),
2717                    rcstr!(""),
2718                    false,
2719                    Pattern::new(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file()),
2720                )
2721                .await?
2722                .into_iter()
2723                .map(|m| m.name())
2724                .collect::<Vec<_>>()
2725            );
2726
2727            anyhow::Ok(())
2728        })
2729        .await
2730        .unwrap();
2731    }
2732}