turbopack/module_options/
rule_condition.rs

1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3use smallvec::SmallVec;
4use turbo_esregex::EsRegex;
5use turbo_tasks::{NonLocalValue, ReadRef, ResolvedVc, primitives::Regex, trace::TraceRawVcs};
6use turbo_tasks_fs::{FileSystemPath, glob::Glob};
7use turbopack_core::{
8    reference_type::ReferenceType, source::Source, virtual_source::VirtualSource,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize, TraceRawVcs, PartialEq, Eq, NonLocalValue)]
12pub enum RuleCondition {
13    All(Vec<RuleCondition>),
14    Any(Vec<RuleCondition>),
15    Not(Box<RuleCondition>),
16    ReferenceType(ReferenceType),
17    ResourceIsVirtualSource,
18    ResourcePathEquals(FileSystemPath),
19    ResourcePathHasNoExtension,
20    ResourcePathEndsWith(String),
21    ResourcePathInDirectory(String),
22    ResourcePathInExactDirectory(FileSystemPath),
23    ContentTypeStartsWith(String),
24    ContentTypeEmpty,
25    ResourcePathRegex(#[turbo_tasks(trace_ignore)] Regex),
26    ResourcePathEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
27    /// For paths that are within the same filesystem as the `base`, it need to
28    /// match the relative path from base to resource. This includes `./` or
29    /// `../` prefix. For paths in a different filesystem, it need to match
30    /// the resource path in that filesystem without any prefix. This means
31    /// any glob starting with `./` or `../` will only match paths in the
32    /// project. Globs starting with `**` can match any path.
33    ResourcePathGlob {
34        base: FileSystemPath,
35        #[turbo_tasks(trace_ignore)]
36        glob: ReadRef<Glob>,
37    },
38    ResourceBasePathGlob(#[turbo_tasks(trace_ignore)] ReadRef<Glob>),
39}
40
41impl RuleCondition {
42    pub fn all(conditions: Vec<RuleCondition>) -> RuleCondition {
43        RuleCondition::All(conditions)
44    }
45
46    pub fn any(conditions: Vec<RuleCondition>) -> RuleCondition {
47        RuleCondition::Any(conditions)
48    }
49
50    #[allow(clippy::should_implement_trait)]
51    pub fn not(condition: RuleCondition) -> RuleCondition {
52        RuleCondition::Not(Box::new(condition))
53    }
54}
55
56impl RuleCondition {
57    pub async fn matches(
58        &self,
59        source: ResolvedVc<Box<dyn Source>>,
60        path: &FileSystemPath,
61        reference_type: &ReferenceType,
62    ) -> Result<bool> {
63        enum Op<'a> {
64            All(&'a [RuleCondition]), // Remaining conditions in an All
65            Any(&'a [RuleCondition]), // Remaining conditions in an Any
66            Not,                      // Inverts the previous condition
67        }
68
69        // Evaluates the condition returning the result and possibly pushing additional operations
70        // onto the stack as a kind of continuation.
71        async fn process_condition<'a, const SZ: usize>(
72            source: ResolvedVc<Box<dyn Source + 'static>>,
73            path: &FileSystemPath,
74            reference_type: &ReferenceType,
75            stack: &mut SmallVec<[Op<'a>; SZ]>,
76            mut cond: &'a RuleCondition,
77        ) -> Result<bool, anyhow::Error> {
78            // Use a loop to avoid recursion and unnecessary stack operations.
79            loop {
80                match cond {
81                    RuleCondition::All(conditions) => {
82                        if conditions.is_empty() {
83                            return Ok(true);
84                        } else {
85                            if conditions.len() > 1 {
86                                stack.push(Op::All(&conditions[1..]));
87                            }
88                            cond = &conditions[0];
89                            // jump directly to the next condition, no need to deal with
90                            // the stack.
91                            continue;
92                        }
93                    }
94                    RuleCondition::Any(conditions) => {
95                        if conditions.is_empty() {
96                            return Ok(false);
97                        } else {
98                            if conditions.len() > 1 {
99                                stack.push(Op::Any(&conditions[1..]));
100                            }
101                            cond = &conditions[0];
102                            continue;
103                        }
104                    }
105                    RuleCondition::Not(inner) => {
106                        stack.push(Op::Not);
107                        cond = inner.as_ref();
108                        continue;
109                    }
110                    RuleCondition::ReferenceType(condition_ty) => {
111                        return Ok(condition_ty.includes(reference_type));
112                    }
113                    RuleCondition::ResourceIsVirtualSource => {
114                        return Ok(ResolvedVc::try_downcast_type::<VirtualSource>(source).is_some());
115                    }
116                    RuleCondition::ResourcePathEquals(other) => {
117                        return Ok(path == other);
118                    }
119                    RuleCondition::ResourcePathEndsWith(end) => {
120                        return Ok(path.path.ends_with(end));
121                    }
122                    RuleCondition::ResourcePathHasNoExtension => {
123                        return Ok(if let Some(i) = path.path.rfind('.') {
124                            if let Some(j) = path.path.rfind('/') {
125                                j > i
126                            } else {
127                                false
128                            }
129                        } else {
130                            true
131                        });
132                    }
133                    RuleCondition::ResourcePathInDirectory(dir) => {
134                        return Ok(path.path.starts_with(&format!("{dir}/"))
135                            || path.path.contains(&format!("/{dir}/")));
136                    }
137                    RuleCondition::ResourcePathInExactDirectory(parent_path) => {
138                        return Ok(path.is_inside_ref(parent_path));
139                    }
140                    RuleCondition::ContentTypeStartsWith(start) => {
141                        let content_type = &source.ident().await?.content_type;
142                        return Ok(content_type
143                            .as_ref()
144                            .is_some_and(|ct| ct.starts_with(start)));
145                    }
146                    RuleCondition::ContentTypeEmpty => {
147                        return Ok(source.ident().await?.content_type.is_none());
148                    }
149                    RuleCondition::ResourcePathGlob { glob, base } => {
150                        return Ok(if let Some(rel_path) = base.get_relative_path_to(path) {
151                            glob.matches(&rel_path)
152                        } else {
153                            glob.matches(&path.path)
154                        });
155                    }
156                    RuleCondition::ResourceBasePathGlob(glob) => {
157                        let basename = path
158                            .path
159                            .rsplit_once('/')
160                            .map_or(path.path.as_str(), |(_, b)| b);
161                        return Ok(glob.matches(basename));
162                    }
163                    RuleCondition::ResourcePathRegex(_) => {
164                        bail!("ResourcePathRegex not implemented yet");
165                    }
166                    RuleCondition::ResourcePathEsRegex(regex) => {
167                        return Ok(regex.is_match(&path.path));
168                    }
169                }
170            }
171        }
172        // Allocate a small inline stack to avoid heap allocations in the common case where
173        // conditions are not deeply stacked.  Additionally we take care to avoid stack
174        // operations unless strictly necessary.
175        const EXPECTED_SIZE: usize = 8;
176        let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
177        let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
178        while let Some(op) = stack.pop() {
179            match op {
180                Op::All(remaining) => {
181                    // Previous was true, keep going
182                    if result {
183                        if remaining.len() > 1 {
184                            stack.push(Op::All(&remaining[1..]));
185                        }
186                        result = process_condition(
187                            source,
188                            path,
189                            reference_type,
190                            &mut stack,
191                            &remaining[0],
192                        )
193                        .await?;
194                    }
195                }
196                Op::Any(remaining) => {
197                    // Previous was false, keep going
198                    if !result {
199                        if remaining.len() > 1 {
200                            stack.push(Op::Any(&remaining[1..]));
201                        }
202                        // If the stack didn't change, we can loop inline, but we would still need
203                        // to pop the item.  This might be faster since we would avoid the `match`
204                        // but overall, that is quite minor for an enum with 3 cases.
205                        result = process_condition(
206                            source,
207                            path,
208                            reference_type,
209                            &mut stack,
210                            &remaining[0],
211                        )
212                        .await?;
213                    }
214                }
215                Op::Not => {
216                    result = !result;
217                }
218            }
219        }
220        Ok(result)
221    }
222}
223
224#[cfg(test)]
225pub mod tests {
226    use turbo_tasks::Vc;
227    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
228    use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
229    use turbopack_core::{asset::AssetContent, file_source::FileSource};
230
231    use super::*;
232
233    #[tokio::test]
234    async fn test_rule_condition_leaves() {
235        crate::register();
236        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
237            BackendOptions::default(),
238            noop_backing_storage(),
239        ));
240        tt.run_once(async { run_leaves_test().await })
241            .await
242            .unwrap();
243    }
244
245    #[turbo_tasks::function]
246    pub async fn run_leaves_test() -> Result<()> {
247        let fs = VirtualFileSystem::new();
248        let virtual_path = fs.root().await?.join("foo.js")?;
249        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
250            virtual_path.clone(),
251            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
252        ))
253        .to_resolved()
254        .await?;
255
256        let non_virtual_path = fs.root().await?.join("bar.js")?;
257        let non_virtual_source =
258            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
259                .to_resolved()
260                .await?;
261
262        {
263            let condition = RuleCondition::ReferenceType(ReferenceType::Runtime);
264            assert!(
265                condition
266                    .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
267                    .await
268                    .unwrap()
269            );
270            assert!(
271                !condition
272                    .matches(
273                        non_virtual_source,
274                        &non_virtual_path,
275                        &ReferenceType::Css(
276                            turbopack_core::reference_type::CssReferenceSubType::Compose
277                        )
278                    )
279                    .await
280                    .unwrap()
281            );
282        }
283
284        {
285            let condition = RuleCondition::ResourceIsVirtualSource;
286            assert!(
287                condition
288                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
289                    .await
290                    .unwrap()
291            );
292            assert!(
293                !condition
294                    .matches(
295                        non_virtual_source,
296                        &non_virtual_path,
297                        &ReferenceType::Undefined
298                    )
299                    .await
300                    .unwrap()
301            );
302        }
303        {
304            let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
305            assert!(
306                condition
307                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
308                    .await
309                    .unwrap()
310            );
311            assert!(
312                !condition
313                    .matches(
314                        non_virtual_source,
315                        &non_virtual_path,
316                        &ReferenceType::Undefined
317                    )
318                    .await
319                    .unwrap()
320            );
321        }
322        {
323            let condition = RuleCondition::ResourcePathHasNoExtension;
324            assert!(
325                condition
326                    .matches(
327                        virtual_source,
328                        &fs.root().await?.join("foo")?,
329                        &ReferenceType::Undefined
330                    )
331                    .await
332                    .unwrap()
333            );
334            assert!(
335                !condition
336                    .matches(
337                        non_virtual_source,
338                        &non_virtual_path,
339                        &ReferenceType::Undefined
340                    )
341                    .await
342                    .unwrap()
343            );
344        }
345        {
346            let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
347            assert!(
348                condition
349                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
350                    .await
351                    .unwrap()
352            );
353            assert!(
354                !condition
355                    .matches(
356                        non_virtual_source,
357                        &non_virtual_path,
358                        &ReferenceType::Undefined
359                    )
360                    .await
361                    .unwrap()
362            );
363        }
364        anyhow::Ok(())
365    }
366
367    #[tokio::test]
368    async fn test_rule_condition_tree() {
369        crate::register();
370        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
371            BackendOptions::default(),
372            noop_backing_storage(),
373        ));
374        tt.run_once(async { run_rule_condition_tree_test().await })
375            .await
376            .unwrap();
377    }
378
379    #[turbo_tasks::function]
380    pub async fn run_rule_condition_tree_test() -> Result<()> {
381        let fs = VirtualFileSystem::new();
382        let virtual_path = fs.root().await?.join("foo.js")?;
383        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
384            virtual_path.clone(),
385            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
386        ))
387        .to_resolved()
388        .await?;
389
390        let non_virtual_path = fs.root().await?.join("bar.js")?;
391        let non_virtual_source =
392            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
393                .to_resolved()
394                .await?;
395
396        {
397            // not
398            let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
399            assert!(
400                !condition
401                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
402                    .await
403                    .unwrap()
404            );
405            assert!(
406                condition
407                    .matches(
408                        non_virtual_source,
409                        &non_virtual_path,
410                        &ReferenceType::Undefined
411                    )
412                    .await
413                    .unwrap()
414            );
415        }
416        {
417            // any
418            // Only one of the conditions matches our virtual source
419            let condition = RuleCondition::any(vec![
420                RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
421                RuleCondition::ResourceIsVirtualSource,
422                RuleCondition::ResourcePathHasNoExtension,
423            ]);
424            assert!(
425                condition
426                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
427                    .await
428                    .unwrap()
429            );
430            assert!(
431                !condition
432                    .matches(
433                        non_virtual_source,
434                        &non_virtual_path,
435                        &ReferenceType::Undefined
436                    )
437                    .await
438                    .unwrap()
439            );
440        }
441        {
442            // all
443            // Only one of the conditions matches our virtual source
444            let condition = RuleCondition::all(vec![
445                RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
446                RuleCondition::ResourceIsVirtualSource,
447                RuleCondition::ResourcePathEquals(virtual_path.clone()),
448            ]);
449            assert!(
450                condition
451                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
452                    .await
453                    .unwrap()
454            );
455            assert!(
456                !condition
457                    .matches(
458                        non_virtual_source,
459                        &non_virtual_path,
460                        &ReferenceType::Undefined
461                    )
462                    .await
463                    .unwrap()
464            );
465        }
466        {
467            // bigger tree
468
469            // Build a simple tree to cover our various composite conditions
470            let condition = RuleCondition::all(vec![
471                RuleCondition::ResourceIsVirtualSource,
472                RuleCondition::ResourcePathEquals(virtual_path.clone()),
473                RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
474                RuleCondition::Any(vec![
475                    RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
476                    RuleCondition::ContentTypeEmpty,
477                ]),
478            ]);
479            assert!(
480                condition
481                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
482                    .await
483                    .unwrap()
484            );
485            assert!(
486                !condition
487                    .matches(
488                        non_virtual_source,
489                        &non_virtual_path,
490                        &ReferenceType::Undefined
491                    )
492                    .await
493                    .unwrap()
494            );
495        }
496        anyhow::Ok(())
497    }
498}