1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use smallvec::SmallVec;
5
6use crate::atomic::Atomic;
7
8pub type AtomicVec = SmallVec<[Atomic; 2]>;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct Union {
17 pub types: AtomicVec,
18 pub possibly_undefined: bool,
20 pub from_docblock: bool,
22}
23
24impl Union {
25 pub fn empty() -> Self {
28 Self {
29 types: SmallVec::new(),
30 possibly_undefined: false,
31 from_docblock: false,
32 }
33 }
34
35 pub fn single(atomic: Atomic) -> Self {
36 let mut types = SmallVec::new();
37 types.push(atomic);
38 Self {
39 types,
40 possibly_undefined: false,
41 from_docblock: false,
42 }
43 }
44
45 pub fn mixed() -> Self {
46 Self::single(Atomic::TMixed)
47 }
48
49 pub fn void() -> Self {
50 Self::single(Atomic::TVoid)
51 }
52
53 pub fn never() -> Self {
54 Self::single(Atomic::TNever)
55 }
56
57 pub fn null() -> Self {
58 Self::single(Atomic::TNull)
59 }
60
61 pub fn bool() -> Self {
62 Self::single(Atomic::TBool)
63 }
64
65 pub fn int() -> Self {
66 Self::single(Atomic::TInt)
67 }
68
69 pub fn float() -> Self {
70 Self::single(Atomic::TFloat)
71 }
72
73 pub fn string() -> Self {
74 Self::single(Atomic::TString)
75 }
76
77 pub fn nullable(atomic: Atomic) -> Self {
79 let mut types = SmallVec::new();
80 types.push(atomic);
81 types.push(Atomic::TNull);
82 Self {
83 types,
84 possibly_undefined: false,
85 from_docblock: false,
86 }
87 }
88
89 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
91 let mut u = Self::empty();
92 for a in atomics {
93 u.add_type(a);
94 }
95 u
96 }
97
98 pub fn is_empty(&self) -> bool {
101 self.types.is_empty()
102 }
103
104 pub fn is_single(&self) -> bool {
105 self.types.len() == 1
106 }
107
108 pub fn is_nullable(&self) -> bool {
109 self.types.iter().any(|t| matches!(t, Atomic::TNull))
110 }
111
112 pub fn is_mixed(&self) -> bool {
113 self.types.iter().any(|t| match t {
114 Atomic::TMixed => true,
115 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
116 _ => false,
117 })
118 }
119
120 pub fn is_never(&self) -> bool {
121 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
122 }
123
124 pub fn is_void(&self) -> bool {
125 self.is_single() && matches!(self.types[0], Atomic::TVoid)
126 }
127
128 pub fn can_be_falsy(&self) -> bool {
129 self.types.iter().any(|t| t.can_be_falsy())
130 }
131
132 pub fn can_be_truthy(&self) -> bool {
133 self.types.iter().any(|t| t.can_be_truthy())
134 }
135
136 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
137 self.types.iter().any(f)
138 }
139
140 pub fn has_named_object(&self, fqcn: &str) -> bool {
141 self.types.iter().any(|t| match t {
142 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
143 _ => false,
144 })
145 }
146
147 pub fn add_type(&mut self, atomic: Atomic) {
152 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
154 return;
155 }
156
157 if matches!(atomic, Atomic::TMixed) {
159 self.types.clear();
160 self.types.push(Atomic::TMixed);
161 return;
162 }
163
164 if self.types.contains(&atomic) {
166 return;
167 }
168
169 if let Atomic::TLiteralInt(_) = &atomic {
171 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
172 return;
173 }
174 }
175 if let Atomic::TLiteralString(_) = &atomic {
177 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
178 return;
179 }
180 }
181 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
183 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
184 {
185 return;
186 }
187 if matches!(atomic, Atomic::TInt) {
189 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
190 }
191 if matches!(atomic, Atomic::TString) {
193 self.types
194 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
195 }
196 if matches!(atomic, Atomic::TBool) {
198 self.types
199 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
200 }
201
202 if matches!(atomic, Atomic::TNever) {
204 if !self.types.is_empty() {
205 return;
206 }
207 } else {
208 self.types.retain(|t| !matches!(t, Atomic::TNever));
209 }
210
211 self.types.push(atomic);
212 }
213
214 pub fn remove_null(&self) -> Union {
218 self.filter(|t| !matches!(t, Atomic::TNull))
219 }
220
221 pub fn remove_false(&self) -> Union {
223 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
224 }
225
226 pub fn core_type(&self) -> Union {
228 self.remove_null().remove_false()
229 }
230
231 pub fn narrow_to_truthy(&self) -> Union {
233 if self.is_mixed() {
234 return Union::mixed();
235 }
236 let narrowed = self.filter(|t| t.can_be_truthy());
237 narrowed.filter(|t| match t {
239 Atomic::TLiteralInt(0) => false,
240 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
241 Atomic::TLiteralFloat(0, 0) => false,
242 _ => true,
243 })
244 }
245
246 pub fn narrow_to_falsy(&self) -> Union {
248 if self.is_mixed() {
249 return Union::from_vec(vec![
250 Atomic::TNull,
251 Atomic::TFalse,
252 Atomic::TLiteralInt(0),
253 Atomic::TLiteralString("".into()),
254 ]);
255 }
256 self.filter(|t| t.can_be_falsy())
257 }
258
259 pub fn narrow_instanceof(&self, class: &str) -> Union {
265 let narrowed_ty = Atomic::TNamedObject {
266 fqcn: class.into(),
267 type_params: vec![],
268 };
269 let has_object = self.types.iter().any(|t| {
271 matches!(
272 t,
273 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
275 });
276 if has_object || self.is_empty() {
277 Union::single(narrowed_ty)
278 } else {
279 Union::single(narrowed_ty)
282 }
283 }
284
285 pub fn narrow_to_string(&self) -> Union {
287 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
288 }
289
290 pub fn narrow_to_int(&self) -> Union {
292 self.filter(|t| {
293 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
294 })
295 }
296
297 pub fn narrow_to_float(&self) -> Union {
299 self.filter(|t| {
300 matches!(
301 t,
302 Atomic::TFloat
303 | Atomic::TLiteralFloat(..)
304 | Atomic::TMixed
305 | Atomic::TScalar
306 | Atomic::TNumeric
307 )
308 })
309 }
310
311 pub fn narrow_to_bool(&self) -> Union {
313 self.filter(|t| {
314 matches!(
315 t,
316 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
317 )
318 })
319 }
320
321 pub fn narrow_to_null(&self) -> Union {
323 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
324 }
325
326 pub fn narrow_to_array(&self) -> Union {
328 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
329 }
330
331 pub fn narrow_to_object(&self) -> Union {
333 self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
334 }
335
336 pub fn narrow_to_callable(&self) -> Union {
338 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
339 }
340
341 pub fn narrow_to_scalar(&self) -> Union {
343 self.filter(|t| {
344 matches!(
345 t,
346 Atomic::TString
347 | Atomic::TLiteralString(..)
348 | Atomic::TNumericString
349 | Atomic::TInt
350 | Atomic::TLiteralInt(..)
351 | Atomic::TFloat
352 | Atomic::TLiteralFloat(..)
353 | Atomic::TBool
354 | Atomic::TTrue
355 | Atomic::TFalse
356 | Atomic::TScalar
357 | Atomic::TMixed
358 )
359 })
360 }
361
362 pub fn narrow_to_iterable(&self) -> Union {
365 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
366 }
367
368 pub fn narrow_to_countable(&self) -> Union {
371 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
372 }
373
374 pub fn narrow_to_resource(&self) -> Union {
378 self.filter(|t| matches!(t, Atomic::TMixed))
380 }
381
382 pub fn merge(a: &Union, b: &Union) -> Union {
387 let mut result = a.clone();
388 for atomic in &b.types {
389 result.add_type(atomic.clone());
390 }
391 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
392 result
393 }
394
395 pub fn intersect_with(&self, other: &Union) -> Union {
399 if self.is_mixed() {
400 return other.clone();
401 }
402 if other.is_mixed() {
403 return self.clone();
404 }
405 let mut result = Union::empty();
407 for a in &self.types {
408 for b in &other.types {
409 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
410 result.add_type(a.clone());
411 break;
412 }
413 }
414 }
415 if result.is_empty() {
416 Union::never()
417 } else {
418 result
419 }
420 }
421
422 pub fn substitute_templates(
426 &self,
427 bindings: &std::collections::HashMap<Arc<str>, Union>,
428 ) -> Union {
429 if bindings.is_empty() {
430 return self.clone();
431 }
432 let mut result = Union::empty();
433 result.possibly_undefined = self.possibly_undefined;
434 result.from_docblock = self.from_docblock;
435 for atomic in &self.types {
436 match atomic {
437 Atomic::TTemplateParam { name, .. } => {
438 if let Some(resolved) = bindings.get(name) {
439 for t in &resolved.types {
440 result.add_type(t.clone());
441 }
442 } else {
443 result.add_type(atomic.clone());
444 }
445 }
446 Atomic::TArray { key, value } => {
447 result.add_type(Atomic::TArray {
448 key: Box::new(key.substitute_templates(bindings)),
449 value: Box::new(value.substitute_templates(bindings)),
450 });
451 }
452 Atomic::TList { value } => {
453 result.add_type(Atomic::TList {
454 value: Box::new(value.substitute_templates(bindings)),
455 });
456 }
457 Atomic::TNonEmptyArray { key, value } => {
458 result.add_type(Atomic::TNonEmptyArray {
459 key: Box::new(key.substitute_templates(bindings)),
460 value: Box::new(value.substitute_templates(bindings)),
461 });
462 }
463 Atomic::TNonEmptyList { value } => {
464 result.add_type(Atomic::TNonEmptyList {
465 value: Box::new(value.substitute_templates(bindings)),
466 });
467 }
468 Atomic::TKeyedArray {
469 properties,
470 is_open,
471 is_list,
472 } => {
473 use crate::atomic::KeyedProperty;
474 let new_props = properties
475 .iter()
476 .map(|(k, prop)| {
477 (
478 k.clone(),
479 KeyedProperty {
480 ty: prop.ty.substitute_templates(bindings),
481 optional: prop.optional,
482 },
483 )
484 })
485 .collect();
486 result.add_type(Atomic::TKeyedArray {
487 properties: new_props,
488 is_open: *is_open,
489 is_list: *is_list,
490 });
491 }
492 Atomic::TCallable {
493 params,
494 return_type,
495 } => {
496 result.add_type(Atomic::TCallable {
497 params: params.as_ref().map(|ps| {
498 ps.iter()
499 .map(|p| substitute_in_fn_param(p, bindings))
500 .collect()
501 }),
502 return_type: return_type
503 .as_ref()
504 .map(|r| Box::new(r.substitute_templates(bindings))),
505 });
506 }
507 Atomic::TClosure {
508 params,
509 return_type,
510 this_type,
511 } => {
512 result.add_type(Atomic::TClosure {
513 params: params
514 .iter()
515 .map(|p| substitute_in_fn_param(p, bindings))
516 .collect(),
517 return_type: Box::new(return_type.substitute_templates(bindings)),
518 this_type: this_type
519 .as_ref()
520 .map(|t| Box::new(t.substitute_templates(bindings))),
521 });
522 }
523 Atomic::TConditional {
524 subject,
525 if_true,
526 if_false,
527 } => {
528 result.add_type(Atomic::TConditional {
529 subject: Box::new(subject.substitute_templates(bindings)),
530 if_true: Box::new(if_true.substitute_templates(bindings)),
531 if_false: Box::new(if_false.substitute_templates(bindings)),
532 });
533 }
534 Atomic::TIntersection { parts } => {
535 result.add_type(Atomic::TIntersection {
536 parts: parts
537 .iter()
538 .map(|p| p.substitute_templates(bindings))
539 .collect(),
540 });
541 }
542 Atomic::TNamedObject { fqcn, type_params } => {
543 if type_params.is_empty() && !fqcn.contains('\\') {
550 if let Some(resolved) = bindings.get(fqcn.as_ref()) {
551 for t in &resolved.types {
552 result.add_type(t.clone());
553 }
554 continue;
555 }
556 }
557 let new_params = type_params
558 .iter()
559 .map(|p| p.substitute_templates(bindings))
560 .collect();
561 result.add_type(Atomic::TNamedObject {
562 fqcn: fqcn.clone(),
563 type_params: new_params,
564 });
565 }
566 _ => {
567 result.add_type(atomic.clone());
568 }
569 }
570 }
571 result
572 }
573
574 pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
580 if other.is_mixed() {
581 return true;
582 }
583 if self.is_never() {
584 return true; }
586 self.types
587 .iter()
588 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
589 }
590
591 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
594 let mut result = Union::empty();
595 result.possibly_undefined = self.possibly_undefined;
596 result.from_docblock = self.from_docblock;
597 for atomic in &self.types {
598 if f(atomic) {
599 result.types.push(atomic.clone());
600 }
601 }
602 result
603 }
604
605 pub fn possibly_undefined(mut self) -> Self {
607 self.possibly_undefined = true;
608 self
609 }
610
611 pub fn from_docblock(mut self) -> Self {
613 self.from_docblock = true;
614 self
615 }
616}
617
618fn substitute_in_fn_param(
623 p: &crate::atomic::FnParam,
624 bindings: &std::collections::HashMap<Arc<str>, Union>,
625) -> crate::atomic::FnParam {
626 crate::atomic::FnParam {
627 name: p.name.clone(),
628 ty: p.ty.as_ref().map(|t| {
629 let u = t.to_union();
630 let substituted = u.substitute_templates(bindings);
631 crate::compact::SimpleType::from_union(substituted)
632 }),
633 default: p.default.as_ref().map(|d| {
634 let u = d.to_union();
635 let substituted = u.substitute_templates(bindings);
636 crate::compact::SimpleType::from_union(substituted)
637 }),
638 is_variadic: p.is_variadic,
639 is_byref: p.is_byref,
640 is_optional: p.is_optional,
641 }
642}
643
644fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
649 if sub == sup {
650 return true;
651 }
652 match (sub, sup) {
653 (Atomic::TNever, _) => true,
655 (_, Atomic::TMixed) => true,
657 (Atomic::TMixed, _) => true,
658
659 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
661 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
662 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
663 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
664 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
665 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
666 (Atomic::TPositiveInt, Atomic::TInt) => true,
667 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
668 (Atomic::TNegativeInt, Atomic::TInt) => true,
669 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
670 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
671
672 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
673 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
674 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
675
676 (Atomic::TLiteralString(s), Atomic::TString) => {
677 let _ = s;
678 true
679 }
680 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
681 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
682 (Atomic::TNonEmptyString, Atomic::TString) => true,
683 (Atomic::TNumericString, Atomic::TString) => true,
684 (Atomic::TClassString(_), Atomic::TString) => true,
685 (Atomic::TInterfaceString, Atomic::TString) => true,
686 (Atomic::TEnumString, Atomic::TString) => true,
687 (Atomic::TTraitString, Atomic::TString) => true,
688
689 (Atomic::TTrue, Atomic::TBool) => true,
690 (Atomic::TFalse, Atomic::TBool) => true,
691
692 (Atomic::TInt, Atomic::TNumeric) => true,
693 (Atomic::TFloat, Atomic::TNumeric) => true,
694 (Atomic::TNumericString, Atomic::TNumeric) => true,
695
696 (Atomic::TInt, Atomic::TScalar) => true,
697 (Atomic::TFloat, Atomic::TScalar) => true,
698 (Atomic::TString, Atomic::TScalar) => true,
699 (Atomic::TBool, Atomic::TScalar) => true,
700 (Atomic::TNumeric, Atomic::TScalar) => true,
701 (Atomic::TTrue, Atomic::TScalar) => true,
702 (Atomic::TFalse, Atomic::TScalar) => true,
703
704 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
706 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
707 (Atomic::TSelf { .. }, Atomic::TObject) => true,
708 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
710 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
711 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
713 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
714
715 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
717 (Atomic::TPositiveInt, Atomic::TFloat) => true,
718 (Atomic::TInt, Atomic::TFloat) => true,
719
720 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
722
723 (Atomic::TString, Atomic::TCallable { .. }) => true,
725 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
726 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
727 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
728 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
729
730 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
732 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
734 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
736 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
738 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
740 fqcn.as_ref().eq_ignore_ascii_case("closure")
741 }
742 (Atomic::TClosure { .. }, Atomic::TObject) => true,
743
744 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
746 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
748 && value.is_subtype_of_simple(av)
749 }
750 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
751 value.is_subtype_of_simple(lv)
752 }
753 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
755 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
756 && av.is_subtype_of_simple(lv)
757 }
758 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
759 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
760 && av.is_subtype_of_simple(lv)
761 }
762 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
763 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
764 && av.is_subtype_of_simple(lv)
765 }
766 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
767 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
768 && av.is_subtype_of_simple(lv)
769 }
770 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
772 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
773 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
774 }
775
776 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
778 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
779 }
780
781 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
783
784 (
786 Atomic::TKeyedArray {
787 properties,
788 is_list,
789 ..
790 },
791 Atomic::TList { value: lv },
792 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
793 (
794 Atomic::TKeyedArray {
795 properties,
796 is_list,
797 ..
798 },
799 Atomic::TNonEmptyList { value: lv },
800 ) => {
801 *is_list
802 && !properties.is_empty()
803 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
804 }
805
806 (_, Atomic::TTemplateParam { .. }) => true,
808
809 _ => false,
810 }
811}
812
813#[cfg(test)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn single_is_single() {
823 let u = Union::single(Atomic::TString);
824 assert!(u.is_single());
825 assert!(!u.is_nullable());
826 }
827
828 #[test]
829 fn nullable_has_null() {
830 let u = Union::nullable(Atomic::TString);
831 assert!(u.is_nullable());
832 assert_eq!(u.types.len(), 2);
833 }
834
835 #[test]
836 fn add_type_deduplicates() {
837 let mut u = Union::single(Atomic::TString);
838 u.add_type(Atomic::TString);
839 assert_eq!(u.types.len(), 1);
840 }
841
842 #[test]
843 fn add_type_literal_subsumed_by_base() {
844 let mut u = Union::single(Atomic::TInt);
845 u.add_type(Atomic::TLiteralInt(42));
846 assert_eq!(u.types.len(), 1);
847 assert!(matches!(u.types[0], Atomic::TInt));
848 }
849
850 #[test]
851 fn add_type_base_widens_literals() {
852 let mut u = Union::single(Atomic::TLiteralInt(1));
853 u.add_type(Atomic::TLiteralInt(2));
854 u.add_type(Atomic::TInt);
855 assert_eq!(u.types.len(), 1);
856 assert!(matches!(u.types[0], Atomic::TInt));
857 }
858
859 #[test]
860 fn mixed_subsumes_everything() {
861 let mut u = Union::single(Atomic::TString);
862 u.add_type(Atomic::TMixed);
863 assert_eq!(u.types.len(), 1);
864 assert!(u.is_mixed());
865 }
866
867 #[test]
868 fn remove_null() {
869 let u = Union::nullable(Atomic::TString);
870 let narrowed = u.remove_null();
871 assert!(!narrowed.is_nullable());
872 assert_eq!(narrowed.types.len(), 1);
873 }
874
875 #[test]
876 fn narrow_to_truthy_removes_null_false() {
877 let mut u = Union::empty();
878 u.add_type(Atomic::TString);
879 u.add_type(Atomic::TNull);
880 u.add_type(Atomic::TFalse);
881 let truthy = u.narrow_to_truthy();
882 assert!(!truthy.is_nullable());
883 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
884 }
885
886 #[test]
887 fn merge_combines_types() {
888 let a = Union::single(Atomic::TString);
889 let b = Union::single(Atomic::TInt);
890 let merged = Union::merge(&a, &b);
891 assert_eq!(merged.types.len(), 2);
892 }
893
894 #[test]
895 fn subtype_literal_int_under_int() {
896 let sub = Union::single(Atomic::TLiteralInt(5));
897 let sup = Union::single(Atomic::TInt);
898 assert!(sub.is_subtype_of_simple(&sup));
899 }
900
901 #[test]
902 fn subtype_never_is_bottom() {
903 let never = Union::never();
904 let string = Union::single(Atomic::TString);
905 assert!(never.is_subtype_of_simple(&string));
906 }
907
908 #[test]
909 fn subtype_everything_under_mixed() {
910 let string = Union::single(Atomic::TString);
911 let mixed = Union::mixed();
912 assert!(string.is_subtype_of_simple(&mixed));
913 }
914
915 #[test]
916 fn template_substitution() {
917 let mut bindings = std::collections::HashMap::new();
918 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
919
920 let tmpl = Union::single(Atomic::TTemplateParam {
921 name: Arc::from("T"),
922 as_type: Box::new(Union::mixed()),
923 defining_entity: Arc::from("MyClass"),
924 });
925
926 let resolved = tmpl.substitute_templates(&bindings);
927 assert_eq!(resolved.types.len(), 1);
928 assert!(matches!(resolved.types[0], Atomic::TString));
929 }
930
931 #[test]
932 fn intersection_is_object() {
933 let parts = vec![
934 Union::single(Atomic::TNamedObject {
935 fqcn: Arc::from("Iterator"),
936 type_params: vec![],
937 }),
938 Union::single(Atomic::TNamedObject {
939 fqcn: Arc::from("Countable"),
940 type_params: vec![],
941 }),
942 ];
943 let atomic = Atomic::TIntersection { parts };
944 assert!(atomic.is_object());
945 assert!(!atomic.can_be_falsy());
946 assert!(atomic.can_be_truthy());
947 }
948
949 #[test]
950 fn intersection_display_two_parts() {
951 let parts = vec![
952 Union::single(Atomic::TNamedObject {
953 fqcn: Arc::from("Iterator"),
954 type_params: vec![],
955 }),
956 Union::single(Atomic::TNamedObject {
957 fqcn: Arc::from("Countable"),
958 type_params: vec![],
959 }),
960 ];
961 let u = Union::single(Atomic::TIntersection { parts });
962 assert_eq!(format!("{u}"), "Iterator&Countable");
963 }
964
965 #[test]
966 fn intersection_display_three_parts() {
967 let parts = vec![
968 Union::single(Atomic::TNamedObject {
969 fqcn: Arc::from("A"),
970 type_params: vec![],
971 }),
972 Union::single(Atomic::TNamedObject {
973 fqcn: Arc::from("B"),
974 type_params: vec![],
975 }),
976 Union::single(Atomic::TNamedObject {
977 fqcn: Arc::from("C"),
978 type_params: vec![],
979 }),
980 ];
981 let u = Union::single(Atomic::TIntersection { parts });
982 assert_eq!(format!("{u}"), "A&B&C");
983 }
984
985 #[test]
986 fn intersection_in_nullable_union_display() {
987 let intersection = Atomic::TIntersection {
988 parts: vec![
989 Union::single(Atomic::TNamedObject {
990 fqcn: Arc::from("Iterator"),
991 type_params: vec![],
992 }),
993 Union::single(Atomic::TNamedObject {
994 fqcn: Arc::from("Countable"),
995 type_params: vec![],
996 }),
997 ],
998 };
999 let mut u = Union::single(intersection);
1000 u.add_type(Atomic::TNull);
1001 assert!(u.is_nullable());
1002 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1003 }
1004
1005 fn t_param(name: &str) -> Union {
1008 Union::single(Atomic::TTemplateParam {
1009 name: Arc::from(name),
1010 as_type: Box::new(Union::mixed()),
1011 defining_entity: Arc::from("Fn"),
1012 })
1013 }
1014
1015 fn bindings_t_string() -> std::collections::HashMap<Arc<str>, Union> {
1016 let mut b = std::collections::HashMap::new();
1017 b.insert(Arc::from("T"), Union::single(Atomic::TString));
1018 b
1019 }
1020
1021 #[test]
1022 fn substitute_non_empty_array_key_and_value() {
1023 let ty = Union::single(Atomic::TNonEmptyArray {
1024 key: Box::new(t_param("T")),
1025 value: Box::new(t_param("T")),
1026 });
1027 let result = ty.substitute_templates(&bindings_t_string());
1028 assert_eq!(result.types.len(), 1);
1029 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1030 panic!("expected TNonEmptyArray");
1031 };
1032 assert!(matches!(key.types[0], Atomic::TString));
1033 assert!(matches!(value.types[0], Atomic::TString));
1034 }
1035
1036 #[test]
1037 fn substitute_non_empty_list_value() {
1038 let ty = Union::single(Atomic::TNonEmptyList {
1039 value: Box::new(t_param("T")),
1040 });
1041 let result = ty.substitute_templates(&bindings_t_string());
1042 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1043 panic!("expected TNonEmptyList");
1044 };
1045 assert!(matches!(value.types[0], Atomic::TString));
1046 }
1047
1048 #[test]
1049 fn substitute_keyed_array_property_types() {
1050 use crate::atomic::{ArrayKey, KeyedProperty};
1051 use indexmap::IndexMap;
1052 let mut props = IndexMap::new();
1053 props.insert(
1054 ArrayKey::String(Arc::from("name")),
1055 KeyedProperty {
1056 ty: t_param("T"),
1057 optional: false,
1058 },
1059 );
1060 props.insert(
1061 ArrayKey::String(Arc::from("tag")),
1062 KeyedProperty {
1063 ty: t_param("T"),
1064 optional: true,
1065 },
1066 );
1067 let ty = Union::single(Atomic::TKeyedArray {
1068 properties: props,
1069 is_open: true,
1070 is_list: false,
1071 });
1072 let result = ty.substitute_templates(&bindings_t_string());
1073 let Atomic::TKeyedArray {
1074 properties,
1075 is_open,
1076 is_list,
1077 } = &result.types[0]
1078 else {
1079 panic!("expected TKeyedArray");
1080 };
1081 assert!(is_open);
1082 assert!(!is_list);
1083 assert!(matches!(
1084 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1085 Atomic::TString
1086 ));
1087 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1088 assert!(matches!(
1089 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1090 Atomic::TString
1091 ));
1092 }
1093
1094 #[test]
1095 fn substitute_callable_params_and_return() {
1096 use crate::atomic::FnParam;
1097 let ty = Union::single(Atomic::TCallable {
1098 params: Some(vec![FnParam {
1099 name: Arc::from("x"),
1100 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1101 default: None,
1102 is_variadic: false,
1103 is_byref: false,
1104 is_optional: false,
1105 }]),
1106 return_type: Some(Box::new(t_param("T"))),
1107 });
1108 let result = ty.substitute_templates(&bindings_t_string());
1109 let Atomic::TCallable {
1110 params,
1111 return_type,
1112 } = &result.types[0]
1113 else {
1114 panic!("expected TCallable");
1115 };
1116 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1117 let param_union = param_ty.to_union();
1118 assert!(matches!(param_union.types[0], Atomic::TString));
1119 let ret = return_type.as_ref().unwrap();
1120 assert!(matches!(ret.types[0], Atomic::TString));
1121 }
1122
1123 #[test]
1124 fn substitute_callable_bare_no_panic() {
1125 let ty = Union::single(Atomic::TCallable {
1127 params: None,
1128 return_type: None,
1129 });
1130 let result = ty.substitute_templates(&bindings_t_string());
1131 assert!(matches!(
1132 result.types[0],
1133 Atomic::TCallable {
1134 params: None,
1135 return_type: None
1136 }
1137 ));
1138 }
1139
1140 #[test]
1141 fn substitute_closure_params_return_and_this() {
1142 use crate::atomic::FnParam;
1143 let ty = Union::single(Atomic::TClosure {
1144 params: vec![FnParam {
1145 name: Arc::from("a"),
1146 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1147 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1148 is_variadic: true,
1149 is_byref: true,
1150 is_optional: true,
1151 }],
1152 return_type: Box::new(t_param("T")),
1153 this_type: Some(Box::new(t_param("T"))),
1154 });
1155 let result = ty.substitute_templates(&bindings_t_string());
1156 let Atomic::TClosure {
1157 params,
1158 return_type,
1159 this_type,
1160 } = &result.types[0]
1161 else {
1162 panic!("expected TClosure");
1163 };
1164 let p = ¶ms[0];
1165 let ty_union = p.ty.as_ref().unwrap().to_union();
1166 let default_union = p.default.as_ref().unwrap().to_union();
1167 assert!(matches!(ty_union.types[0], Atomic::TString));
1168 assert!(matches!(default_union.types[0], Atomic::TString));
1169 assert!(p.is_variadic);
1171 assert!(p.is_byref);
1172 assert!(p.is_optional);
1173 assert!(matches!(return_type.types[0], Atomic::TString));
1174 assert!(matches!(
1175 this_type.as_ref().unwrap().types[0],
1176 Atomic::TString
1177 ));
1178 }
1179
1180 #[test]
1181 fn substitute_conditional_all_branches() {
1182 let ty = Union::single(Atomic::TConditional {
1183 subject: Box::new(t_param("T")),
1184 if_true: Box::new(t_param("T")),
1185 if_false: Box::new(Union::single(Atomic::TInt)),
1186 });
1187 let result = ty.substitute_templates(&bindings_t_string());
1188 let Atomic::TConditional {
1189 subject,
1190 if_true,
1191 if_false,
1192 } = &result.types[0]
1193 else {
1194 panic!("expected TConditional");
1195 };
1196 assert!(matches!(subject.types[0], Atomic::TString));
1197 assert!(matches!(if_true.types[0], Atomic::TString));
1198 assert!(matches!(if_false.types[0], Atomic::TInt));
1199 }
1200
1201 #[test]
1202 fn substitute_intersection_parts() {
1203 let ty = Union::single(Atomic::TIntersection {
1204 parts: vec![
1205 Union::single(Atomic::TNamedObject {
1206 fqcn: Arc::from("Countable"),
1207 type_params: vec![],
1208 }),
1209 t_param("T"),
1210 ],
1211 });
1212 let result = ty.substitute_templates(&bindings_t_string());
1213 let Atomic::TIntersection { parts } = &result.types[0] else {
1214 panic!("expected TIntersection");
1215 };
1216 assert_eq!(parts.len(), 2);
1217 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1218 assert!(matches!(parts[1].types[0], Atomic::TString));
1219 }
1220
1221 #[test]
1222 fn substitute_no_template_params_identity() {
1223 let ty = Union::single(Atomic::TInt);
1224 let result = ty.substitute_templates(&bindings_t_string());
1225 assert!(matches!(result.types[0], Atomic::TInt));
1226 }
1227}