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