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::TNamedObject { fqcn, type_params } => {
400 if type_params.is_empty() && !fqcn.contains('\\') {
407 if let Some(resolved) = bindings.get(fqcn.as_ref()) {
408 for t in &resolved.types {
409 result.add_type(t.clone());
410 }
411 continue;
412 }
413 }
414 let new_params = type_params
415 .iter()
416 .map(|p| p.substitute_templates(bindings))
417 .collect();
418 result.add_type(Atomic::TNamedObject {
419 fqcn: fqcn.clone(),
420 type_params: new_params,
421 });
422 }
423 _ => {
424 result.add_type(atomic.clone());
425 }
426 }
427 }
428 result
429 }
430
431 pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
437 if other.is_mixed() {
438 return true;
439 }
440 if self.is_never() {
441 return true; }
443 self.types
444 .iter()
445 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
446 }
447
448 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
451 let mut result = Union::empty();
452 result.possibly_undefined = self.possibly_undefined;
453 result.from_docblock = self.from_docblock;
454 for atomic in &self.types {
455 if f(atomic) {
456 result.types.push(atomic.clone());
457 }
458 }
459 result
460 }
461
462 pub fn possibly_undefined(mut self) -> Self {
464 self.possibly_undefined = true;
465 self
466 }
467
468 pub fn from_docblock(mut self) -> Self {
470 self.from_docblock = true;
471 self
472 }
473}
474
475fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
480 if sub == sup {
481 return true;
482 }
483 match (sub, sup) {
484 (Atomic::TNever, _) => true,
486 (_, Atomic::TMixed) => true,
488 (Atomic::TMixed, _) => true,
489
490 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
492 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
493 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
494 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
495 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
496 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
497 (Atomic::TPositiveInt, Atomic::TInt) => true,
498 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
499 (Atomic::TNegativeInt, Atomic::TInt) => true,
500 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
501 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
502
503 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
504 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
505 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
506
507 (Atomic::TLiteralString(s), Atomic::TString) => {
508 let _ = s;
509 true
510 }
511 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
512 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
513 (Atomic::TNonEmptyString, Atomic::TString) => true,
514 (Atomic::TNumericString, Atomic::TString) => true,
515 (Atomic::TClassString(_), Atomic::TString) => true,
516 (Atomic::TInterfaceString, Atomic::TString) => true,
517 (Atomic::TEnumString, Atomic::TString) => true,
518 (Atomic::TTraitString, Atomic::TString) => true,
519
520 (Atomic::TTrue, Atomic::TBool) => true,
521 (Atomic::TFalse, Atomic::TBool) => true,
522
523 (Atomic::TInt, Atomic::TNumeric) => true,
524 (Atomic::TFloat, Atomic::TNumeric) => true,
525 (Atomic::TNumericString, Atomic::TNumeric) => true,
526
527 (Atomic::TInt, Atomic::TScalar) => true,
528 (Atomic::TFloat, Atomic::TScalar) => true,
529 (Atomic::TString, Atomic::TScalar) => true,
530 (Atomic::TBool, Atomic::TScalar) => true,
531 (Atomic::TNumeric, Atomic::TScalar) => true,
532 (Atomic::TTrue, Atomic::TScalar) => true,
533 (Atomic::TFalse, Atomic::TScalar) => true,
534
535 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
537 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
538 (Atomic::TSelf { .. }, Atomic::TObject) => true,
539 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
541 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
542 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
544 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
545
546 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
548 (Atomic::TPositiveInt, Atomic::TFloat) => true,
549 (Atomic::TInt, Atomic::TFloat) => true,
550
551 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
553
554 (Atomic::TString, Atomic::TCallable { .. }) => true,
556 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
557 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
558 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
559 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
560
561 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
563 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
565 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
567 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
569 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
571 fqcn.as_ref().eq_ignore_ascii_case("closure")
572 }
573 (Atomic::TClosure { .. }, Atomic::TObject) => true,
574
575 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
577 matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
579 && value.is_subtype_of_simple(av)
580 }
581 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
582 value.is_subtype_of_simple(lv)
583 }
584 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
586 matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
587 && av.is_subtype_of_simple(lv)
588 }
589 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
590 matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
591 && av.is_subtype_of_simple(lv)
592 }
593 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
594 matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
595 && av.is_subtype_of_simple(lv)
596 }
597 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
598 matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
599 && av.is_subtype_of_simple(lv)
600 }
601 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
603 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
604 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
605 }
606
607 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
609 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
610 }
611
612 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
614
615 (
617 Atomic::TKeyedArray {
618 properties,
619 is_list,
620 ..
621 },
622 Atomic::TList { value: lv },
623 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
624 (
625 Atomic::TKeyedArray {
626 properties,
627 is_list,
628 ..
629 },
630 Atomic::TNonEmptyList { value: lv },
631 ) => {
632 *is_list
633 && !properties.is_empty()
634 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
635 }
636
637 (_, Atomic::TTemplateParam { .. }) => true,
639
640 _ => false,
641 }
642}
643
644#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn single_is_single() {
654 let u = Union::single(Atomic::TString);
655 assert!(u.is_single());
656 assert!(!u.is_nullable());
657 }
658
659 #[test]
660 fn nullable_has_null() {
661 let u = Union::nullable(Atomic::TString);
662 assert!(u.is_nullable());
663 assert_eq!(u.types.len(), 2);
664 }
665
666 #[test]
667 fn add_type_deduplicates() {
668 let mut u = Union::single(Atomic::TString);
669 u.add_type(Atomic::TString);
670 assert_eq!(u.types.len(), 1);
671 }
672
673 #[test]
674 fn add_type_literal_subsumed_by_base() {
675 let mut u = Union::single(Atomic::TInt);
676 u.add_type(Atomic::TLiteralInt(42));
677 assert_eq!(u.types.len(), 1);
678 assert!(matches!(u.types[0], Atomic::TInt));
679 }
680
681 #[test]
682 fn add_type_base_widens_literals() {
683 let mut u = Union::single(Atomic::TLiteralInt(1));
684 u.add_type(Atomic::TLiteralInt(2));
685 u.add_type(Atomic::TInt);
686 assert_eq!(u.types.len(), 1);
687 assert!(matches!(u.types[0], Atomic::TInt));
688 }
689
690 #[test]
691 fn mixed_subsumes_everything() {
692 let mut u = Union::single(Atomic::TString);
693 u.add_type(Atomic::TMixed);
694 assert_eq!(u.types.len(), 1);
695 assert!(u.is_mixed());
696 }
697
698 #[test]
699 fn remove_null() {
700 let u = Union::nullable(Atomic::TString);
701 let narrowed = u.remove_null();
702 assert!(!narrowed.is_nullable());
703 assert_eq!(narrowed.types.len(), 1);
704 }
705
706 #[test]
707 fn narrow_to_truthy_removes_null_false() {
708 let mut u = Union::empty();
709 u.add_type(Atomic::TString);
710 u.add_type(Atomic::TNull);
711 u.add_type(Atomic::TFalse);
712 let truthy = u.narrow_to_truthy();
713 assert!(!truthy.is_nullable());
714 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
715 }
716
717 #[test]
718 fn merge_combines_types() {
719 let a = Union::single(Atomic::TString);
720 let b = Union::single(Atomic::TInt);
721 let merged = Union::merge(&a, &b);
722 assert_eq!(merged.types.len(), 2);
723 }
724
725 #[test]
726 fn subtype_literal_int_under_int() {
727 let sub = Union::single(Atomic::TLiteralInt(5));
728 let sup = Union::single(Atomic::TInt);
729 assert!(sub.is_subtype_of_simple(&sup));
730 }
731
732 #[test]
733 fn subtype_never_is_bottom() {
734 let never = Union::never();
735 let string = Union::single(Atomic::TString);
736 assert!(never.is_subtype_of_simple(&string));
737 }
738
739 #[test]
740 fn subtype_everything_under_mixed() {
741 let string = Union::single(Atomic::TString);
742 let mixed = Union::mixed();
743 assert!(string.is_subtype_of_simple(&mixed));
744 }
745
746 #[test]
747 fn template_substitution() {
748 let mut bindings = std::collections::HashMap::new();
749 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
750
751 let tmpl = Union::single(Atomic::TTemplateParam {
752 name: Arc::from("T"),
753 as_type: Box::new(Union::mixed()),
754 defining_entity: Arc::from("MyClass"),
755 });
756
757 let resolved = tmpl.substitute_templates(&bindings);
758 assert_eq!(resolved.types.len(), 1);
759 assert!(matches!(resolved.types[0], Atomic::TString));
760 }
761}