next.js/turbopack/crates/turbopack-core/src/resolve/pattern.rs
pattern.rs2842 lines106.9 KB
use std::{
    collections::{VecDeque, hash_map::Entry},
    mem::take,
    sync::LazyLock,
};

use anyhow::{Result, bail};
use bincode::{Decode, Encode};
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::Instrument;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{
    NonLocalValue, TaskInput, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs,
};
use turbo_tasks_fs::{
    FileSystemPath, LinkContent, LinkType, RawDirectoryContent, RawDirectoryEntry,
};
use turbo_unix_path::normalize_path;

#[turbo_tasks::value]
#[derive(Hash, Clone, Debug, Default, ValueToString)]
#[value_to_string(self.describe_as_string())]
pub enum Pattern {
    Constant(RcStr),
    #[default]
    Dynamic,
    DynamicNoSlash,
    Alternatives(Vec<Pattern>),
    Concatenation(Vec<Pattern>),
}

/// manually implement TaskInput to avoid recursion in the implementation of `resolve_input` in the
/// derived implementation.  We can instead use the default implementation since `Pattern` contains
/// no VCs.
impl TaskInput for Pattern {
    fn is_transient(&self) -> bool {
        // We contain no vcs so they cannot be transient.
        false
    }
}

fn concatenation_push_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
    if let Pattern::Constant(ref s) = pat
        && let Some(Pattern::Constant(last)) = list.last_mut()
    {
        let mut buf = last.to_string();
        buf.push_str(s);
        *last = buf.into();
        return;
    }
    list.push(pat);
}

fn concatenation_push_front_or_merge_item(list: &mut Vec<Pattern>, pat: Pattern) {
    if let Pattern::Constant(s) = pat {
        if let Some(Pattern::Constant(first)) = list.iter_mut().next() {
            let mut buf = s.into_owned();
            buf.push_str(first);

            *first = buf.into();
            return;
        }
        list.insert(0, Pattern::Constant(s));
    } else {
        list.insert(0, pat);
    }
}

fn concatenation_extend_or_merge_items(
    list: &mut Vec<Pattern>,
    mut iter: impl Iterator<Item = Pattern>,
) {
    if let Some(first) = iter.next() {
        concatenation_push_or_merge_item(list, first);
        list.extend(iter);
    }
}

fn longest_common_prefix<'a>(strings: &[&'a str]) -> &'a str {
    if strings.is_empty() {
        return "";
    }
    if let [single] = strings {
        return single;
    }
    let first = strings[0];
    let mut len = first.len();
    for str in &strings[1..] {
        len = std::cmp::min(
            len,
            // TODO these are Unicode Scalar Values, not graphemes
            str.chars()
                .zip(first.chars())
                .take_while(|&(a, b)| a == b)
                .count(),
        );
    }
    &first[..len]
}

fn longest_common_suffix<'a>(strings: &[&'a str]) -> &'a str {
    if strings.is_empty() {
        return "";
    }
    let first = strings[0];
    let mut len = first.len();
    for str in &strings[1..] {
        len = std::cmp::min(
            len,
            // TODO these are Unicode Scalar Values, not graphemes
            str.chars()
                .rev()
                .zip(first.chars().rev())
                .take_while(|&(a, b)| a == b)
                .count(),
        );
    }
    &first[(first.len() - len)..]
}

impl Pattern {
    // TODO this should be removed in favor of pattern resolving
    pub fn as_constant_string(&self) -> Option<&RcStr> {
        match self {
            Pattern::Constant(str) => Some(str),
            _ => None,
        }
    }

