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 glob.matches(&rel_path)
266 } else {
267 glob.matches(&path.path)
268 });
269 }
270 RuleCondition::ResourceBasePathGlob(glob) => {
271 let basename = path
272 .path
273 .rsplit_once('/')
274 .map_or(path.path.as_str(), |(_, b)| b);
275 return Ok(glob.matches(basename));
276 }
277 RuleCondition::ResourcePathEsRegex(regex) => {
278 return Ok(regex.is_match(&path.path));
279 }
280 RuleCondition::ResourceContentEsRegex(regex) => {
281 let content = source.content().file_content().await?;
282 match &*content {
283 FileContent::Content(file_content) => {
284 return Ok(regex.is_match(&file_content.content().to_str()?));
285 }
286 FileContent::NotFound => return Ok(false),
287 }
288 }
289 RuleCondition::ResourceQueryContains(query) => {
290 let ident = source.ident().await?;
291 return Ok(ident.query.contains(query));
292 }
293 RuleCondition::ResourceQueryEquals(query) => {
294 let ident = source.ident().await?;
295 return Ok(ident.query == *query);
296 }
297 RuleCondition::ResourceQueryEsRegex(regex) => {
298 let ident = source.ident().await?;
299 return Ok(regex.is_match(&ident.query));
300 }
301 RuleCondition::ContentTypeGlob(glob) => {
302 let ident = source.ident().await?;
303 return Ok(ident
304 .content_type
305 .as_ref()
306 .is_some_and(|ct| glob.matches(ct)));
307 }
308 RuleCondition::ContentTypeEsRegex(regex) => {
309 let ident = source.ident().await?;
310 return Ok(ident
311 .content_type
312 .as_ref()
313 .is_some_and(|ct| regex.is_match(ct)));
314 }
315 }
316 }
317 }
318 const EXPECTED_SIZE: usize = 8;
322 let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
323 let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
324 while let Some(op) = stack.pop() {
325 match op {
326 Op::All(remaining) => {
327 if result {
329 if remaining.len() > 1 {
330 stack.push(Op::All(&remaining[1..]));
331 }
332 result = process_condition(
333 source,
334 path,
335 reference_type,
336 &mut stack,
337 &remaining[0],
338 )
339 .await?;
340 }
341 }
342 Op::Any(remaining) => {
343 if !result {
345 if remaining.len() > 1 {
346 stack.push(Op::Any(&remaining[1..]));
347 }
348 result = process_condition(
352 source,
353 path,
354 reference_type,
355 &mut stack,
356 &remaining[0],
357 )
358 .await?;
359 }
360 }
361 Op::Not => {
362 result = !result;
363 }
364 }
365 }
366 Ok(result)
367 }
368}
369
370#[cfg(test)]
371pub mod tests {
372 use turbo_tasks::Vc;
373 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
374 use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
375 use turbopack_core::{asset::AssetContent, file_source::FileSource};
376
377 use super::*;
378
379 #[test]
380 fn flatten_any_with_single_child_collapses() {
381 let mut rc = RuleCondition::Any(vec![RuleCondition::True]);
382 rc.flatten();
383 assert_eq!(rc, RuleCondition::True);
384
385 let mut rc = RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty]);
386 rc.flatten();
387 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
388 }
389
390 #[test]
391 fn flatten_any_nested_and_false() {
392 let mut rc = RuleCondition::Any(vec![
393 RuleCondition::False,
394 RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty, RuleCondition::False]),
395 ]);
396 rc.flatten();
397 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
398 }
399
400 #[test]
401 fn flatten_any_short_circuits_on_true() {
402 let mut rc = RuleCondition::Any(vec![
403 RuleCondition::False,
404 RuleCondition::True,
405 RuleCondition::ContentTypeEmpty,
406 ]);
407 rc.flatten();
408 assert_eq!(rc, RuleCondition::True);
409 }
410
411 #[test]
412 fn flatten_any_empty_becomes_false() {
413 let mut rc = RuleCondition::Any(vec![]);
414 rc.flatten();
415 assert_eq!(rc, RuleCondition::False);
416 }
417
418 #[test]
419 fn flatten_all_with_single_child_collapses() {
420 let mut rc = RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]);
421 rc.flatten();
422 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
423
424 let mut rc = RuleCondition::All(vec![RuleCondition::True]);
425 rc.flatten();
426 assert_eq!(rc, RuleCondition::True);
427 }
428
429 #[test]
430 fn flatten_all_nested_and_true() {
431 let mut rc = RuleCondition::All(vec![
432 RuleCondition::True,
433 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty, RuleCondition::True]),
434 ]);
435 rc.flatten();
436 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
437 }
438
439 #[test]
440 fn flatten_all_short_circuits_on_false() {
441 let mut rc = RuleCondition::All(vec![
442 RuleCondition::True,
443 RuleCondition::False,
444 RuleCondition::ContentTypeEmpty,
445 ]);
446 rc.flatten();
447 assert_eq!(rc, RuleCondition::False);
448 }
449
450 #[test]
451 fn flatten_all_empty_becomes_true() {
452 let mut rc = RuleCondition::All(vec![]);
453 rc.flatten();
454 assert_eq!(rc, RuleCondition::True);
455 }
456
457 #[test]
458 fn flatten_not_of_not() {
459 let mut rc = RuleCondition::Not(Box::new(RuleCondition::Not(Box::new(
460 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]),
461 ))));
462 rc.flatten();
463 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
464 }
465
466 #[test]
467 fn flatten_not_constants() {
468 let mut rc = RuleCondition::Not(Box::new(RuleCondition::True));
469 rc.flatten();
470 assert_eq!(rc, RuleCondition::False);
471
472 let mut rc = RuleCondition::Not(Box::new(RuleCondition::False));
473 rc.flatten();
474 assert_eq!(rc, RuleCondition::True);
475 }
476
477 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
478 async fn test_rule_condition_leaves() {
479 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
480 BackendOptions::default(),
481 noop_backing_storage(),
482 ));
483 tt.run_once(async { run_leaves_test_operation().read_strongly_consistent().await })
484 .await
485 .unwrap();
486 }
487
488 #[turbo_tasks::function(operation)]
489 pub async fn run_leaves_test_operation() -> Result<()> {
490 let fs = VirtualFileSystem::new();
491 let virtual_path = fs.root().await?.join("foo.js")?;
492 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
493 virtual_path.clone(),
494 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
495 ))
496 .to_resolved()
497 .await?;
498
499 let non_virtual_path = fs.root().await?.join("bar.js")?;
500 let non_virtual_source =
501 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
502 .to_resolved()
503 .await?;
504
505 {
506 let condition = RuleCondition::ReferenceType(ReferenceTypeCondition::Runtime);
507 assert!(
508 condition
509 .matches(virtual_source, &virtual_path, &ReferenceType::Runtime)
510 .await
511 .unwrap()
512 );
513 assert!(
514 !condition
515 .matches(
516 non_virtual_source,
517 &non_virtual_path,
518 &ReferenceType::Css(
519 turbopack_core::reference_type::CssReferenceSubType::Compose
520 )
521 )
522 .await
523 .unwrap()
524 );
525 }
526
527 {
528 let condition = RuleCondition::ResourceIsVirtualSource;
529 assert!(
530 condition
531 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
532 .await
533 .unwrap()
534 );
535 assert!(
536 !condition
537 .matches(
538 non_virtual_source,
539 &non_virtual_path,
540 &ReferenceType::Undefined
541 )
542 .await
543 .unwrap()
544 );
545 }
546 {
547 let condition = RuleCondition::ResourcePathEquals(virtual_path.clone());
548 assert!(
549 condition
550 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
551 .await
552 .unwrap()
553 );
554 assert!(
555 !condition
556 .matches(
557 non_virtual_source,
558 &non_virtual_path,
559 &ReferenceType::Undefined
560 )
561 .await
562 .unwrap()
563 );
564 }
565 {
566 let condition = RuleCondition::ResourcePathHasNoExtension;
567 assert!(
568 condition
569 .matches(
570 virtual_source,
571 &fs.root().await?.join("foo")?,
572 &ReferenceType::Undefined
573 )
574 .await
575 .unwrap()
576 );
577 assert!(
578 !condition
579 .matches(
580 non_virtual_source,
581 &non_virtual_path,
582 &ReferenceType::Undefined
583 )
584 .await
585 .unwrap()
586 );
587 }
588 {
589 let condition = RuleCondition::ResourcePathEndsWith("foo.js".to_string());
590 assert!(
591 condition
592 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
593 .await
594 .unwrap()
595 );
596 assert!(
597 !condition
598 .matches(
599 non_virtual_source,
600 &non_virtual_path,
601 &ReferenceType::Undefined
602 )
603 .await
604 .unwrap()
605 );
606 }
607 anyhow::Ok(())
608 }
609
610 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
611 async fn test_rule_condition_tree() {
612 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
613 BackendOptions::default(),
614 noop_backing_storage(),
615 ));
616 tt.run_once(async {
617 run_rule_condition_tree_test_operation()
618 .read_strongly_consistent()
619 .await
620 })
621 .await
622 .unwrap();
623 }
624
625 #[turbo_tasks::function(operation)]
626 pub async fn run_rule_condition_tree_test_operation() -> Result<()> {
627 let fs = VirtualFileSystem::new();
628 let virtual_path = fs.root().await?.join("foo.js")?;
629 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
630 virtual_path.clone(),
631 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
632 ))
633 .to_resolved()
634 .await?;
635
636 let non_virtual_path = fs.root().await?.join("bar.js")?;
637 let non_virtual_source =
638 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
639 .to_resolved()
640 .await?;
641
642 {
643 let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
645 assert!(
646 !condition
647 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
648 .await
649 .unwrap()
650 );
651 assert!(
652 condition
653 .matches(
654 non_virtual_source,
655 &non_virtual_path,
656 &ReferenceType::Undefined
657 )
658 .await
659 .unwrap()
660 );
661 }
662 {
663 let condition = RuleCondition::any(vec![
666 RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
667 RuleCondition::ResourceIsVirtualSource,
668 RuleCondition::ResourcePathHasNoExtension,
669 ]);
670 assert!(
671 condition
672 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
673 .await
674 .unwrap()
675 );
676 assert!(
677 !condition
678 .matches(
679 non_virtual_source,
680 &non_virtual_path,
681 &ReferenceType::Undefined
682 )
683 .await
684 .unwrap()
685 );
686 }
687 {
688 let condition = RuleCondition::all(vec![
691 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
692 RuleCondition::ResourceIsVirtualSource,
693 RuleCondition::ResourcePathEquals(virtual_path.clone()),
694 ]);
695 assert!(
696 condition
697 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
698 .await
699 .unwrap()
700 );
701 assert!(
702 !condition
703 .matches(
704 non_virtual_source,
705 &non_virtual_path,
706 &ReferenceType::Undefined
707 )
708 .await
709 .unwrap()
710 );
711 }
712 {
713 let condition = RuleCondition::all(vec![
717 RuleCondition::ResourceIsVirtualSource,
718 RuleCondition::ResourcePathEquals(virtual_path.clone()),
719 RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
720 RuleCondition::Any(vec![
721 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
722 RuleCondition::ContentTypeEmpty,
723 ]),
724 ]);
725 assert!(
726 condition
727 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
728 .await
729 .unwrap()
730 );
731 assert!(
732 !condition
733 .matches(
734 non_virtual_source,
735 &non_virtual_path,
736 &ReferenceType::Undefined
737 )
738 .await
739 .unwrap()
740 );
741 }
742 anyhow::Ok(())
743 }
744}