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