1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use smallvec::SmallVec;
4use std::sync::{Arc, OnceLock};
5
6use crate::atomic::Atomic;
7use crate::symbol::Name;
8
9pub fn empty_type_params() -> Arc<[Type]> {
13 static EMPTY: OnceLock<Arc<[Type]>> = OnceLock::new();
14 EMPTY.get_or_init(|| Arc::from([] as [Type; 0])).clone()
15}
16
17pub fn vec_to_type_params(v: Vec<Type>) -> Arc<[Type]> {
20 if v.is_empty() {
21 empty_type_params()
22 } else {
23 Arc::from(v)
24 }
25}
26
27pub type AtomicVec = SmallVec<[Atomic; 2]>;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CloneValidity {
33 Cloneable,
35 Invalid,
37 PossiblyInvalid,
39 Unknown,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub struct Type {
49 pub types: AtomicVec,
50 pub possibly_undefined: bool,
52 pub from_docblock: bool,
54}
55
56impl Type {
57 pub fn empty() -> Self {
60 Self {
61 types: SmallVec::new(),
62 possibly_undefined: false,
63 from_docblock: false,
64 }
65 }
66
67 pub fn single(atomic: Atomic) -> Self {
68 let mut types = SmallVec::new();
69 types.push(atomic);
70 Self {
71 types,
72 possibly_undefined: false,
73 from_docblock: false,
74 }
75 }
76
77 pub fn mixed() -> Self {
78 Self::single(Atomic::TMixed)
79 }
80
81 pub fn void() -> Self {
82 Self::single(Atomic::TVoid)
83 }
84
85 pub fn never() -> Self {
86 Self::single(Atomic::TNever)
87 }
88
89 pub fn null() -> Self {
90 Self::single(Atomic::TNull)
91 }
92
93 pub fn bool() -> Self {
94 Self::single(Atomic::TBool)
95 }
96
97 pub fn int() -> Self {
98 Self::single(Atomic::TInt)
99 }
100
101 pub fn float() -> Self {
102 Self::single(Atomic::TFloat)
103 }
104
105 pub fn string() -> Self {
106 Self::single(Atomic::TString)
107 }
108
109 pub fn nullable(atomic: Atomic) -> Self {
111 if matches!(atomic, Atomic::TMixed) {
113 return Self::mixed();
114 }
115 let mut types = SmallVec::new();
116 types.push(atomic);
117 types.push(Atomic::TNull);
118 Self {
119 types,
120 possibly_undefined: false,
121 from_docblock: false,
122 }
123 }
124
125 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
127 let mut u = Self::empty();
128 for a in atomics {
129 u.add_type(a);
130 }
131 u
132 }
133
134 pub fn is_empty(&self) -> bool {
137 self.types.is_empty()
138 }
139
140 pub fn is_single(&self) -> bool {
141 self.types.len() == 1
142 }
143
144 pub fn is_nullable(&self) -> bool {
145 self.types.iter().any(|t| matches!(t, Atomic::TNull))
146 }
147
148 pub fn is_mixed(&self) -> bool {
149 self.types.iter().any(|t| match t {
150 Atomic::TMixed => true,
151 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
152 _ => false,
153 })
154 }
155
156 pub fn is_mixed_not_template(&self) -> bool {
161 self.is_mixed()
162 && !self
163 .types
164 .iter()
165 .any(|t| matches!(t, Atomic::TTemplateParam { .. }))
166 }
167
168 pub fn is_never(&self) -> bool {
169 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
170 }
171
172 pub fn clone_validity(&self) -> CloneValidity {
175 if self.types.is_empty() {
176 return CloneValidity::Unknown;
177 }
178 let mut has_non_object = false;
179 let mut has_other = false; for t in &self.types {
181 match t {
182 Atomic::TTemplateParam { as_type, .. } => match as_type.clone_validity() {
183 CloneValidity::Invalid => has_non_object = true,
184 CloneValidity::PossiblyInvalid => {
185 has_non_object = true;
186 has_other = true;
187 }
188 CloneValidity::Cloneable | CloneValidity::Unknown => has_other = true,
189 },
190 other if other.is_definitely_non_object() => has_non_object = true,
191 _ => has_other = true,
192 }
193 }
194 match (has_non_object, has_other) {
195 (true, false) => CloneValidity::Invalid,
196 (true, true) => CloneValidity::PossiblyInvalid,
197 _ => CloneValidity::Cloneable,
198 }
199 }
200
201 pub fn is_void(&self) -> bool {
202 self.is_single() && matches!(self.types[0], Atomic::TVoid)
203 }
204
205 pub fn can_be_falsy(&self) -> bool {
206 self.types.iter().any(|t| t.can_be_falsy())
207 }
208
209 pub fn can_be_truthy(&self) -> bool {
210 self.types.iter().any(|t| t.can_be_truthy())
211 }
212
213 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
214 self.types.iter().any(f)
215 }
216
217 pub fn has_named_object(&self, fqcn: &str) -> bool {
218 self.types.iter().any(|t| match t {
219 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
220 _ => false,
221 })
222 }
223
224 pub fn add_type(&mut self, atomic: Atomic) {
229 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
231 return;
232 }
233
234 if matches!(atomic, Atomic::TMixed) {
236 self.types.clear();
237 self.types.push(Atomic::TMixed);
238 return;
239 }
240
241 let atomic = if let Atomic::TConditional {
244 param_name: _,
245 subject: _,
246 if_true,
247 if_false,
248 } = &atomic
249 {
250 let mut simplified_true = Type::empty();
251 for t in &if_true.types {
252 simplified_true.add_type(t.clone());
253 }
254 let mut simplified_false = Type::empty();
255 for t in &if_false.types {
256 simplified_false.add_type(t.clone());
257 }
258 if simplified_true == simplified_false {
259 for t in simplified_true.types {
260 self.add_type(t);
261 }
262 return;
263 }
264 atomic
265 } else {
266 atomic
267 };
268
269 if self.types.contains(&atomic) {
271 return;
272 }
273
274 if let Atomic::TLiteralInt(_) = &atomic {
276 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
277 return;
278 }
279 }
280 if let Atomic::TLiteralString(_) = &atomic {
282 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
283 return;
284 }
285 }
286 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
288 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
289 {
290 return;
291 }
292 if matches!(atomic, Atomic::TInt) {
294 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
295 }
296 if matches!(atomic, Atomic::TString) {
298 self.types
299 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
300 }
301 if matches!(atomic, Atomic::TBool) {
303 self.types
304 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
305 }
306
307 if matches!(atomic, Atomic::TNever) {
309 if !self.types.is_empty() {
310 return;
311 }
312 } else {
313 self.types.retain(|t| !matches!(t, Atomic::TNever));
314 }
315
316 if let Atomic::TKeyedArray { properties, .. } = &atomic {
319 if properties.is_empty() {
320 for existing in &self.types {
321 match existing {
322 Atomic::TArray { .. }
323 | Atomic::TNonEmptyArray { .. }
324 | Atomic::TList { .. }
325 | Atomic::TNonEmptyList { .. } => {
326 return; }
328 _ => {}
329 }
330 }
331 }
332 }
333
334 let is_generic_array_or_list = matches!(
336 &atomic,
337 Atomic::TArray { .. }
338 | Atomic::TNonEmptyArray { .. }
339 | Atomic::TList { .. }
340 | Atomic::TNonEmptyList { .. }
341 );
342 if is_generic_array_or_list {
343 self.types.retain(|t| {
344 if let Atomic::TKeyedArray { properties, .. } = t {
345 !properties.is_empty()
346 } else {
347 true
348 }
349 });
350 }
351
352 self.types.push(atomic);
353 }
354
355 pub fn remove_null(&self) -> Type {
359 self.filter(|t| !matches!(t, Atomic::TNull))
360 }
361
362 pub fn remove_false(&self) -> Type {
365 let mut result = self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool));
366 if self.types.iter().any(|t| matches!(t, Atomic::TBool)) {
367 result.add_type(Atomic::TTrue);
368 }
369 result
370 }
371
372 pub fn core_type(&self) -> Type {
374 self.remove_null().remove_false()
375 }
376
377 pub fn narrow_to_truthy(&self) -> Type {
379 if self.is_mixed() {
380 return Type::mixed();
381 }
382 let mut result = Type::empty();
383 result.from_docblock = self.from_docblock;
384 for t in &self.types {
385 match t {
386 Atomic::TLiteralInt(0)
388 | Atomic::TLiteralFloat(0, 0)
389 | Atomic::TNull
390 | Atomic::TFalse => {}
391 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => {}
392 Atomic::TBool => result.add_type(Atomic::TTrue),
394 Atomic::TArray { key, value } => result.add_type(Atomic::TNonEmptyArray {
396 key: key.clone(),
397 value: value.clone(),
398 }),
399 Atomic::TList { value } => result.add_type(Atomic::TNonEmptyList {
400 value: value.clone(),
401 }),
402 Atomic::TString => result.add_type(Atomic::TNonEmptyString),
406 Atomic::TNonNegativeInt => result.add_type(Atomic::TPositiveInt),
411 Atomic::TIntRange { min: Some(0), max } if max.is_none_or(|m| m >= 1) => {
412 let atom = if max.is_none() {
413 Atomic::TPositiveInt
414 } else {
415 Atomic::TIntRange {
416 min: Some(1),
417 max: *max,
418 }
419 };
420 result.add_type(atom);
421 }
422 Atomic::TIntRange { min, max: Some(0) } => {
424 let atom = match min {
425 None => Atomic::TNegativeInt,
426 Some(n) if *n <= -1 => Atomic::TIntRange {
427 min: *min,
428 max: Some(-1),
429 },
430 _ => continue, };
432 result.add_type(atom);
433 }
434 t if !t.can_be_truthy() => {}
436 _ => result.add_type(t.clone()),
437 }
438 }
439 result
440 }
441
442 pub fn narrow_to_falsy(&self) -> Type {
444 if self.is_mixed() {
445 return Type::from_vec(vec![
446 Atomic::TNull,
447 Atomic::TFalse,
448 Atomic::TLiteralInt(0),
449 Atomic::TLiteralString("".into()),
450 ]);
451 }
452 let mut result = Type::empty();
453 result.from_docblock = self.from_docblock;
454 for t in &self.types {
455 match t {
456 Atomic::TBool => result.add_type(Atomic::TFalse),
458 Atomic::TInt => result.add_type(Atomic::TLiteralInt(0)),
460 Atomic::TFloat => result.add_type(Atomic::TLiteralFloat(0, 0)),
462 Atomic::TString => {
464 result.add_type(Atomic::TLiteralString("".into()));
465 result.add_type(Atomic::TLiteralString("0".into()));
466 }
467 Atomic::TNumericString => result.add_type(Atomic::TLiteralString("0".into())),
469 Atomic::TNonNegativeInt => result.add_type(Atomic::TLiteralInt(0)),
471 Atomic::TIntRange {
473 min: Some(0),
474 max: Some(_) | None,
475 } => result.add_type(Atomic::TLiteralInt(0)),
476 Atomic::TIntRange { max: Some(0), .. } => result.add_type(Atomic::TLiteralInt(0)),
478 t if !t.can_be_falsy() => {} _ => result.add_type(t.clone()),
480 }
481 }
482 result
483 }
484
485 pub fn narrow_instanceof(&self, class: &str) -> Type {
491 let narrowed_ty = Atomic::TNamedObject {
492 fqcn: class.into(),
493 type_params: empty_type_params(),
494 };
495 let has_object = self.types.iter().any(|t| {
497 matches!(
498 t,
499 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
501 });
502 if has_object || self.is_empty() {
503 Type::single(narrowed_ty)
504 } else {
505 Type::single(narrowed_ty)
508 }
509 }
510
511 pub fn narrow_to_string(&self) -> Type {
513 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
514 }
515
516 pub fn narrow_to_int(&self) -> Type {
518 self.filter(|t| {
519 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
520 })
521 }
522
523 pub fn narrow_to_float(&self) -> Type {
525 self.filter(|t| {
526 matches!(
527 t,
528 Atomic::TFloat
529 | Atomic::TIntegralFloat
530 | Atomic::TLiteralFloat(..)
531 | Atomic::TMixed
532 | Atomic::TScalar
533 | Atomic::TNumeric
534 )
535 })
536 }
537
538 pub fn narrow_to_bool(&self) -> Type {
540 self.filter(|t| {
541 matches!(
542 t,
543 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
544 )
545 })
546 }
547
548 pub fn narrow_to_null(&self) -> Type {
550 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
551 }
552
553 pub fn narrow_to_array(&self) -> Type {
555 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
556 }
557
558 pub fn narrow_to_non_empty_collection(&self) -> Type {
560 let mut out = Type::empty();
561 out.from_docblock = self.from_docblock;
562 for t in &self.types {
563 match t {
564 Atomic::TArray { key, value } => out.add_type(Atomic::TNonEmptyArray {
565 key: key.clone(),
566 value: value.clone(),
567 }),
568 Atomic::TList { value } => out.add_type(Atomic::TNonEmptyList {
569 value: value.clone(),
570 }),
571 _ => out.add_type(t.clone()),
572 }
573 }
574 out
575 }
576
577 pub fn narrow_to_list(&self) -> Type {
585 let mut out = Type::empty();
586 out.from_docblock = self.from_docblock;
587 for t in &self.types {
588 match t {
589 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => out.add_type(t.clone()),
590 Atomic::TArray { key, value } if matches!(key.types.as_slice(), [Atomic::TInt]) => {
591 out.add_type(Atomic::TList {
592 value: value.clone(),
593 });
594 }
595 Atomic::TNonEmptyArray { key, value }
596 if matches!(key.types.as_slice(), [Atomic::TInt]) =>
597 {
598 out.add_type(Atomic::TNonEmptyList {
599 value: value.clone(),
600 });
601 }
602 Atomic::TMixed => out.add_type(Atomic::TList {
603 value: Box::new(Type::mixed()),
604 }),
605 _ => {}
606 }
607 }
608 if out.is_empty() {
609 self.filter(|t| matches!(t, Atomic::TList { .. } | Atomic::TNonEmptyList { .. }))
610 } else {
611 out
612 }
613 }
614
615 pub fn narrow_to_object(&self) -> Type {
620 let mut out = Type::empty();
621 for t in &self.types {
622 if matches!(t, Atomic::TMixed) {
623 out.add_type(Atomic::TObject);
624 } else if t.is_object() {
625 out.add_type(t.clone());
626 }
627 }
628 if out.types.is_empty() {
629 self.filter(|t| t.is_object())
630 } else {
631 out
632 }
633 }
634
635 pub fn narrow_to_callable(&self) -> Type {
642 self.filter(|t| {
643 t.is_callable()
644 || t.is_string()
645 || t.is_array()
646 || t.is_object()
647 || matches!(t, Atomic::TMixed)
648 })
649 }
650
651 pub fn narrow_to_scalar(&self) -> Type {
653 self.filter(|t| {
654 t.is_string()
655 || t.is_int()
656 || matches!(
657 t,
658 Atomic::TFloat
659 | Atomic::TIntegralFloat
660 | Atomic::TLiteralFloat(..)
661 | Atomic::TBool
662 | Atomic::TTrue
663 | Atomic::TFalse
664 | Atomic::TScalar
665 | Atomic::TNumeric
666 | Atomic::TNumericString
667 | Atomic::TMixed
668 )
669 })
670 }
671
672 pub fn narrow_to_iterable(&self) -> Type {
675 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
676 }
677
678 pub fn narrow_to_countable(&self) -> Type {
681 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
682 }
683
684 pub fn narrow_to_resource(&self) -> Type {
688 self.filter(|t| matches!(t, Atomic::TMixed))
690 }
691
692 pub fn narrow_to_class_string(&self) -> Type {
697 let mut out = Type::empty();
698 out.from_docblock = self.from_docblock;
699 for t in &self.types {
700 match t {
701 Atomic::TClassString(_) => out.add_type(t.clone()),
702 _ if t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar) => {
703 out.add_type(Atomic::TClassString(None));
704 }
705 _ => {}
706 }
707 }
708 out
709 }
710
711 pub fn merge(a: &Type, b: &Type) -> Type {
716 if b.types.is_empty() {
718 let mut result = a.clone();
719 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
720 return result;
721 }
722 if a.types.is_empty() {
724 let mut result = b.clone();
725 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
726 return result;
727 }
728 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
730 let mut result = a.clone();
731 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
732 return result;
733 }
734 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
736 return Type {
737 types: smallvec::smallvec![Atomic::TMixed],
738 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
739 from_docblock: a.from_docblock || b.from_docblock,
740 };
741 }
742 let mut result = a.clone();
743 result.merge_with(b);
744 result
745 }
746
747 pub fn merge_with(&mut self, other: &Type) {
749 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
750 self.possibly_undefined |= other.possibly_undefined;
751 return;
752 }
753 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
754 self.types.clear();
755 self.types.push(Atomic::TMixed);
756 self.possibly_undefined |= other.possibly_undefined;
757 return;
758 }
759 for atomic in &other.types {
760 self.add_type(atomic.clone());
761 }
762 self.possibly_undefined |= other.possibly_undefined;
763 }
764
765 pub fn intersect_with(&self, other: &Type) -> Type {
769 if self.is_mixed() {
770 return other.clone();
771 }
772 if other.is_mixed() {
773 return self.clone();
774 }
775 let mut result = Type::empty();
777 for a in &self.types {
778 for b in &other.types {
779 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
780 result.add_type(a.clone());
781 break;
782 }
783 }
784 }
785 if result.is_empty() {
786 Type::never()
787 } else {
788 result
789 }
790 }
791
792 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
796 if bindings.is_empty() {
797 return self.clone();
798 }
799 let mut result = Type::empty();
800 result.possibly_undefined = self.possibly_undefined;
801 result.from_docblock = self.from_docblock;
802 for atomic in &self.types {
803 match atomic {
804 Atomic::TTemplateParam { name, .. } => {
805 if let Some(resolved) = bindings.get(name) {
806 for t in &resolved.types {
807 result.add_type(t.clone());
808 }
809 } else {
810 result.add_type(atomic.clone());
811 }
812 }
813 Atomic::TArray { key, value } => {
814 result.add_type(Atomic::TArray {
815 key: Box::new(key.substitute_templates(bindings)),
816 value: Box::new(value.substitute_templates(bindings)),
817 });
818 }
819 Atomic::TList { value } => {
820 result.add_type(Atomic::TList {
821 value: Box::new(value.substitute_templates(bindings)),
822 });
823 }
824 Atomic::TNonEmptyArray { key, value } => {
825 result.add_type(Atomic::TNonEmptyArray {
826 key: Box::new(key.substitute_templates(bindings)),
827 value: Box::new(value.substitute_templates(bindings)),
828 });
829 }
830 Atomic::TNonEmptyList { value } => {
831 result.add_type(Atomic::TNonEmptyList {
832 value: Box::new(value.substitute_templates(bindings)),
833 });
834 }
835 Atomic::TKeyedArray {
836 properties,
837 is_open,
838 is_list,
839 } => {
840 use crate::atomic::KeyedProperty;
841 let new_props = properties
842 .iter()
843 .map(|(k, prop)| {
844 (
845 k.clone(),
846 KeyedProperty {
847 ty: prop.ty.substitute_templates(bindings),
848 optional: prop.optional,
849 },
850 )
851 })
852 .collect();
853 result.add_type(Atomic::TKeyedArray {
854 properties: new_props,
855 is_open: *is_open,
856 is_list: *is_list,
857 });
858 }
859 Atomic::TCallable {
860 params,
861 return_type,
862 } => {
863 result.add_type(Atomic::TCallable {
864 params: params.as_ref().map(|ps| {
865 ps.iter()
866 .map(|p| substitute_in_fn_param(p, bindings))
867 .collect()
868 }),
869 return_type: return_type
870 .as_ref()
871 .map(|r| Box::new(r.substitute_templates(bindings))),
872 });
873 }
874 Atomic::TClosure {
875 params,
876 return_type,
877 this_type,
878 } => {
879 result.add_type(Atomic::TClosure {
880 params: params
881 .iter()
882 .map(|p| substitute_in_fn_param(p, bindings))
883 .collect(),
884 return_type: Box::new(return_type.substitute_templates(bindings)),
885 this_type: this_type
886 .as_ref()
887 .map(|t| Box::new(t.substitute_templates(bindings))),
888 });
889 }
890 Atomic::TConditional {
891 param_name,
892 subject,
893 if_true,
894 if_false,
895 } => {
896 let new_subject = subject.substitute_templates(bindings);
897 let new_if_true = if_true.substitute_templates(bindings);
898 let new_if_false = if_false.substitute_templates(bindings);
899
900 let resolved = if let Some(name) = param_name {
904 if let Some(bound) = bindings.get(name) {
905 if new_subject.types.len() == 1 {
906 resolve_conditional_branch(
907 &new_subject.types[0],
908 bound,
909 &new_if_true,
910 &new_if_false,
911 )
912 } else {
913 None
914 }
915 } else {
916 None
917 }
918 } else {
919 None
920 };
921
922 if let Some(branch) = resolved {
923 for t in branch.types {
924 result.add_type(t);
925 }
926 } else {
927 result.add_type(Atomic::TConditional {
928 param_name: *param_name,
929 subject: Box::new(new_subject),
930 if_true: Box::new(new_if_true),
931 if_false: Box::new(new_if_false),
932 });
933 }
934 }
935 Atomic::TIntersection { parts } => {
936 result.add_type(Atomic::TIntersection {
937 parts: vec_to_type_params(
938 parts
939 .iter()
940 .map(|p| p.substitute_templates(bindings))
941 .collect(),
942 ),
943 });
944 }
945 Atomic::TNamedObject { fqcn, type_params } => {
946 if type_params.is_empty() && !fqcn.contains('\\') {
953 if let Some(resolved) = bindings.get(fqcn) {
954 for t in &resolved.types {
955 result.add_type(t.clone());
956 }
957 continue;
958 }
959 }
960 let new_params: Vec<Type> = type_params
961 .iter()
962 .map(|p| p.substitute_templates(bindings))
963 .collect();
964 result.add_type(Atomic::TNamedObject {
965 fqcn: *fqcn,
966 type_params: vec_to_type_params(new_params),
967 });
968 }
969 Atomic::TClassString(Some(param_name)) => {
971 if let Some(resolved) = bindings.get(param_name) {
972 for r_atomic in &resolved.types {
973 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
974 Some(*fqcn)
975 } else {
976 None
977 };
978 result.add_type(Atomic::TClassString(cls_name));
979 }
980 } else {
981 result.add_type(atomic.clone());
982 }
983 }
984 _ => {
985 result.add_type(atomic.clone());
986 }
987 }
988 }
989 result
990 }
991
992 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
998 where
999 F: Fn(&str) -> Option<Type>,
1000 {
1001 self.resolve_conditional_inner(&lookup)
1002 }
1003
1004 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
1005 where
1006 F: Fn(&str) -> Option<Type>,
1007 {
1008 let mut result = Type::empty();
1009 for atomic in self.types {
1010 match atomic {
1011 Atomic::TConditional {
1012 ref param_name,
1013 ref subject,
1014 ref if_true,
1015 ref if_false,
1016 } => {
1017 let resolved = if subject.types.len() == 1 {
1018 if let Some(name) = param_name {
1019 if let Some(arg_ty) = lookup(name.as_ref()) {
1020 resolve_conditional_branch(
1021 &subject.types[0],
1022 &arg_ty,
1023 if_true,
1024 if_false,
1025 )
1026 } else {
1027 None
1028 }
1029 } else {
1030 None
1031 }
1032 } else {
1033 None
1034 };
1035
1036 if let Some(branch) = resolved {
1037 for t in branch.resolve_conditional_inner(lookup).types {
1039 result.add_type(t);
1040 }
1041 } else {
1042 for t in if_true.clone().resolve_conditional_inner(lookup).types {
1045 result.add_type(t);
1046 }
1047 for t in if_false.clone().resolve_conditional_inner(lookup).types {
1048 result.add_type(t);
1049 }
1050 }
1051 }
1052 other => result.add_type(other),
1053 }
1054 }
1055 result
1056 }
1057
1058 pub fn is_subtype_structural(&self, other: &Type) -> bool {
1068 if other.is_mixed() {
1069 return true;
1070 }
1071 if self.is_never() {
1072 return true; }
1074 self.types
1075 .iter()
1076 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
1077 }
1078
1079 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
1082 let mut result = Type::empty();
1083 result.possibly_undefined = self.possibly_undefined;
1084 result.from_docblock = self.from_docblock;
1085 for atomic in &self.types {
1086 if f(atomic) {
1087 result.types.push(atomic.clone());
1088 }
1089 }
1090 result
1091 }
1092
1093 pub fn possibly_undefined(mut self) -> Self {
1095 self.possibly_undefined = true;
1096 self
1097 }
1098
1099 pub fn from_docblock(mut self) -> Self {
1101 self.from_docblock = true;
1102 self
1103 }
1104}
1105
1106fn is_string_atomic(a: &Atomic) -> bool {
1111 matches!(
1112 a,
1113 Atomic::TString
1114 | Atomic::TNonEmptyString
1115 | Atomic::TLiteralString(_)
1116 | Atomic::TNumericString
1117 | Atomic::TClassString(_)
1118 | Atomic::TCallableString
1119 )
1120}
1121
1122fn is_array_atomic(a: &Atomic) -> bool {
1123 matches!(
1124 a,
1125 Atomic::TArray { .. }
1126 | Atomic::TNonEmptyArray { .. }
1127 | Atomic::TKeyedArray { .. }
1128 | Atomic::TList { .. }
1129 | Atomic::TNonEmptyList { .. }
1130 )
1131}
1132
1133fn is_list_atomic(a: &Atomic) -> bool {
1134 match a {
1135 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
1136 Atomic::TKeyedArray { is_list, .. } => *is_list,
1137 _ => false,
1138 }
1139}
1140
1141fn resolve_conditional_branch(
1147 subject: &Atomic,
1148 arg_ty: &Type,
1149 if_true: &Type,
1150 if_false: &Type,
1151) -> Option<Type> {
1152 let predicate: fn(&Atomic) -> bool = match subject {
1153 Atomic::TNull => |a| matches!(a, Atomic::TNull),
1154 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
1155 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
1156 Atomic::TString => is_string_atomic,
1157 Atomic::TList { .. } => is_list_atomic,
1158 Atomic::TArray { .. } => is_array_atomic,
1159 _ => return None,
1160 };
1161
1162 if arg_ty.types.is_empty() {
1163 return None;
1164 }
1165 let all_match = arg_ty.types.iter().all(&predicate);
1166 let none_match = !arg_ty.types.iter().any(predicate);
1167 if all_match {
1168 Some(if_true.clone())
1169 } else if none_match {
1170 Some(if_false.clone())
1171 } else {
1172 None
1173 }
1174}
1175
1176fn substitute_in_fn_param(
1181 p: &crate::atomic::FnParam,
1182 bindings: &FxHashMap<Name, Type>,
1183) -> crate::atomic::FnParam {
1184 crate::atomic::FnParam {
1185 name: p.name,
1186 ty: p.ty.as_ref().map(|t| {
1187 let u = t.to_union();
1188 let substituted = u.substitute_templates(bindings);
1189 crate::compact::SimpleType::from_union(substituted)
1190 }),
1191 out_ty: p.out_ty.as_ref().map(|t| {
1192 let u = t.to_union();
1193 let substituted = u.substitute_templates(bindings);
1194 crate::compact::SimpleType::from_union(substituted)
1195 }),
1196 default: p.default.as_ref().map(|d| {
1197 let u = d.to_union();
1198 let substituted = u.substitute_templates(bindings);
1199 crate::compact::SimpleType::from_union(substituted)
1200 }),
1201 is_variadic: p.is_variadic,
1202 is_byref: p.is_byref,
1203 is_optional: p.is_optional,
1204 }
1205}
1206
1207fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1212 if sub == sup {
1213 return true;
1214 }
1215 match (sub, sup) {
1216 (Atomic::TNever, _) => true,
1218 (_, Atomic::TMixed) => true,
1220 (Atomic::TMixed, _) => true,
1221 (_, Atomic::TTemplateParam { as_type, .. }) => {
1226 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1227 }
1228
1229 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1231 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1232 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1233 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1234 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1235 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1236 (Atomic::TPositiveInt, Atomic::TInt) => true,
1237 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1238 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1239 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1240 (Atomic::TNegativeInt, Atomic::TInt) => true,
1241 (Atomic::TNegativeInt, Atomic::TNumeric) => true,
1242 (Atomic::TNegativeInt, Atomic::TScalar) => true,
1243 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1244 (Atomic::TNonNegativeInt, Atomic::TNumeric) => true,
1245 (Atomic::TNonNegativeInt, Atomic::TScalar) => true,
1246 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1247 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1248 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1249 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1251 max.is_none() && min.is_none_or(|m| m <= 1)
1252 }
1253 (Atomic::TNegativeInt, Atomic::TIntRange { min, max }) => {
1255 min.is_none() && max.is_none_or(|m| m >= -1)
1256 }
1257 (Atomic::TNonNegativeInt, Atomic::TIntRange { min, max }) => {
1259 max.is_none() && min.is_none_or(|m| m <= 0)
1260 }
1261 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TPositiveInt) => {
1263 sub_min.is_some_and(|lo| lo >= 1)
1264 }
1265 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TNonNegativeInt) => {
1266 sub_min.is_some_and(|lo| lo >= 0)
1267 }
1268 (Atomic::TIntRange { max: sub_max, .. }, Atomic::TNegativeInt) => {
1269 sub_max.is_some_and(|hi| hi <= -1)
1270 }
1271 (
1273 Atomic::TIntRange {
1274 min: sub_min,
1275 max: sub_max,
1276 },
1277 Atomic::TIntRange {
1278 min: sup_min,
1279 max: sup_max,
1280 },
1281 ) => {
1282 let lower_ok = match (sub_min, sup_min) {
1283 (_, None) => true,
1284 (None, Some(_)) => false,
1285 (Some(sl), Some(su)) => sl >= su,
1286 };
1287 let upper_ok = match (sub_max, sup_max) {
1288 (None, None) | (Some(_), None) => true,
1289 (None, Some(_)) => false,
1290 (Some(sl), Some(su)) => sl <= su,
1291 };
1292 lower_ok && upper_ok
1293 }
1294
1295 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1296 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1297 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1298
1299 (Atomic::TLiteralString(s), Atomic::TString) => {
1300 let _ = s;
1301 true
1302 }
1303 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1304 let _ = s;
1305 true
1306 }
1307 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1308 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1309 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1312 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1313 (Atomic::TNonEmptyString, Atomic::TString) => true,
1314 (Atomic::TCallableString, Atomic::TString) => true,
1315 (Atomic::TNumericString, Atomic::TNonEmptyString) => true,
1317 (Atomic::TNumericString, Atomic::TString) => true,
1318 (Atomic::TClassString(_), Atomic::TString) => true,
1319 (Atomic::TInterfaceString, Atomic::TString) => true,
1320 (Atomic::TEnumString, Atomic::TString) => true,
1321 (Atomic::TTraitString, Atomic::TString) => true,
1322
1323 (Atomic::TTrue, Atomic::TBool) => true,
1324 (Atomic::TFalse, Atomic::TBool) => true,
1325
1326 (Atomic::TInt, Atomic::TNumeric) => true,
1327 (Atomic::TFloat, Atomic::TNumeric) => true,
1328 (Atomic::TIntegralFloat, Atomic::TNumeric) => true,
1329 (Atomic::TNumericString, Atomic::TNumeric) => true,
1330
1331 (Atomic::TInt, Atomic::TScalar) => true,
1332 (Atomic::TFloat, Atomic::TScalar) => true,
1333 (Atomic::TIntegralFloat, Atomic::TScalar) => true,
1334 (Atomic::TString, Atomic::TScalar) => true,
1335 (Atomic::TBool, Atomic::TScalar) => true,
1336 (Atomic::TNumeric, Atomic::TScalar) => true,
1337 (Atomic::TTrue, Atomic::TScalar) => true,
1338 (Atomic::TFalse, Atomic::TScalar) => true,
1339
1340 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1342 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1343 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1344 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1346 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1347 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1349 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1350 (
1354 Atomic::TNamedObject {
1355 fqcn: sub_fqcn,
1356 type_params: sub_params,
1357 },
1358 Atomic::TNamedObject {
1359 fqcn: sup_fqcn,
1360 type_params: sup_params,
1361 },
1362 ) => {
1363 sub_fqcn == sup_fqcn
1364 && (sup_params.is_empty() || type_params_compatible(sub_params, sup_params))
1365 }
1366
1367 (Atomic::TIntegralFloat, Atomic::TFloat) => true,
1369
1370 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1372 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1373 (Atomic::TNegativeInt, Atomic::TFloat) => true,
1374 (Atomic::TNonNegativeInt, Atomic::TFloat) => true,
1375 (Atomic::TInt, Atomic::TFloat) => true,
1376 (Atomic::TIntRange { .. }, Atomic::TFloat) => true,
1377
1378 (Atomic::TLiteralInt(n), Atomic::TIntRange { min, max }) => {
1380 min.is_none_or(|lo| *n >= lo) && max.is_none_or(|hi| *n <= hi)
1381 }
1382
1383 (Atomic::TString, Atomic::TCallable { .. }) => true,
1385 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1386 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1387 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1388 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1389 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1390
1391 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1393 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1395 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1397 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1399 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1401 fqcn.as_ref().eq_ignore_ascii_case("closure")
1402 }
1403 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1404 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1406 fqcn.as_ref().eq_ignore_ascii_case("closure")
1407 }
1408 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1410 fqcn.as_ref().eq_ignore_ascii_case("closure")
1411 }
1412
1413 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1415 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1416 }
1417 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1418 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1419 }
1420 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1421 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1422 }
1423 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1424 value.is_subtype_structural(lv)
1425 }
1426 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1428 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1429 && av.is_subtype_structural(lv)
1430 }
1431 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1432 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1433 && av.is_subtype_structural(lv)
1434 }
1435 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1436 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1437 && av.is_subtype_structural(lv)
1438 }
1439 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1440 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1441 && av.is_subtype_structural(lv)
1442 }
1443 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1445 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1446 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1447 }
1448
1449 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1451 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1452 }
1453
1454 (
1460 Atomic::TKeyedArray {
1461 properties,
1462 is_open,
1463 ..
1464 },
1465 Atomic::TArray { key, value },
1466 ) => {
1467 if *is_open {
1468 return true;
1469 }
1470 properties.iter().all(|(prop_key, prop)| {
1471 let key_atomic = match prop_key {
1472 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1473 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1474 };
1475 if !Type::single(key_atomic).is_subtype_structural(key) {
1476 return false; }
1478 let has_named_obj = prop.ty.types.iter().any(|a| {
1480 matches!(
1481 a,
1482 Atomic::TNamedObject { .. }
1483 | Atomic::TSelf { .. }
1484 | Atomic::TStaticObject { .. }
1485 | Atomic::TClosure { .. }
1486 | Atomic::TTemplateParam { .. }
1487 )
1488 });
1489 has_named_obj || prop.ty.is_subtype_structural(value)
1490 })
1491 }
1492 (
1493 Atomic::TKeyedArray {
1494 properties,
1495 is_open,
1496 ..
1497 },
1498 Atomic::TNonEmptyArray { key, value },
1499 ) => {
1500 if *is_open {
1501 return !properties.is_empty();
1502 }
1503 properties.iter().any(|(_, p)| !p.optional)
1504 && properties.iter().all(|(prop_key, prop)| {
1505 let key_atomic = match prop_key {
1506 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1507 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1508 };
1509 if !Type::single(key_atomic).is_subtype_structural(key) {
1510 return false;
1511 }
1512 let has_named_obj = prop.ty.types.iter().any(|a| {
1513 matches!(
1514 a,
1515 Atomic::TNamedObject { .. }
1516 | Atomic::TSelf { .. }
1517 | Atomic::TStaticObject { .. }
1518 | Atomic::TClosure { .. }
1519 | Atomic::TTemplateParam { .. }
1520 )
1521 });
1522 has_named_obj || prop.ty.is_subtype_structural(value)
1523 })
1524 }
1525
1526 (
1528 Atomic::TKeyedArray {
1529 properties,
1530 is_list,
1531 ..
1532 },
1533 Atomic::TList { value: lv },
1534 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1535 (
1536 Atomic::TKeyedArray {
1537 properties,
1538 is_list,
1539 ..
1540 },
1541 Atomic::TNonEmptyList { value: lv },
1542 ) => {
1543 *is_list
1544 && !properties.is_empty()
1545 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1546 }
1547
1548 _ => false,
1549 }
1550}
1551
1552fn type_params_compatible(sub: &[Type], sup: &[Type]) -> bool {
1558 if sub.len() != sup.len() {
1559 return false;
1560 }
1561 sub.iter()
1562 .zip(sup.iter())
1563 .all(|(a, b)| a == b || (is_empty_array_literal(a) && is_array_like(b)))
1564}
1565
1566fn is_empty_array_literal(t: &Type) -> bool {
1569 !t.types.is_empty()
1570 && t.types.iter().all(
1571 |atom| matches!(atom, Atomic::TKeyedArray { properties, .. } if properties.is_empty()),
1572 )
1573}
1574
1575fn is_array_like(t: &Type) -> bool {
1577 !t.types.is_empty() && t.types.iter().all(|atom| atom.is_array())
1578}
1579
1580#[cfg(test)]
1585mod tests {
1586 use std::sync::Arc;
1587
1588 use super::*;
1589
1590 #[test]
1591 fn single_is_single() {
1592 let u = Type::single(Atomic::TString);
1593 assert!(u.is_single());
1594 assert!(!u.is_nullable());
1595 }
1596
1597 #[test]
1598 fn nullable_has_null() {
1599 let u = Type::nullable(Atomic::TString);
1600 assert!(u.is_nullable());
1601 assert_eq!(u.types.len(), 2);
1602 }
1603
1604 #[test]
1605 fn add_type_deduplicates() {
1606 let mut u = Type::single(Atomic::TString);
1607 u.add_type(Atomic::TString);
1608 assert_eq!(u.types.len(), 1);
1609 }
1610
1611 #[test]
1612 fn add_type_literal_subsumed_by_base() {
1613 let mut u = Type::single(Atomic::TInt);
1614 u.add_type(Atomic::TLiteralInt(42));
1615 assert_eq!(u.types.len(), 1);
1616 assert!(matches!(u.types[0], Atomic::TInt));
1617 }
1618
1619 #[test]
1620 fn add_type_base_widens_literals() {
1621 let mut u = Type::single(Atomic::TLiteralInt(1));
1622 u.add_type(Atomic::TLiteralInt(2));
1623 u.add_type(Atomic::TInt);
1624 assert_eq!(u.types.len(), 1);
1625 assert!(matches!(u.types[0], Atomic::TInt));
1626 }
1627
1628 #[test]
1629 fn mixed_subsumes_everything() {
1630 let mut u = Type::single(Atomic::TString);
1631 u.add_type(Atomic::TMixed);
1632 assert_eq!(u.types.len(), 1);
1633 assert!(u.is_mixed());
1634 }
1635
1636 #[test]
1637 fn remove_null() {
1638 let u = Type::nullable(Atomic::TString);
1639 let narrowed = u.remove_null();
1640 assert!(!narrowed.is_nullable());
1641 assert_eq!(narrowed.types.len(), 1);
1642 }
1643
1644 #[test]
1645 fn narrow_to_truthy_removes_null_false() {
1646 let mut u = Type::empty();
1647 u.add_type(Atomic::TString);
1648 u.add_type(Atomic::TNull);
1649 u.add_type(Atomic::TFalse);
1650 let truthy = u.narrow_to_truthy();
1651 assert!(!truthy.is_nullable());
1652 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1653 }
1654
1655 #[test]
1656 fn merge_combines_types() {
1657 let a = Type::single(Atomic::TString);
1658 let b = Type::single(Atomic::TInt);
1659 let merged = Type::merge(&a, &b);
1660 assert_eq!(merged.types.len(), 2);
1661 }
1662
1663 #[test]
1664 fn subtype_literal_int_under_int() {
1665 let sub = Type::single(Atomic::TLiteralInt(5));
1666 let sup = Type::single(Atomic::TInt);
1667 assert!(sub.is_subtype_structural(&sup));
1668 }
1669
1670 #[test]
1671 fn subtype_never_is_bottom() {
1672 let never = Type::never();
1673 let string = Type::single(Atomic::TString);
1674 assert!(never.is_subtype_structural(&string));
1675 }
1676
1677 #[test]
1678 fn subtype_everything_under_mixed() {
1679 let string = Type::single(Atomic::TString);
1680 let mixed = Type::mixed();
1681 assert!(string.is_subtype_structural(&mixed));
1682 }
1683
1684 #[test]
1685 fn template_substitution() {
1686 let mut bindings = FxHashMap::default();
1687 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1688
1689 let tmpl = Type::single(Atomic::TTemplateParam {
1690 name: Name::new("T"),
1691 as_type: Box::new(Type::mixed()),
1692 defining_entity: Name::new("MyClass"),
1693 });
1694
1695 let resolved = tmpl.substitute_templates(&bindings);
1696 assert_eq!(resolved.types.len(), 1);
1697 assert!(matches!(resolved.types[0], Atomic::TString));
1698 }
1699
1700 #[test]
1701 fn intersection_is_object() {
1702 let parts = vec![
1703 Type::single(Atomic::TNamedObject {
1704 fqcn: Name::new("Iterator"),
1705 type_params: empty_type_params(),
1706 }),
1707 Type::single(Atomic::TNamedObject {
1708 fqcn: Name::new("Countable"),
1709 type_params: empty_type_params(),
1710 }),
1711 ];
1712 let atomic = Atomic::TIntersection {
1713 parts: vec_to_type_params(parts),
1714 };
1715 assert!(atomic.is_object());
1716 assert!(!atomic.can_be_falsy());
1717 assert!(atomic.can_be_truthy());
1718 }
1719
1720 #[test]
1721 fn intersection_display_two_parts() {
1722 let parts = vec![
1723 Type::single(Atomic::TNamedObject {
1724 fqcn: Name::new("Iterator"),
1725 type_params: empty_type_params(),
1726 }),
1727 Type::single(Atomic::TNamedObject {
1728 fqcn: Name::new("Countable"),
1729 type_params: empty_type_params(),
1730 }),
1731 ];
1732 let u = Type::single(Atomic::TIntersection {
1733 parts: vec_to_type_params(parts),
1734 });
1735 assert_eq!(format!("{u}"), "Iterator&Countable");
1736 }
1737
1738 #[test]
1739 fn intersection_display_three_parts() {
1740 let parts = vec![
1741 Type::single(Atomic::TNamedObject {
1742 fqcn: Name::new("A"),
1743 type_params: empty_type_params(),
1744 }),
1745 Type::single(Atomic::TNamedObject {
1746 fqcn: Name::new("B"),
1747 type_params: empty_type_params(),
1748 }),
1749 Type::single(Atomic::TNamedObject {
1750 fqcn: Name::new("C"),
1751 type_params: empty_type_params(),
1752 }),
1753 ];
1754 let u = Type::single(Atomic::TIntersection {
1755 parts: vec_to_type_params(parts),
1756 });
1757 assert_eq!(format!("{u}"), "A&B&C");
1758 }
1759
1760 #[test]
1761 fn intersection_in_nullable_union_display() {
1762 let intersection = Atomic::TIntersection {
1763 parts: vec_to_type_params(vec![
1764 Type::single(Atomic::TNamedObject {
1765 fqcn: Name::new("Iterator"),
1766 type_params: empty_type_params(),
1767 }),
1768 Type::single(Atomic::TNamedObject {
1769 fqcn: Name::new("Countable"),
1770 type_params: empty_type_params(),
1771 }),
1772 ]),
1773 };
1774 let mut u = Type::single(intersection);
1775 u.add_type(Atomic::TNull);
1776 assert!(u.is_nullable());
1777 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1778 }
1779
1780 fn t_param(name: &str) -> Type {
1783 Type::single(Atomic::TTemplateParam {
1784 name: Name::new(name),
1785 as_type: Box::new(Type::mixed()),
1786 defining_entity: Name::new("Fn"),
1787 })
1788 }
1789
1790 fn bindings_t_string() -> FxHashMap<Name, Type> {
1791 let mut b = FxHashMap::default();
1792 b.insert(Name::new("T"), Type::single(Atomic::TString));
1793 b
1794 }
1795
1796 #[test]
1797 fn substitute_non_empty_array_key_and_value() {
1798 let ty = Type::single(Atomic::TNonEmptyArray {
1799 key: Box::new(t_param("T")),
1800 value: Box::new(t_param("T")),
1801 });
1802 let result = ty.substitute_templates(&bindings_t_string());
1803 assert_eq!(result.types.len(), 1);
1804 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1805 panic!("expected TNonEmptyArray");
1806 };
1807 assert!(matches!(key.types[0], Atomic::TString));
1808 assert!(matches!(value.types[0], Atomic::TString));
1809 }
1810
1811 #[test]
1812 fn substitute_non_empty_list_value() {
1813 let ty = Type::single(Atomic::TNonEmptyList {
1814 value: Box::new(t_param("T")),
1815 });
1816 let result = ty.substitute_templates(&bindings_t_string());
1817 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1818 panic!("expected TNonEmptyList");
1819 };
1820 assert!(matches!(value.types[0], Atomic::TString));
1821 }
1822
1823 #[test]
1824 fn substitute_keyed_array_property_types() {
1825 use crate::atomic::{ArrayKey, KeyedProperty};
1826 use indexmap::IndexMap;
1827 let mut props = IndexMap::new();
1828 props.insert(
1829 ArrayKey::String(Arc::from("name")),
1830 KeyedProperty {
1831 ty: t_param("T"),
1832 optional: false,
1833 },
1834 );
1835 props.insert(
1836 ArrayKey::String(Arc::from("tag")),
1837 KeyedProperty {
1838 ty: t_param("T"),
1839 optional: true,
1840 },
1841 );
1842 let ty = Type::single(Atomic::TKeyedArray {
1843 properties: props,
1844 is_open: true,
1845 is_list: false,
1846 });
1847 let result = ty.substitute_templates(&bindings_t_string());
1848 let Atomic::TKeyedArray {
1849 properties,
1850 is_open,
1851 is_list,
1852 } = &result.types[0]
1853 else {
1854 panic!("expected TKeyedArray");
1855 };
1856 assert!(is_open);
1857 assert!(!is_list);
1858 assert!(matches!(
1859 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1860 Atomic::TString
1861 ));
1862 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1863 assert!(matches!(
1864 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1865 Atomic::TString
1866 ));
1867 }
1868
1869 #[test]
1870 fn substitute_callable_params_and_return() {
1871 use crate::atomic::FnParam;
1872 let ty = Type::single(Atomic::TCallable {
1873 params: Some(vec![FnParam {
1874 name: Name::new("x"),
1875 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1876 out_ty: None,
1877 default: None,
1878 is_variadic: false,
1879 is_byref: false,
1880 is_optional: false,
1881 }]),
1882 return_type: Some(Box::new(t_param("T"))),
1883 });
1884 let result = ty.substitute_templates(&bindings_t_string());
1885 let Atomic::TCallable {
1886 params,
1887 return_type,
1888 } = &result.types[0]
1889 else {
1890 panic!("expected TCallable");
1891 };
1892 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1893 let param_union = param_ty.to_union();
1894 assert!(matches!(param_union.types[0], Atomic::TString));
1895 let ret = return_type.as_ref().unwrap();
1896 assert!(matches!(ret.types[0], Atomic::TString));
1897 }
1898
1899 #[test]
1900 fn substitute_callable_bare_no_panic() {
1901 let ty = Type::single(Atomic::TCallable {
1903 params: None,
1904 return_type: None,
1905 });
1906 let result = ty.substitute_templates(&bindings_t_string());
1907 assert!(matches!(
1908 result.types[0],
1909 Atomic::TCallable {
1910 params: None,
1911 return_type: None
1912 }
1913 ));
1914 }
1915
1916 #[test]
1917 fn substitute_closure_params_return_and_this() {
1918 use crate::atomic::FnParam;
1919 let ty = Type::single(Atomic::TClosure {
1920 params: vec![FnParam {
1921 name: Name::new("a"),
1922 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1923 out_ty: None,
1924 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1925 is_variadic: true,
1926 is_byref: true,
1927 is_optional: true,
1928 }],
1929 return_type: Box::new(t_param("T")),
1930 this_type: Some(Box::new(t_param("T"))),
1931 });
1932 let result = ty.substitute_templates(&bindings_t_string());
1933 let Atomic::TClosure {
1934 params,
1935 return_type,
1936 this_type,
1937 } = &result.types[0]
1938 else {
1939 panic!("expected TClosure");
1940 };
1941 let p = ¶ms[0];
1942 let ty_union = p.ty.as_ref().unwrap().to_union();
1943 let default_union = p.default.as_ref().unwrap().to_union();
1944 assert!(matches!(ty_union.types[0], Atomic::TString));
1945 assert!(matches!(default_union.types[0], Atomic::TString));
1946 assert!(p.is_variadic);
1948 assert!(p.is_byref);
1949 assert!(p.is_optional);
1950 assert!(matches!(return_type.types[0], Atomic::TString));
1951 assert!(matches!(
1952 this_type.as_ref().unwrap().types[0],
1953 Atomic::TString
1954 ));
1955 }
1956
1957 #[test]
1958 fn substitute_conditional_all_branches() {
1959 let ty = Type::single(Atomic::TConditional {
1960 param_name: None,
1961 subject: Box::new(t_param("T")),
1962 if_true: Box::new(t_param("T")),
1963 if_false: Box::new(Type::single(Atomic::TInt)),
1964 });
1965 let result = ty.substitute_templates(&bindings_t_string());
1966 let Atomic::TConditional {
1967 param_name: _,
1968 subject,
1969 if_true,
1970 if_false,
1971 } = &result.types[0]
1972 else {
1973 panic!("expected TConditional");
1974 };
1975 assert!(matches!(subject.types[0], Atomic::TString));
1976 assert!(matches!(if_true.types[0], Atomic::TString));
1977 assert!(matches!(if_false.types[0], Atomic::TInt));
1978 }
1979
1980 #[test]
1981 fn resolve_conditional_is_null_non_null_arg() {
1982 let ty = Type::single(Atomic::TConditional {
1983 param_name: Some(Name::new("x")),
1984 subject: Box::new(Type::single(Atomic::TNull)),
1985 if_true: Box::new(Type::single(Atomic::TInt)),
1986 if_false: Box::new(Type::single(Atomic::TString)),
1987 });
1988 let result = ty.resolve_conditional_returns(|name| {
1989 if name == "x" {
1990 Some(Type::single(Atomic::TString)) } else {
1992 None
1993 }
1994 });
1995 assert!(result.types.len() == 1);
1996 assert!(matches!(result.types[0], Atomic::TString));
1997 }
1998
1999 #[test]
2000 fn resolve_conditional_is_null_null_arg() {
2001 let ty = Type::single(Atomic::TConditional {
2002 param_name: Some(Name::new("x")),
2003 subject: Box::new(Type::single(Atomic::TNull)),
2004 if_true: Box::new(Type::single(Atomic::TInt)),
2005 if_false: Box::new(Type::single(Atomic::TString)),
2006 });
2007 let result = ty.resolve_conditional_returns(|name| {
2008 if name == "x" {
2009 Some(Type::single(Atomic::TNull)) } else {
2011 None
2012 }
2013 });
2014 assert!(result.types.len() == 1);
2015 assert!(matches!(result.types[0], Atomic::TInt));
2016 }
2017
2018 #[test]
2019 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
2020 let mut nullable_str = Type::single(Atomic::TString);
2021 nullable_str.add_type(Atomic::TNull);
2022 let ty = Type::single(Atomic::TConditional {
2023 param_name: Some(Name::new("x")),
2024 subject: Box::new(Type::single(Atomic::TNull)),
2025 if_true: Box::new(Type::single(Atomic::TInt)),
2026 if_false: Box::new(Type::single(Atomic::TString)),
2027 });
2028 let result = ty.resolve_conditional_returns(|name| {
2029 if name == "x" {
2030 Some(nullable_str.clone())
2031 } else {
2032 None
2033 }
2034 });
2035 assert_eq!(result.types.len(), 2);
2037 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2038 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2039 }
2040
2041 #[test]
2042 fn resolve_conditional_nested_widens_inner_branch() {
2043 let inner = Type::single(Atomic::TConditional {
2046 param_name: Some(Name::new("x")),
2047 subject: Box::new(Type::single(Atomic::TString)),
2048 if_true: Box::new(Type::single(Atomic::TString)),
2049 if_false: Box::new(Type::single(Atomic::TFloat)),
2050 });
2051 let ty = Type::single(Atomic::TConditional {
2052 param_name: Some(Name::new("x")),
2053 subject: Box::new(Type::single(Atomic::TNull)),
2054 if_true: Box::new(Type::single(Atomic::TInt)),
2055 if_false: Box::new(inner),
2056 });
2057 let result = ty.resolve_conditional_returns(|_| None);
2059 assert!(
2060 result
2061 .types
2062 .iter()
2063 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2064 "no TConditional should survive: {:?}",
2065 result.types
2066 );
2067 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
2068 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
2069 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
2070 }
2071
2072 #[test]
2073 fn resolve_conditional_nested_resolves_inner_branch() {
2074 let inner = Type::single(Atomic::TConditional {
2078 param_name: Some(Name::new("x")),
2079 subject: Box::new(Type::single(Atomic::TString)),
2080 if_true: Box::new(Type::single(Atomic::TString)),
2081 if_false: Box::new(Type::single(Atomic::TFloat)),
2082 });
2083 let ty = Type::single(Atomic::TConditional {
2084 param_name: Some(Name::new("x")),
2085 subject: Box::new(Type::single(Atomic::TNull)),
2086 if_true: Box::new(Type::single(Atomic::TInt)),
2087 if_false: Box::new(inner),
2088 });
2089 let result = ty.resolve_conditional_returns(|name| {
2091 if name == "x" {
2092 Some(Type::single(Atomic::TString))
2093 } else {
2094 None
2095 }
2096 });
2097 assert!(
2098 result
2099 .types
2100 .iter()
2101 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2102 "no TConditional should survive: {:?}",
2103 result.types
2104 );
2105 assert_eq!(result.types.len(), 1);
2106 assert!(matches!(result.types[0], Atomic::TString));
2107 }
2108
2109 #[test]
2110 fn substitute_intersection_parts() {
2111 let ty = Type::single(Atomic::TIntersection {
2112 parts: vec_to_type_params(vec![
2113 Type::single(Atomic::TNamedObject {
2114 fqcn: Name::new("Countable"),
2115 type_params: empty_type_params(),
2116 }),
2117 t_param("T"),
2118 ]),
2119 });
2120 let result = ty.substitute_templates(&bindings_t_string());
2121 let Atomic::TIntersection { parts } = &result.types[0] else {
2122 panic!("expected TIntersection");
2123 };
2124 assert_eq!(parts.len(), 2);
2125 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
2126 assert!(matches!(parts[1].types[0], Atomic::TString));
2127 }
2128
2129 #[test]
2130 fn substitute_no_template_params_identity() {
2131 let ty = Type::single(Atomic::TInt);
2132 let result = ty.substitute_templates(&bindings_t_string());
2133 assert!(matches!(result.types[0], Atomic::TInt));
2134 }
2135}