    /// Whether the pattern has any significant constant parts (everything except `/`).
    /// E.g. `<dynamic>/<dynamic>` doesn't really have constant parts
    pub fn has_constant_parts(&self) -> bool {
        match self {
            Pattern::Constant(str) => str != "/",
            Pattern::Dynamic | Pattern::DynamicNoSlash => false,
            Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
                list.iter().any(|p| p.has_constant_parts())
            }
        }
    }

    pub fn has_dynamic_parts(&self) -> bool {
        match self {
            Pattern::Constant(_) => false,
            Pattern::Dynamic | Pattern::DynamicNoSlash => true,
            Pattern::Alternatives(list) | Pattern::Concatenation(list) => {
                list.iter().any(|p| p.has_dynamic_parts())
            }
        }
    }

    pub fn constant_prefix(&self) -> &str {
        // The normalized pattern is an Alternative of maximally merged
        // Concatenations, so extracting the first/only Concatenation child
        // elements is enough.

        if let Pattern::Constant(c) = self {
            return c;
        }

        fn collect_constant_prefix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
            match pattern {
                Pattern::Constant(c) => {
                    result.push(c.as_str());
                }
                Pattern::Concatenation(list) => {
                    if let Some(Pattern::Constant(first)) = list.first() {
                        result.push(first.as_str());
                    }
                }
                Pattern::Alternatives(_) => {
                    panic!("for constant_prefix a Pattern must be normalized");
                }
                Pattern::Dynamic | Pattern::DynamicNoSlash => {}
            }
        }

        let mut strings: Vec<&str> = vec![];
        match self {
            c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
                collect_constant_prefix(c, &mut strings);
            }
            Pattern::Alternatives(list) => {
                for c in list {
                    collect_constant_prefix(c, &mut strings);
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
        }
        longest_common_prefix(&strings)
    }

    pub fn constant_suffix(&self) -> &str {
        // The normalized pattern is an Alternative of maximally merged
        // Concatenations, so extracting the first/only Concatenation child
        // elements is enough.

        fn collect_constant_suffix<'a: 'b, 'b>(pattern: &'a Pattern, result: &mut Vec<&'b str>) {
            match pattern {
                Pattern::Constant(c) => {
                    result.push(c.as_str());
                }
                Pattern::Concatenation(list) => {
                    if let Some(Pattern::Constant(first)) = list.last() {
                        result.push(first.as_str());
                    }
                }
                Pattern::Alternatives(_) => {
                    panic!("for constant_suffix a Pattern must be normalized");
                }
                Pattern::Dynamic | Pattern::DynamicNoSlash => {}
            }
        }

        let mut strings: Vec<&str> = vec![];
        match self {
            c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
                collect_constant_suffix(c, &mut strings);
            }
            Pattern::Alternatives(list) => {
                for c in list {
                    collect_constant_suffix(c, &mut strings);
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
        }
        longest_common_suffix(&strings)
    }

    pub fn strip_prefix(&self, prefix: &str) -> Result<Option<Self>> {
        if self.must_match(prefix) {
            let mut pat = self.clone();
            pat.strip_prefix_len(prefix.len())?;
            Ok(Some(pat))
        } else {
            Ok(None)
        }
    }

    pub fn strip_prefix_len(&mut self, len: usize) -> Result<()> {
        fn strip_prefix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) -> Result<()> {
            match pattern {
                Pattern::Constant(c) => {
                    let c_len = c.len();
                    if *chars_to_strip >= c_len {
                        *c = rcstr!("");
                    } else {
                        *c = (&c[*chars_to_strip..]).into();
                    }
                    *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
                }
                Pattern::Concatenation(list) => {
                    for c in list {
                        if *chars_to_strip > 0 {
                            strip_prefix_internal(c, chars_to_strip)?;
                        }
                    }
                }
                Pattern::Alternatives(_) => {
                    bail!("strip_prefix pattern must be normalized");
                }
                Pattern::Dynamic | Pattern::DynamicNoSlash => {
                    bail!("strip_prefix prefix is too long");
                }
            }
            Ok(())
        }

        match &mut *self {
            c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
                let mut len_local = len;
                strip_prefix_internal(c, &mut len_local)?;
            }
            Pattern::Alternatives(list) => {
                for c in list {
                    let mut len_local = len;
                    strip_prefix_internal(c, &mut len_local)?;
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                if len > 0 {
                    bail!(
                        "strip_prefix prefix ({}) is too long: {}",
                        len,
                        self.describe_as_string()
                    );
                }
            }
        };

        self.normalize();

        Ok(())
    }

    pub fn strip_suffix_len(&mut self, len: usize) {
        fn strip_suffix_internal(pattern: &mut Pattern, chars_to_strip: &mut usize) {
            match pattern {
                Pattern::Constant(c) => {
                    let c_len = c.len();
                    if *chars_to_strip >= c_len {
                        *c = rcstr!("");
                    } else {
                        *c = (&c[..(c_len - *chars_to_strip)]).into();
                    }
                    *chars_to_strip = (*chars_to_strip).saturating_sub(c_len);
                }
                Pattern::Concatenation(list) => {
                    for c in list.iter_mut().rev() {
                        if *chars_to_strip > 0 {
                            strip_suffix_internal(c, chars_to_strip);
                        }
                    }
                }
                Pattern::Alternatives(_) => {
                    panic!("for strip_suffix a Pattern must be normalized");
                }
                Pattern::Dynamic | Pattern::DynamicNoSlash => {
                    panic!("strip_suffix suffix is too long");
                }
            }
        }

        match &mut *self {
            c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
                let mut len_local = len;
                strip_suffix_internal(c, &mut len_local);
            }
            Pattern::Alternatives(list) => {
                for c in list {
                    let mut len_local = len;
                    strip_suffix_internal(c, &mut len_local);
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                if len > 0 {
                    panic!("strip_suffix suffix is too long");
                }
            }
        };

        self.normalize()
    }

    /// Replace all `*`s in `template` with self.
    ///
    /// Handle top-level alternatives separately so that multiple star placeholders
    /// match the same pattern instead of the whole alternative.
    pub fn spread_into_star(&self, template: &str) -> Pattern {
        if template.contains("*") {
            let alternatives: Box<dyn Iterator<Item = &Pattern>> = match self {
                Pattern::Alternatives(list) => Box::new(list.iter()),
                c => Box::new(std::iter::once(c)),
            };

            let mut result = Pattern::alternatives(alternatives.map(|pat| {
                let mut split = template.split("*");
                let mut concatenation: Vec<Pattern> = Vec::with_capacity(3);

                // There are at least two elements in the iterator
                concatenation.push(Pattern::Constant(split.next().unwrap().into()));

                for part in split {
                    concatenation.push(pat.clone());
                    if !part.is_empty() {
                        concatenation.push(Pattern::Constant(part.into()));
                    }
                }
                Pattern::Concatenation(concatenation)
            }));

            result.normalize();
            result
        } else {
            Pattern::Constant(template.into())
        }
    }

    /// Appends something to end the pattern.
    pub fn extend(&mut self, concatenated: impl Iterator<Item = Self>) {
        if let Pattern::Concatenation(list) = self {
            concatenation_extend_or_merge_items(list, concatenated);
        } else {
            let mut vec = vec![take(self)];
            for item in concatenated {
                if let Pattern::Concatenation(more) = item {
                    concatenation_extend_or_merge_items(&mut vec, more.into_iter());
                } else {
                    concatenation_push_or_merge_item(&mut vec, item);
                }
            }
            *self = Pattern::Concatenation(vec);
        }
    }

    /// Appends something to end the pattern.
    pub fn push(&mut self, pat: Pattern) {
        if let Pattern::Constant(this) = &*self
            && this.is_empty()
        {
            // Short-circuit to replace empty constants with the appended pattern
            *self = pat;
            return;
        }
        if let Pattern::Constant(pat) = &pat
            && pat.is_empty()
        {
            // Short-circuit to ignore when trying to append an empty string.
            return;
        }

        match (self, pat) {
            (Pattern::Concatenation(list), Pattern::Concatenation(more)) => {
                concatenation_extend_or_merge_items(list, more.into_iter());
            }
            (Pattern::Concatenation(list), pat) => {
                concatenation_push_or_merge_item(list, pat);
            }
            (this, Pattern::Concatenation(mut list)) => {
                concatenation_push_front_or_merge_item(&mut list, take(this));
                *this = Pattern::Concatenation(list);
            }
            (Pattern::Constant(str), Pattern::Constant(other)) => {
                let mut buf = str.to_string();
                buf.push_str(&other);
                *str = buf.into();
            }
            (this, pat) => {
                *this = Pattern::Concatenation(vec![take(this), pat]);
            }
        }
    }

    /// Prepends something to front of the pattern.
    pub fn push_front(&mut self, pat: Pattern) {
        match (self, pat) {
            (Pattern::Concatenation(list), Pattern::Concatenation(mut more)) => {
                concatenation_extend_or_merge_items(&mut more, take(list).into_iter());
                *list = more;
            }
            (Pattern::Concatenation(list), pat) => {
                concatenation_push_front_or_merge_item(list, pat);
            }
            (this, Pattern::Concatenation(mut list)) => {
                concatenation_push_or_merge_item(&mut list, take(this));
                *this = Pattern::Concatenation(list);
            }
            (Pattern::Constant(str), Pattern::Constant(other)) => {
                let mut buf = other.into_owned();

                buf.push_str(str);
                *str = buf.into();
            }
            (this, pat) => {
                *this = Pattern::Concatenation(vec![pat, take(this)]);
            }
        }
    }

    pub fn alternatives(alts: impl IntoIterator<Item = Pattern>) -> Self {
        let mut list = Vec::new();
        for alt in alts {
            if let Pattern::Alternatives(inner) = alt {
                list.extend(inner);
            } else {
                list.push(alt)
            }
        }
        Self::Alternatives(list)
    }

    pub fn concat(items: impl IntoIterator<Item = Pattern>) -> Self {
        let mut items = items.into_iter();
        let mut current = items.next().unwrap_or_default();
        for item in items {
            current.push(item);
        }
        current
    }

    /// Normalizes paths by
    /// - processing path segments: `.` and `..`
    /// - normalizing windows filepaths by replacing `\` with `/`
    ///
    /// The Pattern must have already been processed by [Self::normalize].
    /// Returns [Option::None] if any of the patterns attempt to navigate out of the root.
    pub fn with_normalized_path(&self) -> Option<Pattern> {
        let mut new = self.clone();

        #[derive(Debug)]
        enum PathElement {
            Segment(Pattern),
            Separator,
        }

        fn normalize_path_internal(pattern: &mut Pattern) -> Option<()> {
            match pattern {
                Pattern::Constant(c) => {
                    let normalized = c.replace('\\', "/");
                    *c = RcStr::from(normalize_path(normalized.as_str())?);
                    Some(())
                }
                Pattern::Dynamic | Pattern::DynamicNoSlash => Some(()),
                Pattern::Concatenation(list) => {
                    let mut segments = Vec::new();
                    for segment in list.iter() {
                        match segment {
                            Pattern::Constant(str) => {
                                let mut iter = str.split('/').peekable();
                                while let Some(segment) = iter.next() {
                                    match segment {
                                        "." | "" => {
                                            // Ignore empty segments
                                            continue;
                                        }
                                        ".." => {
                                            if segments.is_empty() {
                                                // Leaving root
                                                return None;
                                            }

                                            if let Some(PathElement::Separator) = segments.last()
                                                && let Some(PathElement::Segment(
                                                    Pattern::Constant(_),
                                                )) = segments.get(segments.len() - 2)
                                            {
                                                // Resolve `foo/..`
                                                segments.truncate(segments.len() - 2);
                                                continue;
                                            }

                                            // Keep it, can't pop non-constant segment.
                                            segments.push(PathElement::Segment(Pattern::Constant(
                                                rcstr!(".."),
                                            )));
                                        }
                                        segment => {
                                            segments.push(PathElement::Segment(Pattern::Constant(
                                                segment.into(),
                                            )));
                                        }
                                    }

                                    if iter.peek().is_some() {
                                        // If not last, add separator
                                        segments.push(PathElement::Separator);
                                    }
                                }
                            }
                            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                                segments.push(PathElement::Segment(segment.clone()));
                            }
                            Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
                                panic!("for with_normalized_path the Pattern must be normalized");
                            }
                        }
                    }
                    let separator = rcstr!("/");
                    *list = segments
                        .into_iter()
                        .map(|c| match c {
                            PathElement::Segment(p) => p,
                            PathElement::Separator => Pattern::Constant(separator.clone()),
                        })
                        .collect();
                    Some(())
                }
                Pattern::Alternatives(_) => {
                    panic!("for with_normalized_path the Pattern must be normalized");
                }
            }
        }

        match &mut new {
            c @ Pattern::Constant(_) | c @ Pattern::Concatenation(_) => {
                normalize_path_internal(c)?;
            }
            Pattern::Alternatives(list) => {
                for c in list {
                    normalize_path_internal(c)?;
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
        }

        new.normalize();
        Some(new)
    }

    /// Order into Alternatives -> Concatenation -> Constant/Dynamic
    /// Merge when possible
    pub fn normalize(&mut self) {
        match self {
            Pattern::Dynamic | Pattern::DynamicNoSlash | Pattern::Constant(_) => {
                // already normalized
            }
            Pattern::Alternatives(list) => {
                for alt in list.iter_mut() {
                    alt.normalize();
                }
                let mut new_alternatives = Vec::new();
                let mut has_dynamic = false;
                for alt in list.drain(..) {
                    if let Pattern::Alternatives(inner) = alt {
                        for alt in inner {
                            if alt == Pattern::Dynamic {
                                if !has_dynamic {
                                    has_dynamic = true;
                                    new_alternatives.push(alt);
                                }
                            } else {
                                new_alternatives.push(alt);
                            }
                        }
                    } else if alt == Pattern::Dynamic {
                        if !has_dynamic {
                            has_dynamic = true;
                            new_alternatives.push(alt);
                        }
                    } else {
                        new_alternatives.push(alt);
                    }
                }
                if new_alternatives.len() == 1 {
                    *self = new_alternatives.into_iter().next().unwrap();
                } else {
                    *list = new_alternatives;
                }
            }
            Pattern::Concatenation(list) => {
                let mut has_alternatives = false;
                for part in list.iter_mut() {
                    part.normalize();
                    if let Pattern::Alternatives(_) = part {
                        has_alternatives = true;
                    }
                }
                if has_alternatives {
                    // list has items that are one of these
                    // * Alternatives -> [Concatenation] -> ...
                    // * [Concatenation] -> ...
                    let mut new_alternatives: Vec<Vec<Pattern>> = vec![Vec::new()];
                    for part in list.drain(..) {
                        if let Pattern::Alternatives(list) = part {
                            // list is [Concatenation] -> ...
                            let mut combined = Vec::new();
                            for alt2 in list.iter() {
                                for mut alt in new_alternatives.clone() {
                                    if let Pattern::Concatenation(parts) = alt2 {
                                        alt.extend(parts.clone());
                                    } else {
                                        alt.push(alt2.clone());
                                    }
                                    combined.push(alt)
                                }
                            }
                            new_alternatives = combined;
                        } else {
                            // part is [Concatenation] -> ...
                            for alt in new_alternatives.iter_mut() {
                                if let Pattern::Concatenation(ref parts) = part {
                                    alt.extend(parts.clone());
                                } else {
                                    alt.push(part.clone());
                                }
                            }
                        }
                    }
                    // new_alternatives has items in that form:
                    // * [Concatenation] -> ...
                    *self = Pattern::Alternatives(
                        new_alternatives
                            .into_iter()
                            .map(|parts| {
                                if parts.len() == 1 {
                                    parts.into_iter().next().unwrap()
                                } else {
                                    Pattern::Concatenation(parts)
                                }
                            })
                            .collect(),
                    );
                    // The recursive call will deduplicate the alternatives after simplifying them
                    self.normalize();
                } else {
                    let mut new_parts = Vec::new();
                    for part in list.drain(..) {
                        fn add_part(part: Pattern, new_parts: &mut Vec<Pattern>) {
                            match part {
                                Pattern::Constant(c) => {
                                    if !c.is_empty() {
                                        if let Some(Pattern::Constant(last)) = new_parts.last_mut()
                                        {
                                            let mut buf = last.to_string();
                                            buf.push_str(&c);
                                            *last = buf.into();
                                        } else {
                                            new_parts.push(Pattern::Constant(c));
                                        }
                                    }
                                }
                                Pattern::Dynamic => {
                                    if let Some(Pattern::Dynamic | Pattern::DynamicNoSlash) =
                                        new_parts.last()
                                    {
                                        // do nothing
                                    } else {
                                        new_parts.push(Pattern::Dynamic);
                                    }
                                }
                                Pattern::DynamicNoSlash => {
                                    if let Some(Pattern::DynamicNoSlash) = new_parts.last() {
                                        // do nothing
                                    } else {
                                        new_parts.push(Pattern::DynamicNoSlash);
                                    }
                                }
                                Pattern::Concatenation(parts) => {
                                    for part in parts {
                                        add_part(part, new_parts);
                                    }
                                }
                                Pattern::Alternatives(_) => unreachable!(),
                            }
                        }

                        add_part(part, &mut new_parts);
                    }
                    if new_parts.len() == 1 {
                        *self = new_parts.into_iter().next().unwrap();
                    } else {
                        *list = new_parts;
                    }
                }
            }
        }
    }

    pub fn is_empty(&self) -> bool {
        match self {
            Pattern::Constant(s) => s.is_empty(),
            Pattern::Dynamic | Pattern::DynamicNoSlash => false,
            Pattern::Concatenation(parts) => parts.iter().all(|p| p.is_empty()),
            Pattern::Alternatives(parts) => parts.iter().all(|p| p.is_empty()),
        }
    }

    pub fn filter_could_match(&self, value: &str) -> Option<Pattern> {
        if let Pattern::Alternatives(list) = self {
            let new_list = list
                .iter()
                .filter(|alt| alt.could_match(value))
                .cloned()
                .collect::<Vec<_>>();
            if new_list.is_empty() {
                None
            } else {
                Some(Pattern::Alternatives(new_list))
            }
        } else if self.could_match(value) {
            Some(self.clone())
        } else {
            None
        }
    }

    pub fn filter_could_not_match(&self, value: &str) -> Option<Pattern> {
        if let Pattern::Alternatives(list) = self {
            let new_list = list
                .iter()
                .filter(|alt| !alt.could_match(value))
                .cloned()
                .collect::<Vec<_>>();
            if new_list.is_empty() {
                None
            } else {
                Some(Pattern::Alternatives(new_list))
            }
        } else if self.could_match(value) {
            None
        } else {
            Some(self.clone())
        }
    }

    pub fn split_could_match(&self, value: &str) -> (Option<Pattern>, Option<Pattern>) {
        if let Pattern::Alternatives(list) = self {
            let mut could_match_list = Vec::new();
            let mut could_not_match_list = Vec::new();
            for alt in list.iter() {
                if alt.could_match(value) {
                    could_match_list.push(alt.clone());
                } else {
                    could_not_match_list.push(alt.clone());
                }
            }
            (
                if could_match_list.is_empty() {
                    None
                } else if could_match_list.len() == 1 {
                    Some(could_match_list.into_iter().next().unwrap())
                } else {
                    Some(Pattern::Alternatives(could_match_list))
                },
                if could_not_match_list.is_empty() {
                    None
                } else if could_not_match_list.len() == 1 {
                    Some(could_not_match_list.into_iter().next().unwrap())
                } else {
                    Some(Pattern::Alternatives(could_not_match_list))
                },
            )
        } else if self.could_match(value) {
            (Some(self.clone()), None)
        } else {
            (None, Some(self.clone()))
        }
    }

    pub fn is_match(&self, value: &str) -> bool {
        if let Pattern::Alternatives(list) = self {
            list.iter().any(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .is_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .is_match()
        }
    }

    /// Like [`Pattern::is_match`], but does not consider any dynamic
    /// pattern matching
    pub fn is_match_ignore_dynamic(&self, value: &str) -> bool {
        if let Pattern::Alternatives(list) = self {
            list.iter().any(|alt| {
                alt.match_internal(value, None, InNodeModules::False, true)
                    .is_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, true)
                .is_match()
        }
    }

    pub fn match_position(&self, value: &str) -> Option<usize> {
        if let Pattern::Alternatives(list) = self {
            list.iter().position(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .is_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .is_match()
                .then_some(0)
        }
    }

    pub fn could_match_others(&self, value: &str) -> bool {
        if let Pattern::Alternatives(list) = self {
            list.iter().any(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .could_match_others()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .could_match_others()
        }
    }

    /// Returns true if all matches of the pattern start with `value`.
    pub fn must_match(&self, value: &str) -> bool {
        if let Pattern::Alternatives(list) = self {
            list.iter().all(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .could_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .could_match()
        }
    }

    /// Returns true the pattern could match something that starts with `value`.
    pub fn could_match(&self, value: &str) -> bool {
        if let Pattern::Alternatives(list) = self {
            list.iter().any(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .could_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .could_match()
        }
    }

    pub fn could_match_position(&self, value: &str) -> Option<usize> {
        if let Pattern::Alternatives(list) = self {
            list.iter().position(|alt| {
                alt.match_internal(value, None, InNodeModules::False, false)
                    .could_match()
            })
        } else {
            self.match_internal(value, None, InNodeModules::False, false)
                .could_match()
                .then_some(0)
        }
    }
    fn match_internal<'a>(
        &self,
        mut value: &'a str,
        mut any_offset: Option<usize>,
        mut in_node_modules: InNodeModules,
        ignore_dynamic: bool,
    ) -> MatchResult<'a> {
        match self {
            Pattern::Constant(c) => {
                if let Some(offset) = any_offset {
                    if let Some(index) = value.find(&**c) {
                        if index <= offset {
                            MatchResult::Consumed {
                                remaining: &value[index + c.len()..],
                                any_offset: None,
                                in_node_modules: InNodeModules::check(c),
                            }
                        } else {
                            MatchResult::None
                        }
                    } else if offset >= value.len() {
                        MatchResult::Partial
                    } else {
                        MatchResult::None
                    }
                } else if value.starts_with(&**c) {
                    MatchResult::Consumed {
                        remaining: &value[c.len()..],
                        any_offset: None,
                        in_node_modules: InNodeModules::check(c),
                    }
                } else if c.starts_with(value) {
                    MatchResult::Partial
                } else {
                    MatchResult::None
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
                    Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
                });
                static FORBIDDEN_MATCH: LazyLock<Regex> =
                    LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
                if in_node_modules == InNodeModules::FolderSlashMatched
                    || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
                {
                    MatchResult::None
                } else if let Some(m) = FORBIDDEN.find(value) {
                    MatchResult::Consumed {
                        remaining: value,
                        any_offset: Some(m.start()),
                        in_node_modules: InNodeModules::False,
                    }
                } else if FORBIDDEN_MATCH.find(value).is_some() {
                    MatchResult::Partial
                } else if ignore_dynamic {
                    MatchResult::None
                } else {
                    let match_length = matches!(self, Pattern::DynamicNoSlash)
                        .then(|| value.find("/"))
                        .flatten()
                        .unwrap_or(value.len());
                    MatchResult::Consumed {
                        remaining: value,
                        any_offset: Some(match_length),
                        in_node_modules: InNodeModules::False,
                    }
                }
            }
            Pattern::Alternatives(_) => {
                panic!("for matching a Pattern must be normalized {self:?}")
            }
            Pattern::Concatenation(list) => {
                for part in list {
                    match part.match_internal(value, any_offset, in_node_modules, ignore_dynamic) {
                        MatchResult::None => return MatchResult::None,
                        MatchResult::Partial => return MatchResult::Partial,
                        MatchResult::Consumed {
                            remaining: new_value,
                            any_offset: new_any_offset,
                            in_node_modules: new_in_node_modules,
                        } => {
                            value = new_value;
                            any_offset = new_any_offset;
                            in_node_modules = new_in_node_modules
                        }
                    }
                }
                MatchResult::Consumed {
                    remaining: value,
                    any_offset,
                    in_node_modules,
                }
            }
        }
    }

    /// Same as `match_internal`, but additionally pushing matched dynamic elements into the given
    /// result list.
    fn match_collect_internal<'a>(
        &self,
        mut value: &'a str,
        mut any_offset: Option<usize>,
        mut in_node_modules: InNodeModules,
        dynamics: &mut VecDeque<&'a str>,
    ) -> MatchResult<'a> {
        match self {
            Pattern::Constant(c) => {
                if let Some(offset) = any_offset {
                    if let Some(index) = value.find(&**c) {
                        if index <= offset {
                            if index > 0 {
                                dynamics.push_back(&value[..index]);
                            }
                            MatchResult::Consumed {
                                remaining: &value[index + c.len()..],
                                any_offset: None,
                                in_node_modules: InNodeModules::check(c),
                            }
                        } else {
                            MatchResult::None
                        }
                    } else if offset >= value.len() {
                        MatchResult::Partial
                    } else {
                        MatchResult::None
                    }
                } else if value.starts_with(&**c) {
                    MatchResult::Consumed {
                        remaining: &value[c.len()..],
                        any_offset: None,
                        in_node_modules: InNodeModules::check(c),
                    }
                } else if c.starts_with(value) {
                    MatchResult::Partial
                } else {
                    MatchResult::None
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
                    Regex::new(r"(/|^)(ROOT|\.|/|(node_modules|__tests?__)(/|$))").unwrap()
                });
                static FORBIDDEN_MATCH: LazyLock<Regex> =
                    LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
                if in_node_modules == InNodeModules::FolderSlashMatched
                    || (in_node_modules == InNodeModules::FolderMatched && value.starts_with('/'))
                {
                    MatchResult::None
                } else if let Some(m) = FORBIDDEN.find(value) {
                    MatchResult::Consumed {
                        remaining: value,
                        any_offset: Some(m.start()),
                        in_node_modules: InNodeModules::False,
                    }
                } else if FORBIDDEN_MATCH.find(value).is_some() {
                    MatchResult::Partial
                } else {
                    let match_length = matches!(self, Pattern::DynamicNoSlash)
                        .then(|| value.find("/"))
                        .flatten()
                        .unwrap_or(value.len());
                    MatchResult::Consumed {
                        remaining: value,
                        any_offset: Some(match_length),
                        in_node_modules: InNodeModules::False,
                    }
                }
            }
            Pattern::Alternatives(_) => {
                panic!("for matching a Pattern must be normalized {self:?}")
            }
            Pattern::Concatenation(list) => {
                for part in list {
                    match part.match_collect_internal(value, any_offset, in_node_modules, dynamics)
                    {
                        MatchResult::None => return MatchResult::None,
                        MatchResult::Partial => return MatchResult::Partial,
                        MatchResult::Consumed {
                            remaining: new_value,
                            any_offset: new_any_offset,
                            in_node_modules: new_in_node_modules,
                        } => {
                            value = new_value;
                            any_offset = new_any_offset;
                            in_node_modules = new_in_node_modules
                        }
                    }
                }
                if let Some(offset) = any_offset
                    && offset == value.len()
                {
                    dynamics.push_back(value);
                }
                MatchResult::Consumed {
                    remaining: value,
                    any_offset,
                    in_node_modules,
                }
            }
        }
    }

    pub fn next_constants<'a>(&'a self, value: &str) -> Option<Vec<(&'a str, bool)>> {
        if let Pattern::Alternatives(list) = self {
            let mut results = Vec::new();
            for alt in list.iter() {
                match alt.next_constants_internal(value, None) {
                    NextConstantUntilResult::NoMatch => {}
                    NextConstantUntilResult::PartialDynamic => {
                        return None;
                    }
                    NextConstantUntilResult::Partial(s, end) => {
                        results.push((s, end));
                    }
                    NextConstantUntilResult::Consumed(rem, None) => {
                        if rem.is_empty() {
                            results.push(("", true));
                        }
                    }
                    NextConstantUntilResult::Consumed(rem, Some(any)) => {
                        if any == rem.len() {
                            // can match anything
                            // we don't have constant only matches
                            return None;
                        }
                    }
                }
            }
            Some(results)
        } else {
            match self.next_constants_internal(value, None) {
                NextConstantUntilResult::NoMatch => None,
                NextConstantUntilResult::PartialDynamic => None,
                NextConstantUntilResult::Partial(s, e) => Some(vec![(s, e)]),
                NextConstantUntilResult::Consumed(_, _) => None,
            }
        }
    }

    fn next_constants_internal<'a, 'b>(
        &'a self,
        mut value: &'b str,
        mut any_offset: Option<usize>,
    ) -> NextConstantUntilResult<'a, 'b> {
        match self {
            Pattern::Constant(c) => {
                if let Some(offset) = any_offset {
                    if let Some(index) = value.find(&**c) {
                        if index <= offset {
                            NextConstantUntilResult::Consumed(&value[index + c.len()..], None)
                        } else {
                            NextConstantUntilResult::NoMatch
                        }
                    } else if offset >= value.len() {
                        NextConstantUntilResult::PartialDynamic
                    } else {
                        NextConstantUntilResult::NoMatch
                    }
                } else if let Some(stripped) = value.strip_prefix(&**c) {
                    NextConstantUntilResult::Consumed(stripped, None)
                } else if let Some(stripped) = c.strip_prefix(value) {
                    NextConstantUntilResult::Partial(stripped, true)
                } else {
                    NextConstantUntilResult::NoMatch
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {
                static FORBIDDEN: LazyLock<Regex> = LazyLock::new(|| {
                    Regex::new(r"(/|^)(\.|(node_modules|__tests?__)(/|$))").unwrap()
                });
                static FORBIDDEN_MATCH: LazyLock<Regex> =
                    LazyLock::new(|| Regex::new(r"\.d\.ts$|\.map$").unwrap());
                if let Some(m) = FORBIDDEN.find(value) {
                    NextConstantUntilResult::Consumed(value, Some(m.start()))
                } else if FORBIDDEN_MATCH.find(value).is_some() {
                    NextConstantUntilResult::PartialDynamic
                } else {
                    NextConstantUntilResult::Consumed(value, Some(value.len()))
                }
            }
            Pattern::Alternatives(_) => {
                panic!("for next_constants() the Pattern must be normalized");
            }
            Pattern::Concatenation(list) => {
                let mut iter = list.iter();
                while let Some(part) = iter.next() {
                    match part.next_constants_internal(value, any_offset) {
                        NextConstantUntilResult::NoMatch => {
                            return NextConstantUntilResult::NoMatch;
                        }
                        NextConstantUntilResult::PartialDynamic => {
                            return NextConstantUntilResult::PartialDynamic;
                        }
                        NextConstantUntilResult::Partial(r, end) => {
                            return NextConstantUntilResult::Partial(
                                r,
                                end && iter.next().is_none(),
                            );
                        }
                        NextConstantUntilResult::Consumed(new_value, new_any_offset) => {
                            value = new_value;
                            any_offset = new_any_offset;
                        }
                    }
                }
                NextConstantUntilResult::Consumed(value, any_offset)
            }
        }
    }

    pub fn or_any_nested_file(&self) -> Self {
        let mut new = self.clone();
        new.push(Pattern::Constant(rcstr!("/")));
        new.push(Pattern::Dynamic);
        new.normalize();
        Pattern::alternatives([self.clone(), new])
    }

    /// Calls `cb` on all constants that are at the end of the pattern and
    /// replaces the given final constant with the returned pattern. Returns
    /// true if replacements were performed.
    pub fn replace_final_constants(
        &mut self,
        cb: &mut impl FnMut(&RcStr) -> Option<Pattern>,
    ) -> bool {
        let mut replaced = false;
        match self {
            Pattern::Constant(c) => {
                if let Some(replacement) = cb(c) {
                    *self = replacement;
                    replaced = true;
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
            Pattern::Alternatives(list) => {
                for i in list {
                    replaced = i.replace_final_constants(cb) || replaced;
                }
            }
            Pattern::Concatenation(list) => {
                if let Some(i) = list.last_mut() {
                    replaced = i.replace_final_constants(cb) || replaced;
                }
            }
        }
        replaced
    }

    /// Calls `cb` on all constants and replaces the them with the returned pattern. Returns true if
    /// replacements were performed.
    pub fn replace_constants(&mut self, cb: &impl Fn(&RcStr) -> Option<Pattern>) -> bool {
        let mut replaced = false;
        match self {
            Pattern::Constant(c) => {
                if let Some(replacement) = cb(c) {
                    *self = replacement;
                    replaced = true;
                }
            }
            Pattern::Dynamic | Pattern::DynamicNoSlash => {}
            Pattern::Concatenation(list) | Pattern::Alternatives(list) => {
                for i in list {
                    replaced = i.replace_constants(cb) || replaced;
                }
            }
        }
        replaced
    }

    /// Matches the given string against self, and applies the match onto the target pattern.
    ///
    /// The two patterns should have a similar structure (same number of alternatives and dynamics)
    /// and only differ in the constant contents.
    pub fn match_apply_template(&self, value: &str, target: &Pattern) -> Option<String> {
        let match_idx = self.match_position(value)?;
        let source = match self {
            Pattern::Alternatives(list) => list.get(match_idx),
            Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
                if match_idx == 0 =>
            {
                Some(self)
            }
            _ => None,
        }?;
        let target = match target {
            Pattern::Alternatives(list) => list.get(match_idx),
            Pattern::Constant(_) | Pattern::Dynamic | Pattern::Concatenation(_)
                if match_idx == 0 =>
            {
                Some(target)
            }
            _ => None,
        }?;

        let mut dynamics = VecDeque::new();
        // This is definitely a match, because it matched above in `self.match_position(value)`
        source.match_collect_internal(value, None, InNodeModules::False, &mut dynamics);

        let mut result = "".to_string();
        match target {
            Pattern::Constant(c) => result.push_str(c),
            Pattern::Dynamic | Pattern::DynamicNoSlash => result.push_str(dynamics.pop_front()?),
            Pattern::Concatenation(list) => {
                for c in list {
                    match c {
                        Pattern::Constant(c) => result.push_str(c),
                        Pattern::Dynamic | Pattern::DynamicNoSlash => {
                            result.push_str(dynamics.pop_front()?)
                        }
                        Pattern::Alternatives(_) | Pattern::Concatenation(_) => {
                            panic!("Pattern must be normalized")
                        }
                    }
                }
            }
            Pattern::Alternatives(_) => panic!("Pattern must be normalized"),
        }
        if !dynamics.is_empty() {
            return None;
        }

        Some(result)
    }
}

impl Pattern {
    pub fn new(mut pattern: Pattern) -> Vc<Self> {
        pattern.normalize();
        Pattern::new_internal(pattern)
    }
}

#[turbo_tasks::value_impl]
impl Pattern {
    #[turbo_tasks::function]
    fn new_internal(pattern: Pattern) -> Vc<Self> {
        Self::cell(pattern)
    }
}

#[derive(PartialEq, Debug)]
enum InNodeModules {
    False,
    // Inside of a match ending in `node_modules`
    FolderMatched,
    // Inside of a match ending in `node_modules/`
    FolderSlashMatched,
}
impl InNodeModules {
    fn check(value: &str) -> Self {
        if value.ends_with("node_modules/") {
            InNodeModules::FolderSlashMatched
        } else if value.ends_with("node_modules") {
            InNodeModules::FolderMatched
        } else {
            InNodeModules::False
        }
    }
}

#[derive(PartialEq, Debug)]
enum MatchResult<'a> {
    /// No match
    None,
    /// Matches only a part of the pattern before reaching the end of the string
    Partial,
    /// Matches the whole pattern (but maybe not the whole string)
    Consumed {
        /// Part of the string remaining after matching the whole pattern
        remaining: &'a str,
        /// Set when the pattern ends with a dynamic part. The dynamic part
        /// could match n bytes more of the string.
        any_offset: Option<usize>,
        /// Set when the pattern ends with `node_modules` or `node_modules/` (and a following
        /// Pattern::Dynamic would thus match all existing packages)
        in_node_modules: InNodeModules,
    },
}

impl MatchResult<'_> {
    /// Returns true if the whole pattern matches the whole string
    fn is_match(&self) -> bool {
        match self {
            MatchResult::None => false,
            MatchResult::Partial => false,
            MatchResult::Consumed {
                remaining: rem,
                any_offset,
                in_node_modules: _,
            } => {
                if let Some(offset) = any_offset {
                    *offset == rem.len()
                } else {
                    rem.is_empty()
                }
            }
        }
    }

    /// Returns true if (at least a part of) the pattern matches the whole
    /// string and can also match more bytes in the string
    fn could_match_others(&self) -> bool {
        match self {
            MatchResult::None => false,
            MatchResult::Partial => true,
            MatchResult::Consumed {
                remaining: rem,
                any_offset,
                in_node_modules: _,
            } => {
                if let Some(offset) = any_offset {
                    *offset == rem.len()
                } else {
                    false
                }
            }
        }
    }

    /// Returns true if (at least a part of) the pattern matches the whole
    /// string
    fn could_match(&self) -> bool {
        match self {
            MatchResult::None => false,
            MatchResult::Partial => true,
            MatchResult::Consumed {
                remaining: rem,
                any_offset,
                in_node_modules: _,
            } => {
                if let Some(offset) = any_offset {
                    *offset == rem.len()
                } else {
                    rem.is_empty()
                }
            }
        }
    }
}

#[derive(PartialEq, Debug)]
enum NextConstantUntilResult<'a, 'b> {
    NoMatch,
    PartialDynamic,
    Partial(&'a str, bool),
    Consumed(&'b str, Option<usize>),
}

impl From<RcStr> for Pattern {
    fn from(s: RcStr) -> Self {
        Pattern::Constant(s)
    }
}

impl Pattern {
    pub fn describe_as_string(&self) -> String {
        match self {
            Pattern::Constant(c) => format!("'{c}'"),
            Pattern::Dynamic => "<dynamic>".to_string(),
            Pattern::DynamicNoSlash => "<dynamic no slash>".to_string(),
            Pattern::Alternatives(list) => format!(
                "({})",
                list.iter()
                    .map(|i| i.describe_as_string())
                    .collect::<Vec<_>>()
                    .join(" | ")
            ),
            Pattern::Concatenation(list) => list
                .iter()
                .map(|i| i.describe_as_string())
                .collect::<Vec<_>>()
                .join(" "),
        }
    }
}

#[derive(
    Debug, PartialEq, Eq, Clone, TraceRawVcs, ValueDebugFormat, NonLocalValue, Encode, Decode,
)]
pub enum PatternMatch {
    File(RcStr, FileSystemPath),
    Directory(RcStr, FileSystemPath),
}

impl PatternMatch {
    pub fn path(&self) -> Vc<FileSystemPath> {
        match self {
            PatternMatch::File(_, path) | PatternMatch::Directory(_, path) => path.clone().cell(),
        }
    }

    pub fn name(&self) -> &str {
        match self {
            PatternMatch::File(name, _) | PatternMatch::Directory(name, _) => name.as_str(),
        }
    }
}

// TODO this isn't super efficient
// avoid storing a large list of matches
#[turbo_tasks::value(transparent)]
#[derive(Debug)]
pub struct PatternMatches(Vec<PatternMatch>);

/// Find all files or directories that match the provided `pattern` with the
/// specified `lookup_dir` directory. `prefix` is the already matched part of
/// the pattern that leads to the `lookup_dir` directory. When
/// `force_in_lookup_dir` is set, leaving the `lookup_dir` directory by
/// matching `..` is not allowed.
///
/// Symlinks will not be resolved. It's expected that the caller resolves
/// symlinks when they are interested in that.
#[turbo_tasks::function]
pub async fn read_matches(
    lookup_dir: FileSystemPath,
    prefix: RcStr,
    force_in_lookup_dir: bool,
    pattern: Vc<Pattern>,
) -> Result<Vc<PatternMatches>> {
    let mut prefix = prefix.to_string();
    let pat = pattern.await?;
    let mut results = Vec::new();
    let mut nested = Vec::new();
    let slow_path = if let Some(constants) = pat.next_constants(&prefix) {
        if constants
            .iter()
            .all(|(str, until_end)| *until_end || str.contains('/'))
        {
            // Fast path: There is a finite list of possible strings that include at least
            // one path segment We will enumerate the list instead of the
            // directory
            let mut handled = FxHashSet::default();
            let mut read_dir_results = FxHashMap::default();
            for (index, (str, until_end)) in constants.into_iter().enumerate() {
                if until_end {
                    if !handled.insert(str) {
                        continue;
                    }
                    let (parent_path, last_segment) = split_last_segment(str);
                    if last_segment.is_empty() {
                        // This means we don't have a last segment, so we just have a directory
                        let joined = if force_in_lookup_dir {
                            lookup_dir.try_join_inside(parent_path)
                        } else {
                            lookup_dir.try_join(parent_path)
                        };
                        let Some(fs_path) = joined else {
                            continue;
                        };
                        results.push((
                            index,
                            PatternMatch::Directory(concat(&prefix, str).into(), fs_path),
                        ));
                        continue;
                    }
                    let entry = read_dir_results.entry(parent_path);
                    let read_dir = match entry {
                        Entry::Occupied(e) => Some(e.into_mut()),
                        Entry::Vacant(e) => {
                            let path_option = if force_in_lookup_dir {
                                lookup_dir.try_join_inside(parent_path)
                            } else {
                                lookup_dir.try_join(parent_path)
                            };
                            if let Some(path) = path_option {
                                Some(e.insert((path.raw_read_dir().await?, path)))
                            } else {
                                None
                            }
                        }
                    };
                    let Some((read_dir, parent_fs_path)) = read_dir else {
                        continue;
                    };
                    let RawDirectoryContent::Entries(entries) = &**read_dir else {
                        continue;
                    };
                    let Some(entry) = entries.get(last_segment) else {
                        continue;
                    };
                    match *entry {
                        RawDirectoryEntry::File => {
                            results.push((
                                index,
                                PatternMatch::File(
                                    concat(&prefix, str).into(),
                                    parent_fs_path.join(last_segment)?,
                                ),
                            ));
                        }
                        RawDirectoryEntry::Directory => results.push((
                            index,
                            PatternMatch::Directory(
                                concat(&prefix, str).into(),
                                parent_fs_path.join(last_segment)?,
                            ),
                        )),
                        RawDirectoryEntry::Symlink => {
                            let fs_path = parent_fs_path.join(last_segment)?;
                            let LinkContent::Link { link_type, .. } = &*fs_path.read_link().await?
                            else {
                                continue;
                            };
                            let path = concat(&prefix, str).into();
                            if link_type.contains(LinkType::DIRECTORY) {
                                results.push((index, PatternMatch::Directory(path, fs_path)));
                            } else {
                                results.push((index, PatternMatch::File(path, fs_path)))
                            }
                        }
                        _ => {}
                    }
                } else {
                    let subpath = &str[..=str.rfind('/').unwrap()];
                    if handled.insert(subpath) {
                        let joined = if force_in_lookup_dir {
                            lookup_dir.try_join_inside(subpath)
                        } else {
                            lookup_dir.try_join(subpath)
                        };
                        let Some(fs_path) = joined else {
                            continue;
                        };
                        nested.push((
                            index,
                            read_matches(
                                fs_path.clone(),
                                concat(&prefix, subpath).into(),
                                force_in_lookup_dir,
                                pattern,
                            ),
                        ));
                    }
                }
            }
            false
        } else {
            true
        }
    } else {
        true
    };

    if slow_path {
        async {
            // Slow path: There are infinite matches for the pattern
            // We will enumerate the filesystem to find matches
            if !force_in_lookup_dir {
                // {prefix}..
                prefix.push_str("..");
                if let Some(pos) = pat.match_position(&prefix) {
                    results.push((
                        pos,
                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
                    ));
                }

                // {prefix}../
                prefix.push('/');
                if let Some(pos) = pat.match_position(&prefix) {
                    results.push((
                        pos,
                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.parent()),
                    ));
                }
                if let Some(pos) = pat.could_match_position(&prefix) {
                    nested.push((
                        pos,
                        read_matches(lookup_dir.parent(), prefix.clone().into(), false, pattern),
                    ));
                }
                prefix.pop();
                prefix.pop();
                prefix.pop();
            }
            {
                prefix.push('.');
                // {prefix}.
                if let Some(pos) = pat.match_position(&prefix) {
                    results.push((
                        pos,
                        PatternMatch::Directory(prefix.clone().into(), lookup_dir.clone()),
                    ));
                }
                prefix.pop();
            }
            if prefix.is_empty() {
                if let Some(pos) = pat.match_position("./") {
                    results.push((
                        pos,
                        PatternMatch::Directory(rcstr!("./"), lookup_dir.clone()),
                    ));
                }
                if let Some(pos) = pat.could_match_position("./") {
                    nested.push((
                        pos,
                        read_matches(lookup_dir.clone(), rcstr!("./"), false, pattern),
                    ));
                }
            } else {
                prefix.push('/');
                // {prefix}/
                if let Some(pos) = pat.could_match_position(&prefix) {
                    nested.push((
                        pos,
                        read_matches(
                            lookup_dir.clone(),
                            prefix.to_string().into(),
                            false,
                            pattern,
                        ),
                    ));
                }
                prefix.pop();
                prefix.push_str("./");
                // {prefix}./
                if let Some(pos) = pat.could_match_position(&prefix) {
                    nested.push((
                        pos,
                        read_matches(
                            lookup_dir.clone(),
                            prefix.to_string().into(),
                            false,
                            pattern,
                        ),
                    ));
                }
                prefix.pop();
                prefix.pop();
            }
            match &*lookup_dir.raw_read_dir().await? {
                RawDirectoryContent::Entries(map) => {
                    for (key, entry) in map.iter() {
                        match entry {
                            RawDirectoryEntry::File => {
                                let len = prefix.len();
                                prefix.push_str(key);
                                // {prefix}{key}
                                if let Some(pos) = pat.match_position(&prefix) {
                                    let path = lookup_dir.join(key)?;
                                    results.push((
                                        pos,
                                        PatternMatch::File(prefix.clone().into(), path),
                                    ));
                                }
                                prefix.truncate(len)
                            }
                            RawDirectoryEntry::Directory => {
                                let len = prefix.len();
                                prefix.push_str(key);
                                // {prefix}{key}
                                if prefix.ends_with('/') {
                                    prefix.pop();
                                }
                                if let Some(pos) = pat.match_position(&prefix) {
                                    let path = lookup_dir.join(key)?;
                                    results.push((
                                        pos,
                                        PatternMatch::Directory(prefix.clone().into(), path),
                                    ));
                                }
                                prefix.push('/');
                                // {prefix}{key}/
                                if let Some(pos) = pat.match_position(&prefix) {
                                    let path = lookup_dir.join(key)?;
                                    results.push((
                                        pos,
                                        PatternMatch::Directory(prefix.clone().into(), path),
                                    ));
                                }
                                if let Some(pos) = pat.could_match_position(&prefix) {
                                    let path = lookup_dir.join(key)?;
                                    nested.push((
                                        pos,
                                        read_matches(path, prefix.clone().into(), true, pattern),
                                    ));
                                }
                                prefix.truncate(len)
                            }
                            RawDirectoryEntry::Symlink => {
                                let len = prefix.len();
                                prefix.push_str(key);
                                // {prefix}{key}
                                if prefix.ends_with('/') {
                                    prefix.pop();
                                }
                                if let Some(pos) = pat.match_position(&prefix) {
                                    let fs_path = lookup_dir.join(key)?;
                                    if let LinkContent::Link { link_type, .. } =
                                        &*fs_path.read_link().await?
                                    {
                                        if link_type.contains(LinkType::DIRECTORY) {
                                            results.push((
                                                pos,
                                                PatternMatch::Directory(
                                                    prefix.clone().into(),
                                                    fs_path,
                                                ),
                                            ));
                                        } else {
                                            results.push((
                                                pos,
                                                PatternMatch::File(prefix.clone().into(), fs_path),
                                            ));
                                        }
                                    }
                                }
                                prefix.push('/');
                                if let Some(pos) = pat.match_position(&prefix) {
                                    let fs_path = lookup_dir.join(key)?;
                                    if let LinkContent::Link { link_type, .. } =
                                        &*fs_path.read_link().await?
                                        && link_type.contains(LinkType::DIRECTORY)
                                    {
                                        results.push((
                                            pos,
                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
                                        ));
                                    }
                                }
                                if let Some(pos) = pat.could_match_position(&prefix) {
                                    let fs_path = lookup_dir.join(key)?;
                                    if let LinkContent::Link { link_type, .. } =
                                        &*fs_path.read_link().await?
                                        && link_type.contains(LinkType::DIRECTORY)
                                    {
                                        results.push((
                                            pos,
                                            PatternMatch::Directory(prefix.clone().into(), fs_path),
                                        ));
                                    }
                                }
                                prefix.truncate(len)
                            }
                            RawDirectoryEntry::Other => {}
                        }
                    }
                }
                RawDirectoryContent::NotFound => {}
            };
            anyhow::Ok(())
        }
        .instrument(tracing::trace_span!("read_matches slow_path"))
        .await?;
    }
    if results.is_empty() && nested.len() == 1 {
        Ok(nested.into_iter().next().unwrap().1)
    } else {
        for (pos, nested) in nested.into_iter() {
            results.extend(nested.await?.iter().cloned().map(|p| (pos, p)));
        }
        results.sort_by(|(a, am), (b, bm)| (*a).cmp(b).then_with(|| am.name().cmp(bm.name())));
        Ok(Vc::cell(
            results.into_iter().map(|(_, p)| p).collect::<Vec<_>>(),
        ))
    }
}

