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};
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, 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(ReadRef<EsRegex>),
37 ResourceContentEsRegex(ReadRef<EsRegex>),
38 ResourcePathGlob {
45 base: FileSystemPath,
46 glob: ReadRef<Glob>,
47 },
48 ResourceBasePathGlob(ReadRef<Glob>),
49 ResourceQueryContains(String),
50 ResourceQueryEquals(String),
51 ResourceQueryEsRegex(ReadRef<EsRegex>),
52 ContentTypeGlob(ReadRef<Glob>),
53 ContentTypeEsRegex(ReadRef<EsRegex>),
54}
55
56impl RuleCondition {
57 pub fn all(conditions: Vec<RuleCondition>) -> RuleCondition {
58 RuleCondition::All(conditions)
59 }
60
61 pub fn any(conditions: Vec<RuleCondition>) -> RuleCondition {
62 RuleCondition::Any(conditions)
63 }
64
65 #[allow(clippy::should_implement_trait)]
66 pub fn not(condition: RuleCondition) -> RuleCondition {
67 RuleCondition::Not(Box::new(condition))
68 }
69
70 pub fn flatten(&mut self) {
76 match self {
77 RuleCondition::Any(conds) => {
78 let mut needs_flattening = false;
80 for c in conds.iter_mut() {
81 c.flatten();
82 if *c == RuleCondition::True {
83 *self = RuleCondition::True;
85 return;
86 }
87 needs_flattening = needs_flattening
88 || matches!(c, RuleCondition::Any(_) | RuleCondition::False);
89 }
90
91 if needs_flattening {
92 *conds = take(conds)
93 .into_iter()
94 .flat_map(|c| match c {
95 RuleCondition::Any(nested) => {
96 debug_assert!(!nested.is_empty(), "empty Any should be False");
97 Either::Left(nested.into_iter())
98 }
99 RuleCondition::False => Either::Right(Either::Left(iter::empty())),
100 c => Either::Right(Either::Right(iter::once(c))),
101 })
102 .collect();
103 }
104
105 match conds.len() {
106 0 => *self = RuleCondition::False,
107 1 => *self = take(conds).into_iter().next().unwrap(),
108 _ => {}
109 }
110 }
111 RuleCondition::All(conds) => {
112 let mut needs_flattening = false;
114 for c in conds.iter_mut() {
115 c.flatten();
116 if *c == RuleCondition::False {
117 *self = RuleCondition::False;
119 return;
120 }
121 needs_flattening = needs_flattening
122 || matches!(c, RuleCondition::All(_) | RuleCondition::True);
123 }
124
125 if needs_flattening {
126 *conds = take(conds)
127 .into_iter()
128 .flat_map(|c| match c {
129 RuleCondition::All(nested) => {
130 debug_assert!(!nested.is_empty(), "empty All should be True");
131 Either::Left(nested.into_iter())
132 }
133 RuleCondition::True => Either::Right(Either::Left(iter::empty())),
134 c => Either::Right(Either::Right(iter::once(c))),
135 })
136 .collect();
137 }
138
139 match conds.len() {
140 0 => *self = RuleCondition::True,
141 1 => *self = take(conds).into_iter().next().unwrap(),
142 _ => {}
143 }
144 }
145 RuleCondition::Not(cond) => {
146 match &mut **cond {
147 RuleCondition::Not(inner) => {
149 let inner = &mut **inner;
150 inner.flatten();
151 *self = replace(inner, RuleCondition::False)
154 }
155 RuleCondition::True => *self = RuleCondition::False,
156 RuleCondition::False => *self = RuleCondition::True,
157 other => other.flatten(),
158 }
159 }
160 _ => {}
161 }
162 }
163
164 pub async fn matches(
165 &self,
166 source: ResolvedVc<Box<dyn Source>>,
167 path: &FileSystemPath,
168 reference_type: &ReferenceType,
169 ) -> Result<bool> {
170 enum Op<'a> {
171 All(&'a [RuleCondition]), Any(&'a [RuleCondition]), Not, }
175
176 async fn process_condition<'a, const SZ: usize>(
179 source: ResolvedVc<Box<dyn Source + 'static>>,
180 path: &FileSystemPath,
181 reference_type: &ReferenceType,
182 stack: &mut SmallVec<[Op<'a>; SZ]>,
183 mut cond: &'a RuleCondition,
184 ) -> Result<bool, anyhow::Error> {
185 loop {
187 match cond {
188 RuleCondition::All(conditions) => {
189 if conditions.is_empty() {
190 return Ok(true);
191 } else {
192 if conditions.len() > 1 {
193 stack.push(Op::All(&conditions[1..]));
194 }
195 cond = &conditions[0];
196 continue;
199 }
200 }
201 RuleCondition::Any(conditions) => {
202 if conditions.is_empty() {
203 return Ok(false);
204 } else {
205 if conditions.len() > 1 {
206 stack.push(Op::Any(&conditions[1..]));
207 }
208 cond = &conditions[0];
209 continue;
210 }
211 }
212 RuleCondition::Not(inner) => {
213 stack.push(Op::Not);
214 cond = inner.as_ref();
215 continue;
216 }
217 RuleCondition::True => {
218 return Ok(true);
219 }
220 RuleCondition::False => {
221 return Ok(false);
222 }
223 RuleCondition::ReferenceType(condition_ty) => {
224 return Ok(condition_ty.includes(reference_type));
225 }
226 RuleCondition::ResourceIsVirtualSource => {
227 return Ok(ResolvedVc::try_downcast_type::<VirtualSource>(source).is_some());
228 }
229 RuleCondition::ResourcePathEquals(other) => {
230 return Ok(path == other);
231 }
232 RuleCondition::ResourcePathEndsWith(end) => {
233 return Ok(path.path.ends_with(end));
234 }
235 RuleCondition::ResourcePathHasNoExtension => {
236 return Ok(if let Some(i) = path.path.rfind('.') {
237 if let Some(j) = path.path.rfind('/') {
238 j > i
239 } else {
240 false
241 }
242 } else {
243 true
244 });
245 }
246 RuleCondition::ResourcePathInDirectory(dir) => {
247 return Ok(path.path.starts_with(&format!("{dir}/"))
248 || path.path.contains(&format!("/{dir}/")));
249 }
250 RuleCondition::ResourcePathInExactDirectory(parent_path) => {
251 return Ok(path.is_inside_ref(parent_path));
252 }
253 RuleCondition::ContentTypeStartsWith(start) => {
254 let content_type = &source.ident().await?.content_type;
255 return Ok(content_type
256 .as_ref()
257 .is_some_and(|ct| ct.starts_with(start.as_str())));
258 }
259 RuleCondition::ContentTypeEmpty => {
260 return Ok(source.ident().await?.content_type.is_none());
261 }
262 RuleCondition::ResourcePathGlob { glob, base } => {
263 return Ok(if let Some(rel_path) = base.get_relative_request_to(path) {
264 glob.matches(&rel_path)
265 } else {
266 glob.matches(&path.path)
267 });
268 }
269 RuleCondition::ResourceBasePathGlob(glob) => {
270 let basename = path
271 .path
272 .rsplit_once('/')
273 .map_or(path.path.as_str(), |(_, b)| b);
274 return Ok(glob.matches(basename));
275 }
276 RuleCondition::ResourcePathEsRegex(regex) => {
277 return Ok(regex.is_match(&path.path));
278 }
279 RuleCondition::ResourceContentEsRegex(regex) => {
280 let content = source.content().file_content().await?;
281 match &*content {
282 FileContent::Content(file_content) => {
283 return Ok(regex.is_match(&file_content.content().to_str()?));
284 }
285 FileContent::NotFound => return Ok(false),
286 }
287 }
288 RuleCondition::ResourceQueryContains(query) => {
289 let ident = source.ident().await?;
290 return Ok(ident.query.contains(query));
291 }
292 RuleCondition::ResourceQueryEquals(query) => {
293 let ident = source.ident().await?;
294 return Ok(ident.query == *query);
295 }
296 RuleCondition::ResourceQueryEsRegex(regex) => {
297 let ident = source.ident().await?;
298 return Ok(regex.is_match(&ident.query));
299 }
300 RuleCondition::ContentTypeGlob(glob) => {
301 let ident = source.ident().await?;
302 return Ok(ident
303 .content_type
304 .as_ref()
305 .is_some_and(|ct| glob.matches(ct)));
306 }
307 RuleCondition::ContentTypeEsRegex(regex) => {
308 let ident = source.ident().await?;
309 return Ok(ident
310 .content_type
311 .as_ref()
312 .is_some_and(|ct| regex.is_match(ct)));
313 }
314 }
315 }
316 }
317 const EXPECTED_SIZE: usize = 8;
321 let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
322 let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
323 while let Some(op) = stack.pop() {
324 match op {
325 Op::All(remaining) => {
326 if result {
328 if remaining.len() > 1 {
329 stack.push(Op::All(&remaining[1..]));
330 }
331 result = process_condition(
332 source,
333 path,
334 reference_type,
335 &mut stack,
336 &remaining[0],
337 )
338 .await?;
339 }
340 }
341 Op::Any(remaining) => {
342 if !result {
344 if remaining.len() > 1 {
345 stack.push(Op::Any(&remaining[1..]));
346 }
347 result = process_condition(
351 source,
352 path,
353 reference_type,
354 &mut stack,
355 &remaining[0],
356 )
357 .await?;
358 }
359 }
360 Op::Not => {
361 result = !result;
362 }
363 }
364 }
365 Ok(result)
366 }
367}
368
369#[cfg(test)]
370pub mod tests {
371 use turbo_tasks::Vc;
372 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
373 use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
374 use turbopack_core::{asset::AssetContent, file_source::FileSource};
375
376 use super::*;
377
378 #[test]
379 fn flatten_any_with_single_child_collapses() {
380 let mut rc = RuleCondition::Any(vec![RuleCondition::True]);
381 rc.flatten();
382 assert_eq!(rc, RuleCondition::True);
383
384 let mut rc = RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty]);
385 rc.flatten();
386 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
387 }
388
389 #[test]
390 fn flatten_any_nested_and_false() {
391 let mut rc = RuleCondition::Any(vec![
392 RuleCondition::False,
393 RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty, RuleCondition::False]),
394 ]);
395 rc.flatten();
396 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
397 }
398
399 #[test]
400 fn flatten_any_short_circuits_on_true() {
401 let mut rc = RuleCondition::Any(vec![
402 RuleCondition::False,
403 RuleCondition::True,
404 RuleCondition::ContentTypeEmpty,
405 ]);
406 rc.flatten();
407 assert_eq!(rc, RuleCondition::True);
408 }
409
410 #[test]
411 fn flatten_any_empty_becomes_false() {
412 let mut rc = RuleCondition::Any(vec![]);
413 rc.flatten();
414 assert_eq!(rc, RuleCondition::False);
415 }
416
417 #[test]
418 fn flatten_all_with_single_child_collapses() {
419 let mut rc = RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]);
420 rc.flatten();
421 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
422
423 let mut rc = RuleCondition::All(vec![RuleCondition::True]);
424 rc.flatten();
425 assert_eq!(rc, RuleCondition::True);
426 }
427
428 #[test]
429 fn flatten_all_nested_and_true() {
430 let mut rc = RuleCondition::All(vec![
431 RuleCondition::True,
432 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty, RuleCondition::True]),
433 ]);
434 rc.flatten();
435 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
436 }
437
438 #[test]
439 fn flatten_all_short_circuits_on_false() {
440 let mut rc = RuleCondition::All(vec![
441 RuleCondition::True,
442 RuleCondition::False,
443 RuleCondition::ContentTypeEmpty,
444 ]);
445 rc.flatten();
446 assert_eq!(rc, RuleCondition::False);
447 }
448
449 #[test]
450 fn flatten_all_empty_becomes_true() {
451 let mut rc = RuleCondition::All(vec![]);
452 rc.flatten();
453 assert_eq!(rc, RuleCondition::True);
454 }
455
456 #[test]
457 fn flatten_not_of_not() {
458 let mut rc = RuleCondition::Not(Box::new(RuleCondition::Not(Box::new(
459 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]),
460 ))));
461 rc.flatten();
462 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
463 }
464
465 #[test]
466 fn flatten_not_constants() {
467 let mut rc = RuleCondition::Not(Box::new(RuleCondition::True));
468 rc.flatten();
469 assert_eq!(rc, RuleCondition::False);
470
471 let mut rc = RuleCondition::Not(Box::new(RuleCondition::False));
472 rc.flatten();
473 assert_eq!(rc, RuleCondition::True);
474 }
475
476 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
477 async fn test_rule_condition_leaves() {
478 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
479 BackendOptions::default(),
480 noop_backing_storage(),
481 ));
482 tt.run_once(async { run_leaves_test_operation().read_strongly_consistent().await })
483 .await
484 .unwrap();
485 }
486
487 #[turbo_tasks::function(operation, root)]
488 pub async fn run_leaves_test_operation() -> Result<()> {
489 let fs = VirtualFileSystem::new();
490 let virtual_path = fs.root().await?.join("foo.js")?;
491 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
492 virtual_path.clone(),
493 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
494 ))
495 .to_resolved()
496 .await?;
497
498 let non_virtual_path = fs.root().await?.join("bar.js")?;
499 let non_virtual_source =
500 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
501 .to_resolved()
502 .await?;
503
504 {
505 let condition = RuleCondition::ReferenceType(ReferenceTypeCondition::Runtime);
506 assert!(
507 condition
508 .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
509 .await
510 .unwrap()
511 );
512 assert!(
513 !condition
514 .matches(
515 non_virtual_source,
516 &non_virtual_path,
517 &ReferenceType::Css(
518 turbopack_core::reference_type::CssReferenceSubType::Compose
519 )
520 )
521 .await
522 .unwrap()
523 );
524 }
525
526 {
527 let condition = RuleCondition::ResourceIsVirtualSource;
528 assert!(
529 condition
530 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
531 .await
532 .unwrap()
533 );
534 assert!(
535 !condition
536 .matches(
537 non_virtual_source,
538 &non_virtual_path,
539 &ReferenceType::Undefined
540 )
541 .await
542 .unwrap()
543 );
544 }
545 {
546 let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
547 assert!(
548 condition
549 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
550 .await
551 .unwrap()
552 );
553 assert!(
554 !condition
555 .matches(
556 non_virtual_source,
557 &non_virtual_path,
558 &ReferenceType::Undefined
559 )
560 .await
561 .unwrap()
562 );
563 }
564 {
565 let condition = RuleCondition::ResourcePathHasNoExtension;
566 assert!(
567 condition
568 .matches(
569 virtual_source,
570 &fs.root().await?.join("foo")?,
571 &ReferenceType::Undefined
572 )
573 .await
574 .unwrap()
575 );
576 assert!(
577 !condition
578 .matches(
579 non_virtual_source,
580 &non_virtual_path,
581 &ReferenceType::Undefined
582 )
583 .await
584 .unwrap()
585 );
586 }
587 {
588 let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
589 assert!(
590 condition
591 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
592 .await
593 .unwrap()
594 );
595 assert!(
596 !condition
597 .matches(
598 non_virtual_source,
599 &non_virtual_path,
600 &ReferenceType::Undefined
601 )
602 .await
603 .unwrap()
604 );
605 }
606 anyhow::Ok(())
607 }
608
609 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
610 async fn test_rule_condition_tree() {
611 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
612 BackendOptions::default(),
613 noop_backing_storage(),
614 ));
615 tt.run_once(async {
616 run_rule_condition_tree_test_operation()
617 .read_strongly_consistent()
618 .await
619 })
620 .await
621 .unwrap();
622 }
623
624 #[turbo_tasks::function(operation, root)]
625 pub async fn run_rule_condition_tree_test_operation() -> Result<()> {
626 let fs = VirtualFileSystem::new();
627 let virtual_path = fs.root().await?.join("foo.js")?;
628 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
629 virtual_path.clone(),
630 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
631 ))
632 .to_resolved()
633 .await?;
634
635 let non_virtual_path = fs.root().await?.join("bar.js")?;
636 let non_virtual_source =
637 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
638 .to_resolved()
639 .await?;
640
641 {
642 let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
644 assert!(
645 !condition
646 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
647 .await
648 .unwrap()
649 );
650 assert!(
651 condition
652 .matches(
653 non_virtual_source,
654 &non_virtual_path,
655 &ReferenceType::Undefined
656 )
657 .await
658 .unwrap()
659 );
660 }
661 {
662 let condition = RuleCondition::any(vec![
665 RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
666 RuleCondition::ResourceIsVirtualSource,
667 RuleCondition::ResourcePathHasNoExtension,
668 ]);
669 assert!(
670 condition
671 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
672 .await
673 .unwrap()
674 );
675 assert!(
676 !condition
677 .matches(
678 non_virtual_source,
679 &non_virtual_path,
680 &ReferenceType::Undefined
681 )
682 .await
683 .unwrap()
684 );
685 }
686 {
687 let condition = RuleCondition::all(vec![
690 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
691 RuleCondition::ResourceIsVirtualSource,
692 RuleCondition::ResourcePathEquals(virtual_path.clone()),
693 ]);
694 assert!(
695 condition
696 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
697 .await
698 .unwrap()
699 );
700 assert!(
701 !condition
702 .matches(
703 non_virtual_source,
704 &non_virtual_path,
705 &ReferenceType::Undefined
706 )
707 .await
708 .unwrap()
709 );
710 }
711 {
712 let condition = RuleCondition::all(vec![
716 RuleCondition::ResourceIsVirtualSource,
717 RuleCondition::ResourcePathEquals(virtual_path.clone()),
718 RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
719 RuleCondition::Any(vec![
720 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
721 RuleCondition::ContentTypeEmpty,
722 ]),
723 ]);
724 assert!(
725 condition
726 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
727 .await
728 .unwrap()
729 );
730 assert!(
731 !condition
732 .matches(
733 non_virtual_source,
734 &non_virtual_path,
735 &ReferenceType::Undefined
736 )
737 .await
738 .unwrap()
739 );
740 }
741 anyhow::Ok(())
742 }
743}