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