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