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, 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| matches!(t, Atomic::TMixed))
114 }
115
116 pub fn is_never(&self) -> bool {
117 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
118 }
119
120 pub fn is_void(&self) -> bool {
121 self.is_single() && matches!(self.types[0], Atomic::TVoid)
122 }
123
124 pub fn can_be_falsy(&self) -> bool {
125 self.types.iter().any(|t| t.can_be_falsy())
126 }
127
128 pub fn can_be_truthy(&self) -> bool {
129 self.types.iter().any(|t| t.can_be_truthy())
130 }
131
132 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
133 self.types.iter().any(f)
134 }
135
136 pub fn has_named_object(&self, fqcn: &str) -> bool {
137 self.types.iter().any(|t| match t {
138 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
139 _ => false,
140 })
141 }
142
143 pub fn add_type(&mut self, atomic: Atomic) {
148 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
150 return;
151 }
152
153 if matches!(atomic, Atomic::TMixed) {
155 self.types.clear();
156 self.types.push(Atomic::TMixed);
157 return;
158 }
159
160 if self.types.contains(&atomic) {
162 return;
163 }
164
165 if let Atomic::TLiteralInt(_) = &atomic {
167 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
168 return;
169 }
170 }
171 if let Atomic::TLiteralString(_) = &atomic {
173 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
174 return;
175 }
176 }
177 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
179 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
180 {
181 return;
182 }
183 if matches!(atomic, Atomic::TInt) {
185 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
186 }
187 if matches!(atomic, Atomic::TString) {
189 self.types
190 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
191 }
192 if matches!(atomic, Atomic::TBool) {
194 self.types
195 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
196 }
197
198 self.types.push(atomic);
199 }
200
201 pub fn remove_null(&self) -> Union {
205 self.filter(|t| !matches!(t, Atomic::TNull))
206 }
207
208 pub fn remove_false(&self) -> Union {
210 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
211 }
212
213 pub fn narrow_to_truthy(&self) -> Union {
215 if self.is_mixed() {
216 return Union::mixed();
217 }
218 let narrowed = self.filter(|t| t.can_be_truthy());
219 narrowed.filter(|t| match t {
221 Atomic::TLiteralInt(0) => false,
222 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
223 Atomic::TLiteralFloat(0, 0) => false,
224 _ => true,
225 })
226 }
227
228 pub fn narrow_to_falsy(&self) -> Union {
230 if self.is_mixed() {
231 return Union::from_vec(vec![
232 Atomic::TNull,
233 Atomic::TFalse,
234 Atomic::TLiteralInt(0),
235 Atomic::TLiteralString("".into()),
236 ]);
237 }
238 self.filter(|t| t.can_be_falsy())
239 }
240
241 pub fn narrow_instanceof(&self, class: &str) -> Union {
247 let narrowed_ty = Atomic::TNamedObject {
248 fqcn: class.into(),
249 type_params: vec![],
250 };
251 let has_object = self.types.iter().any(|t| {
253 matches!(
254 t,
255 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
257 });
258 if has_object || self.is_empty() {
259 Union::single(narrowed_ty)
260 } else {
261 Union::single(narrowed_ty)
264 }
265 }
266
267 pub fn narrow_to_string(&self) -> Union {
269 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
270 }
271
272 pub fn narrow_to_int(&self) -> Union {
274 self.filter(|t| {
275 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
276 })
277 }
278
279 pub fn narrow_to_float(&self) -> Union {
281 self.filter(|t| {
282 matches!(
283 t,
284 Atomic::TFloat
285 | Atomic::TLiteralFloat(..)
286 | Atomic::TMixed
287 | Atomic::TScalar
288 | Atomic::TNumeric
289 )
290 })
291 }
292
293 pub fn narrow_to_bool(&self) -> Union {
295 self.filter(|t| {
296 matches!(
297 t,
298 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
299 )
300 })
301 }
302
303 pub fn narrow_to_null(&self) -> Union {
305 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
306 }
307
308 pub fn narrow_to_array(&self) -> Union {
310 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
311 }
312
313 pub fn narrow_to_object(&self) -> Union {
315 self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
316 }
317
318 pub fn narrow_to_callable(&self) -> Union {
320 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
321 }
322
323 pub fn merge(a: &Union, b: &Union) -> Union {
328 let mut result = a.clone();
329 for atomic in &b.types {
330 result.add_type(atomic.clone());
331 }
332 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
333 result
334 }
335
336 pub fn intersect_with(&self, other: &Union) -> Union {
340 if self.is_mixed() {
341 return other.clone();
342 }
343 if other.is_mixed() {
344 return self.clone();
345 }
346 let mut result = Union::empty();
348 for a in &self.types {
349 for b in &other.types {
350 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
351 result.add_type(a.clone());
352 break;
353 }
354 }
355 }
356 if result.is_empty() {
358 other.clone()
359 } else {
360 result
361 }
362 }
363
364 pub fn substitute_templates(
368 &self,
369 bindings: &std::collections::HashMap<Arc<str>, Union>,
370 ) -> Union {
371 if bindings.is_empty() {
372 return self.clone();
373 }
374 let mut result = Union::empty();
375 result.possibly_undefined = self.possibly_undefined;
376 result.from_docblock = self.from_docblock;
377 for atomic in &self.types {
378 match atomic {
379 Atomic::TTemplateParam { name, .. } => {
380 if let Some(resolved) = bindings.get(name) {
381 for t in &resolved.types {
382 result.add_type(t.clone());
383 }
384 } else {
385 result.add_type(atomic.clone());
386 }
387 }
388 Atomic::TArray { key, value } => {
389 result.add_type(Atomic::TArray {
390 key: Box::new(key.substitute_templates(bindings)),
391 value: Box::new(value.substitute_templates(bindings)),
392 });
393 }
394 Atomic::TList { value } => {
395 result.add_type(Atomic::TList {
396 value: Box::new(value.substitute_templates(bindings)),
397 });
398 }
399 Atomic::TNonEmptyArray { key, value } => {
400 result.add_type(Atomic::TNonEmptyArray {
401 key: Box::new(key.substitute_templates(bindings)),
402 value: Box::new(value.substitute_templates(bindings)),
403 });
404 }
405 Atomic::TNonEmptyList { value } => {
406 result.add_type(Atomic::TNonEmptyList {
407 value: Box::new(value.substitute_templates(bindings)),
408 });
409 }
410 Atomic::TKeyedArray {
411 properties,
412 is_open,
413 is_list,
414 } => {
415 use crate::atomic::KeyedProperty;
416 let new_props = properties
417 .iter()
418 .map(|(k, prop)| {
419 (
420 k.clone(),
421 KeyedProperty {
422 ty: prop.ty.substitute_templates(bindings),
423 optional: prop.optional,
424 },
425 )
426 })
427 .collect();
428 result.add_type(Atomic::TKeyedArray {
429 properties: new_props,
430 is_open: *is_open,
431 is_list: *is_list,
432 });
433 }
434 Atomic::TCallable {
435 params,
436 return_type,
437 } => {
438 result.add_type(Atomic::TCallable {
439 params: params.as_ref().map(|ps| {
440 ps.iter()
441 .map(|p| substitute_in_fn_param(p, bindings))
442 .collect()
443 }),
444 return_type: return_type
445 .as_ref()
446 .map(|r| Box::new(r.substitute_templates(bindings))),
447 });
448 }
449 Atomic::TClosure {
450 params,
451 return_type,
452 this_type,
453 } => {
454 result.add_type(Atomic::TClosure {
455 params: params
456 .iter()
457 .map(|p| substitute_in_fn_param(p, bindings))
458 .collect(),
459 return_type: Box::new(return_type.substitute_templates(bindings)),
460 this_type: this_type
461 .as_ref()
462 .map(|t| Box::new(t.substitute_templates(bindings))),
463 });
464 }
465 Atomic::TConditional {
466 subject,
467 if_true,
468 if_false,
469 } => {
470 result.add_type(Atomic::TConditional {
471 subject: Box::new(subject.substitute_templates(bindings)),
472 if_true: Box::new(if_true.substitute_templates(bindings)),
473 if_false: Box::new(if_false.substitute_templates(bindings)),
474 });
475 }
476 Atomic::TIntersection { parts } => {
477 result.add_type(Atomic::TIntersection {
478 parts: parts
479 .iter()
480 .map(|p| p.substitute_templates(bindings))
481 .collect(),
482 });
483 }
484 Atomic::TNamedObject { fqcn, type_params } => {
485 if type_params.is_empty() && !fqcn.contains('\\') {
492 if let Some(resolved) = bindings.get(fqcn.as_ref()) {
493 for t in &resolved.types {
494 result.add_type(t.clone());
495 }
496 continue;
497 }
498 }
499 let new_params = type_params
500 .iter()
501 .map(|p| p.substitute_templates(bindings))
502 .collect();
503 result.add_type(Atomic::TNamedObject {
504 fqcn: fqcn.clone(),
505 type_params: new_params,
506 });
507 }
508 _ => {
509 result.add_type(atomic.clone());
510 }
511 }
512 }
513 result
514 }
515
516 pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
522 if other.is_mixed() {
523 return true;
524 }
525 if self.is_never() {
526 return true; }
528 self.types
529 .iter()
530 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
531 }
532
533 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
536 let mut result = Union::empty();
537 result.possibly_undefined = self.possibly_undefined;
538 result.from_docblock = self.from_docblock;
539 for atomic in &self.types {
540 if f(atomic) {
541 result.types.push(atomic.clone());
542 }
543 }
544 result
545 }
546
547 pub fn possibly_undefined(mut self) -> Self {
549 self.possibly_undefined = true;
550 self
551 }
552
553 pub fn from_docblock(mut self) -> Self {
555 self.from_docblock = true;
556 self
557 }
558}
559
560fn substitute_in_fn_param(
565 p: &crate::atomic::FnParam,
566 bindings: &std::collections::HashMap<Arc<str>, Union>,
567) -> crate::atomic::FnParam {
568 crate::atomic::FnParam {
569 name: p.name.clone(),
570 ty: p.ty.as_ref().map(|t| {
571 let u = t.to_union();
572 let substituted = u.substitute_templates(bindings);
573 crate::compact::SimpleType::from_union(substituted)
574 }),
575 default: p.default.as_ref().map(|d| {
576 let u = d.to_union();
577 let substituted = u.substitute_templates(bindings);
578 crate::compact::SimpleType::from_union(substituted)
579 }),
580 is_variadic: p.is_variadic,
581 is_byref: p.is_byref,
582 is_optional: p.is_optional,
583 }
584}
585
586fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
591 if sub == sup {
592 return true;
593 }
594 match (sub, sup) {
595 (Atomic::TNever, _) => true,
597 (_, Atomic::TMixed) => true,
599 (Atomic::TMixed, _) => true,
600
601 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
603 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
604 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
605 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
606 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
607 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
608 (Atomic::TPositiveInt, Atomic::TInt) => true,
609 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
610 (Atomic::TNegativeInt, Atomic::TInt) => true,
611 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
612 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
613
614 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
615 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
616 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
617
618 (Atomic::TLiteralString(s), Atomic::TString) => {
619 let _ = s;
620 true
621 }
622 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
623 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
624 (Atomic::TNonEmptyString, Atomic::TString) => true,
625 (Atomic::TNumericString, Atomic::TString) => true,
626 (Atomic::TClassString(_), Atomic::TString) => true,
627 (Atomic::TInterfaceString, Atomic::TString) => true,
628 (Atomic::TEnumString, Atomic::TString) => true,
629 (Atomic::TTraitString, Atomic::TString) => true,
630
631 (Atomic::TTrue, Atomic::TBool) => true,
632 (Atomic::TFalse, Atomic::TBool) => true,
633
634 (Atomic::TInt, Atomic::TNumeric) => true,
635 (Atomic::TFloat, Atomic::TNumeric) => true,
636 (Atomic::TNumericString, Atomic::TNumeric) => true,
637
638 (Atomic::TInt, Atomic::TScalar) => true,
639 (Atomic::TFloat, Atomic::TScalar) => true,
640 (Atomic::TString, Atomic::TScalar) => true,
641 (Atomic::TBool, Atomic::TScalar) => true,
642 (Atomic::TNumeric, Atomic::TScalar) => true,
643 (Atomic::TTrue, Atomic::TScalar) => true,
644 (Atomic::TFalse, Atomic::TScalar) => true,
645
646 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
648 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
649 (Atomic::TSelf { .. }, Atomic::TObject) => true,
650 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
652 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
653 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
655 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
656
657 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
659 (Atomic::TPositiveInt, Atomic::TFloat) => true,
660 (Atomic::TInt, Atomic::TFloat) => true,
661
662 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
664
665 (Atomic::TString, Atomic::TCallable { .. }) => true,
667 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
668 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
669 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
670 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
671
672 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
674 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
676 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
678 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
680 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
682 fqcn.as_ref().eq_ignore_ascii_case("closure")
683 }
684 (Atomic::TClosure { .. }, Atomic::TObject) => true,
685
686 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
688 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
690 && value.is_subtype_of_simple(av)
691 }
692 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
693 value.is_subtype_of_simple(lv)
694 }
695 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
697 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
698 && av.is_subtype_of_simple(lv)
699 }
700 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
701 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
702 && av.is_subtype_of_simple(lv)
703 }
704 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
705 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
706 && av.is_subtype_of_simple(lv)
707 }
708 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
709 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
710 && av.is_subtype_of_simple(lv)
711 }
712 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
714 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
715 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
716 }
717
718 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
720 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
721 }
722
723 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
725
726 (
728 Atomic::TKeyedArray {
729 properties,
730 is_list,
731 ..
732 },
733 Atomic::TList { value: lv },
734 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
735 (
736 Atomic::TKeyedArray {
737 properties,
738 is_list,
739 ..
740 },
741 Atomic::TNonEmptyList { value: lv },
742 ) => {
743 *is_list
744 && !properties.is_empty()
745 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
746 }
747
748 (_, Atomic::TTemplateParam { .. }) => true,
750
751 _ => false,
752 }
753}
754
755#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
764 fn single_is_single() {
765 let u = Union::single(Atomic::TString);
766 assert!(u.is_single());
767 assert!(!u.is_nullable());
768 }
769
770 #[test]
771 fn nullable_has_null() {
772 let u = Union::nullable(Atomic::TString);
773 assert!(u.is_nullable());
774 assert_eq!(u.types.len(), 2);
775 }
776
777 #[test]
778 fn add_type_deduplicates() {
779 let mut u = Union::single(Atomic::TString);
780 u.add_type(Atomic::TString);
781 assert_eq!(u.types.len(), 1);
782 }
783
784 #[test]
785 fn add_type_literal_subsumed_by_base() {
786 let mut u = Union::single(Atomic::TInt);
787 u.add_type(Atomic::TLiteralInt(42));
788 assert_eq!(u.types.len(), 1);
789 assert!(matches!(u.types[0], Atomic::TInt));
790 }
791
792 #[test]
793 fn add_type_base_widens_literals() {
794 let mut u = Union::single(Atomic::TLiteralInt(1));
795 u.add_type(Atomic::TLiteralInt(2));
796 u.add_type(Atomic::TInt);
797 assert_eq!(u.types.len(), 1);
798 assert!(matches!(u.types[0], Atomic::TInt));
799 }
800
801 #[test]
802 fn mixed_subsumes_everything() {
803 let mut u = Union::single(Atomic::TString);
804 u.add_type(Atomic::TMixed);
805 assert_eq!(u.types.len(), 1);
806 assert!(u.is_mixed());
807 }
808
809 #[test]
810 fn remove_null() {
811 let u = Union::nullable(Atomic::TString);
812 let narrowed = u.remove_null();
813 assert!(!narrowed.is_nullable());
814 assert_eq!(narrowed.types.len(), 1);
815 }
816
817 #[test]
818 fn narrow_to_truthy_removes_null_false() {
819 let mut u = Union::empty();
820 u.add_type(Atomic::TString);
821 u.add_type(Atomic::TNull);
822 u.add_type(Atomic::TFalse);
823 let truthy = u.narrow_to_truthy();
824 assert!(!truthy.is_nullable());
825 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
826 }
827
828 #[test]
829 fn merge_combines_types() {
830 let a = Union::single(Atomic::TString);
831 let b = Union::single(Atomic::TInt);
832 let merged = Union::merge(&a, &b);
833 assert_eq!(merged.types.len(), 2);
834 }
835
836 #[test]
837 fn subtype_literal_int_under_int() {
838 let sub = Union::single(Atomic::TLiteralInt(5));
839 let sup = Union::single(Atomic::TInt);
840 assert!(sub.is_subtype_of_simple(&sup));
841 }
842
843 #[test]
844 fn subtype_never_is_bottom() {
845 let never = Union::never();
846 let string = Union::single(Atomic::TString);
847 assert!(never.is_subtype_of_simple(&string));
848 }
849
850 #[test]
851 fn subtype_everything_under_mixed() {
852 let string = Union::single(Atomic::TString);
853 let mixed = Union::mixed();
854 assert!(string.is_subtype_of_simple(&mixed));
855 }
856
857 #[test]
858 fn template_substitution() {
859 let mut bindings = std::collections::HashMap::new();
860 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
861
862 let tmpl = Union::single(Atomic::TTemplateParam {
863 name: Arc::from("T"),
864 as_type: Box::new(Union::mixed()),
865 defining_entity: Arc::from("MyClass"),
866 });
867
868 let resolved = tmpl.substitute_templates(&bindings);
869 assert_eq!(resolved.types.len(), 1);
870 assert!(matches!(resolved.types[0], Atomic::TString));
871 }
872
873 #[test]
874 fn intersection_is_object() {
875 let parts = vec![
876 Union::single(Atomic::TNamedObject {
877 fqcn: Arc::from("Iterator"),
878 type_params: vec![],
879 }),
880 Union::single(Atomic::TNamedObject {
881 fqcn: Arc::from("Countable"),
882 type_params: vec![],
883 }),
884 ];
885 let atomic = Atomic::TIntersection { parts };
886 assert!(atomic.is_object());
887 assert!(!atomic.can_be_falsy());
888 assert!(atomic.can_be_truthy());
889 }
890
891 #[test]
892 fn intersection_display_two_parts() {
893 let parts = vec![
894 Union::single(Atomic::TNamedObject {
895 fqcn: Arc::from("Iterator"),
896 type_params: vec![],
897 }),
898 Union::single(Atomic::TNamedObject {
899 fqcn: Arc::from("Countable"),
900 type_params: vec![],
901 }),
902 ];
903 let u = Union::single(Atomic::TIntersection { parts });
904 assert_eq!(format!("{u}"), "Iterator&Countable");
905 }
906
907 #[test]
908 fn intersection_display_three_parts() {
909 let parts = vec![
910 Union::single(Atomic::TNamedObject {
911 fqcn: Arc::from("A"),
912 type_params: vec![],
913 }),
914 Union::single(Atomic::TNamedObject {
915 fqcn: Arc::from("B"),
916 type_params: vec![],
917 }),
918 Union::single(Atomic::TNamedObject {
919 fqcn: Arc::from("C"),
920 type_params: vec![],
921 }),
922 ];
923 let u = Union::single(Atomic::TIntersection { parts });
924 assert_eq!(format!("{u}"), "A&B&C");
925 }
926
927 #[test]
928 fn intersection_in_nullable_union_display() {
929 let intersection = Atomic::TIntersection {
930 parts: vec![
931 Union::single(Atomic::TNamedObject {
932 fqcn: Arc::from("Iterator"),
933 type_params: vec![],
934 }),
935 Union::single(Atomic::TNamedObject {
936 fqcn: Arc::from("Countable"),
937 type_params: vec![],
938 }),
939 ],
940 };
941 let mut u = Union::single(intersection);
942 u.add_type(Atomic::TNull);
943 assert!(u.is_nullable());
944 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
945 }
946
947 fn t_param(name: &str) -> Union {
950 Union::single(Atomic::TTemplateParam {
951 name: Arc::from(name),
952 as_type: Box::new(Union::mixed()),
953 defining_entity: Arc::from("Fn"),
954 })
955 }
956
957 fn bindings_t_string() -> std::collections::HashMap<Arc<str>, Union> {
958 let mut b = std::collections::HashMap::new();
959 b.insert(Arc::from("T"), Union::single(Atomic::TString));
960 b
961 }
962
963 #[test]
964 fn substitute_non_empty_array_key_and_value() {
965 let ty = Union::single(Atomic::TNonEmptyArray {
966 key: Box::new(t_param("T")),
967 value: Box::new(t_param("T")),
968 });
969 let result = ty.substitute_templates(&bindings_t_string());
970 assert_eq!(result.types.len(), 1);
971 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
972 panic!("expected TNonEmptyArray");
973 };
974 assert!(matches!(key.types[0], Atomic::TString));
975 assert!(matches!(value.types[0], Atomic::TString));
976 }
977
978 #[test]
979 fn substitute_non_empty_list_value() {
980 let ty = Union::single(Atomic::TNonEmptyList {
981 value: Box::new(t_param("T")),
982 });
983 let result = ty.substitute_templates(&bindings_t_string());
984 let Atomic::TNonEmptyList { value } = &result.types[0] else {
985 panic!("expected TNonEmptyList");
986 };
987 assert!(matches!(value.types[0], Atomic::TString));
988 }
989
990 #[test]
991 fn substitute_keyed_array_property_types() {
992 use crate::atomic::{ArrayKey, KeyedProperty};
993 use indexmap::IndexMap;
994 let mut props = IndexMap::new();
995 props.insert(
996 ArrayKey::String(Arc::from("name")),
997 KeyedProperty {
998 ty: t_param("T"),
999 optional: false,
1000 },
1001 );
1002 props.insert(
1003 ArrayKey::String(Arc::from("tag")),
1004 KeyedProperty {
1005 ty: t_param("T"),
1006 optional: true,
1007 },
1008 );
1009 let ty = Union::single(Atomic::TKeyedArray {
1010 properties: props,
1011 is_open: true,
1012 is_list: false,
1013 });
1014 let result = ty.substitute_templates(&bindings_t_string());
1015 let Atomic::TKeyedArray {
1016 properties,
1017 is_open,
1018 is_list,
1019 } = &result.types[0]
1020 else {
1021 panic!("expected TKeyedArray");
1022 };
1023 assert!(is_open);
1024 assert!(!is_list);
1025 assert!(matches!(
1026 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1027 Atomic::TString
1028 ));
1029 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1030 assert!(matches!(
1031 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1032 Atomic::TString
1033 ));
1034 }
1035
1036 #[test]
1037 fn substitute_callable_params_and_return() {
1038 use crate::atomic::FnParam;
1039 let ty = Union::single(Atomic::TCallable {
1040 params: Some(vec![FnParam {
1041 name: Arc::from("x"),
1042 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1043 default: None,
1044 is_variadic: false,
1045 is_byref: false,
1046 is_optional: false,
1047 }]),
1048 return_type: Some(Box::new(t_param("T"))),
1049 });
1050 let result = ty.substitute_templates(&bindings_t_string());
1051 let Atomic::TCallable {
1052 params,
1053 return_type,
1054 } = &result.types[0]
1055 else {
1056 panic!("expected TCallable");
1057 };
1058 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1059 let param_union = param_ty.to_union();
1060 assert!(matches!(param_union.types[0], Atomic::TString));
1061 let ret = return_type.as_ref().unwrap();
1062 assert!(matches!(ret.types[0], Atomic::TString));
1063 }
1064
1065 #[test]
1066 fn substitute_callable_bare_no_panic() {
1067 let ty = Union::single(Atomic::TCallable {
1069 params: None,
1070 return_type: None,
1071 });
1072 let result = ty.substitute_templates(&bindings_t_string());
1073 assert!(matches!(
1074 result.types[0],
1075 Atomic::TCallable {
1076 params: None,
1077 return_type: None
1078 }
1079 ));
1080 }
1081
1082 #[test]
1083 fn substitute_closure_params_return_and_this() {
1084 use crate::atomic::FnParam;
1085 let ty = Union::single(Atomic::TClosure {
1086 params: vec![FnParam {
1087 name: Arc::from("a"),
1088 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1089 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1090 is_variadic: true,
1091 is_byref: true,
1092 is_optional: true,
1093 }],
1094 return_type: Box::new(t_param("T")),
1095 this_type: Some(Box::new(t_param("T"))),
1096 });
1097 let result = ty.substitute_templates(&bindings_t_string());
1098 let Atomic::TClosure {
1099 params,
1100 return_type,
1101 this_type,
1102 } = &result.types[0]
1103 else {
1104 panic!("expected TClosure");
1105 };
1106 let p = ¶ms[0];
1107 let ty_union = p.ty.as_ref().unwrap().to_union();
1108 let default_union = p.default.as_ref().unwrap().to_union();
1109 assert!(matches!(ty_union.types[0], Atomic::TString));
1110 assert!(matches!(default_union.types[0], Atomic::TString));
1111 assert!(p.is_variadic);
1113 assert!(p.is_byref);
1114 assert!(p.is_optional);
1115 assert!(matches!(return_type.types[0], Atomic::TString));
1116 assert!(matches!(
1117 this_type.as_ref().unwrap().types[0],
1118 Atomic::TString
1119 ));
1120 }
1121
1122 #[test]
1123 fn substitute_conditional_all_branches() {
1124 let ty = Union::single(Atomic::TConditional {
1125 subject: Box::new(t_param("T")),
1126 if_true: Box::new(t_param("T")),
1127 if_false: Box::new(Union::single(Atomic::TInt)),
1128 });
1129 let result = ty.substitute_templates(&bindings_t_string());
1130 let Atomic::TConditional {
1131 subject,
1132 if_true,
1133 if_false,
1134 } = &result.types[0]
1135 else {
1136 panic!("expected TConditional");
1137 };
1138 assert!(matches!(subject.types[0], Atomic::TString));
1139 assert!(matches!(if_true.types[0], Atomic::TString));
1140 assert!(matches!(if_false.types[0], Atomic::TInt));
1141 }
1142
1143 #[test]
1144 fn substitute_intersection_parts() {
1145 let ty = Union::single(Atomic::TIntersection {
1146 parts: vec![
1147 Union::single(Atomic::TNamedObject {
1148 fqcn: Arc::from("Countable"),
1149 type_params: vec![],
1150 }),
1151 t_param("T"),
1152 ],
1153 });
1154 let result = ty.substitute_templates(&bindings_t_string());
1155 let Atomic::TIntersection { parts } = &result.types[0] else {
1156 panic!("expected TIntersection");
1157 };
1158 assert_eq!(parts.len(), 2);
1159 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1160 assert!(matches!(parts[1].types[0], Atomic::TString));
1161 }
1162
1163 #[test]
1164 fn substitute_no_template_params_identity() {
1165 let ty = Union::single(Atomic::TInt);
1166 let result = ty.substitute_templates(&bindings_t_string());
1167 assert!(matches!(result.types[0], Atomic::TInt));
1168 }
1169}