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 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 pub fn flatten(&mut self) {
77 match self {
78 RuleCondition::Any(conds) => {
79 let mut needs_flattening = false;
81 for c in conds.iter_mut() {
82 c.flatten();
83 if *c == RuleCondition::True {
84 *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 let mut needs_flattening = false;
115 for c in conds.iter_mut() {
116 c.flatten();
117 if *c == RuleCondition::False {
118 *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 RuleCondition::Not(inner) => {
150 let inner = &mut **inner;
151 inner.flatten();
152 *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]), Any(&'a [RuleCondition]), Not, }
176
177 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 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 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 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 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 if !result {
349 if remaining.len() > 1 {
350 stack.push(Op::Any(&remaining[1..]));
351 }
352 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 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 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 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 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}