Skip to main content

turbopack_core/resolve/
pattern.rs

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