1use std::{
2 iter,
3 mem::{replace, take},
4};
5
6use anyhow::{Result, bail};
7use either::Either;
8use serde::{Deserialize, Serialize};
9use smallvec::SmallVec;
10use turbo_esregex::EsRegex;
11use turbo_tasks::{NonLocalValue, ReadRef, ResolvedVc, primitives::Regex, 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 ResourcePathRegex(#[turbo_tasks(trace_ignore)] Regex),
34 ResourcePathEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
35 ResourceContentEsRegex(#[turbo_tasks(trace_ignore)] ReadRef<EsRegex>),
36 ResourcePathGlob {
43 base: FileSystemPath,
44 #[turbo_tasks(trace_ignore)]
45 glob: ReadRef<Glob>,
46 },
47 ResourceBasePathGlob(#[turbo_tasks(trace_ignore)] ReadRef<Glob>),
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::ResourcePathRegex(_) => {
271 bail!("ResourcePathRegex not implemented yet");
272 }
273 RuleCondition::ResourcePathEsRegex(regex) => {
274 return Ok(regex.is_match(&path.path));
275 }
276 RuleCondition::ResourceContentEsRegex(regex) => {
277 let content = source.content().file_content().await?;
278 match &*content {
279 FileContent::Content(file_content) => {
280 return Ok(regex.is_match(&file_content.content().to_str()?));
281 }
282 FileContent::NotFound => return Ok(false),
283 }
284 }
285 }
286 }
287 }
288 const EXPECTED_SIZE: usize = 8;
292 let mut stack = SmallVec::<[Op; EXPECTED_SIZE]>::with_capacity(EXPECTED_SIZE);
293 let mut result = process_condition(source, path, reference_type, &mut stack, self).await?;
294 while let Some(op) = stack.pop() {
295 match op {
296 Op::All(remaining) => {
297 if result {
299 if remaining.len() > 1 {
300 stack.push(Op::All(&remaining[1..]));
301 }
302 result = process_condition(
303 source,
304 path,
305 reference_type,
306 &mut stack,
307 &remaining[0],
308 )
309 .await?;
310 }
311 }
312 Op::Any(remaining) => {
313 if !result {
315 if remaining.len() > 1 {
316 stack.push(Op::Any(&remaining[1..]));
317 }
318 result = process_condition(
322 source,
323 path,
324 reference_type,
325 &mut stack,
326 &remaining[0],
327 )
328 .await?;
329 }
330 }
331 Op::Not => {
332 result = !result;
333 }
334 }
335 }
336 Ok(result)
337 }
338}
339
340#[cfg(test)]
341pub mod tests {
342 use turbo_tasks::Vc;
343 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
344 use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem};
345 use turbopack_core::{asset::AssetContent, file_source::FileSource};
346
347 use super::*;
348
349 #[test]
350 fn flatten_any_with_single_child_collapses() {
351 let mut rc = RuleCondition::Any(vec![RuleCondition::True]);
352 rc.flatten();
353 assert_eq!(rc, RuleCondition::True);
354
355 let mut rc = RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty]);
356 rc.flatten();
357 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
358 }
359
360 #[test]
361 fn flatten_any_nested_and_false() {
362 let mut rc = RuleCondition::Any(vec![
363 RuleCondition::False,
364 RuleCondition::Any(vec![RuleCondition::ContentTypeEmpty, RuleCondition::False]),
365 ]);
366 rc.flatten();
367 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
368 }
369
370 #[test]
371 fn flatten_any_short_circuits_on_true() {
372 let mut rc = RuleCondition::Any(vec![
373 RuleCondition::False,
374 RuleCondition::True,
375 RuleCondition::ContentTypeEmpty,
376 ]);
377 rc.flatten();
378 assert_eq!(rc, RuleCondition::True);
379 }
380
381 #[test]
382 fn flatten_any_empty_becomes_false() {
383 let mut rc = RuleCondition::Any(vec![]);
384 rc.flatten();
385 assert_eq!(rc, RuleCondition::False);
386 }
387
388 #[test]
389 fn flatten_all_with_single_child_collapses() {
390 let mut rc = RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]);
391 rc.flatten();
392 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
393
394 let mut rc = RuleCondition::All(vec![RuleCondition::True]);
395 rc.flatten();
396 assert_eq!(rc, RuleCondition::True);
397 }
398
399 #[test]
400 fn flatten_all_nested_and_true() {
401 let mut rc = RuleCondition::All(vec![
402 RuleCondition::True,
403 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty, RuleCondition::True]),
404 ]);
405 rc.flatten();
406 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
407 }
408
409 #[test]
410 fn flatten_all_short_circuits_on_false() {
411 let mut rc = RuleCondition::All(vec![
412 RuleCondition::True,
413 RuleCondition::False,
414 RuleCondition::ContentTypeEmpty,
415 ]);
416 rc.flatten();
417 assert_eq!(rc, RuleCondition::False);
418 }
419
420 #[test]
421 fn flatten_all_empty_becomes_true() {
422 let mut rc = RuleCondition::All(vec![]);
423 rc.flatten();
424 assert_eq!(rc, RuleCondition::True);
425 }
426
427 #[test]
428 fn flatten_not_of_not() {
429 let mut rc = RuleCondition::Not(Box::new(RuleCondition::Not(Box::new(
430 RuleCondition::All(vec![RuleCondition::ContentTypeEmpty]),
431 ))));
432 rc.flatten();
433 assert_eq!(rc, RuleCondition::ContentTypeEmpty);
434 }
435
436 #[test]
437 fn flatten_not_constants() {
438 let mut rc = RuleCondition::Not(Box::new(RuleCondition::True));
439 rc.flatten();
440 assert_eq!(rc, RuleCondition::False);
441
442 let mut rc = RuleCondition::Not(Box::new(RuleCondition::False));
443 rc.flatten();
444 assert_eq!(rc, RuleCondition::True);
445 }
446
447 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
448 async fn test_rule_condition_leaves() {
449 crate::register();
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 crate::register();
584 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
585 BackendOptions::default(),
586 noop_backing_storage(),
587 ));
588 tt.run_once(async { run_rule_condition_tree_test().await })
589 .await
590 .unwrap();
591 }
592
593 #[turbo_tasks::function]
594 pub async fn run_rule_condition_tree_test() -> Result<()> {
595 let fs = VirtualFileSystem::new();
596 let virtual_path = fs.root().await?.join("foo.js")?;
597 let virtual_source = Vc::upcast::<Box<dyn Source>>(VirtualSource::new(
598 virtual_path.clone(),
599 AssetContent::File(FileContent::NotFound.cell().to_resolved().await?).cell(),
600 ))
601 .to_resolved()
602 .await?;
603
604 let non_virtual_path = fs.root().await?.join("bar.js")?;
605 let non_virtual_source =
606 Vc::upcast::<Box<dyn Source>>(FileSource::new(non_virtual_path.clone()))
607 .to_resolved()
608 .await?;
609
610 {
611 let condition = RuleCondition::not(RuleCondition::ResourceIsVirtualSource);
613 assert!(
614 !condition
615 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
616 .await
617 .unwrap()
618 );
619 assert!(
620 condition
621 .matches(
622 non_virtual_source,
623 &non_virtual_path,
624 &ReferenceType::Undefined
625 )
626 .await
627 .unwrap()
628 );
629 }
630 {
631 let condition = RuleCondition::any(vec![
634 RuleCondition::ResourcePathInDirectory("doesnt/exist".to_string()),
635 RuleCondition::ResourceIsVirtualSource,
636 RuleCondition::ResourcePathHasNoExtension,
637 ]);
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::all(vec![
659 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
660 RuleCondition::ResourceIsVirtualSource,
661 RuleCondition::ResourcePathEquals(virtual_path.clone()),
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![
685 RuleCondition::ResourceIsVirtualSource,
686 RuleCondition::ResourcePathEquals(virtual_path.clone()),
687 RuleCondition::Not(Box::new(RuleCondition::ResourcePathHasNoExtension)),
688 RuleCondition::Any(vec![
689 RuleCondition::ResourcePathEndsWith("foo.js".to_string()),
690 RuleCondition::ContentTypeEmpty,
691 ]),
692 ]);
693 assert!(
694 condition
695 .matches(virtual_source, &virtual_path, &ReferenceType::Undefined)
696 .await
697 .unwrap()
698 );
699 assert!(
700 !condition
701 .matches(
702 non_virtual_source,
703 &non_virtual_path,
704 &ReferenceType::Undefined
705 )
706 .await
707 .unwrap()
708 );
709 }
710 anyhow::Ok(())
711 }
712}