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| t.substitute_templates(bindings)),
571 default: p.default.as_ref().map(|d| d.substitute_templates(bindings)),
572 is_variadic: p.is_variadic,
573 is_byref: p.is_byref,
574 is_optional: p.is_optional,
575 }
576}
577
578fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
583 if sub == sup {
584 return true;
585 }
586 match (sub, sup) {
587 (Atomic::TNever, _) => true,
589 (_, Atomic::TMixed) => true,
591 (Atomic::TMixed, _) => true,
592
593 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
595 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
596 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
597 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
598 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
599 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
600 (Atomic::TPositiveInt, Atomic::TInt) => true,
601 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
602 (Atomic::TNegativeInt, Atomic::TInt) => true,
603 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
604 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
605
606 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
607 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
608 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
609
610 (Atomic::TLiteralString(s), Atomic::TString) => {
611 let _ = s;
612 true
613 }
614 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
615 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
616 (Atomic::TNonEmptyString, Atomic::TString) => true,
617 (Atomic::TNumericString, Atomic::TString) => true,
618 (Atomic::TClassString(_), Atomic::TString) => true,
619 (Atomic::TInterfaceString, Atomic::TString) => true,
620 (Atomic::TEnumString, Atomic::TString) => true,
621 (Atomic::TTraitString, Atomic::TString) => true,
622
623 (Atomic::TTrue, Atomic::TBool) => true,
624 (Atomic::TFalse, Atomic::TBool) => true,
625
626 (Atomic::TInt, Atomic::TNumeric) => true,
627 (Atomic::TFloat, Atomic::TNumeric) => true,
628 (Atomic::TNumericString, Atomic::TNumeric) => true,
629
630 (Atomic::TInt, Atomic::TScalar) => true,
631 (Atomic::TFloat, Atomic::TScalar) => true,
632 (Atomic::TString, Atomic::TScalar) => true,
633 (Atomic::TBool, Atomic::TScalar) => true,
634 (Atomic::TNumeric, Atomic::TScalar) => true,
635 (Atomic::TTrue, Atomic::TScalar) => true,
636 (Atomic::TFalse, Atomic::TScalar) => true,
637
638 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
640 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
641 (Atomic::TSelf { .. }, Atomic::TObject) => true,
642 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
644 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
645 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
647 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
648
649 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
651 (Atomic::TPositiveInt, Atomic::TFloat) => true,
652 (Atomic::TInt, Atomic::TFloat) => true,
653
654 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
656
657 (Atomic::TString, Atomic::TCallable { .. }) => true,
659 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
660 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
661 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
662 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
663
664 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
666 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
668 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
670 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
672 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
674 fqcn.as_ref().eq_ignore_ascii_case("closure")
675 }
676 (Atomic::TClosure { .. }, Atomic::TObject) => true,
677
678 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
680 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
682 && value.is_subtype_of_simple(av)
683 }
684 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
685 value.is_subtype_of_simple(lv)
686 }
687 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
689 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
690 && av.is_subtype_of_simple(lv)
691 }
692 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
693 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
694 && av.is_subtype_of_simple(lv)
695 }
696 (Atomic::TNonEmptyArray { 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::TNonEmptyArray { 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::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
706 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
707 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
708 }
709
710 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
712 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
713 }
714
715 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
717
718 (
720 Atomic::TKeyedArray {
721 properties,
722 is_list,
723 ..
724 },
725 Atomic::TList { value: lv },
726 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
727 (
728 Atomic::TKeyedArray {
729 properties,
730 is_list,
731 ..
732 },
733 Atomic::TNonEmptyList { value: lv },
734 ) => {
735 *is_list
736 && !properties.is_empty()
737 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
738 }
739
740 (_, Atomic::TTemplateParam { .. }) => true,
742
743 _ => false,
744 }
745}
746
747#[cfg(test)]
752mod tests {
753 use super::*;
754
755 #[test]
756 fn single_is_single() {
757 let u = Union::single(Atomic::TString);
758 assert!(u.is_single());
759 assert!(!u.is_nullable());
760 }
761
762 #[test]
763 fn nullable_has_null() {
764 let u = Union::nullable(Atomic::TString);
765 assert!(u.is_nullable());
766 assert_eq!(u.types.len(), 2);
767 }
768
769 #[test]
770 fn add_type_deduplicates() {
771 let mut u = Union::single(Atomic::TString);
772 u.add_type(Atomic::TString);
773 assert_eq!(u.types.len(), 1);
774 }
775
776 #[test]
777 fn add_type_literal_subsumed_by_base() {
778 let mut u = Union::single(Atomic::TInt);
779 u.add_type(Atomic::TLiteralInt(42));
780 assert_eq!(u.types.len(), 1);
781 assert!(matches!(u.types[0], Atomic::TInt));
782 }
783
784 #[test]
785 fn add_type_base_widens_literals() {
786 let mut u = Union::single(Atomic::TLiteralInt(1));
787 u.add_type(Atomic::TLiteralInt(2));
788 u.add_type(Atomic::TInt);
789 assert_eq!(u.types.len(), 1);
790 assert!(matches!(u.types[0], Atomic::TInt));
791 }
792
793 #[test]
794 fn mixed_subsumes_everything() {
795 let mut u = Union::single(Atomic::TString);
796 u.add_type(Atomic::TMixed);
797 assert_eq!(u.types.len(), 1);
798 assert!(u.is_mixed());
799 }
800
801 #[test]
802 fn remove_null() {
803 let u = Union::nullable(Atomic::TString);
804 let narrowed = u.remove_null();
805 assert!(!narrowed.is_nullable());
806 assert_eq!(narrowed.types.len(), 1);
807 }
808
809 #[test]
810 fn narrow_to_truthy_removes_null_false() {
811 let mut u = Union::empty();
812 u.add_type(Atomic::TString);
813 u.add_type(Atomic::TNull);
814 u.add_type(Atomic::TFalse);
815 let truthy = u.narrow_to_truthy();
816 assert!(!truthy.is_nullable());
817 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
818 }
819
820 #[test]
821 fn merge_combines_types() {
822 let a = Union::single(Atomic::TString);
823 let b = Union::single(Atomic::TInt);
824 let merged = Union::merge(&a, &b);
825 assert_eq!(merged.types.len(), 2);
826 }
827
828 #[test]
829 fn subtype_literal_int_under_int() {
830 let sub = Union::single(Atomic::TLiteralInt(5));
831 let sup = Union::single(Atomic::TInt);
832 assert!(sub.is_subtype_of_simple(&sup));
833 }
834
835 #[test]
836 fn subtype_never_is_bottom() {
837 let never = Union::never();
838 let string = Union::single(Atomic::TString);
839 assert!(never.is_subtype_of_simple(&string));
840 }
841
842 #[test]
843 fn subtype_everything_under_mixed() {
844 let string = Union::single(Atomic::TString);
845 let mixed = Union::mixed();
846 assert!(string.is_subtype_of_simple(&mixed));
847 }
848
849 #[test]
850 fn template_substitution() {
851 let mut bindings = std::collections::HashMap::new();
852 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
853
854 let tmpl = Union::single(Atomic::TTemplateParam {
855 name: Arc::from("T"),
856 as_type: Box::new(Union::mixed()),
857 defining_entity: Arc::from("MyClass"),
858 });
859
860 let resolved = tmpl.substitute_templates(&bindings);
861 assert_eq!(resolved.types.len(), 1);
862 assert!(matches!(resolved.types[0], Atomic::TString));
863 }
864
865 #[test]
866 fn intersection_is_object() {
867 let parts = vec![
868 Union::single(Atomic::TNamedObject {
869 fqcn: Arc::from("Iterator"),
870 type_params: vec![],
871 }),
872 Union::single(Atomic::TNamedObject {
873 fqcn: Arc::from("Countable"),
874 type_params: vec![],
875 }),
876 ];
877 let atomic = Atomic::TIntersection { parts };
878 assert!(atomic.is_object());
879 assert!(!atomic.can_be_falsy());
880 assert!(atomic.can_be_truthy());
881 }
882
883 #[test]
884 fn intersection_display_two_parts() {
885 let parts = vec![
886 Union::single(Atomic::TNamedObject {
887 fqcn: Arc::from("Iterator"),
888 type_params: vec![],
889 }),
890 Union::single(Atomic::TNamedObject {
891 fqcn: Arc::from("Countable"),
892 type_params: vec![],
893 }),
894 ];
895 let u = Union::single(Atomic::TIntersection { parts });
896 assert_eq!(format!("{u}"), "Iterator&Countable");
897 }
898
899 #[test]
900 fn intersection_display_three_parts() {
901 let parts = vec![
902 Union::single(Atomic::TNamedObject {
903 fqcn: Arc::from("A"),
904 type_params: vec![],
905 }),
906 Union::single(Atomic::TNamedObject {
907 fqcn: Arc::from("B"),
908 type_params: vec![],
909 }),
910 Union::single(Atomic::TNamedObject {
911 fqcn: Arc::from("C"),
912 type_params: vec![],
913 }),
914 ];
915 let u = Union::single(Atomic::TIntersection { parts });
916 assert_eq!(format!("{u}"), "A&B&C");
917 }
918
919 #[test]
920 fn intersection_in_nullable_union_display() {
921 let intersection = Atomic::TIntersection {
922 parts: vec![
923 Union::single(Atomic::TNamedObject {
924 fqcn: Arc::from("Iterator"),
925 type_params: vec![],
926 }),
927 Union::single(Atomic::TNamedObject {
928 fqcn: Arc::from("Countable"),
929 type_params: vec![],
930 }),
931 ],
932 };
933 let mut u = Union::single(intersection);
934 u.add_type(Atomic::TNull);
935 assert!(u.is_nullable());
936 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
937 }
938
939 fn t_param(name: &str) -> Union {
942 Union::single(Atomic::TTemplateParam {
943 name: Arc::from(name),
944 as_type: Box::new(Union::mixed()),
945 defining_entity: Arc::from("Fn"),
946 })
947 }
948
949 fn bindings_t_string() -> std::collections::HashMap<Arc<str>, Union> {
950 let mut b = std::collections::HashMap::new();
951 b.insert(Arc::from("T"), Union::single(Atomic::TString));
952 b
953 }
954
955 #[test]
956 fn substitute_non_empty_array_key_and_value() {
957 let ty = Union::single(Atomic::TNonEmptyArray {
958 key: Box::new(t_param("T")),
959 value: Box::new(t_param("T")),
960 });
961 let result = ty.substitute_templates(&bindings_t_string());
962 assert_eq!(result.types.len(), 1);
963 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
964 panic!("expected TNonEmptyArray");
965 };
966 assert!(matches!(key.types[0], Atomic::TString));
967 assert!(matches!(value.types[0], Atomic::TString));
968 }
969
970 #[test]
971 fn substitute_non_empty_list_value() {
972 let ty = Union::single(Atomic::TNonEmptyList {
973 value: Box::new(t_param("T")),
974 });
975 let result = ty.substitute_templates(&bindings_t_string());
976 let Atomic::TNonEmptyList { value } = &result.types[0] else {
977 panic!("expected TNonEmptyList");
978 };
979 assert!(matches!(value.types[0], Atomic::TString));
980 }
981
982 #[test]
983 fn substitute_keyed_array_property_types() {
984 use crate::atomic::{ArrayKey, KeyedProperty};
985 use indexmap::IndexMap;
986 let mut props = IndexMap::new();
987 props.insert(
988 ArrayKey::String(Arc::from("name")),
989 KeyedProperty {
990 ty: t_param("T"),
991 optional: false,
992 },
993 );
994 props.insert(
995 ArrayKey::String(Arc::from("tag")),
996 KeyedProperty {
997 ty: t_param("T"),
998 optional: true,
999 },
1000 );
1001 let ty = Union::single(Atomic::TKeyedArray {
1002 properties: props,
1003 is_open: true,
1004 is_list: false,
1005 });
1006 let result = ty.substitute_templates(&bindings_t_string());
1007 let Atomic::TKeyedArray {
1008 properties,
1009 is_open,
1010 is_list,
1011 } = &result.types[0]
1012 else {
1013 panic!("expected TKeyedArray");
1014 };
1015 assert!(is_open);
1016 assert!(!is_list);
1017 assert!(matches!(
1018 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1019 Atomic::TString
1020 ));
1021 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1022 assert!(matches!(
1023 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1024 Atomic::TString
1025 ));
1026 }
1027
1028 #[test]
1029 fn substitute_callable_params_and_return() {
1030 use crate::atomic::FnParam;
1031 let ty = Union::single(Atomic::TCallable {
1032 params: Some(vec![FnParam {
1033 name: Arc::from("x"),
1034 ty: Some(t_param("T")),
1035 default: None,
1036 is_variadic: false,
1037 is_byref: false,
1038 is_optional: false,
1039 }]),
1040 return_type: Some(Box::new(t_param("T"))),
1041 });
1042 let result = ty.substitute_templates(&bindings_t_string());
1043 let Atomic::TCallable {
1044 params,
1045 return_type,
1046 } = &result.types[0]
1047 else {
1048 panic!("expected TCallable");
1049 };
1050 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1051 assert!(matches!(param_ty.types[0], Atomic::TString));
1052 let ret = return_type.as_ref().unwrap();
1053 assert!(matches!(ret.types[0], Atomic::TString));
1054 }
1055
1056 #[test]
1057 fn substitute_callable_bare_no_panic() {
1058 let ty = Union::single(Atomic::TCallable {
1060 params: None,
1061 return_type: None,
1062 });
1063 let result = ty.substitute_templates(&bindings_t_string());
1064 assert!(matches!(
1065 result.types[0],
1066 Atomic::TCallable {
1067 params: None,
1068 return_type: None
1069 }
1070 ));
1071 }
1072
1073 #[test]
1074 fn substitute_closure_params_return_and_this() {
1075 use crate::atomic::FnParam;
1076 let ty = Union::single(Atomic::TClosure {
1077 params: vec![FnParam {
1078 name: Arc::from("a"),
1079 ty: Some(t_param("T")),
1080 default: Some(t_param("T")),
1081 is_variadic: true,
1082 is_byref: true,
1083 is_optional: true,
1084 }],
1085 return_type: Box::new(t_param("T")),
1086 this_type: Some(Box::new(t_param("T"))),
1087 });
1088 let result = ty.substitute_templates(&bindings_t_string());
1089 let Atomic::TClosure {
1090 params,
1091 return_type,
1092 this_type,
1093 } = &result.types[0]
1094 else {
1095 panic!("expected TClosure");
1096 };
1097 let p = ¶ms[0];
1098 assert!(matches!(p.ty.as_ref().unwrap().types[0], Atomic::TString));
1099 assert!(matches!(
1100 p.default.as_ref().unwrap().types[0],
1101 Atomic::TString
1102 ));
1103 assert!(p.is_variadic);
1105 assert!(p.is_byref);
1106 assert!(p.is_optional);
1107 assert!(matches!(return_type.types[0], Atomic::TString));
1108 assert!(matches!(
1109 this_type.as_ref().unwrap().types[0],
1110 Atomic::TString
1111 ));
1112 }
1113
1114 #[test]
1115 fn substitute_conditional_all_branches() {
1116 let ty = Union::single(Atomic::TConditional {
1117 subject: Box::new(t_param("T")),
1118 if_true: Box::new(t_param("T")),
1119 if_false: Box::new(Union::single(Atomic::TInt)),
1120 });
1121 let result = ty.substitute_templates(&bindings_t_string());
1122 let Atomic::TConditional {
1123 subject,
1124 if_true,
1125 if_false,
1126 } = &result.types[0]
1127 else {
1128 panic!("expected TConditional");
1129 };
1130 assert!(matches!(subject.types[0], Atomic::TString));
1131 assert!(matches!(if_true.types[0], Atomic::TString));
1132 assert!(matches!(if_false.types[0], Atomic::TInt));
1133 }
1134
1135 #[test]
1136 fn substitute_intersection_parts() {
1137 let ty = Union::single(Atomic::TIntersection {
1138 parts: vec![
1139 Union::single(Atomic::TNamedObject {
1140 fqcn: Arc::from("Countable"),
1141 type_params: vec![],
1142 }),
1143 t_param("T"),
1144 ],
1145 });
1146 let result = ty.substitute_templates(&bindings_t_string());
1147 let Atomic::TIntersection { parts } = &result.types[0] else {
1148 panic!("expected TIntersection");
1149 };
1150 assert_eq!(parts.len(), 2);
1151 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1152 assert!(matches!(parts[1].types[0], Atomic::TString));
1153 }
1154
1155 #[test]
1156 fn substitute_no_template_params_identity() {
1157 let ty = Union::single(Atomic::TInt);
1158 let result = ty.substitute_templates(&bindings_t_string());
1159 assert!(matches!(result.types[0], Atomic::TInt));
1160 }
1161}