Skip to main content

turbopack/module_options/
rule_condition.rs

1use std::{
2    iter,
3    mem::{replace, take},
4};
5
6use anyhow::Result;
7use bincode::{Decode, Encode};
8use either::Either;
9use smallvec::SmallVec;
10use turbo_esregex::EsRegex;
11use turbo_tasks::{NonLocalValue, ReadRef, ResolvedVc, trace::TraceRawVcs};
12use turbo_tasks_fs::{FileContent, FileSystemPath, glob::Glob};
13use turbopack_core::{
14    asset::Asset,
15    reference_type::{ReferenceType, ReferenceTypeCondition},
16    source::Source,
17    virtual_source::VirtualSource,
18};
19
20#[derive(Debug, Clone, TraceRawVcs, PartialEq, Eq, NonLocalValue, Encode, Decode)]
21pub enum RuleCondition {
22    All(Vec<RuleCondition>),
23    Any(Vec<RuleCondition>),
24    Not(Box<RuleCondition>),
25    True,
26    False,
27    ReferenceType(ReferenceTypeCondition),
28    ResourceIsVirtualSource,
29    ResourcePathEquals(FileSystemPath),
30    ResourcePathHasNoExtension,
31    ResourcePathEndsWith(String),
32    ResourcePathInDirectory(String),
33    ResourcePathInExactDirectory(FileSystemPath),
34    ContentTypeStartsWith(String),
35    ContentTypeEmpty,
36    ResourcePathEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
37    ResourceContentEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
38    /// For paths that are within the same filesystem as the `base`, it need to
39    /// match the relative path from base to resource. This includes `./` or
40    /// `../` prefix. For paths in a different filesystem, it need to match
41    /// the resource path in that filesystem without any prefix. This means
42    /// any glob starting with `./` or `../` will only match paths in the
43    /// project. Globs starting with `**` can match any path.
44    ResourcePathGlob {
45        base: FileSystemPath,
46        #[turbo_tasks(trace_ignore)]
47        glob: ReadRef<Glob>,
48    },
49    ResourceBasePathGlob(#[turbo_tasks(trace_ignore)] ReadRef<Glob>),
50    ResourceQueryContains(String),
51    ResourceQueryEquals(String),
52    ResourceQueryEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
53    ContentTypeGlob(#[turbo_tasks(trace_ignore)] ReadRef<Glob>),
54    ContentTypeEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
55}
56
57impl RuleCondition {
58    pub fn all(conditions: Vec<RuleCondition>) -> RuleCondition {
59        RuleCondition::All(conditions)
60    }
61
62    pub fn any(conditions: Vec<RuleCondition>) -> RuleCondition {
63        RuleCondition::Any(conditions)
64    }
65
66    #[allow(clippy::should_implement_trait)]
67    pub fn not(condition: RuleCondition) -> RuleCondition {
68        RuleCondition::Not(Box::new(condition))
69    }
70
71    /// Slightly optimize a `RuleCondition` by flattening nested `Any`, `All`, or `Not` variants.
72    ///
73    /// Does not apply general re-ordering of rules (which may also be a valid optimization using a
74    /// cost heuristic), but does flatten constant `True` and `False` conditions, potentially
75    /// skipping other rules.
76    pub fn flatten(&mut self) {
77        match self {
78            RuleCondition::Any(conds) => {
79                // fast path: flatten children in-place and avoid constructing an additional vec
80                let mut needs_flattening = false;
81                for c in conds.iter_mut() {
82                    c.flatten();
83                    if *c == RuleCondition::True {
84                        // short-circuit: all conditions are side-effect free
85                        *self = RuleCondition::True;
86                        return;
87                    }
88                    needs_flattening = needs_flattening
89                        || matches!(c, RuleCondition::Any(_) | RuleCondition::False);
90                }
91
92                if needs_flattening {
93                    *conds = take(conds)
94                        .into_iter()
95                        .flat_map(|c| match c {
96                            RuleCondition::Any(nested) => {
97                                debug_assert!(!nested.is_empty(), "empty Any should be False");
98                                Either::Left(nested.into_iter())
99                            }
100                            RuleCondition::False => Either::Right(Either::Left(iter::empty())),
101                            c => Either::Right(Either::Right(iter::once(c))),
102                        })
103                        .collect();
104                }
105
106                match conds.len() {
107                    0 => *self = RuleCondition::False,
108                    1 => *self = take(conds).into_iter().next().unwrap(),
109                    _ => {}
110                }
111            }
112            RuleCondition::All(conds) => {
113                // fast path: flatten children in-place and avoid constructing an additional vec
114                let mut needs_flattening = false;
115                for c in conds.iter_mut() {
116                    c.flatten();
117                    if *c == RuleCondition::False {
118                        // short-circuit: all conditions are side-effect free
119                        *self = RuleCondition::False;
120                        return;
121                    }
122                    needs_flattening = needs_flattening
123                        || matches!(c, RuleCondition::All(_) | RuleCondition::True);
124                }
125
126                if needs_flattening {
127                    *conds = take(conds)
128                        .into_iter()
129                        .flat_map(|c| match c {
130                            RuleCondition::All(nested) => {
131                                debug_assert!(!nested.is_empty(), "empty All should be True");
132                                Either::Left(nested.into_iter())
133                            }
134                            RuleCondition::True => Either::Right(Either::Left(iter::empty())),
135                            c => Either::Right(Either::Right(iter::once(c))),
136                        })
137                        .collect();
138                }
139
140                match conds.len() {
141                    0 => *self = RuleCondition::True,
142                    1 => *self = take(conds).into_iter().next().unwrap(),
143                    _ => {}
144                }
145            }
146            RuleCondition::Not(cond) => {
147                match &mut **cond {
148                    // nested `Not`s negate each other
149                    RuleCondition::Not(inner) => {
150                        let inner = &mut **inner;
151                        inner.flatten();
152                        // Use `replace` with a dummy condition instead of `take` since
153                        // `RuleCondition` doesn't implement `Default`.
154                        *self = replace(inner, RuleCondition::False)
155                    }
156                    RuleCondition::True => *self = RuleCondition::False,
157                    RuleCondition::False => *self = RuleCondition::True,
158                    other => other.flatten(),
159                }
160            }
161            _ => {}
162        }
163    }
164
165    pub async fn matches(
166        &self,
167        source: ResolvedVc<Box<dyn Source>>,
168        path: &FileSystemPath,
169        reference_type: &ReferenceType,
170    ) -> Result<bool> {
171        enum Op<'a> {
172            All(&'a [RuleCondition]), // Remaining conditions in an All
173            Any(&'a [RuleCondition]), // Remaining conditions in an Any
174            Not,                      // Inverts the previous condition
175        }
176
177        // Evaluates the condition returning the result and possibly pushing additional operations
178        // onto the stack as a kind of continuation.
179        async fn process_condition<'a, const SZ: usize>(
180            source: ResolvedVc<Box<dyn Source + 'static>>,
181            path: &FileSystemPath,
182            reference_type: &ReferenceType,
183            stack: &mut SmallVec<[Op<'a>; SZ]>,
184            mut cond: &'a RuleCondition,
185        ) -> Result<bool, anyhow::Error> {
186            // Use a loop to avoid recursion and unnecessary stack operations.
187            loop {
188                match cond {
189                    RuleCondition::All(conditions) => {
190                        if conditions.is_empty() {
191                            return Ok(true);
192                        } else {
193                            if conditions.len() > 1 {
194                                stack.push(Op::All(&conditions[1..]));
195                            }
196                            cond = &conditions[0];
197                            // jump directly to the next condition, no need to deal with
198                            // the stack.
199                            continue;
200                        }
201                    }
202                    RuleCondition::Any(conditions) => {
203                        if conditions.is_empty() {
204                            return Ok(false);
205                        } else {
206                            if conditions.len() > 1 {
207                                stack.push(Op::Any(&conditions[1..]));
208                            }
209                            cond = &conditions[0];
210                            continue;
211                        }
212                    }
213                    RuleCondition::Not(inner) => {
214                        stack.push(Op::Not);
215                        cond = inner.as_ref();
216                        continue;
217                    }
218                    RuleCondition::True => {
219                        return Ok(true);
220                    }
221                    RuleCondition::False => {
222                        return Ok(false);
223                    }
224                    RuleCondition::ReferenceType(condition_ty) => {
225                        return Ok(condition_ty.includes(reference_type));
226                    }
227                    RuleCondition::ResourceIsVirtualSource => {
228                        return Ok(ResolvedVc::try_downcast_type::<VirtualSource>(source).is_some());
229                    }
230                    RuleCondition::ResourcePathEquals(other) => {
231                        return Ok(path == other);
232                    }
233                    RuleCondition::ResourcePathEndsWith(end) => {
234                        return Ok(path.path.ends_with(end));
235                    }
236                    RuleCondition::ResourcePathHasNoExtension => {
237                        return Ok(if let Some(i) = path.path.rfind('.') {
238                            if let Some(j) = path.path.rfind('/') {
239                                j > i
240                            } else {
241                                false
242                            }
243                        } else {
244                            true
245                        });
246                    }
247                    RuleCondition::ResourcePathInDirectory(dir) => {
248                        return Ok(path.path.starts_with(&format!("{dir}/"))
249                            || path.path.contains(&format!("/{dir}/")));
250                    }
251                    RuleCondition::ResourcePathInExactDirectory(parent_path) => {
252                        return Ok(path.is_inside_ref(parent_path));
253                    }
254                    RuleCondition::ContentTypeStartsWith(start) => {
255                        let content_type = &source.ident().await?.content_type;
256                        return Ok(content_type
257                            .as_ref()
258                            .is_some_and(|ct| ct.starts_with(start.as_str())));
259                    }
260                    RuleCondition::ContentTypeEmpty => {
261                        return Ok(source.ident().await?.content_type.is_none());
262                    }
263                    RuleCondition::ResourcePathGlob { glob, base } => {
264                        return Ok(if let Some(rel_path) = base.get_relative_path_to(path) {
265                            if rel_path.starts_with("../") {
266                                glob.matches(&rel_path)
267                            } else {
268                                glob.matches(&format!("./{rel_path}"))
269                            }
270                        } else {
271                            glob.matches(&path.path)
272                        });
273                    }
274                    RuleCondition::ResourceBasePathGlob(glob) => {
275                        let basename = path
276                            .path
277                            .rsplit_once('/')
278                            .map_or(path.path.as_str(), |(_, b)| b);
279                        return Ok(glob.matches(basename));
280                    }
281                    RuleCondition::ResourcePathEsRegex(regex) => {
282                        return Ok(regex.is_match(&path.path));
283                    }
284                    RuleCondition::ResourceContentEsRegex(regex) => {
285                        let content = source.content().file_content().await?;
286                        match &*content {
287                            FileContent::Content(file_content) => {
288                                return Ok(regex.is_match(&file_content.content().to_str()?));
289                            }
290                            FileContent::NotFound => return Ok(false),
291                        }
292                    }
293                    RuleCondition::ResourceQueryContains(query) => {
294                        let ident = source.ident().await?;
295                        return Ok(ident.query.contains(query));
296                    }
297                    RuleCondition::ResourceQueryEquals(query) => {
298                        let ident = source.ident().await?;
299                        return Ok(ident.query == *query);
300                    }
301                    RuleCondition::ResourceQueryEsRegex(regex) => {
302                        let ident = source.ident().await?;
303                        return Ok(regex.is_match(&ident.query));
304                    }
305                    RuleCondition::ContentTypeGlob(glob) => {
306                        let ident = source.ident().await?;
307                        return Ok(ident
308                            .content_type
309                            .as_ref()
310                            .is_some_and(|ct| glob.matches(ct)));
311                    }
312                    RuleCondition::ContentTypeEsRegex(regex) => {
313                        let ident = source.ident().await?;
314                        return Ok(ident
315                            .content_type
316                            .as_ref()
317                            .is_some_and(|ct| regex.is_match(ct)));
318                    }
319                }
320            }
321        }
322        // Allocate a small inline stack to avoid heap allocations in the common case where
323        // conditions are not deeply stacked.  Additionally we take care to avoid stack
324        // operations unless strictly necessary.
325        const EXPECTED_SIZE: usize = 8;
326        let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
327        let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
328        while let Some(op) = stack.pop() {
329            match op {
330                Op::All(remaining) => {
331                    // Previous was true, keep going
332                    if result {
333                        if remaining.len() > 1 {
334                            stack.push(Op::All(&remaining[1..]));
335                        }
336                        result = process_condition(
337                            source,
338                            path,
339                            reference_type,
340                            &mut stack,
341                            &remaining[0],
342                        )
343                        .await?;
344                    }
345                }
346                Op::Any(remaining) => {
347                    // Previous was false, keep going
348                    if !result {
349                        if remaining.len() > 1 {
350                            stack.push(Op::Any(&remaining[1..]));
351                        }
352                        // If the stack didn't change, we can loop inline, but we would still need
353                        // to pop the item.  This might be faster since we would avoid the `match`
354                        // but overall, that is quite minor for an enum with 3 cases.
355                        result = process_condition(
356                            source,
357                            path,
358                            reference_type,
359                            &mut stack,
360                            &remaining[0],
361                        )
362                        .await?;
363                    }
364                }
365                Op::Not => {
366                    result = !result;
367                }
368            }
369        }
370        Ok(result)
371    }
372}
373
374#[cfg(test)]
375pub mod tests {
376    use turbo_tasks::Vc;
377    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
378    use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
379    use turbopack_core::{asset::AssetContent, file_source::FileSource};
380
381    use super::*;
382
383    #[test]
384    fn flatten_any_with_single_child_collapses() {
385        let mut rc = RuleCondition::Any(vec![RuleCondition::True]);
386        rc.flatten();
387        assert_eq!(rc, RuleCondition::True);
388
389        let mut rc = RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty]);
390        rc.flatten();
391        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
392    }
393
394    #[test]
395    fn flatten_any_nested_and_false() {
396        let mut rc = RuleCondition::Any(vec![
397            RuleCondition::False,
398            RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty, RuleCondition::False]),
399        ]);
400        rc.flatten();
401        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
402    }
403
404    #[test]
405    fn flatten_any_short_circuits_on_true() {
406        let mut rc = RuleCondition::Any(vec![
407            RuleCondition::False,
408            RuleCondition::True,
409            RuleCondition::ContentTypeEmpty,
410        ]);
411        rc.flatten();
412        assert_eq!(rc, RuleCondition::True);
413    }
414
415    #[test]
416    fn flatten_any_empty_becomes_false() {
417        let mut rc = RuleCondition::Any(vec![]);
418        rc.flatten();
419        assert_eq!(rc, RuleCondition::False);
420    }
421
422    #[test]
423    fn flatten_all_with_single_child_collapses() {
424        let mut rc = RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]);
425        rc.flatten();
426        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
427
428        let mut rc = RuleCondition::All(vec![RuleCondition::True]);
429        rc.flatten();
430        assert_eq!(rc, RuleCondition::True);
431    }
432
433    #[test]
434    fn flatten_all_nested_and_true() {
435        let mut rc = RuleCondition::All(vec![
436            RuleCondition::True,
437            RuleCondition::All(vec![RuleCondition::ContentTypeEmpty, RuleCondition::True]),
438        ]);
439        rc.flatten();
440        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
441    }
442
443    #[test]
444    fn flatten_all_short_circuits_on_false() {
445        let mut rc = RuleCondition::All(vec![
446            RuleCondition::True,
447            RuleCondition::False,
448            RuleCondition::ContentTypeEmpty,
449        ]);
450        rc.flatten();
451        assert_eq!(rc, RuleCondition::False);
452    }
453
454    #[test]
455    fn flatten_all_empty_becomes_true() {
456        let mut rc = RuleCondition::All(vec![]);
457        rc.flatten();
458        assert_eq!(rc, RuleCondition::True);
459    }
460
461    #[test]
462    fn flatten_not_of_not() {
463        let mut rc = RuleCondition::Not(Box::new(RuleCondition::Not(Box::new(
464            RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]),
465        ))));
466        rc.flatten();
467        assert_eq!(rc, RuleCondition::ContentTypeEmpty);
468    }
469
470    #[test]
471    fn flatten_not_constants() {
472        let mut rc = RuleCondition::Not(Box::new(RuleCondition::True));
473        rc.flatten();
474        assert_eq!(rc, RuleCondition::False);
475
476        let mut rc = RuleCondition::Not(Box::new(RuleCondition::False));
477        rc.flatten();
478        assert_eq!(rc, RuleCondition::True);
479    }
480
481    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
482    async fn test_rule_condition_leaves() {
483        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
484            BackendOptions::default(),
485            noop_backing_storage(),
486        ));
487        tt.run_once(async { run_leaves_test_operation().read_strongly_consistent().await })
488            .await
489            .unwrap();
490    }
491
492    #[turbo_tasks::function(operation, root)]
493    pub async fn run_leaves_test_operation() -> Result<()> {
494        let fs = VirtualFileSystem::new();
495        let virtual_path = fs.root().await?.join("foo.js")?;
496        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
497            virtual_path.clone(),
498            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
499        ))
500        .to_resolved()
501        .await?;
502
503        let non_virtual_path = fs.root().await?.join("bar.js")?;
504        let non_virtual_source =
505            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
506                .to_resolved()
507                .await?;
508
509        {
510            let condition = RuleCondition::ReferenceType(ReferenceTypeCondition::Runtime);
511            assert!(
512                condition
513                    .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
514                    .await
515                    .unwrap()
516            );
517            assert!(
518                !condition
519                    .matches(
520                        non_virtual_source,
521                        &non_virtual_path,
522                        &ReferenceType::Css(
523                            turbopack_core::reference_type::CssReferenceSubType::Compose
524                        )
525                    )
526                    .await
527                    .unwrap()
528            );
529        }
530
531        {
532            let condition = RuleCondition::ResourceIsVirtualSource;
533            assert!(
534                condition
535                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
536                    .await
537                    .unwrap()
538            );
539            assert!(
540                !condition
541                    .matches(
542                        non_virtual_source,
543                        &non_virtual_path,
544                        &ReferenceType::Undefined
545                    )
546                    .await
547                    .unwrap()
548            );
549        }
550        {
551            let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
552            assert!(
553                condition
554                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
555                    .await
556                    .unwrap()
557            );
558            assert!(
559                !condition
560                    .matches(
561                        non_virtual_source,
562                        &non_virtual_path,
563                        &ReferenceType::Undefined
564                    )
565                    .await
566                    .unwrap()
567            );
568        }
569        {
570            let condition = RuleCondition::ResourcePathHasNoExtension;
571            assert!(
572                condition
573                    .matches(
574                        virtual_source,
575                        &fs.root().await?.join("foo")?,
576                        &ReferenceType::Undefined
577                    )
578                    .await
579                    .unwrap()
580            );
581            assert!(
582                !condition
583                    .matches(
584                        non_virtual_source,
585                        &non_virtual_path,
586                        &ReferenceType::Undefined
587                    )
588                    .await
589                    .unwrap()
590            );
591        }
592        {
593            let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
594            assert!(
595                condition
596                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
597                    .await
598                    .unwrap()
599            );
600            assert!(
601                !condition
602                    .matches(
603                        non_virtual_source,
604                        &non_virtual_path,
605                        &ReferenceType::Undefined
606                    )
607                    .await
608                    .unwrap()
609            );
610        }
611        anyhow::Ok(())
612    }
613
614    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
615    async fn test_rule_condition_tree() {
616        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
617            BackendOptions::default(),
618            noop_backing_storage(),
619        ));
620        tt.run_once(async {
621            run_rule_condition_tree_test_operation()
622                .read_strongly_consistent()
623                .await
624        })
625        .await
626        .unwrap();
627    }
628
629    #[turbo_tasks::function(operation, root)]
630    pub async fn run_rule_condition_tree_test_operation() -> Result<()> {
631        let fs = VirtualFileSystem::new();
632        let virtual_path = fs.root().await?.join("foo.js")?;
633        let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
634            virtual_path.clone(),
635            AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
636        ))
637        .to_resolved()
638        .await?;
639
640        let non_virtual_path = fs.root().await?.join("bar.js")?;
641        let non_virtual_source =
642            Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
643                .to_resolved()
644                .await?;
645
646        {
647            // not
648            let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
649            assert!(
650                !condition
651                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
652                    .await
653                    .unwrap()
654            );
655            assert!(
656                condition
657                    .matches(
658                        non_virtual_source,
659                        &non_virtual_path,
660                        &ReferenceType::Undefined
661                    )
662                    .await
663                    .unwrap()
664            );
665        }
666        {
667            // any
668            // Only one of the conditions matches our virtual source
669            let condition = RuleCondition::any(vec![
670                RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
671                RuleCondition::ResourceIsVirtualSource,
672                RuleCondition::ResourcePathHasNoExtension,
673            ]);
674            assert!(
675                condition
676                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
677                    .await
678                    .unwrap()
679            );
680            assert!(
681                !condition
682                    .matches(
683                        non_virtual_source,
684                        &non_virtual_path,
685                        &ReferenceType::Undefined
686                    )
687                    .await
688                    .unwrap()
689            );
690        }
691        {
692            // all
693            // Only one of the conditions matches our virtual source
694            let condition = RuleCondition::all(vec![
695                RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
696                RuleCondition::ResourceIsVirtualSource,
697                RuleCondition::ResourcePathEquals(virtual_path.clone()),
698            ]);
699            assert!(
700                condition
701                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
702                    .await
703                    .unwrap()
704            );
705            assert!(
706                !condition
707                    .matches(
708                        non_virtual_source,
709                        &non_virtual_path,
710                        &ReferenceType::Undefined
711                    )
712                    .await
713                    .unwrap()
714            );
715        }
716        {
717            // bigger tree
718
719            // Build a simple tree to cover our various composite conditions
720            let condition = RuleCondition::all(vec![
721                RuleCondition::ResourceIsVirtualSource,
722                RuleCondition::ResourcePathEquals(virtual_path.clone()),
723                RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
724                RuleCondition::Any(vec![
725                    RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
726                    RuleCondition::ContentTypeEmpty,
727                ]),
728            ]);
729            assert!(
730                condition
731                    .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
732                    .await
733                    .unwrap()
734            );
735            assert!(
736                !condition
737                    .matches(
738                        non_virtual_source,
739                        &non_virtual_path,
740                        &ReferenceType::Undefined
741                    )
742                    .await
743                    .unwrap()
744            );
745        }
746        anyhow::Ok(())
747    }
748}