fn concat(a: &str, b: &str) -> String {
    let mut result = String::with_capacity(a.len() + b.len());
    result.push_str(a);
    result.push_str(b);
    result
}

/// Returns the parent folder and the last segment of the path. When the last segment is unknown (e.
/// g. when using `../`) it returns the full path and an empty string.
fn split_last_segment(path: &str) -> (&str, &str) {
    if let Some((remaining_path, last_segment)) = path.rsplit_once('/') {
        match last_segment {
            "" => split_last_segment(remaining_path),
            "." => split_last_segment(remaining_path),
            ".." => match split_last_segment(remaining_path) {
                (_, "") => (path, ""),
                (parent_path, _) => split_last_segment(parent_path),
            },
            _ => (remaining_path, last_segment),
        }
    } else {
        match path {
            "" => ("", ""),
            "." => ("", ""),
            ".." => ("..", ""),
            _ => ("", path),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::Path;

    use rstest::*;
    use turbo_rcstr::{RcStr, rcstr};
    use turbo_tasks::Vc;
    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
    use turbo_tasks_fs::{DiskFileSystem, FileSystem};

    use super::{
        Pattern, longest_common_prefix, longest_common_suffix, read_matches, split_last_segment,
    };

    #[test]
    fn longest_common_prefix_test() {
        assert_eq!(longest_common_prefix(&["ab"]), "ab");
        assert_eq!(longest_common_prefix(&["ab", "cd", "ef"]), "");
        assert_eq!(longest_common_prefix(&["ab1", "ab23", "ab456"]), "ab");
        assert_eq!(longest_common_prefix(&["abc", "abc", "abc"]), "abc");
        assert_eq!(longest_common_prefix(&["abc", "a", "abc"]), "a");
    }

    #[test]
    fn longest_common_suffix_test() {
        assert_eq!(longest_common_suffix(&["ab"]), "ab");
        assert_eq!(longest_common_suffix(&["ab", "cd", "ef"]), "");
        assert_eq!(longest_common_suffix(&["1ab", "23ab", "456ab"]), "ab");
        assert_eq!(longest_common_suffix(&["abc", "abc", "abc"]), "abc");
        assert_eq!(longest_common_suffix(&["abc", "c", "abc"]), "c");
    }

    #[test]
    fn normalize() {
        let a = Pattern::Constant(rcstr!("a"));
        let b = Pattern::Constant(rcstr!("b"));
        let c = Pattern::Constant(rcstr!("c"));
        let s = Pattern::Constant(rcstr!("/"));
        let d = Pattern::Dynamic;
        {
            let mut p = Pattern::Concatenation(vec![
                Pattern::Alternatives(vec![a.clone(), b.clone()]),
                s.clone(),
                c.clone(),
            ]);
            p.normalize();
            assert_eq!(
                p,
                Pattern::Alternatives(vec![
                    Pattern::Constant(rcstr!("a/c")),
                    Pattern::Constant(rcstr!("b/c")),
                ])
            );
        }

        #[allow(clippy::redundant_clone)] // alignment
        {
            let mut p = Pattern::Concatenation(vec![
                Pattern::Alternatives(vec![a.clone(), b.clone(), d.clone()]),
                s.clone(),
                Pattern::Alternatives(vec![b.clone(), c.clone(), d.clone()]),
            ]);
            p.normalize();

            assert_eq!(
                p,
                Pattern::Alternatives(vec![
                    Pattern::Constant(rcstr!("a/b")),
                    Pattern::Constant(rcstr!("b/b")),
                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/b"))]),
                    Pattern::Constant(rcstr!("a/c")),
                    Pattern::Constant(rcstr!("b/c")),
                    Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/c"))]),
                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
                    Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
                    Pattern::Concatenation(vec![
                        Pattern::Dynamic,
                        Pattern::Constant(rcstr!("/")),
                        Pattern::Dynamic
                    ]),
                ])
            );
        }

        #[allow(clippy::redundant_clone)] // alignment
        {
            let mut p = Pattern::Alternatives(vec![a.clone()]);
            p.normalize();

            assert_eq!(p, a);
        }

        #[allow(clippy::redundant_clone)] // alignment
        {
            let mut p = Pattern::Alternatives(vec![Pattern::Dynamic, Pattern::Dynamic]);
            p.normalize();

            assert_eq!(p, Pattern::Dynamic);
        }
    }

    #[test]
    fn with_normalized_path() {
        assert!(
            Pattern::Constant(rcstr!("a/../.."))
                .with_normalized_path()
                .is_none()
        );
        assert_eq!(
            Pattern::Constant(rcstr!("a/b/../c"))
                .with_normalized_path()
                .unwrap(),
            Pattern::Constant(rcstr!("a/c"))
        );
        assert_eq!(
            Pattern::Alternatives(vec![
                Pattern::Constant(rcstr!("a/b/../c")),
                Pattern::Constant(rcstr!("a/b/../c/d"))
            ])
            .with_normalized_path()
            .unwrap(),
            Pattern::Alternatives(vec![
                Pattern::Constant(rcstr!("a/c")),
                Pattern::Constant(rcstr!("a/c/d"))
            ])
        );
        assert_eq!(
            Pattern::Constant(rcstr!("a/b/"))
                .with_normalized_path()
                .unwrap(),
            Pattern::Constant(rcstr!("a/b"))
        );

        // Dynamic is a segment itself
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("../c"))
            ])
            .with_normalized_path()
            .unwrap(),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("../c"))
            ])
        );

        // Dynamic is part of a segment
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("../c"))
            ])
            .with_normalized_path()
            .unwrap(),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("../c"))
            ])
        );
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("src/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".js"))
            ])
            .with_normalized_path()
            .unwrap(),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("src/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".js"))
            ])
        );
    }

    #[test]
    fn is_match() {
        let pat = Pattern::Concatenation(vec![
            Pattern::Constant(rcstr!(".")),
            Pattern::Constant(rcstr!("/")),
            Pattern::Dynamic,
            Pattern::Constant(rcstr!(".js")),
        ]);
        assert!(pat.could_match(""));
        assert!(pat.could_match("./"));
        assert!(!pat.is_match("./"));
        assert!(pat.is_match("./index.js"));
        assert!(!pat.is_match("./index"));
        assert!(pat.is_match("./foo/index.js"));
        assert!(pat.is_match("./foo/bar/index.js"));

        // forbidden:
        assert!(!pat.is_match("./../index.js"));
        assert!(!pat.is_match("././index.js"));
        assert!(!pat.is_match("./.git/index.js"));
        assert!(!pat.is_match("./inner/../index.js"));
        assert!(!pat.is_match("./inner/./index.js"));
        assert!(!pat.is_match("./inner/.git/index.js"));
        assert!(!pat.could_match("./../"));
        assert!(!pat.could_match("././"));
        assert!(!pat.could_match("./.git/"));
        assert!(!pat.could_match("./inner/../"));
        assert!(!pat.could_match("./inner/./"));
        assert!(!pat.could_match("./inner/.git/"));
    }

    #[test]
    fn is_match_dynamic_no_slash() {
        let pat = Pattern::Concatenation(vec![
            Pattern::Constant(rcstr!(".")),
            Pattern::Constant(rcstr!("/")),
            Pattern::DynamicNoSlash,
            Pattern::Constant(rcstr!(".js")),
        ]);
        assert!(pat.could_match(""));
        assert!(pat.could_match("./"));
        assert!(!pat.is_match("./"));
        assert!(pat.is_match("./index.js"));
        assert!(!pat.is_match("./index"));
        assert!(!pat.is_match("./foo/index.js"));
        assert!(!pat.is_match("./foo/bar/index.js"));
    }

    #[test]
    fn constant_prefix() {
        assert_eq!(
            Pattern::Constant(rcstr!("a/b/c.js")).constant_prefix(),
            "a/b/c.js",
        );

        let pat = Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("a/b/x")),
            Pattern::Constant(rcstr!("a/b/y")),
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]),
        ]);
        assert_eq!(pat.constant_prefix(), "a/b/");
    }

    #[test]
    fn constant_suffix() {
        assert_eq!(
            Pattern::Constant(rcstr!("a/b/c.js")).constant_suffix(),
            "a/b/c.js",
        );

        let pat = Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("a/b/x.js")),
            Pattern::Constant(rcstr!("a/b/y.js")),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b/c/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".js")),
            ]),
        ]);
        assert_eq!(pat.constant_suffix(), ".js");
    }

    #[test]
    fn strip_prefix() {
        fn strip(mut pat: Pattern, n: usize) -> Pattern {
            pat.strip_prefix_len(n).unwrap();
            pat
        }

        assert_eq!(
            strip(Pattern::Constant(rcstr!("a/b")), 0),
            Pattern::Constant(rcstr!("a/b"))
        );

        assert_eq!(
            strip(
                Pattern::Alternatives(vec![
                    Pattern::Constant(rcstr!("a/b/x")),
                    Pattern::Constant(rcstr!("a/b/y")),
                ]),
                2
            ),
            Pattern::Alternatives(vec![
                Pattern::Constant(rcstr!("b/x")),
                Pattern::Constant(rcstr!("b/y")),
            ])
        );

        assert_eq!(
            strip(
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("a/")),
                    Pattern::Constant(rcstr!("b")),
                    Pattern::Constant(rcstr!("/")),
                    Pattern::Constant(rcstr!("y/")),
                    Pattern::Dynamic
                ]),
                4
            ),
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("y/")), Pattern::Dynamic]),
        );
    }

    #[test]
    fn strip_suffix() {
        fn strip(mut pat: Pattern, n: usize) -> Pattern {
            pat.strip_suffix_len(n);
            pat
        }

        assert_eq!(
            strip(Pattern::Constant(rcstr!("a/b")), 0),
            Pattern::Constant(rcstr!("a/b"))
        );

        assert_eq!(
            strip(
                Pattern::Alternatives(vec![
                    Pattern::Constant(rcstr!("x/b/a")),
                    Pattern::Constant(rcstr!("y/b/a")),
                ]),
                2
            ),
            Pattern::Alternatives(vec![
                Pattern::Constant(rcstr!("x/b")),
                Pattern::Constant(rcstr!("y/b")),
            ])
        );

        assert_eq!(
            strip(
                Pattern::Concatenation(vec![
                    Pattern::Dynamic,
                    Pattern::Constant(rcstr!("/a/")),
                    Pattern::Constant(rcstr!("b")),
                    Pattern::Constant(rcstr!("/")),
                    Pattern::Constant(rcstr!("y/")),
                ]),
                4
            ),
            Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!("/a/")),]),
        );
    }

    #[test]
    fn spread_into_star() {
        let pat = Pattern::Constant(rcstr!("xyz"));
        assert_eq!(
            pat.spread_into_star("before/after"),
            Pattern::Constant(rcstr!("before/after")),
        );

        let pat =
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/b/c/")), Pattern::Dynamic]);
        assert_eq!(
            pat.spread_into_star("before/*/after"),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("before/a/b/c/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("/after"))
            ])
        );

        let pat = Pattern::Alternatives(vec![
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("a/")), Pattern::Dynamic]),
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("b/")), Pattern::Dynamic]),
        ]);
        assert_eq!(
            pat.spread_into_star("before/*/after"),
            Pattern::Alternatives(vec![
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("before/a/")),
                    Pattern::Dynamic,
                    Pattern::Constant(rcstr!("/after"))
                ]),
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("before/b/")),
                    Pattern::Dynamic,
                    Pattern::Constant(rcstr!("/after"))
                ]),
            ])
        );

        let pat = Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("a")),
            Pattern::Constant(rcstr!("b")),
        ]);
        assert_eq!(
            pat.spread_into_star("before/*/*"),
            Pattern::Alternatives(vec![
                Pattern::Constant(rcstr!("before/a/a")),
                Pattern::Constant(rcstr!("before/b/b")),
            ])
        );

        let pat = Pattern::Dynamic;
        assert_eq!(
            pat.spread_into_star("before/*/*"),
            Pattern::Concatenation(vec![
                // TODO currently nothing ensures that both Dynamic parts are equal
                Pattern::Constant(rcstr!("before/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!("/")),
                Pattern::Dynamic
            ])
        );
    }

    #[rstest]
    #[case::dynamic(Pattern::Dynamic)]
    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
    fn dynamic_match(#[case] pat: Pattern) {
        assert!(pat.could_match(""));
        assert!(pat.is_match("index.js"));

        // forbidden:
        assert!(!pat.could_match("./"));
        assert!(!pat.is_match("./"));
        assert!(!pat.could_match("."));
        assert!(!pat.is_match("."));
        assert!(!pat.could_match("../"));
        assert!(!pat.is_match("../"));
        assert!(!pat.could_match(".."));
        assert!(!pat.is_match(".."));
        assert!(!pat.is_match("./../index.js"));
        assert!(!pat.is_match("././index.js"));
        assert!(!pat.is_match("./.git/index.js"));
        assert!(!pat.is_match("./inner/../index.js"));
        assert!(!pat.is_match("./inner/./index.js"));
        assert!(!pat.is_match("./inner/.git/index.js"));
        assert!(!pat.could_match("./../"));
        assert!(!pat.could_match("././"));
        assert!(!pat.could_match("./.git/"));
        assert!(!pat.could_match("./inner/../"));
        assert!(!pat.could_match("./inner/./"));
        assert!(!pat.could_match("./inner/.git/"));
        assert!(!pat.could_match("dir//"));
        assert!(!pat.could_match("dir//dir"));
        assert!(!pat.could_match("dir///dir"));
        assert!(!pat.could_match("/"));
        assert!(!pat.could_match("//"));
        assert!(!pat.could_match("/ROOT/"));

        assert!(!pat.could_match("node_modules"));
        assert!(!pat.could_match("node_modules/package"));
        assert!(!pat.could_match("nested/node_modules"));
        assert!(!pat.could_match("nested/node_modules/package"));

        // forbidden match
        assert!(pat.could_match("file.map"));
        assert!(!pat.is_match("file.map"));
        assert!(pat.is_match("file.map/file.js"));
        assert!(!pat.is_match("file.d.ts"));
        assert!(!pat.is_match("file.d.ts.map"));
        assert!(!pat.is_match("file.d.ts.map"));
        assert!(!pat.is_match("dir/file.d.ts.map"));
        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
        assert!(pat.could_match("dir/inner/file.d.ts.map"));
    }

    #[rstest]
    #[case::slash(Pattern::Concatenation(vec![Pattern::Constant(rcstr!("node_modules/")),Pattern::Dynamic]))]
    #[case::nested(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file())]
    fn dynamic_match_node_modules(#[case] pat: Pattern) {
        assert!(!pat.is_match("node_modules/package"));
        assert!(!pat.could_match("node_modules/package"));
        assert!(!pat.is_match("node_modules/package/index.js"));
        assert!(!pat.could_match("node_modules/package/index.js"));
    }

    #[rstest]
    fn dynamic_match2() {
        let pat = Pattern::Concatenation(vec![
            Pattern::Dynamic,
            Pattern::Constant(rcstr!("/")),
            Pattern::Dynamic,
        ]);
        assert!(pat.could_match("dir"));
        assert!(pat.could_match("dir/"));
        assert!(pat.is_match("dir/index.js"));

        // forbidden:
        assert!(!pat.could_match("./"));
        assert!(!pat.is_match("./"));
        assert!(!pat.could_match("."));
        assert!(!pat.is_match("."));
        assert!(!pat.could_match("../"));
        assert!(!pat.is_match("../"));
        assert!(!pat.could_match(".."));
        assert!(!pat.is_match(".."));
        assert!(!pat.is_match("./../index.js"));
        assert!(!pat.is_match("././index.js"));
        assert!(!pat.is_match("./.git/index.js"));
        assert!(!pat.is_match("./inner/../index.js"));
        assert!(!pat.is_match("./inner/./index.js"));
        assert!(!pat.is_match("./inner/.git/index.js"));
        assert!(!pat.could_match("./../"));
        assert!(!pat.could_match("././"));
        assert!(!pat.could_match("./.git/"));
        assert!(!pat.could_match("./inner/../"));
        assert!(!pat.could_match("./inner/./"));
        assert!(!pat.could_match("./inner/.git/"));
        assert!(!pat.could_match("dir//"));
        assert!(!pat.could_match("dir//dir"));
        assert!(!pat.could_match("dir///dir"));
        assert!(!pat.could_match("/ROOT/"));

        assert!(!pat.could_match("node_modules"));
        assert!(!pat.could_match("node_modules/package"));
        assert!(!pat.could_match("nested/node_modules"));
        assert!(!pat.could_match("nested/node_modules/package"));

        // forbidden match
        assert!(pat.could_match("dir/file.map"));
        assert!(!pat.is_match("dir/file.map"));
        assert!(pat.is_match("file.map/file.js"));
        assert!(!pat.is_match("dir/file.d.ts"));
        assert!(!pat.is_match("dir/file.d.ts.map"));
        assert!(!pat.is_match("dir/file.d.ts.map"));
        assert!(!pat.is_match("dir/file.d.ts.map"));
        assert!(!pat.is_match("dir/inner/file.d.ts.map"));
        assert!(pat.could_match("dir/inner/file.d.ts.map"));
    }

    #[rstest]
    #[case::dynamic(Pattern::Dynamic)]
    #[case::dynamic_concat(Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]))]
    #[case::dynamic_concat2(Pattern::Concatenation(vec![
        Pattern::Dynamic,
        Pattern::Constant(rcstr!("/")),
        Pattern::Dynamic,
    ]))]
    #[case::dynamic_alt_concat(Pattern::alternatives(vec![
        Pattern::Concatenation(vec![
            Pattern::Dynamic,
            Pattern::Constant(rcstr!("/")),
            Pattern::Dynamic,
        ]),
        Pattern::Dynamic,
    ]))]
    fn split_could_match(#[case] pat: Pattern) {
        let (abs, rel) = pat.split_could_match("/ROOT/");
        assert!(abs.is_none());
        assert!(rel.is_some());
    }

    #[rstest]
    #[case::dynamic(Pattern::Dynamic, "feijf", None)]
    #[case::dynamic_concat(
        Pattern::Concatenation(vec![Pattern::Dynamic, Pattern::Constant(rcstr!(".js"))]),
        "hello.", None
    )]
    #[case::constant(Pattern::Constant(rcstr!("Hello World")), "Hello ", Some(vec![("World", true)]))]
    #[case::alternatives(
        Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("Hello World")),
            Pattern::Constant(rcstr!("Hello All"))
        ]), "Hello ", Some(vec![("World", true), ("All", true)])
    )]
    #[case::alternatives_non_end(
        Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("Hello World")),
            Pattern::Constant(rcstr!("Hello All")),
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("Hello more")), Pattern::Dynamic])
        ]), "Hello ", Some(vec![("World", true), ("All", true), ("more", false)])
    )]
    #[case::request_with_extensions(
        Pattern::Alternatives(vec![
            Pattern::Constant(rcstr!("./file.js")),
            Pattern::Constant(rcstr!("./file.ts")),
            Pattern::Constant(rcstr!("./file.cjs")),
        ]), "./", Some(vec![("file.js", true), ("file.ts", true), ("file.cjs", true)])
    )]
    fn next_constants(
        #[case] pat: Pattern,
        #[case] value: &str,
        #[case] expected: Option<Vec<(&str, bool)>>,
    ) {
        assert_eq!(pat.next_constants(value), expected);
    }

    #[test]
    fn replace_final_constants() {
        fn f(mut p: Pattern, cb: &mut impl FnMut(&RcStr) -> Option<Pattern>) -> Pattern {
            p.replace_final_constants(cb);
            p
        }

        let mut js_to_ts_tsx = |c: &RcStr| -> Option<Pattern> {
            c.strip_suffix(".js").map(|rest| {
                let new_ending = Pattern::Alternatives(vec![
                    Pattern::Constant(rcstr!(".ts")),
                    Pattern::Constant(rcstr!(".tsx")),
                    Pattern::Constant(rcstr!(".js")),
                ]);
                if !rest.is_empty() {
                    Pattern::Concatenation(vec![Pattern::Constant(rest.into()), new_ending])
                } else {
                    new_ending
                }
            })
        };

        assert_eq!(
            f(
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!(".")),
                    Pattern::Constant(rcstr!("/")),
                    Pattern::Dynamic,
                    Pattern::Alternatives(vec![
                        Pattern::Constant(rcstr!(".js")),
                        Pattern::Constant(rcstr!(".node")),
                    ])
                ]),
                &mut js_to_ts_tsx
            ),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!(".")),
                Pattern::Constant(rcstr!("/")),
                Pattern::Dynamic,
                Pattern::Alternatives(vec![
                    Pattern::Alternatives(vec![
                        Pattern::Constant(rcstr!(".ts")),
                        Pattern::Constant(rcstr!(".tsx")),
                        Pattern::Constant(rcstr!(".js")),
                    ]),
                    Pattern::Constant(rcstr!(".node")),
                ])
            ]),
        );
        assert_eq!(
            f(
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!(".")),
                    Pattern::Constant(rcstr!("/")),
                    Pattern::Constant(rcstr!("abc.js")),
                ]),
                &mut js_to_ts_tsx
            ),
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!(".")),
                Pattern::Constant(rcstr!("/")),
                Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("abc")),
                    Pattern::Alternatives(vec![
                        Pattern::Constant(rcstr!(".ts")),
                        Pattern::Constant(rcstr!(".tsx")),
                        Pattern::Constant(rcstr!(".js")),
                    ])
                ]),
            ])
        );
    }

    #[test]
    fn match_apply_template() {
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".ts")),
            ])
            .match_apply_template(
                "a/b/foo.ts",
                &Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("@/a/b/")),
                    Pattern::Dynamic,
                    Pattern::Constant(rcstr!(".js")),
                ])
            )
            .as_deref(),
            Some("@/a/b/foo.js")
        );
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("b/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".ts")),
            ])
            .match_apply_template(
                "a/b/foo.ts",
                &Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("@/a/b/")),
                    Pattern::Dynamic,
                    Pattern::Constant(rcstr!(".js")),
                ])
            )
            .as_deref(),
            None,
        );
        assert_eq!(
            Pattern::Concatenation(vec![
                Pattern::Constant(rcstr!("a/b/")),
                Pattern::Dynamic,
                Pattern::Constant(rcstr!(".ts")),
            ])
            .match_apply_template(
                "a/b/foo.ts",
                &Pattern::Concatenation(vec![
                    Pattern::Constant(rcstr!("@/a/b/x")),
                    Pattern::Constant(rcstr!(".js")),
                ])
            )
            .as_deref(),
            None,
        );
        assert_eq!(
            Pattern::Concatenation(vec![Pattern::Constant(rcstr!("./sub/")), Pattern::Dynamic])
                .match_apply_template(
                    "./sub/file1",
                    &Pattern::Concatenation(vec![
                        Pattern::Constant(rcstr!("@/sub/")),
                        Pattern::Dynamic
                    ])
                )
                .as_deref(),
            Some("@/sub/file1"),
        );
    }

    #[test]
    fn test_split_last_segment() {
        assert_eq!(split_last_segment(""), ("", ""));
        assert_eq!(split_last_segment("a"), ("", "a"));
        assert_eq!(split_last_segment("a/"), ("", "a"));
        assert_eq!(split_last_segment("a/b"), ("a", "b"));
        assert_eq!(split_last_segment("a/b/"), ("a", "b"));
        assert_eq!(split_last_segment("a/b/c"), ("a/b", "c"));
        assert_eq!(split_last_segment("a/b/."), ("a", "b"));
        assert_eq!(split_last_segment("a/b/.."), ("", "a"));
        assert_eq!(split_last_segment("a/b/c/.."), ("a", "b"));
        assert_eq!(split_last_segment("a/b/c/../.."), ("", "a"));
        assert_eq!(split_last_segment("a/b/c/d/../.."), ("a", "b"));
        assert_eq!(split_last_segment("a/b/c/../d/.."), ("a", "b"));
        assert_eq!(split_last_segment("a/b/../c/d/.."), ("a/b/..", "c"));
        assert_eq!(split_last_segment("."), ("", ""));
        assert_eq!(split_last_segment("./"), ("", ""));
        assert_eq!(split_last_segment(".."), ("..", ""));
        assert_eq!(split_last_segment("../"), ("..", ""));
        assert_eq!(split_last_segment("./../"), ("./..", ""));
        assert_eq!(split_last_segment("../../"), ("../..", ""));
        assert_eq!(split_last_segment("../../."), ("../..", ""));
        assert_eq!(split_last_segment("../.././"), ("../..", ""));
        assert_eq!(split_last_segment("a/.."), ("", ""));
        assert_eq!(split_last_segment("a/../"), ("", ""));
        assert_eq!(split_last_segment("a/../.."), ("a/../..", ""));
        assert_eq!(split_last_segment("a/../../"), ("a/../..", ""));
        assert_eq!(split_last_segment("a/././../"), ("", ""));
        assert_eq!(split_last_segment("../a"), ("..", "a"));
        assert_eq!(split_last_segment("../a/"), ("..", "a"));
        assert_eq!(split_last_segment("../../a"), ("../..", "a"));
        assert_eq!(split_last_segment("../../a/"), ("../..", "a"));
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_read_matches() {
        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
            BackendOptions::default(),
            noop_backing_storage(),
        ));
        tt.run_once(async {
            #[turbo_tasks::value]
            struct ReadMatchesOutput {
                dynamic: Vec<String>,
                dynamic_file_suffix: Vec<String>,
                node_modules_dynamic: Vec<String>,
                extension_ordering: Vec<String>,
                subpath_ordering: Vec<String>,
            }

            #[turbo_tasks::function(operation)]
            async fn read_matches_operation() -> anyhow::Result<Vc<ReadMatchesOutput>> {
                let root = DiskFileSystem::new(
                    rcstr!("test"),
                    Vc::cell(
                        Path::new(env!("CARGO_MANIFEST_DIR"))
                            .join("tests/pattern/read_matches")
                            .to_str()
                            .unwrap()
                            .into(),
                    ),
                )
                .root()
                .owned()
                .await?;

                let dynamic = read_matches(
                    root.clone(),
                    rcstr!(""),
                    false,
                    Pattern::new(Pattern::Dynamic),
                )
                .await?
                .into_iter()
                .map(|m| m.name().to_string())
                .collect::<Vec<_>>();

                let dynamic_file_suffix = read_matches(
                    root.clone(),
                    rcstr!(""),
                    false,
                    Pattern::new(Pattern::concat([
                        Pattern::Constant(rcstr!("sub/foo")),
                        Pattern::Dynamic,
                    ])),
                )
                .await?
                .into_iter()
                .map(|m| m.name().to_string())
                .collect::<Vec<_>>();

                let node_modules_dynamic = read_matches(
                    root.clone(),
                    rcstr!(""),
                    false,
                    Pattern::new(Pattern::Constant(rcstr!("node_modules")).or_any_nested_file()),
                )
                .await?
                .into_iter()
                .map(|m| m.name().to_string())
                .collect::<Vec<_>>();

                // Test: extension ordering is preserved (fast path, until_end=true)
                // When both Component.web.tsx and Component.tsx exist, the order of
                // alternatives determines which comes first in results.
                let extension_ordering = read_matches(
                    root.clone(),
                    rcstr!(""),
                    false,
                    Pattern::new(Pattern::Alternatives(vec![
                        Pattern::Constant(rcstr!("extensions/Component")),
                        Pattern::Constant(rcstr!("extensions/Component.web.tsx")),
                        Pattern::Constant(rcstr!("extensions/Component.tsx")),
                    ])),
                )
                .await?
                .into_iter()
                .map(|m| m.name().to_string())
                .collect::<Vec<_>>();

                // Test: subpath ordering is preserved (fast path, until_end=false)
                // When alternatives route to different subdirectories, the index ordering
                // must be respected. This exercises the fix for the hardcoded `0` bug.
                let subpath_ordering = read_matches(
                    root.clone(),
                    rcstr!(""),
                    false,
                    Pattern::new({
                        let mut p = Pattern::Alternatives(vec![
                            Pattern::Concatenation(vec![
                                Pattern::Constant(rcstr!("prio/a/")),
                                Pattern::Dynamic,
                            ]),
                            Pattern::Concatenation(vec![
                                Pattern::Constant(rcstr!("prio/b/")),
                                Pattern::Dynamic,
                            ]),
                        ]);
                        p.normalize();
                        p
                    }),
                )
                .await?
                .into_iter()
                .map(|m| m.name().to_string())
                .collect::<Vec<_>>();

                Ok(ReadMatchesOutput {
                    dynamic,
                    dynamic_file_suffix,
                    node_modules_dynamic,
                    extension_ordering,
                    subpath_ordering,
                }
                .cell())
            }

            let matches = read_matches_operation().read_strongly_consistent().await?;

            // node_modules shouldn't be matched by Dynamic here
            assert_eq!(
                matches.dynamic,
                &[
                    "extensions",
                    "extensions/",
                    "extensions/Component.tsx",
                    "extensions/Component.web.tsx",
                    "index.js",
                    "prio",
                    "prio/",
                    "prio/a",
                    "prio/a/",
                    "prio/a/Component.tsx",
                    "prio/b",
                    "prio/b/",
                    "prio/b/Component.tsx",
                    "sub",
                    "sub/",
                    "sub/foo-a.js",
                    "sub/foo-b.js",
                ]
            );

            // basic dynamic file suffix
            assert_eq!(
                matches.dynamic_file_suffix,
                &["sub/foo-a.js", "sub/foo-b.js"]
            );

            // read_matches "node_modules/<dynamic>" should not return anything inside. We never
            // want to enumerate the list of packages here.
            assert_eq!(matches.node_modules_dynamic, &["node_modules"]);

            // extension ordering: .web.tsx (index 1) must come before .tsx (index 2)
            assert_eq!(
                matches.extension_ordering,
                &["extensions/Component.web.tsx", "extensions/Component.tsx",]
            );

            // subpath ordering: prio/a/ alternatives (index 0) must come before prio/b/
            // alternatives (index 1). This verifies the fix for the hardcoded `0` bug in
            // the until_end=false branch of the fast path.
            assert!(
                matches
                    .subpath_ordering
                    .iter()
                    .position(|s| s.starts_with("prio/a/"))
                    .unwrap()
                    < matches
                        .subpath_ordering
                        .iter()
                        .position(|s| s.starts_with("prio/b/"))
                        .unwrap(),
                "Expected prio/a/ results before prio/b/ results, got: {:?}",
                matches.subpath_ordering
            );

            Ok(())
        })
        .await
        .unwrap();
    }
}
Quest for Codev2.0.0
/
SIGN IN