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 regex::Regex;
9use rustc_hash::{FxHashMap, FxHashSet};
10use serde::{Deserialize, Serialize};
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(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
1232        let mut replaced = false;
1233        match self {
1234            Pattern::Constant(c) => {
1235                if let Some(replacement) = cb(c) {
1236                    *self = replacement;
1237                    replaced = true;
1238                }
1239            }
1240            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1241            Pattern::Alternatives(list) => {
1242                for i in list {
1243                    replaced = i.replace_final_constants(cb) || replaced;
1244                }
1245            }
1246            Pattern::Concatenation(list) => {
1247                if let Some(i) = list.last_mut() {
1248                    replaced = i.replace_final_constants(cb) || replaced;
1249                }
1250            }
1251        }
1252        replaced
1253    }
1254
1255    /// Calls `cb` on all constants and replaces the them with the returned pattern. Returns true if
1256    /// replacements were performed.
1257    pub fn replace_constants(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
1258        let mut replaced = false;
1259        match self {
1260            Pattern::Constant(c) => {
1261                if let Some(replacement) = cb(c) {
1262                    *self = replacement;
1263                    replaced = true;
1264                }
1265            }
1266            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
1267            Pattern::Concatenation(list) | Pattern::Alternatives(list) => {
1268                for i in list {
1269                    replaced = i.replace_constants(cb) || replaced;
1270                }
1271            }
1272        }
1273        replaced
1274    }
1275
1276    /// Matches the given string against self, and applies the match onto the target pattern.
1277    ///
1278    /// The two patterns should have a similar structure (same number of alternatives and dynamics)
1279    /// and only differ in the constant contents.
1280    pub fn match_apply_template(&self, value: &str, target: &Pattern) -> Option<String> {
1281        let match_idx = self.match_position(value)?;
1282        let source = match self {
1283            Pattern::Alternatives(list) => list.get(match_idx),
1284            Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1285                if match_idx == 0 =>
1286            {
1287                Some(self)
1288            }
1289            _ => None,
1290        }?;
1291        let target = match target {
1292            Pattern::Alternatives(list) => list.get(match_idx),
1293            Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
1294                if match_idx == 0 =>
1295            {
1296                Some(target)
1297            }
1298            _ => None,
1299        }?;
1300
1301        let mut dynamics = VecDeque::new();
1302        // This is definitely a match, because it matched above in `self.match_position(value)`
1303        source.match_collect_internal(value, None, InNodeModules::False, &mut dynamics);
1304
1305        let mut result = "".to_string();
1306        match target {
1307            Pattern::Constant(c) => result.push_str(c),
1308            Pattern::Dynamic | Pattern::DynamicNoSlash => result.push_str(dynamics.pop_front()?),
1309            Pattern::Concatenation(list) => {
1310                for c in list {
1311                    match c {
1312                        Pattern::Constant(c) => result.push_str(c),
1313                        Pattern::Dynamic | Pattern::DynamicNoSlash => {
1314                            result.push_str(dynamics.pop_front()?)
1315                        }
1316                        Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
1317                            panic!("Pattern must be normalized")
1318                        }
1319                    }
1320                }
1321            }
1322            Pattern::Alternatives(_) => panic!("Pattern must be normalized"),
1323        }
1324        if !dynamics.is_empty() {
1325            return None;
1326        }
1327
1328        Some(result)
1329    }
1330}
1331
1332impl Pattern {
1333    pub fn new(mut pattern: Pattern) -> Vc<Self> {
1334        pattern.normalize();
1335        Pattern::new_internal(pattern)
1336    }
1337}
1338
1339#[turbo_tasks::value_impl]
1340impl Pattern {
1341    #[turbo_tasks::function]
1342    fn new_internal(pattern: Pattern) -> Vc<Self> {
1343        Self::cell(pattern)
1344    }
1345}
1346
1347#[derive(PartialEq, Debug)]
1348enum InNodeModules {
1349    False,
1350    // Inside of a match ending in `node_modules`
1351    FolderMatched,
1352    // Inside of a match ending in `node_modules/`
1353    FolderSlashMatched,
1354}
1355impl InNodeModules {
1356    fn check(value: &str) -> Self {
1357        if value.ends_with("node_modules/") {
1358            InNodeModules::FolderSlashMatched
1359        } else if value.ends_with("node_modules") {
1360            InNodeModules::FolderMatched
1361        } else {
1362            InNodeModules::False
1363        }
1364    }
1365}
1366
1367#[derive(PartialEq, Debug)]
1368enum MatchResult<'a> {
1369    /// No match
1370    None,
1371    /// Matches only a part of the pattern before reaching the end of the string
1372    Partial,
1373    /// Matches the whole pattern (but maybe not the whole string)
1374    Consumed {
1375        /// Part of the string remaining after matching the whole pattern
1376        remaining: &'a str,
1377        /// Set when the pattern ends with a dynamic part. The dynamic part
1378        /// could match n bytes more of the string.
1379        any_offset: Option<usize>,
1380        /// Set when the pattern ends with `node_modules` or `node_modules/` (and a following
1381        /// Pattern::Dynamic would thus match all existing packages)
1382        in_node_modules: InNodeModules,
1383    },
1384}
1385
1386impl MatchResult<'_> {
1387    /// Returns true if the whole pattern matches the whole string
1388    fn is_match(&self) -> bool {
1389        match self {
1390            MatchResult::None => false,
1391            MatchResult::Partial => false,
1392            MatchResult::Consumed {
1393                remaining: rem,
1394                any_offset,
1395                in_node_modules: _,
1396            } => {
1397                if let Some(offset) = any_offset {
1398                    *offset == rem.len()
1399                } else {
1400                    rem.is_empty()
1401                }
1402            }
1403        }
1404    }
1405
1406    /// Returns true if (at least a part of) the pattern matches the whole
1407    /// string and can also match more bytes in the string
1408    fn could_match_others(&self) -> bool {
1409        match self {
1410            MatchResult::None => false,
1411            MatchResult::Partial => true,
1412            MatchResult::Consumed {
1413                remaining: rem,
1414                any_offset,
1415                in_node_modules: _,
1416            } => {
1417                if let Some(offset) = any_offset {
1418                    *offset == rem.len()
1419                } else {
1420                    false
1421                }
1422            }
1423        }
1424    }
1425
1426    /// Returns true if (at least a part of) the pattern matches the whole
1427    /// string
1428    fn could_match(&self) -> bool {
1429        match self {
1430            MatchResult::None => false,
1431            MatchResult::Partial => true,
1432            MatchResult::Consumed {
1433                remaining: rem,
1434                any_offset,
1435                in_node_modules: _,
1436            } => {
1437                if let Some(offset) = any_offset {
1438                    *offset == rem.len()
1439                } else {
1440                    rem.is_empty()
1441                }
1442            }
1443        }
1444    }
1445}
1446
1447#[derive(PartialEq, Debug)]
1448enum NextConstantUntilResult<'a, 'b> {
1449    NoMatch,
1450    PartialDynamic,
1451    Partial(&'a str, bool),
1452    Consumed(&'b str, Option<usize>),
1453}
1454
1455impl From<RcStr> for Pattern {
1456    fn from(s: RcStr) -> Self {
1457        Pattern::Constant(s)
1458    }
1459}
1460
1461impl Pattern {
1462    pub fn describe_as_string(&self) -> String {
1463        match self {
1464            Pattern::Constant(c) => format!("'{c}'"),
1465            Pattern::Dynamic => "<dynamic>".to_string(),
1466            Pattern::DynamicNoSlash => "<dynamic no slash>".to_string(),
1467            Pattern::Alternatives(list) => format!(
1468                "({})",
1469                list.iter()
1470                    .map(|i| i.describe_as_string())
1471                    .collect::<Vec<_>>()
1472                    .join(" | ")
1473            ),
1474            Pattern::Concatenation(list) => list
1475                .iter()
1476                .map(|i| i.describe_as_string())
1477                .collect::<Vec<_>>()
1478                .join(" "),
1479        }
1480    }
1481}
1482
1483#[turbo_tasks::value_impl]
1484impl ValueToString for Pattern {
1485    #[turbo_tasks::function]
1486    fn to_string(&self) -> Vc<RcStr> {
1487        Vc::cell(self.describe_as_string().into())
1488    }
1489}
1490
1491#[derive(
1492    Debug,
1493    PartialEq,
1494    Eq,
1495    Clone,
1496    TraceRawVcs,
1497    Serialize,
1498    Deserialize,
1499    ValueDebugFormat,
1500    NonLocalValue,
1501)]
1502pub enum PatternMatch {
1503    File(RcStr, FileSystemPath),
1504    Directory(RcStr, FileSystemPath),
1505}
1506
1507impl PatternMatch {
1508    pub fn path(&self) -> Vc<FileSystemPath> {
1509        match self {
1510            PatternMatch::File(_, path) | PatternMatch::Directory(_, path) => path.clone().cell(),
1511        }
1512    }
1513
1514    pub fn name(&self) -> &str {
1515        match self {
1516            PatternMatch::File(name, _) | PatternMatch::Directory(name, _) => name.as_str(),
1517        }
1518    }
1519}
1520
1521// TODO this isn't super efficient
1522// avoid storing a large list of matches
1523#[turbo_tasks::value(transparent)]
1524pub struct PatternMatches(Vec<PatternMatch>);
1525
1526/// Find all files or directories that match the provided `pattern` with the
1527/// specified `lookup_dir` directory. `prefix` is the already matched part of
1528/// the pattern that leads to the `lookup_dir` directory. When
1529/// `force_in_lookup_dir` is set, leaving the `lookup_dir` directory by
1530/// matching `..` is not allowed.
1531///
1532/// Symlinks will not be resolved. It's expected that the caller resolves
1533/// symlinks when they are interested in that.
1534#[turbo_tasks::function]
1535pub async fn read_matches(
1536    lookup_dir: FileSystemPath,
1537    prefix: RcStr,
1538    force_in_lookup_dir: bool,
1539    pattern: Vc<Pattern>,
1540) -> Result<Vc<PatternMatches>> {
1541    let mut prefix = prefix.to_string();
1542    let pat = pattern.await?;
1543    let mut results = Vec::new();
1544    let mut nested = Vec::new();
1545    let slow_path = if let Some(constants) = pat.next_constants(&prefix) {
1546        if constants
1547            .iter()
1548            .all(|(str, until_end)| *until_end || str.contains('/'))
1549        {
1550            // Fast path: There is a finite list of possible strings that include at least
1551            // one path segment We will enumerate the list instead of the
1552            // directory
1553            let mut handled = FxHashSet::default();
1554            let mut read_dir_results = FxHashMap::default();
1555            for (index, (str, until_end)) in constants.into_iter().enumerate() {
1556                if until_end {
1557                    if !handled.insert(str) {
1558                        continue;
1559                    }
1560                    let (parent_path, last_segment) = split_last_segment(str);
1561                    if last_segment.is_empty() {
1562                        // This means we don't have a last segment, so we just have a directory
1563                        let joined = if force_in_lookup_dir {
1564                            lookup_dir.try_join_inside(parent_path)?
1565                        } else {
1566                            lookup_dir.try_join(parent_path)?
1567                        };
1568                        let Some(fs_path) = joined else {
1569                            continue;
1570                        };
1571                        results.push((
1572                            index,
1573                            PatternMatch::Directory(concat(&prefix, str).into(), fs_path),
1574                        ));
1575                        continue;
1576                    }
1577                    let entry = read_dir_results.entry(parent_path);
1578                    let read_dir = match entry {
1579                        Entry::Occupied(e) => Some(e.into_mut()),
1580                        Entry::Vacant(e) => {
1581                            let path_option = if force_in_lookup_dir {
1582                                lookup_dir.try_join_inside(parent_path)?
1583                            } else {
1584                                lookup_dir.try_join(parent_path)?
1585                            };
1586                            if let Some(path) = path_option {
1587                                Some(e.insert((path.raw_read_dir().await?, path)))
1588                            } else {
1589                                None
1590                            }
1591                        }
1592                    };
1593                    let Some((read_dir, parent_fs_path)) = read_dir else {
1594                        continue;
1595                    };
1596                    let RawDirectoryContent::Entries(entries) = &**read_dir else {
1597                        continue;
1598                    };
1599                    let Some(entry) = entries.get(last_segment) else {
1600                        continue;
1601                    };
1602                    match *entry {
1603                        RawDirectoryEntry::File => {
1604                            results.push((
1605                                index,
1606                                PatternMatch::File(
1607                                    concat(&prefix, str).into(),
1608                                    parent_fs_path.join(last_segment)?,
1609                                ),
1610                            ));
1611                        }
1612                        RawDirectoryEntry::Directory => results.push((
1613                            index,
1614                            PatternMatch::Directory(
1615                                concat(&prefix, str).into(),
1616                                parent_fs_path.join(last_segment)?,
1617                            ),
1618                        )),
1619                        RawDirectoryEntry::Symlink => {
1620                            let fs_path = parent_fs_path.join(last_segment)?;
1621                            let LinkContent::Link { link_type, .. } = &*fs_path.read_link().await?
1622                            else {
1623                                continue;
1624                            };
1625                            let path = concat(&prefix, str).into();
1626                            if link_type.contains(LinkType::DIRECTORY) {
1627                                results.push((index, PatternMatch::Directory(path, fs_path)));
1628                            } else {
1629                                results.push((index, PatternMatch::File(path, fs_path)))
1630                            }
1631                        }
1632                        _ => {}
1633                    }
1634                } else {
1635                    let subpath = &str[..=str.rfind('/').unwrap()];
1636                    if handled.insert(subpath) {
1637                        let joined = if force_in_lookup_dir {
1638                            lookup_dir.try_join_inside(subpath)?
1639                        } else {
1640                            lookup_dir.try_join(subpath)?
1641                        };
1642                        let Some(fs_path) = joined else {
1643                            continue;
1644                        };
1645                        nested.push((
1646                            0,
1647                            read_matches(
1648                                fs_path.clone(),
1649                                concat(&prefix, subpath).into(),
1650                                force_in_lookup_dir,
1651                                pattern,
1652                            ),
1653                        ));
1654                    }
1655                }
1656            }
1657            false
1658        } else {
1659            true
1660        }
1661    } else {
1662        true
1663    };
1664
1665    if slow_path {
1666        async {
1667            // Slow path: There are infinite matches for the pattern
1668            // We will enumerate the filesystem to find matches
1669            if !force_in_lookup_dir {
1670                // {prefix}..
1671                prefix.push_str("..");
1672                if let Some(pos) = pat.match_position(&prefix) {
1673                    results.push((
1674                        pos,
1675                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1676                    ));
1677                }
1678
1679                // {prefix}../
1680                prefix.push('/');
1681                if let Some(pos) = pat.match_position(&prefix) {
1682                    results.push((
1683                        pos,
1684                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
1685                    ));
1686                }
1687                if let Some(pos) = pat.could_match_position(&prefix) {
1688                    nested.push((
1689                        pos,
1690                        read_matches(lookup_dir.parent(), prefix.clone().into(), false, pattern),
1691                    ));
1692                }
1693                prefix.pop();
1694                prefix.pop();
1695                prefix.pop();
1696            }
1697            {
1698                prefix.push('.');
1699                // {prefix}.
1700                if let Some(pos) = pat.match_position(&prefix) {
1701                    results.push((
1702                        pos,
1703                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.clone()),
1704                    ));
1705                }
1706                prefix.pop();
1707            }
1708            if prefix.is_empty() {
1709                if let Some(pos) = pat.match_position("./") {
1710                    results.push((
1711                        pos,
1712                        PatternMatch::Directory(rcstr!("./"), lookup_dir.clone()),
1713                    ));
1714                }
1715                if let Some(pos) = pat.could_match_position("./") {
1716                    nested.push((
1717                        pos,
1718                        read_matches(lookup_dir.clone(), rcstr!("./"), false, pattern),
1719                    ));
1720                }
1721            } else {
1722                prefix.push('/');
1723                // {prefix}/
1724                if let Some(pos) = pat.could_match_position(&prefix) {
1725                    nested.push((
1726                        pos,
1727                        read_matches(
1728                            lookup_dir.clone(),
1729                            prefix.to_string().into(),
1730                            false,
1731                            pattern,
1732                        ),
1733                    ));
1734                }
1735                prefix.pop();
1736                prefix.push_str("./");
1737                // {prefix}./
1738                if let Some(pos) = pat.could_match_position(&prefix) {
1739                    nested.push((
1740                        pos,
1741                        read_matches(
1742                            lookup_dir.clone(),
1743                            prefix.to_string().into(),
1744                            false,
1745                            pattern,
1746                        ),
1747                    ));
1748                }
1749                prefix.pop();
1750                prefix.pop();
1751            }
1752            match &*lookup_dir.raw_read_dir().await? {
1753                RawDirectoryContent::Entries(map) => {
1754                    for (key, entry) in map.iter() {
1755                        match entry {
1756                            RawDirectoryEntry::File => {
1757                                let len = prefix.len();
1758                                prefix.push_str(key);
1759                                // {prefix}{key}
1760                                if let Some(pos) = pat.match_position(&prefix) {
1761                                    let path = lookup_dir.join(key)?;
1762                                    results.push((
1763                                        pos,
1764                                        PatternMatch::File(prefix.clone().into(), path),
1765                                    ));
1766                                }
1767                                prefix.truncate(len)
1768                            }
1769                            RawDirectoryEntry::Directory => {
1770                                let len = prefix.len();
1771                                prefix.push_str(key);
1772                                // {prefix}{key}
1773                                if prefix.ends_with('/') {
1774                                    prefix.pop();
1775                                }
1776                                if let Some(pos) = pat.match_position(&prefix) {
1777                                    let path = lookup_dir.join(key)?;
1778                                    results.push((
1779                                        pos,
1780                                        PatternMatch::Directory(prefix.clone().into(), path),
1781                                    ));
1782                                }
1783                                prefix.push('/');
1784                                // {prefix}{key}/
1785                                if let Some(pos) = pat.match_position(&prefix) {
1786                                    let path = lookup_dir.join(key)?;
1787                                    results.push((
1788                                        pos,
1789                                        PatternMatch::Directory(prefix.clone().into(), path),
1790                                    ));
1791                                }
1792                                if let Some(pos) = pat.could_match_position(&prefix) {
1793                                    let path = lookup_dir.join(key)?;
1794                                    nested.push((
1795                                        pos,
1796                                        read_matches(path, prefix.clone().into(), true, pattern),
1797                                    ));
1798                                }
1799                                prefix.truncate(len)
1800                            }
1801                            RawDirectoryEntry::Symlink => {
1802                                let len = prefix.len();
1803                                prefix.push_str(key);
1804                                // {prefix}{key}
1805                                if prefix.ends_with('/') {
1806                                    prefix.pop();
1807                                }
1808                                if let Some(pos) = pat.match_position(&prefix) {
1809                                    let fs_path = lookup_dir.join(key)?;
1810                                    if let LinkContent::Link { link_type, .. } =
1811                                        &*fs_path.read_link().await?
1812                                    {
1813                                        if link_type.contains(LinkType::DIRECTORY) {
1814                                            results.push((
1815                                                pos,
1816                                                PatternMatch::Directory(
1817                                                    prefix.clone().into(),
1818                                                    fs_path,
1819                                                ),
1820                                            ));
1821                                        } else {
1822                                            results.push((
1823                                                pos,
1824                                                PatternMatch::File(prefix.clone().into(), fs_path),
1825                                            ));
1826                                        }
1827                                    }
1828                                }
1829                                prefix.push('/');
1830                                if let Some(pos) = pat.match_position(&prefix) {
1831                                    let fs_path = lookup_dir.join(key)?;
1832                                    if let LinkContent::Link { link_type, .. } =
1833                                        &*fs_path.read_link().await?
1834                                        && link_type.contains(LinkType::DIRECTORY)
1835                                    {
1836                                        results.push((
1837                                            pos,
1838                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
1839                                        ));
1840                                    }
1841                                }
1842                                if let Some(pos) = pat.could_match_position(&prefix) {
1843                                    let fs_path = lookup_dir.join(key)?;
1844                                    if let LinkContent::Link { link_type, .. } =
1845                                        &*fs_path.read_link().await?
1846                                        && link_type.contains(LinkType::DIRECTORY)
1847                                    {
1848                                        results.push((
1849                                            pos,
1850                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
1851                                        ));
1852                                    }
1853                                }
1854                                prefix.truncate(len)
1855                            }
1856                            RawDirectoryEntry::Other => {}
1857                        }
1858                    }
1859                }
1860                RawDirectoryContent::NotFound => {}
1861            };
1862            anyhow::Ok(())
1863        }
1864        .instrument(tracing::trace_span!("read_matches slow_path"))
1865        .await?;
1866    }
1867    if results.is_empty() && nested.len() == 1 {
1868        Ok(nested.into_iter().next().unwrap().1)
1869    } else {
1870        for (pos, nested) in nested.into_iter() {
1871            results.extend(nested.await?.iter().cloned().map(|p| (pos, p)));
1872        }
1873        results.sort_by(|(a, am), (b, bm)| (*a).cmp(b).then_with(|| am.name().cmp(bm.name())));
1874        Ok(Vc::cell(
1875            results.into_iter().map(|(_, p)| p).collect::<Vec<_>>(),
1876        ))
1877    }
1878}
1879
1880fn concat(a: &str, b: &str) -> String {
1881    let mut result = String::with_capacity(a.len() + b.len());
1882    result.push_str(a);
1883    result.push_str(b);
1884    result
1885}
1886
1887/// Returns the parent folder and the last segment of the path. When the last segment is unknown (e.
1888/// g. when using `../`) it returns the full path and an empty string.
1889fn split_last_segment(path: &str) -> (&str, &str) {
1890    if let Some((remaining_path, last_segment)) = path.rsplit_once('/') {
1891        match last_segment {
1892            "" => split_last_segment(remaining_path),
1893            "." => split_last_segment(remaining_path),
1894            ".." => match split_last_segment(remaining_path) {
1895                (_, "") => (path, ""),
1896                (parent_path, _) => split_last_segment(parent_path),
1897            },
1898            _ => (remaining_path, last_segment),
1899        }
1900    } else {
1901        match path {
1902            "" => ("", ""),
1903            "." => ("", ""),
1904            ".." => ("..", ""),
1905            _ => ("", path),
1906        }
1907    }
1908}
1909
1910#[cfg(test)]
1911mod tests {
1912    use std::path::Path;
1913
1914    use rstest::*;
1915    use turbo_rcstr::{RcStr, rcstr};
1916    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1917    use turbo_tasks_fs::{DiskFileSystem, FileSystem};
1918
1919    use super::{
1920        Pattern, longest_common_prefix, longest_common_suffix, read_matches, split_last_segment,
1921    };
1922
1923    #[test]
1924    fn longest_common_prefix_test() {
1925        assert_eq!(longest_common_prefix(&["ab"]), "ab");
1926        assert_eq!(longest_common_prefix(&["ab", "cd", "ef"]), "");
1927        assert_eq!(longest_common_prefix(&["ab1", "ab23", "ab456"]), "ab");
1928        assert_eq!(longest_common_prefix(&["abc", "abc", "abc"]), "abc");
1929        assert_eq!(longest_common_prefix(&["abc", "a", "abc"]), "a");
1930    }
1931
1932    #[test]
1933    fn longest_common_suffix_test() {
1934        assert_eq!(longest_common_suffix(&["ab"]), "ab");
1935        assert_eq!(longest_common_suffix(&["ab", "cd", "ef"]), "");
1936        assert_eq!(longest_common_suffix(&["1ab", "23ab", "456ab"]), "ab");
1937        assert_eq!(longest_common_suffix(&["abc", "abc", "abc"]), "abc");
1938        assert_eq!(longest_common_suffix(&["abc", "c", "abc"]), "c");
1939    }
1940
1941    #[test]
1942    fn normalize() {
1943        let a = Pattern::Constant(rcstr!("a"));
1944        let b = Pattern::Constant(rcstr!("b"));
1945        let c = Pattern::Constant(rcstr!("c"));
1946        let s = Pattern::Constant(rcstr!("/"));
1947        let d = Pattern::Dynamic;
1948        {
1949            let mut p = Pattern::Concatenation(vec![
1950                Pattern::Alternatives(vec![a.clone(), b.clone()]),
1951                s.clone(),
1952                c.clone(),
1953            ]);
1954            p.normalize();
1955            assert_eq!(
1956                p,
1957                Pattern::Alternatives(vec![
1958                    Pattern::Constant(rcstr!("a/c")),
1959                    Pattern::Constant(rcstr!("b/c")),
1960                ])
1961            );
1962        }
1963
1964        #[allow(clippy::redundant_clone)] // alignment
1965        {
1966            let mut p = Pattern::Concatenation(vec![
1967                Pattern::Alternatives(vec![a.clone(), b.clone(), d.clone()]),
1968                s.clone(),
1969                Pattern::Alternatives(vec![b.clone(), c.clone(), d.clone()]),
1970            ]);
1971            p.normalize();
1972
1973            assert_eq!(
1974                p,
1975                Pattern::Alternatives(vec![
1976                    Pattern::Constant(rcstr!("a/b")),
1977                    Pattern::Constant(rcstr!("b/b")),
1978                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/b"))]),
1979                    Pattern::Constant(rcstr!("a/c")),
1980                    Pattern::Constant(rcstr!("b/c")),
1981                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/c"))]),
1982                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
1983                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
1984                    Pattern::Concatenation(vec![
1985                        Pattern::Dynamic,
1986                        Pattern::Constant(rcstr!("/")),
1987                        Pattern::Dynamic
1988                    ]),
1989                ])
1990            );
1991        }
1992
1993        #[allow(clippy::redundant_clone)] // alignment
1994        {
1995            let mut p = Pattern::Alternatives(vec![a.clone()]);
1996            p.normalize();
1997
1998            assert_eq!(p, a);
1999        }
2000
2001        #[allow(clippy::redundant_clone)] // alignment
2002        {
2003            let mut p = Pattern::Alternatives(vec![Pattern::Dynamic, Pattern::Dynamic]);
2004            p.normalize();
2005
2006            assert_eq!(p, Pattern::Dynamic);
2007        }
2008    }
2009
2010    #[test]
2011    fn with_normalized_path() {
2012        assert!(
2013            Pattern::Constant(rcstr!("a/../.."))
2014                .with_normalized_path()
2015                .is_none()
2016        );
2017        assert_eq!(
2018            Pattern::Constant(rcstr!("a/b/../c"))
2019                .with_normalized_path()
2020                .unwrap(),
2021            Pattern::Constant(rcstr!("a/c"))
2022        );
2023        assert_eq!(
2024            Pattern::Alternatives(vec![
2025                Pattern::Constant(rcstr!("a/b/../c")),
2026                Pattern::Constant(rcstr!("a/b/../c/d"))
2027            ])
2028            .with_normalized_path()
2029            .unwrap(),
2030            Pattern::Alternatives(vec![
2031                Pattern::Constant(rcstr!("a/c")),
2032                Pattern::Constant(rcstr!("a/c/d"))
2033            ])
2034        );
2035        assert_eq!(
2036            Pattern::Constant(rcstr!("a/b/"))
2037                .with_normalized_path()
2038                .unwrap(),
2039            Pattern::Constant(rcstr!("a/b"))
2040        );
2041
2042        // Dynamic is a segment itself
2043        assert_eq!(
2044            Pattern::Concatenation(vec![
2045                Pattern::Constant(rcstr!("a/b/")),
2046                Pattern::Dynamic,
2047                Pattern::Constant(rcstr!("../c"))
2048            ])
2049            .with_normalized_path()
2050            .unwrap(),
2051            Pattern::Concatenation(vec![
2052                Pattern::Constant(rcstr!("a/b/")),
2053                Pattern::Dynamic,
2054                Pattern::Constant(rcstr!("../c"))
2055            ])
2056        );
2057
2058        // Dynamic is part of a segment
2059        assert_eq!(
2060            Pattern::Concatenation(vec![
2061                Pattern::Constant(rcstr!("a/b")),
2062                Pattern::Dynamic,
2063                Pattern::Constant(rcstr!("../c"))
2064            ])
2065            .with_normalized_path()
2066            .unwrap(),
2067            Pattern::Concatenation(vec![
2068                Pattern::Constant(rcstr!("a/b")),
2069                Pattern::Dynamic,
2070                Pattern::Constant(rcstr!("../c"))
2071            ])
2072        );
2073        assert_eq!(
2074            Pattern::Concatenation(vec![
2075                Pattern::Constant(rcstr!("src/")),
2076                Pattern::Dynamic,
2077                Pattern::Constant(rcstr!(".js"))
2078            ])
2079            .with_normalized_path()
2080            .unwrap(),
2081            Pattern::Concatenation(vec![
2082                Pattern::Constant(rcstr!("src/")),
2083                Pattern::Dynamic,
2084                Pattern::Constant(rcstr!(".js"))
2085            ])
2086        );
2087    }
2088
2089    #[test]
2090    fn is_match() {
2091        let pat = Pattern::Concatenation(vec![
2092            Pattern::Constant(rcstr!(".")),
2093            Pattern::Constant(rcstr!("/")),
2094            Pattern::Dynamic,
2095            Pattern::Constant(rcstr!(".js")),
2096        ]);
2097        assert!(pat.could_match(""));
2098        assert!(pat.could_match("./"));
2099        assert!(!pat.is_match("./"));
2100        assert!(pat.is_match("./index.js"));
2101        assert!(!pat.is_match("./index"));
2102        assert!(pat.is_match("./foo/index.js"));
2103        assert!(pat.is_match("./foo/bar/index.js"));
2104
2105        // forbidden:
2106        assert!(!pat.is_match("./../index.js"));
2107        assert!(!pat.is_match("././index.js"));
2108        assert!(!pat.is_match("./.git/index.js"));
2109        assert!(!pat.is_match("./inner/../index.js"));
2110        assert!(!pat.is_match("./inner/./index.js"));
2111        assert!(!pat.is_match("./inner/.git/index.js"));
2112        assert!(!pat.could_match("./../"));
2113        assert!(!pat.could_match("././"));
2114        assert!(!pat.could_match("./.git/"));
2115        assert!(!pat.could_match("./inner/../"));
2116        assert!(!pat.could_match("./inner/./"));
2117        assert!(!pat.could_match("./inner/.git/"));
2118    }
2119
2120    #[test]
2121    fn is_match_dynamic_no_slash() {
2122        let pat = Pattern::Concatenation(vec![
2123            Pattern::Constant(rcstr!(".")),
2124            Pattern::Constant(rcstr!("/")),
2125            Pattern::DynamicNoSlash,
2126            Pattern::Constant(rcstr!(".js")),
2127        ]);
2128        assert!(pat.could_match(""));
2129        assert!(pat.could_match("./"));
2130        assert!(!pat.is_match("./"));
2131        assert!(pat.is_match("./index.js"));
2132        assert!(!pat.is_match("./index"));
2133        assert!(!pat.is_match("./foo/index.js"));
2134        assert!(!pat.is_match("./foo/bar/index.js"));
2135    }
2136
2137    #[test]
2138    fn constant_prefix() {
2139        assert_eq!(
2140            Pattern::Constant(rcstr!("a/b/c.js")).constant_prefix(),
2141            "a/b/c.js",
2142        );
2143
2144        let pat = Pattern::Alternatives(vec![
2145            Pattern::Constant(rcstr!("a/b/x")),
2146            Pattern::Constant(rcstr!("a/b/y")),
2147            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]),
2148        ]);
2149        assert_eq!(pat.constant_prefix(), "a/b/");
2150    }
2151
2152    #[test]
2153    fn constant_suffix() {
2154        assert_eq!(
2155            Pattern::Constant(rcstr!("a/b/c.js")).constant_suffix(),
2156            "a/b/c.js",
2157        );
2158
2159        let pat = Pattern::Alternatives(vec![
2160            Pattern::Constant(rcstr!("a/b/x.js")),
2161            Pattern::Constant(rcstr!("a/b/y.js")),
2162            Pattern::Concatenation(vec![
2163                Pattern::Constant(rcstr!("a/b/c/")),
2164                Pattern::Dynamic,
2165                Pattern::Constant(rcstr!(".js")),
2166            ]),
2167        ]);
2168        assert_eq!(pat.constant_suffix(), ".js");
2169    }
2170
2171    #[test]
2172    fn strip_prefix() {
2173        fn strip(mut pat: Pattern, n: usize) -> Pattern {
2174            pat.strip_prefix_len(n).unwrap();
2175            pat
2176        }
2177
2178        assert_eq!(
2179            strip(Pattern::Constant(rcstr!("a/b")), 0),
2180            Pattern::Constant(rcstr!("a/b"))
2181        );
2182
2183        assert_eq!(
2184            strip(
2185                Pattern::Alternatives(vec![
2186                    Pattern::Constant(rcstr!("a/b/x")),
2187                    Pattern::Constant(rcstr!("a/b/y")),
2188                ]),
2189                2
2190            ),
2191            Pattern::Alternatives(vec![
2192                Pattern::Constant(rcstr!("b/x")),
2193                Pattern::Constant(rcstr!("b/y")),
2194            ])
2195        );
2196
2197        assert_eq!(
2198            strip(
2199                Pattern::Concatenation(vec![
2200                    Pattern::Constant(rcstr!("a/")),
2201                    Pattern::Constant(rcstr!("b")),
2202                    Pattern::Constant(rcstr!("/")),
2203                    Pattern::Constant(rcstr!("y/")),
2204                    Pattern::Dynamic
2205                ]),
2206                4
2207            ),
2208            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("y/")), Pattern::Dynamic]),
2209        );
2210    }
2211
2212    #[test]
2213    fn strip_suffix() {
2214        fn strip(mut pat: Pattern, n: usize) -> Pattern {
2215            pat.strip_suffix_len(n);
2216            pat
2217        }
2218
2219        assert_eq!(
2220            strip(Pattern::Constant(rcstr!("a/b")), 0),
2221            Pattern::Constant(rcstr!("a/b"))
2222        );
2223
2224        assert_eq!(
2225            strip(
2226                Pattern::Alternatives(vec![
2227                    Pattern::Constant(rcstr!("x/b/a")),
2228                    Pattern::Constant(rcstr!("y/b/a")),
2229                ]),
2230                2
2231            ),
2232            Pattern::Alternatives(vec![
2233                Pattern::Constant(rcstr!("x/b")),
2234                Pattern::Constant(rcstr!("y/b")),
2235            ])
2236        );
2237
2238        assert_eq!(
2239            strip(
2240                Pattern::Concatenation(vec![
2241                    Pattern::Dynamic,
2242                    Pattern::Constant(rcstr!("/a/")),
2243                    Pattern::Constant(rcstr!("b")),
2244                    Pattern::Constant(rcstr!("/")),
2245                    Pattern::Constant(rcstr!("y/")),
2246                ]),
2247                4
2248            ),
2249            Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/a/")),]),
2250        );
2251    }
2252
2253    #[test]
2254    fn spread_into_star() {
2255        let pat = Pattern::Constant(rcstr!("xyz"));
2256        assert_eq!(
2257            pat.spread_into_star("before/after"),
2258            Pattern::Constant(rcstr!("before/after")),
2259        );
2260
2261        let pat =
2262            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]);
2263        assert_eq!(
2264            pat.spread_into_star("before/*/after"),
2265            Pattern::Concatenation(vec![
2266                Pattern::Constant(rcstr!("before/a/b/c/")),
2267                Pattern::Dynamic,
2268                Pattern::Constant(rcstr!("/after"))
2269            ])
2270        );
2271
2272        let pat = Pattern::Alternatives(vec![
2273            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
2274            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
2275        ]);
2276        assert_eq!(
2277            pat.spread_into_star("before/*/after"),
2278            Pattern::Alternatives(vec![
2279                Pattern::Concatenation(vec![
2280                    Pattern::Constant(rcstr!("before/a/")),
2281                    Pattern::Dynamic,
2282                    Pattern::Constant(rcstr!("/after"))
2283                ]),
2284                Pattern::Concatenation(vec![
2285                    Pattern::Constant(rcstr!("before/b/")),
2286                    Pattern::Dynamic,
2287                    Pattern::Constant(rcstr!("/after"))
2288                ]),
2289            ])
2290        );
2291
2292        let pat = Pattern::Alternatives(vec![
2293            Pattern::Constant(rcstr!("a")),
2294            Pattern::Constant(rcstr!("b")),
2295        ]);
2296        assert_eq!(
2297            pat.spread_into_star("before/*/*"),
2298            Pattern::Alternatives(vec![
2299                Pattern::Constant(rcstr!("before/a/a")),
2300                Pattern::Constant(rcstr!("before/b/b")),
2301            ])
2302        );
2303
2304        let pat = Pattern::Dynamic;
2305        assert_eq!(
2306            pat.spread_into_star("before/*/*"),
2307            Pattern::Concatenation(vec![
2308                // TODO currently nothing ensures that both Dynamic parts are equal
2309                Pattern::Constant(rcstr!("before/")),
2310                Pattern::Dynamic,
2311                Pattern::Constant(rcstr!("/")),
2312                Pattern::Dynamic
2313            ])
2314        );
2315    }
2316
2317    #[rstest]
2318    #[case::dynamic(Pattern::Dynamic)]
2319    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2320    fn dynamic_match(#[case] pat: Pattern) {
2321        assert!(pat.could_match(""));
2322        assert!(pat.is_match("index.js"));
2323
2324        // forbidden:
2325        assert!(!pat.could_match("./"));
2326        assert!(!pat.is_match("./"));
2327        assert!(!pat.could_match("."));
2328        assert!(!pat.is_match("."));
2329        assert!(!pat.could_match("../"));
2330        assert!(!pat.is_match("../"));
2331        assert!(!pat.could_match(".."));
2332        assert!(!pat.is_match(".."));
2333        assert!(!pat.is_match("./../index.js"));
2334        assert!(!pat.is_match("././index.js"));
2335        assert!(!pat.is_match("./.git/index.js"));
2336        assert!(!pat.is_match("./inner/../index.js"));
2337        assert!(!pat.is_match("./inner/./index.js"));
2338        assert!(!pat.is_match("./inner/.git/index.js"));
2339        assert!(!pat.could_match("./../"));
2340        assert!(!pat.could_match("././"));
2341        assert!(!pat.could_match("./.git/"));
2342        assert!(!pat.could_match("./inner/../"));
2343        assert!(!pat.could_match("./inner/./"));
2344        assert!(!pat.could_match("./inner/.git/"));
2345        assert!(!pat.could_match("dir//"));
2346        assert!(!pat.could_match("dir//dir"));
2347        assert!(!pat.could_match("dir///dir"));
2348        assert!(!pat.could_match("/"));
2349        assert!(!pat.could_match("//"));
2350        assert!(!pat.could_match("/ROOT/"));
2351
2352        assert!(!pat.could_match("node_modules"));
2353        assert!(!pat.could_match("node_modules/package"));
2354        assert!(!pat.could_match("nested/node_modules"));
2355        assert!(!pat.could_match("nested/node_modules/package"));
2356
2357        // forbidden match
2358        assert!(pat.could_match("file.map"));
2359        assert!(!pat.is_match("file.map"));
2360        assert!(pat.is_match("file.map/file.js"));
2361        assert!(!pat.is_match("file.d.ts"));
2362        assert!(!pat.is_match("file.d.ts.map"));
2363        assert!(!pat.is_match("file.d.ts.map"));
2364        assert!(!pat.is_match("dir/file.d.ts.map"));
2365        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2366        assert!(pat.could_match("dir/inner/file.d.ts.map"));
2367    }
2368
2369    #[rstest]
2370    #[case::slash(Pattern::Concatenation(vec![Pattern::Constant(rcstr!("node_modules/")),Pattern::Dynamic]))]
2371    #[case::nested(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file())]
2372    fn dynamic_match_node_modules(#[case] pat: Pattern) {
2373        assert!(!pat.is_match("node_modules/package"));
2374        assert!(!pat.could_match("node_modules/package"));
2375        assert!(!pat.is_match("node_modules/package/index.js"));
2376        assert!(!pat.could_match("node_modules/package/index.js"));
2377    }
2378
2379    #[rstest]
2380    fn dynamic_match2() {
2381        let pat = Pattern::Concatenation(vec![
2382            Pattern::Dynamic,
2383            Pattern::Constant(rcstr!("/")),
2384            Pattern::Dynamic,
2385        ]);
2386        assert!(pat.could_match("dir"));
2387        assert!(pat.could_match("dir/"));
2388        assert!(pat.is_match("dir/index.js"));
2389
2390        // forbidden:
2391        assert!(!pat.could_match("./"));
2392        assert!(!pat.is_match("./"));
2393        assert!(!pat.could_match("."));
2394        assert!(!pat.is_match("."));
2395        assert!(!pat.could_match("../"));
2396        assert!(!pat.is_match("../"));
2397        assert!(!pat.could_match(".."));
2398        assert!(!pat.is_match(".."));
2399        assert!(!pat.is_match("./../index.js"));
2400        assert!(!pat.is_match("././index.js"));
2401        assert!(!pat.is_match("./.git/index.js"));
2402        assert!(!pat.is_match("./inner/../index.js"));
2403        assert!(!pat.is_match("./inner/./index.js"));
2404        assert!(!pat.is_match("./inner/.git/index.js"));
2405        assert!(!pat.could_match("./../"));
2406        assert!(!pat.could_match("././"));
2407        assert!(!pat.could_match("./.git/"));
2408        assert!(!pat.could_match("./inner/../"));
2409        assert!(!pat.could_match("./inner/./"));
2410        assert!(!pat.could_match("./inner/.git/"));
2411        assert!(!pat.could_match("dir//"));
2412        assert!(!pat.could_match("dir//dir"));
2413        assert!(!pat.could_match("dir///dir"));
2414        assert!(!pat.could_match("/ROOT/"));
2415
2416        assert!(!pat.could_match("node_modules"));
2417        assert!(!pat.could_match("node_modules/package"));
2418        assert!(!pat.could_match("nested/node_modules"));
2419        assert!(!pat.could_match("nested/node_modules/package"));
2420
2421        // forbidden match
2422        assert!(pat.could_match("dir/file.map"));
2423        assert!(!pat.is_match("dir/file.map"));
2424        assert!(pat.is_match("file.map/file.js"));
2425        assert!(!pat.is_match("dir/file.d.ts"));
2426        assert!(!pat.is_match("dir/file.d.ts.map"));
2427        assert!(!pat.is_match("dir/file.d.ts.map"));
2428        assert!(!pat.is_match("dir/file.d.ts.map"));
2429        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
2430        assert!(pat.could_match("dir/inner/file.d.ts.map"));
2431    }
2432
2433    #[rstest]
2434    #[case::dynamic(Pattern::Dynamic)]
2435    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
2436    #[case::dynamic_concat2(Pattern::Concatenation(vec![
2437        Pattern::Dynamic,
2438        Pattern::Constant(rcstr!("/")),
2439        Pattern::Dynamic,
2440    ]))]
2441    #[case::dynamic_alt_concat(Pattern::alternatives(vec![
2442        Pattern::Concatenation(vec![
2443            Pattern::Dynamic,
2444            Pattern::Constant(rcstr!("/")),
2445            Pattern::Dynamic,
2446        ]),
2447        Pattern::Dynamic,
2448    ]))]
2449    fn split_could_match(#[case] pat: Pattern) {
2450        let (abs, rel) = pat.split_could_match("/ROOT/");
2451        assert!(abs.is_none());
2452        assert!(rel.is_some());
2453    }
2454
2455    #[rstest]
2456    #[case::dynamic(Pattern::Dynamic, "feijf", None)]
2457    #[case::dynamic_concat(
2458        Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]),
2459        "hello.", None
2460    )]
2461    #[case::constant(Pattern::Constant(rcstr!("Hello World")), "Hello ", Some(vec![("World", true)]))]
2462    #[case::alternatives(
2463        Pattern::Alternatives(vec![
2464            Pattern::Constant(rcstr!("Hello World")),
2465            Pattern::Constant(rcstr!("Hello All"))
2466        ]), "Hello ", Some(vec![("World", true), ("All", true)])
2467    )]
2468    #[case::alternatives_non_end(
2469        Pattern::Alternatives(vec![
2470            Pattern::Constant(rcstr!("Hello World")),
2471            Pattern::Constant(rcstr!("Hello All")),
2472            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("Hello more")), Pattern::Dynamic])
2473        ]), "Hello ", Some(vec![("World", true), ("All", true), ("more", false)])
2474    )]
2475    #[case::request_with_extensions(
2476        Pattern::Alternatives(vec![
2477            Pattern::Constant(rcstr!("./file.js")),
2478            Pattern::Constant(rcstr!("./file.ts")),
2479            Pattern::Constant(rcstr!("./file.cjs")),
2480        ]), "./", Some(vec![("file.js", true), ("file.ts", true), ("file.cjs", true)])
2481    )]
2482    fn next_constants(
2483        #[case] pat: Pattern,
2484        #[case] value: &str,
2485        #[case] expected: Option<Vec<(&str, bool)>>,
2486    ) {
2487        assert_eq!(pat.next_constants(value), expected);
2488    }
2489
2490    #[test]
2491    fn replace_final_constants() {
2492        fn f(mut p: Pattern, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> Pattern {
2493            p.replace_final_constants(cb);
2494            p
2495        }
2496
2497        let js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
2498            c.strip_suffix(".js").map(|rest| {
2499                let new_ending = Pattern::Alternatives(vec![
2500                    Pattern::Constant(rcstr!(".ts")),
2501                    Pattern::Constant(rcstr!(".tsx")),
2502                    Pattern::Constant(rcstr!(".js")),
2503                ]);
2504                if !rest.is_empty() {
2505                    Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
2506                } else {
2507                    new_ending
2508                }
2509            })
2510        };
2511
2512        assert_eq!(
2513            f(
2514                Pattern::Concatenation(vec![
2515                    Pattern::Constant(rcstr!(".")),
2516                    Pattern::Constant(rcstr!("/")),
2517                    Pattern::Dynamic,
2518                    Pattern::Alternatives(vec![
2519                        Pattern::Constant(rcstr!(".js")),
2520                        Pattern::Constant(rcstr!(".node")),
2521                    ])
2522                ]),
2523                &js_to_ts_tsx
2524            ),
2525            Pattern::Concatenation(vec![
2526                Pattern::Constant(rcstr!(".")),
2527                Pattern::Constant(rcstr!("/")),
2528                Pattern::Dynamic,
2529                Pattern::Alternatives(vec![
2530                    Pattern::Alternatives(vec![
2531                        Pattern::Constant(rcstr!(".ts")),
2532                        Pattern::Constant(rcstr!(".tsx")),
2533                        Pattern::Constant(rcstr!(".js")),
2534                    ]),
2535                    Pattern::Constant(rcstr!(".node")),
2536                ])
2537            ]),
2538        );
2539        assert_eq!(
2540            f(
2541                Pattern::Concatenation(vec![
2542                    Pattern::Constant(rcstr!(".")),
2543                    Pattern::Constant(rcstr!("/")),
2544                    Pattern::Constant(rcstr!("abc.js")),
2545                ]),
2546                &js_to_ts_tsx
2547            ),
2548            Pattern::Concatenation(vec![
2549                Pattern::Constant(rcstr!(".")),
2550                Pattern::Constant(rcstr!("/")),
2551                Pattern::Concatenation(vec![
2552                    Pattern::Constant(rcstr!("abc")),
2553                    Pattern::Alternatives(vec![
2554                        Pattern::Constant(rcstr!(".ts")),
2555                        Pattern::Constant(rcstr!(".tsx")),
2556                        Pattern::Constant(rcstr!(".js")),
2557                    ])
2558                ]),
2559            ])
2560        );
2561    }
2562
2563    #[test]
2564    fn match_apply_template() {
2565        assert_eq!(
2566            Pattern::Concatenation(vec![
2567                Pattern::Constant(rcstr!("a/b/")),
2568                Pattern::Dynamic,
2569                Pattern::Constant(rcstr!(".ts")),
2570            ])
2571            .match_apply_template(
2572                "a/b/foo.ts",
2573                &Pattern::Concatenation(vec![
2574                    Pattern::Constant(rcstr!("@/a/b/")),
2575                    Pattern::Dynamic,
2576                    Pattern::Constant(rcstr!(".js")),
2577                ])
2578            )
2579            .as_deref(),
2580            Some("@/a/b/foo.js")
2581        );
2582        assert_eq!(
2583            Pattern::Concatenation(vec![
2584                Pattern::Constant(rcstr!("b/")),
2585                Pattern::Dynamic,
2586                Pattern::Constant(rcstr!(".ts")),
2587            ])
2588            .match_apply_template(
2589                "a/b/foo.ts",
2590                &Pattern::Concatenation(vec![
2591                    Pattern::Constant(rcstr!("@/a/b/")),
2592                    Pattern::Dynamic,
2593                    Pattern::Constant(rcstr!(".js")),
2594                ])
2595            )
2596            .as_deref(),
2597            None,
2598        );
2599        assert_eq!(
2600            Pattern::Concatenation(vec![
2601                Pattern::Constant(rcstr!("a/b/")),
2602                Pattern::Dynamic,
2603                Pattern::Constant(rcstr!(".ts")),
2604            ])
2605            .match_apply_template(
2606                "a/b/foo.ts",
2607                &Pattern::Concatenation(vec![
2608                    Pattern::Constant(rcstr!("@/a/b/x")),
2609                    Pattern::Constant(rcstr!(".js")),
2610                ])
2611            )
2612            .as_deref(),
2613            None,
2614        );
2615        assert_eq!(
2616            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("./sub/")), Pattern::Dynamic])
2617                .match_apply_template(
2618                    "./sub/file1",
2619                    &Pattern::Concatenation(vec![
2620                        Pattern::Constant(rcstr!("@/sub/")),
2621                        Pattern::Dynamic
2622                    ])
2623                )
2624                .as_deref(),
2625            Some("@/sub/file1"),
2626        );
2627    }
2628
2629    #[test]
2630    fn test_split_last_segment() {
2631        assert_eq!(split_last_segment(""), ("", ""));
2632        assert_eq!(split_last_segment("a"), ("", "a"));
2633        assert_eq!(split_last_segment("a/"), ("", "a"));
2634        assert_eq!(split_last_segment("a/b"), ("a", "b"));
2635        assert_eq!(split_last_segment("a/b/"), ("a", "b"));
2636        assert_eq!(split_last_segment("a/b/c"), ("a/b", "c"));
2637        assert_eq!(split_last_segment("a/b/."), ("a", "b"));
2638        assert_eq!(split_last_segment("a/b/.."), ("", "a"));
2639        assert_eq!(split_last_segment("a/b/c/.."), ("a", "b"));
2640        assert_eq!(split_last_segment("a/b/c/../.."), ("", "a"));
2641        assert_eq!(split_last_segment("a/b/c/d/../.."), ("a", "b"));
2642        assert_eq!(split_last_segment("a/b/c/../d/.."), ("a", "b"));
2643        assert_eq!(split_last_segment("a/b/../c/d/.."), ("a/b/..", "c"));
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("./../"), ("./..", ""));
2649        assert_eq!(split_last_segment("../../"), ("../..", ""));
2650        assert_eq!(split_last_segment("../../."), ("../..", ""));
2651        assert_eq!(split_last_segment("../.././"), ("../..", ""));
2652        assert_eq!(split_last_segment("a/.."), ("", ""));
2653        assert_eq!(split_last_segment("a/../"), ("", ""));
2654        assert_eq!(split_last_segment("a/../.."), ("a/../..", ""));
2655        assert_eq!(split_last_segment("a/../../"), ("a/../..", ""));
2656        assert_eq!(split_last_segment("a/././../"), ("", ""));
2657        assert_eq!(split_last_segment("../a"), ("..", "a"));
2658        assert_eq!(split_last_segment("../a/"), ("..", "a"));
2659        assert_eq!(split_last_segment("../../a"), ("../..", "a"));
2660        assert_eq!(split_last_segment("../../a/"), ("../..", "a"));
2661    }
2662
2663    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2664    async fn test_read_matches() {
2665        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2666            BackendOptions::default(),
2667            noop_backing_storage(),
2668        ));
2669        tt.run_once(async {
2670            let root = DiskFileSystem::new(
2671                rcstr!("test"),
2672                Path::new(env!("CARGO_MANIFEST_DIR"))
2673                    .join("tests/pattern/read_matches")
2674                    .to_str()
2675                    .unwrap()
2676                    .into(),
2677            )
2678            .root()
2679            .owned()
2680            .await?;
2681
2682            // node_modules shouldn't be matched by Dynamic here
2683            assert_eq!(
2684                vec!["index.js", "sub", "sub/", "sub/foo-a.js", "sub/foo-b.js"],
2685                read_matches(
2686                    root.clone(),
2687                    rcstr!(""),
2688                    false,
2689                    Pattern::new(Pattern::Dynamic),
2690                )
2691                .await?
2692                .into_iter()
2693                .map(|m| m.name())
2694                .collect::<Vec<_>>()
2695            );
2696
2697            // basic dynamic file suffix
2698            assert_eq!(
2699                vec!["sub/foo-a.js", "sub/foo-b.js"],
2700                read_matches(
2701                    root.clone(),
2702                    rcstr!(""),
2703                    false,
2704                    Pattern::new(Pattern::concat([
2705                        Pattern::Constant(rcstr!("sub/foo")),
2706                        Pattern::Dynamic,
2707                    ])),
2708                )
2709                .await?
2710                .into_iter()
2711                .map(|m| m.name())
2712                .collect::<Vec<_>>()
2713            );
2714
2715            // read_matches "node_modules/<dynamic>" should not return anything inside. We never
2716            // want to enumerate the list of packages here.
2717            assert_eq!(
2718                vec!["node_modules"] as Vec<&str>,
2719                read_matches(
2720                    root.clone(),
2721                    rcstr!(""),
2722                    false,
2723                    Pattern::new(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file()),
2724                )
2725                .await?
2726                .into_iter()
2727                .map(|m| m.name())
2728                .collect::<Vec<_>>()
2729            );
2730
2731            anyhow::Ok(())
2732        })
2733        .await
2734        .unwrap();
2735    }
2736}