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_never(&self) -> bool {
157 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
158 }
159
160 pub fn clone_validity(&self) -> CloneValidity {
163 if self.types.is_empty() {
164 return CloneValidity::Unknown;
165 }
166 let mut has_non_object = false;
167 let mut has_other = false; for t in &self.types {
169 match t {
170 Atomic::TTemplateParam { as_type, .. } => match as_type.clone_validity() {
171 CloneValidity::Invalid => has_non_object = true,
172 CloneValidity::PossiblyInvalid => {
173 has_non_object = true;
174 has_other = true;
175 }
176 CloneValidity::Cloneable | CloneValidity::Unknown => has_other = true,
177 },
178 other if other.is_definitely_non_object() => has_non_object = true,
179 _ => has_other = true,
180 }
181 }
182 match (has_non_object, has_other) {
183 (true, false) => CloneValidity::Invalid,
184 (true, true) => CloneValidity::PossiblyInvalid,
185 _ => CloneValidity::Cloneable,
186 }
187 }
188
189 pub fn is_void(&self) -> bool {
190 self.is_single() && matches!(self.types[0], Atomic::TVoid)
191 }
192
193 pub fn can_be_falsy(&self) -> bool {
194 self.types.iter().any(|t| t.can_be_falsy())
195 }
196
197 pub fn can_be_truthy(&self) -> bool {
198 self.types.iter().any(|t| t.can_be_truthy())
199 }
200
201 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
202 self.types.iter().any(f)
203 }
204
205 pub fn has_named_object(&self, fqcn: &str) -> bool {
206 self.types.iter().any(|t| match t {
207 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
208 _ => false,
209 })
210 }
211
212 pub fn add_type(&mut self, atomic: Atomic) {
217 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
219 return;
220 }
221
222 if matches!(atomic, Atomic::TMixed) {
224 self.types.clear();
225 self.types.push(Atomic::TMixed);
226 return;
227 }
228
229 let atomic = if let Atomic::TConditional {
232 param_name: _,
233 subject: _,
234 if_true,
235 if_false,
236 } = &atomic
237 {
238 let mut simplified_true = Type::empty();
239 for t in &if_true.types {
240 simplified_true.add_type(t.clone());
241 }
242 let mut simplified_false = Type::empty();
243 for t in &if_false.types {
244 simplified_false.add_type(t.clone());
245 }
246 if simplified_true == simplified_false {
247 for t in simplified_true.types {
248 self.add_type(t);
249 }
250 return;
251 }
252 atomic
253 } else {
254 atomic
255 };
256
257 if self.types.contains(&atomic) {
259 return;
260 }
261
262 if let Atomic::TLiteralInt(_) = &atomic {
264 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
265 return;
266 }
267 }
268 if let Atomic::TLiteralString(_) = &atomic {
270 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
271 return;
272 }
273 }
274 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
276 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
277 {
278 return;
279 }
280 if matches!(atomic, Atomic::TInt) {
282 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
283 }
284 if matches!(atomic, Atomic::TString) {
286 self.types
287 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
288 }
289 if matches!(atomic, Atomic::TBool) {
291 self.types
292 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
293 }
294
295 if matches!(atomic, Atomic::TNever) {
297 if !self.types.is_empty() {
298 return;
299 }
300 } else {
301 self.types.retain(|t| !matches!(t, Atomic::TNever));
302 }
303
304 if let Atomic::TKeyedArray { properties, .. } = &atomic {
307 if properties.is_empty() {
308 for existing in &self.types {
309 match existing {
310 Atomic::TArray { .. }
311 | Atomic::TNonEmptyArray { .. }
312 | Atomic::TList { .. }
313 | Atomic::TNonEmptyList { .. } => {
314 return; }
316 _ => {}
317 }
318 }
319 }
320 }
321
322 let is_generic_array_or_list = matches!(
324 &atomic,
325 Atomic::TArray { .. }
326 | Atomic::TNonEmptyArray { .. }
327 | Atomic::TList { .. }
328 | Atomic::TNonEmptyList { .. }
329 );
330 if is_generic_array_or_list {
331 self.types.retain(|t| {
332 if let Atomic::TKeyedArray { properties, .. } = t {
333 !properties.is_empty()
334 } else {
335 true
336 }
337 });
338 }
339
340 self.types.push(atomic);
341 }
342
343 pub fn remove_null(&self) -> Type {
347 self.filter(|t| !matches!(t, Atomic::TNull))
348 }
349
350 pub fn remove_false(&self) -> Type {
353 let mut result = self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool));
354 if self.types.iter().any(|t| matches!(t, Atomic::TBool)) {
355 result.add_type(Atomic::TTrue);
356 }
357 result
358 }
359
360 pub fn core_type(&self) -> Type {
362 self.remove_null().remove_false()
363 }
364
365 pub fn narrow_to_truthy(&self) -> Type {
367 if self.is_mixed() {
368 return Type::mixed();
369 }
370 let mut result = Type::empty();
371 result.from_docblock = self.from_docblock;
372 for t in &self.types {
373 match t {
374 Atomic::TLiteralInt(0)
376 | Atomic::TLiteralFloat(0, 0)
377 | Atomic::TNull
378 | Atomic::TFalse => {}
379 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => {}
380 Atomic::TBool => result.add_type(Atomic::TTrue),
382 Atomic::TArray { key, value } => result.add_type(Atomic::TNonEmptyArray {
384 key: key.clone(),
385 value: value.clone(),
386 }),
387 Atomic::TList { value } => result.add_type(Atomic::TNonEmptyList {
388 value: value.clone(),
389 }),
390 Atomic::TString => result.add_type(Atomic::TNonEmptyString),
394 Atomic::TNonNegativeInt => result.add_type(Atomic::TPositiveInt),
399 Atomic::TIntRange { min: Some(0), max } if max.is_none_or(|m| m >= 1) => {
400 let atom = if max.is_none() {
401 Atomic::TPositiveInt
402 } else {
403 Atomic::TIntRange {
404 min: Some(1),
405 max: *max,
406 }
407 };
408 result.add_type(atom);
409 }
410 Atomic::TIntRange { min, max: Some(0) } => {
412 let atom = match min {
413 None => Atomic::TNegativeInt,
414 Some(n) if *n <= -1 => Atomic::TIntRange {
415 min: *min,
416 max: Some(-1),
417 },
418 _ => continue, };
420 result.add_type(atom);
421 }
422 t if !t.can_be_truthy() => {}
424 _ => result.add_type(t.clone()),
425 }
426 }
427 result
428 }
429
430 pub fn narrow_to_falsy(&self) -> Type {
432 if self.is_mixed() {
433 return Type::from_vec(vec![
434 Atomic::TNull,
435 Atomic::TFalse,
436 Atomic::TLiteralInt(0),
437 Atomic::TLiteralString("".into()),
438 ]);
439 }
440 let mut result = Type::empty();
441 result.from_docblock = self.from_docblock;
442 for t in &self.types {
443 match t {
444 Atomic::TBool => result.add_type(Atomic::TFalse),
446 Atomic::TInt => result.add_type(Atomic::TLiteralInt(0)),
448 Atomic::TFloat => result.add_type(Atomic::TLiteralFloat(0, 0)),
450 Atomic::TString => {
452 result.add_type(Atomic::TLiteralString("".into()));
453 result.add_type(Atomic::TLiteralString("0".into()));
454 }
455 Atomic::TNumericString => result.add_type(Atomic::TLiteralString("0".into())),
457 Atomic::TNonNegativeInt => result.add_type(Atomic::TLiteralInt(0)),
459 Atomic::TIntRange {
461 min: Some(0),
462 max: Some(_) | None,
463 } => result.add_type(Atomic::TLiteralInt(0)),
464 Atomic::TIntRange { max: Some(0), .. } => result.add_type(Atomic::TLiteralInt(0)),
466 t if !t.can_be_falsy() => {} _ => result.add_type(t.clone()),
468 }
469 }
470 result
471 }
472
473 pub fn narrow_instanceof(&self, class: &str) -> Type {
479 let narrowed_ty = Atomic::TNamedObject {
480 fqcn: class.into(),
481 type_params: empty_type_params(),
482 };
483 let has_object = self.types.iter().any(|t| {
485 matches!(
486 t,
487 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
489 });
490 if has_object || self.is_empty() {
491 Type::single(narrowed_ty)
492 } else {
493 Type::single(narrowed_ty)
496 }
497 }
498
499 pub fn narrow_to_string(&self) -> Type {
501 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
502 }
503
504 pub fn narrow_to_int(&self) -> Type {
506 self.filter(|t| {
507 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
508 })
509 }
510
511 pub fn narrow_to_float(&self) -> Type {
513 self.filter(|t| {
514 matches!(
515 t,
516 Atomic::TFloat
517 | Atomic::TLiteralFloat(..)
518 | Atomic::TMixed
519 | Atomic::TScalar
520 | Atomic::TNumeric
521 )
522 })
523 }
524
525 pub fn narrow_to_bool(&self) -> Type {
527 self.filter(|t| {
528 matches!(
529 t,
530 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
531 )
532 })
533 }
534
535 pub fn narrow_to_null(&self) -> Type {
537 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
538 }
539
540 pub fn narrow_to_array(&self) -> Type {
542 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
543 }
544
545 pub fn narrow_to_non_empty_collection(&self) -> Type {
547 let mut out = Type::empty();
548 out.from_docblock = self.from_docblock;
549 for t in &self.types {
550 match t {
551 Atomic::TArray { key, value } => out.add_type(Atomic::TNonEmptyArray {
552 key: key.clone(),
553 value: value.clone(),
554 }),
555 Atomic::TList { value } => out.add_type(Atomic::TNonEmptyList {
556 value: value.clone(),
557 }),
558 _ => out.add_type(t.clone()),
559 }
560 }
561 out
562 }
563
564 pub fn narrow_to_list(&self) -> Type {
572 let mut out = Type::empty();
573 out.from_docblock = self.from_docblock;
574 for t in &self.types {
575 match t {
576 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => out.add_type(t.clone()),
577 Atomic::TArray { key, value } if matches!(key.types.as_slice(), [Atomic::TInt]) => {
578 out.add_type(Atomic::TList {
579 value: value.clone(),
580 });
581 }
582 Atomic::TNonEmptyArray { key, value }
583 if matches!(key.types.as_slice(), [Atomic::TInt]) =>
584 {
585 out.add_type(Atomic::TNonEmptyList {
586 value: value.clone(),
587 });
588 }
589 Atomic::TMixed => out.add_type(Atomic::TList {
590 value: Box::new(Type::mixed()),
591 }),
592 _ => {}
593 }
594 }
595 if out.is_empty() {
596 self.filter(|t| matches!(t, Atomic::TList { .. } | Atomic::TNonEmptyList { .. }))
597 } else {
598 out
599 }
600 }
601
602 pub fn narrow_to_object(&self) -> Type {
607 let mut out = Type::empty();
608 for t in &self.types {
609 if matches!(t, Atomic::TMixed) {
610 out.add_type(Atomic::TObject);
611 } else if t.is_object() {
612 out.add_type(t.clone());
613 }
614 }
615 if out.types.is_empty() {
616 self.filter(|t| t.is_object())
617 } else {
618 out
619 }
620 }
621
622 pub fn narrow_to_callable(&self) -> Type {
624 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
625 }
626
627 pub fn narrow_to_scalar(&self) -> Type {
629 self.filter(|t| {
630 t.is_string()
631 || t.is_int()
632 || matches!(
633 t,
634 Atomic::TFloat
635 | Atomic::TLiteralFloat(..)
636 | Atomic::TBool
637 | Atomic::TTrue
638 | Atomic::TFalse
639 | Atomic::TScalar
640 | Atomic::TNumeric
641 | Atomic::TNumericString
642 | Atomic::TMixed
643 )
644 })
645 }
646
647 pub fn narrow_to_iterable(&self) -> Type {
650 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
651 }
652
653 pub fn narrow_to_countable(&self) -> Type {
656 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
657 }
658
659 pub fn narrow_to_resource(&self) -> Type {
663 self.filter(|t| matches!(t, Atomic::TMixed))
665 }
666
667 pub fn merge(a: &Type, b: &Type) -> Type {
672 if b.types.is_empty() {
674 let mut result = a.clone();
675 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
676 return result;
677 }
678 if a.types.is_empty() {
680 let mut result = b.clone();
681 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
682 return result;
683 }
684 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
686 let mut result = a.clone();
687 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
688 return result;
689 }
690 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
692 return Type {
693 types: smallvec::smallvec![Atomic::TMixed],
694 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
695 from_docblock: a.from_docblock || b.from_docblock,
696 };
697 }
698 let mut result = a.clone();
699 result.merge_with(b);
700 result
701 }
702
703 pub fn merge_with(&mut self, other: &Type) {
705 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
706 self.possibly_undefined |= other.possibly_undefined;
707 return;
708 }
709 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
710 self.types.clear();
711 self.types.push(Atomic::TMixed);
712 self.possibly_undefined |= other.possibly_undefined;
713 return;
714 }
715 for atomic in &other.types {
716 self.add_type(atomic.clone());
717 }
718 self.possibly_undefined |= other.possibly_undefined;
719 }
720
721 pub fn intersect_with(&self, other: &Type) -> Type {
725 if self.is_mixed() {
726 return other.clone();
727 }
728 if other.is_mixed() {
729 return self.clone();
730 }
731 let mut result = Type::empty();
733 for a in &self.types {
734 for b in &other.types {
735 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
736 result.add_type(a.clone());
737 break;
738 }
739 }
740 }
741 if result.is_empty() {
742 Type::never()
743 } else {
744 result
745 }
746 }
747
748 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
752 if bindings.is_empty() {
753 return self.clone();
754 }
755 let mut result = Type::empty();
756 result.possibly_undefined = self.possibly_undefined;
757 result.from_docblock = self.from_docblock;
758 for atomic in &self.types {
759 match atomic {
760 Atomic::TTemplateParam { name, .. } => {
761 if let Some(resolved) = bindings.get(name) {
762 for t in &resolved.types {
763 result.add_type(t.clone());
764 }
765 } else {
766 result.add_type(atomic.clone());
767 }
768 }
769 Atomic::TArray { key, value } => {
770 result.add_type(Atomic::TArray {
771 key: Box::new(key.substitute_templates(bindings)),
772 value: Box::new(value.substitute_templates(bindings)),
773 });
774 }
775 Atomic::TList { value } => {
776 result.add_type(Atomic::TList {
777 value: Box::new(value.substitute_templates(bindings)),
778 });
779 }
780 Atomic::TNonEmptyArray { key, value } => {
781 result.add_type(Atomic::TNonEmptyArray {
782 key: Box::new(key.substitute_templates(bindings)),
783 value: Box::new(value.substitute_templates(bindings)),
784 });
785 }
786 Atomic::TNonEmptyList { value } => {
787 result.add_type(Atomic::TNonEmptyList {
788 value: Box::new(value.substitute_templates(bindings)),
789 });
790 }
791 Atomic::TKeyedArray {
792 properties,
793 is_open,
794 is_list,
795 } => {
796 use crate::atomic::KeyedProperty;
797 let new_props = properties
798 .iter()
799 .map(|(k, prop)| {
800 (
801 k.clone(),
802 KeyedProperty {
803 ty: prop.ty.substitute_templates(bindings),
804 optional: prop.optional,
805 },
806 )
807 })
808 .collect();
809 result.add_type(Atomic::TKeyedArray {
810 properties: new_props,
811 is_open: *is_open,
812 is_list: *is_list,
813 });
814 }
815 Atomic::TCallable {
816 params,
817 return_type,
818 } => {
819 result.add_type(Atomic::TCallable {
820 params: params.as_ref().map(|ps| {
821 ps.iter()
822 .map(|p| substitute_in_fn_param(p, bindings))
823 .collect()
824 }),
825 return_type: return_type
826 .as_ref()
827 .map(|r| Box::new(r.substitute_templates(bindings))),
828 });
829 }
830 Atomic::TClosure {
831 params,
832 return_type,
833 this_type,
834 } => {
835 result.add_type(Atomic::TClosure {
836 params: params
837 .iter()
838 .map(|p| substitute_in_fn_param(p, bindings))
839 .collect(),
840 return_type: Box::new(return_type.substitute_templates(bindings)),
841 this_type: this_type
842 .as_ref()
843 .map(|t| Box::new(t.substitute_templates(bindings))),
844 });
845 }
846 Atomic::TConditional {
847 param_name,
848 subject,
849 if_true,
850 if_false,
851 } => {
852 let new_subject = subject.substitute_templates(bindings);
853 let new_if_true = if_true.substitute_templates(bindings);
854 let new_if_false = if_false.substitute_templates(bindings);
855
856 let resolved = if let Some(name) = param_name {
860 if let Some(bound) = bindings.get(name) {
861 if new_subject.types.len() == 1 {
862 resolve_conditional_branch(
863 &new_subject.types[0],
864 bound,
865 &new_if_true,
866 &new_if_false,
867 )
868 } else {
869 None
870 }
871 } else {
872 None
873 }
874 } else {
875 None
876 };
877
878 if let Some(branch) = resolved {
879 for t in branch.types {
880 result.add_type(t);
881 }
882 } else {
883 result.add_type(Atomic::TConditional {
884 param_name: *param_name,
885 subject: Box::new(new_subject),
886 if_true: Box::new(new_if_true),
887 if_false: Box::new(new_if_false),
888 });
889 }
890 }
891 Atomic::TIntersection { parts } => {
892 result.add_type(Atomic::TIntersection {
893 parts: vec_to_type_params(
894 parts
895 .iter()
896 .map(|p| p.substitute_templates(bindings))
897 .collect(),
898 ),
899 });
900 }
901 Atomic::TNamedObject { fqcn, type_params } => {
902 if type_params.is_empty() && !fqcn.contains('\\') {
909 if let Some(resolved) = bindings.get(fqcn) {
910 for t in &resolved.types {
911 result.add_type(t.clone());
912 }
913 continue;
914 }
915 }
916 let new_params: Vec<Type> = type_params
917 .iter()
918 .map(|p| p.substitute_templates(bindings))
919 .collect();
920 result.add_type(Atomic::TNamedObject {
921 fqcn: *fqcn,
922 type_params: vec_to_type_params(new_params),
923 });
924 }
925 Atomic::TClassString(Some(param_name)) => {
927 if let Some(resolved) = bindings.get(param_name) {
928 for r_atomic in &resolved.types {
929 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
930 Some(*fqcn)
931 } else {
932 None
933 };
934 result.add_type(Atomic::TClassString(cls_name));
935 }
936 } else {
937 result.add_type(atomic.clone());
938 }
939 }
940 _ => {
941 result.add_type(atomic.clone());
942 }
943 }
944 }
945 result
946 }
947
948 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
954 where
955 F: Fn(&str) -> Option<Type>,
956 {
957 self.resolve_conditional_inner(&lookup)
958 }
959
960 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
961 where
962 F: Fn(&str) -> Option<Type>,
963 {
964 let mut result = Type::empty();
965 for atomic in self.types {
966 match atomic {
967 Atomic::TConditional {
968 ref param_name,
969 ref subject,
970 ref if_true,
971 ref if_false,
972 } => {
973 let resolved = if subject.types.len() == 1 {
974 if let Some(name) = param_name {
975 if let Some(arg_ty) = lookup(name.as_ref()) {
976 resolve_conditional_branch(
977 &subject.types[0],
978 &arg_ty,
979 if_true,
980 if_false,
981 )
982 } else {
983 None
984 }
985 } else {
986 None
987 }
988 } else {
989 None
990 };
991
992 if let Some(branch) = resolved {
993 for t in branch.resolve_conditional_inner(lookup).types {
995 result.add_type(t);
996 }
997 } else {
998 for t in if_true.clone().resolve_conditional_inner(lookup).types {
1001 result.add_type(t);
1002 }
1003 for t in if_false.clone().resolve_conditional_inner(lookup).types {
1004 result.add_type(t);
1005 }
1006 }
1007 }
1008 other => result.add_type(other),
1009 }
1010 }
1011 result
1012 }
1013
1014 pub fn is_subtype_structural(&self, other: &Type) -> bool {
1024 if other.is_mixed() {
1025 return true;
1026 }
1027 if self.is_never() {
1028 return true; }
1030 self.types
1031 .iter()
1032 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
1033 }
1034
1035 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
1038 let mut result = Type::empty();
1039 result.possibly_undefined = self.possibly_undefined;
1040 result.from_docblock = self.from_docblock;
1041 for atomic in &self.types {
1042 if f(atomic) {
1043 result.types.push(atomic.clone());
1044 }
1045 }
1046 result
1047 }
1048
1049 pub fn possibly_undefined(mut self) -> Self {
1051 self.possibly_undefined = true;
1052 self
1053 }
1054
1055 pub fn from_docblock(mut self) -> Self {
1057 self.from_docblock = true;
1058 self
1059 }
1060}
1061
1062fn is_string_atomic(a: &Atomic) -> bool {
1067 matches!(
1068 a,
1069 Atomic::TString
1070 | Atomic::TNonEmptyString
1071 | Atomic::TLiteralString(_)
1072 | Atomic::TNumericString
1073 | Atomic::TClassString(_)
1074 | Atomic::TCallableString
1075 )
1076}
1077
1078fn is_array_atomic(a: &Atomic) -> bool {
1079 matches!(
1080 a,
1081 Atomic::TArray { .. }
1082 | Atomic::TNonEmptyArray { .. }
1083 | Atomic::TKeyedArray { .. }
1084 | Atomic::TList { .. }
1085 | Atomic::TNonEmptyList { .. }
1086 )
1087}
1088
1089fn is_list_atomic(a: &Atomic) -> bool {
1090 match a {
1091 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
1092 Atomic::TKeyedArray { is_list, .. } => *is_list,
1093 _ => false,
1094 }
1095}
1096
1097fn resolve_conditional_branch(
1103 subject: &Atomic,
1104 arg_ty: &Type,
1105 if_true: &Type,
1106 if_false: &Type,
1107) -> Option<Type> {
1108 let predicate: fn(&Atomic) -> bool = match subject {
1109 Atomic::TNull => |a| matches!(a, Atomic::TNull),
1110 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
1111 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
1112 Atomic::TString => is_string_atomic,
1113 Atomic::TList { .. } => is_list_atomic,
1114 Atomic::TArray { .. } => is_array_atomic,
1115 _ => return None,
1116 };
1117
1118 if arg_ty.types.is_empty() {
1119 return None;
1120 }
1121 let all_match = arg_ty.types.iter().all(&predicate);
1122 let none_match = !arg_ty.types.iter().any(predicate);
1123 if all_match {
1124 Some(if_true.clone())
1125 } else if none_match {
1126 Some(if_false.clone())
1127 } else {
1128 None
1129 }
1130}
1131
1132fn substitute_in_fn_param(
1137 p: &crate::atomic::FnParam,
1138 bindings: &FxHashMap<Name, Type>,
1139) -> crate::atomic::FnParam {
1140 crate::atomic::FnParam {
1141 name: p.name,
1142 ty: p.ty.as_ref().map(|t| {
1143 let u = t.to_union();
1144 let substituted = u.substitute_templates(bindings);
1145 crate::compact::SimpleType::from_union(substituted)
1146 }),
1147 default: p.default.as_ref().map(|d| {
1148 let u = d.to_union();
1149 let substituted = u.substitute_templates(bindings);
1150 crate::compact::SimpleType::from_union(substituted)
1151 }),
1152 is_variadic: p.is_variadic,
1153 is_byref: p.is_byref,
1154 is_optional: p.is_optional,
1155 }
1156}
1157
1158fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1163 if sub == sup {
1164 return true;
1165 }
1166 match (sub, sup) {
1167 (Atomic::TNever, _) => true,
1169 (_, Atomic::TMixed) => true,
1171 (Atomic::TMixed, _) => true,
1172 (_, Atomic::TTemplateParam { as_type, .. }) => {
1177 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1178 }
1179
1180 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1182 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1183 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1184 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1185 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1186 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1187 (Atomic::TPositiveInt, Atomic::TInt) => true,
1188 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1189 (Atomic::TPositiveInt, Atomic::TNumeric) => true,
1190 (Atomic::TPositiveInt, Atomic::TScalar) => true,
1191 (Atomic::TNegativeInt, Atomic::TInt) => true,
1192 (Atomic::TNegativeInt, Atomic::TNumeric) => true,
1193 (Atomic::TNegativeInt, Atomic::TScalar) => true,
1194 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1195 (Atomic::TNonNegativeInt, Atomic::TNumeric) => true,
1196 (Atomic::TNonNegativeInt, Atomic::TScalar) => true,
1197 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1198 (Atomic::TIntRange { .. }, Atomic::TNumeric) => true,
1199 (Atomic::TIntRange { .. }, Atomic::TScalar) => true,
1200 (Atomic::TPositiveInt, Atomic::TIntRange { min, max }) => {
1202 max.is_none() && min.is_none_or(|m| m <= 1)
1203 }
1204 (Atomic::TNegativeInt, Atomic::TIntRange { min, max }) => {
1206 min.is_none() && max.is_none_or(|m| m >= -1)
1207 }
1208 (Atomic::TNonNegativeInt, Atomic::TIntRange { min, max }) => {
1210 max.is_none() && min.is_none_or(|m| m <= 0)
1211 }
1212 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TPositiveInt) => {
1214 sub_min.is_some_and(|lo| lo >= 1)
1215 }
1216 (Atomic::TIntRange { min: sub_min, .. }, Atomic::TNonNegativeInt) => {
1217 sub_min.is_some_and(|lo| lo >= 0)
1218 }
1219 (Atomic::TIntRange { max: sub_max, .. }, Atomic::TNegativeInt) => {
1220 sub_max.is_some_and(|hi| hi <= -1)
1221 }
1222 (
1224 Atomic::TIntRange {
1225 min: sub_min,
1226 max: sub_max,
1227 },
1228 Atomic::TIntRange {
1229 min: sup_min,
1230 max: sup_max,
1231 },
1232 ) => {
1233 let lower_ok = match (sub_min, sup_min) {
1234 (_, None) => true,
1235 (None, Some(_)) => false,
1236 (Some(sl), Some(su)) => sl >= su,
1237 };
1238 let upper_ok = match (sub_max, sup_max) {
1239 (None, None) | (Some(_), None) => true,
1240 (None, Some(_)) => false,
1241 (Some(sl), Some(su)) => sl <= su,
1242 };
1243 lower_ok && upper_ok
1244 }
1245
1246 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1247 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1248 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1249
1250 (Atomic::TLiteralString(s), Atomic::TString) => {
1251 let _ = s;
1252 true
1253 }
1254 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1255 let _ = s;
1256 true
1257 }
1258 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1259 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1260 (Atomic::TLiteralString(_), Atomic::TClassString(_)) => true,
1263 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1264 (Atomic::TNonEmptyString, Atomic::TString) => true,
1265 (Atomic::TCallableString, Atomic::TString) => true,
1266 (Atomic::TNumericString, Atomic::TNonEmptyString) => true,
1268 (Atomic::TNumericString, Atomic::TString) => true,
1269 (Atomic::TClassString(_), Atomic::TString) => true,
1270 (Atomic::TInterfaceString, Atomic::TString) => true,
1271 (Atomic::TEnumString, Atomic::TString) => true,
1272 (Atomic::TTraitString, Atomic::TString) => true,
1273
1274 (Atomic::TTrue, Atomic::TBool) => true,
1275 (Atomic::TFalse, Atomic::TBool) => true,
1276
1277 (Atomic::TInt, Atomic::TNumeric) => true,
1278 (Atomic::TFloat, Atomic::TNumeric) => true,
1279 (Atomic::TNumericString, Atomic::TNumeric) => true,
1280
1281 (Atomic::TInt, Atomic::TScalar) => true,
1282 (Atomic::TFloat, Atomic::TScalar) => true,
1283 (Atomic::TString, Atomic::TScalar) => true,
1284 (Atomic::TBool, Atomic::TScalar) => true,
1285 (Atomic::TNumeric, Atomic::TScalar) => true,
1286 (Atomic::TTrue, Atomic::TScalar) => true,
1287 (Atomic::TFalse, Atomic::TScalar) => true,
1288
1289 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1291 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1292 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1293 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1295 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1296 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1298 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1299 (
1303 Atomic::TNamedObject {
1304 fqcn: sub_fqcn,
1305 type_params: sub_params,
1306 },
1307 Atomic::TNamedObject {
1308 fqcn: sup_fqcn,
1309 type_params: sup_params,
1310 },
1311 ) => sub_fqcn == sup_fqcn && (sup_params.is_empty() || sub_params == sup_params),
1312
1313 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1315 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1316 (Atomic::TNegativeInt, Atomic::TFloat) => true,
1317 (Atomic::TNonNegativeInt, Atomic::TFloat) => true,
1318 (Atomic::TInt, Atomic::TFloat) => true,
1319
1320 (Atomic::TLiteralInt(n), Atomic::TIntRange { min, max }) => {
1322 min.is_none_or(|lo| *n >= lo) && max.is_none_or(|hi| *n <= hi)
1323 }
1324
1325 (Atomic::TString, Atomic::TCallable { .. }) => true,
1327 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1328 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1329 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1330 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1331 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1332
1333 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1335 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1337 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1339 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1341 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1343 fqcn.as_ref().eq_ignore_ascii_case("closure")
1344 }
1345 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1346 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1348 fqcn.as_ref().eq_ignore_ascii_case("closure")
1349 }
1350 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1352 fqcn.as_ref().eq_ignore_ascii_case("closure")
1353 }
1354
1355 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1357 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1358 }
1359 (Atomic::TNonEmptyList { value }, Atomic::TArray { key, value: av }) => {
1360 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1361 }
1362 (Atomic::TNonEmptyList { value }, Atomic::TNonEmptyArray { key, value: av }) => {
1363 Type::single(Atomic::TInt).is_subtype_structural(key) && value.is_subtype_structural(av)
1364 }
1365 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1366 value.is_subtype_structural(lv)
1367 }
1368 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1370 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1371 && av.is_subtype_structural(lv)
1372 }
1373 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1374 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1375 && av.is_subtype_structural(lv)
1376 }
1377 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1378 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1379 && av.is_subtype_structural(lv)
1380 }
1381 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1382 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1383 && av.is_subtype_structural(lv)
1384 }
1385 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1387 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1388 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1389 }
1390
1391 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1393 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1394 }
1395
1396 (
1402 Atomic::TKeyedArray {
1403 properties,
1404 is_open,
1405 ..
1406 },
1407 Atomic::TArray { key, value },
1408 ) => {
1409 if *is_open {
1410 return true;
1411 }
1412 properties.iter().all(|(prop_key, prop)| {
1413 let key_atomic = match prop_key {
1414 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1415 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1416 };
1417 if !Type::single(key_atomic).is_subtype_structural(key) {
1418 return false; }
1420 let has_named_obj = prop.ty.types.iter().any(|a| {
1422 matches!(
1423 a,
1424 Atomic::TNamedObject { .. }
1425 | Atomic::TSelf { .. }
1426 | Atomic::TStaticObject { .. }
1427 | Atomic::TClosure { .. }
1428 | Atomic::TTemplateParam { .. }
1429 )
1430 });
1431 has_named_obj || prop.ty.is_subtype_structural(value)
1432 })
1433 }
1434 (
1435 Atomic::TKeyedArray {
1436 properties,
1437 is_open,
1438 ..
1439 },
1440 Atomic::TNonEmptyArray { key, value },
1441 ) => {
1442 if *is_open {
1443 return !properties.is_empty();
1444 }
1445 properties.iter().any(|(_, p)| !p.optional)
1446 && properties.iter().all(|(prop_key, prop)| {
1447 let key_atomic = match prop_key {
1448 crate::atomic::ArrayKey::String(s) => Atomic::TLiteralString(s.clone()),
1449 crate::atomic::ArrayKey::Int(n) => Atomic::TLiteralInt(*n),
1450 };
1451 if !Type::single(key_atomic).is_subtype_structural(key) {
1452 return false;
1453 }
1454 let has_named_obj = prop.ty.types.iter().any(|a| {
1455 matches!(
1456 a,
1457 Atomic::TNamedObject { .. }
1458 | Atomic::TSelf { .. }
1459 | Atomic::TStaticObject { .. }
1460 | Atomic::TClosure { .. }
1461 | Atomic::TTemplateParam { .. }
1462 )
1463 });
1464 has_named_obj || prop.ty.is_subtype_structural(value)
1465 })
1466 }
1467
1468 (
1470 Atomic::TKeyedArray {
1471 properties,
1472 is_list,
1473 ..
1474 },
1475 Atomic::TList { value: lv },
1476 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1477 (
1478 Atomic::TKeyedArray {
1479 properties,
1480 is_list,
1481 ..
1482 },
1483 Atomic::TNonEmptyList { value: lv },
1484 ) => {
1485 *is_list
1486 && !properties.is_empty()
1487 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1488 }
1489
1490 _ => false,
1491 }
1492}
1493
1494#[cfg(test)]
1499mod tests {
1500 use std::sync::Arc;
1501
1502 use super::*;
1503
1504 #[test]
1505 fn single_is_single() {
1506 let u = Type::single(Atomic::TString);
1507 assert!(u.is_single());
1508 assert!(!u.is_nullable());
1509 }
1510
1511 #[test]
1512 fn nullable_has_null() {
1513 let u = Type::nullable(Atomic::TString);
1514 assert!(u.is_nullable());
1515 assert_eq!(u.types.len(), 2);
1516 }
1517
1518 #[test]
1519 fn add_type_deduplicates() {
1520 let mut u = Type::single(Atomic::TString);
1521 u.add_type(Atomic::TString);
1522 assert_eq!(u.types.len(), 1);
1523 }
1524
1525 #[test]
1526 fn add_type_literal_subsumed_by_base() {
1527 let mut u = Type::single(Atomic::TInt);
1528 u.add_type(Atomic::TLiteralInt(42));
1529 assert_eq!(u.types.len(), 1);
1530 assert!(matches!(u.types[0], Atomic::TInt));
1531 }
1532
1533 #[test]
1534 fn add_type_base_widens_literals() {
1535 let mut u = Type::single(Atomic::TLiteralInt(1));
1536 u.add_type(Atomic::TLiteralInt(2));
1537 u.add_type(Atomic::TInt);
1538 assert_eq!(u.types.len(), 1);
1539 assert!(matches!(u.types[0], Atomic::TInt));
1540 }
1541
1542 #[test]
1543 fn mixed_subsumes_everything() {
1544 let mut u = Type::single(Atomic::TString);
1545 u.add_type(Atomic::TMixed);
1546 assert_eq!(u.types.len(), 1);
1547 assert!(u.is_mixed());
1548 }
1549
1550 #[test]
1551 fn remove_null() {
1552 let u = Type::nullable(Atomic::TString);
1553 let narrowed = u.remove_null();
1554 assert!(!narrowed.is_nullable());
1555 assert_eq!(narrowed.types.len(), 1);
1556 }
1557
1558 #[test]
1559 fn narrow_to_truthy_removes_null_false() {
1560 let mut u = Type::empty();
1561 u.add_type(Atomic::TString);
1562 u.add_type(Atomic::TNull);
1563 u.add_type(Atomic::TFalse);
1564 let truthy = u.narrow_to_truthy();
1565 assert!(!truthy.is_nullable());
1566 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1567 }
1568
1569 #[test]
1570 fn merge_combines_types() {
1571 let a = Type::single(Atomic::TString);
1572 let b = Type::single(Atomic::TInt);
1573 let merged = Type::merge(&a, &b);
1574 assert_eq!(merged.types.len(), 2);
1575 }
1576
1577 #[test]
1578 fn subtype_literal_int_under_int() {
1579 let sub = Type::single(Atomic::TLiteralInt(5));
1580 let sup = Type::single(Atomic::TInt);
1581 assert!(sub.is_subtype_structural(&sup));
1582 }
1583
1584 #[test]
1585 fn subtype_never_is_bottom() {
1586 let never = Type::never();
1587 let string = Type::single(Atomic::TString);
1588 assert!(never.is_subtype_structural(&string));
1589 }
1590
1591 #[test]
1592 fn subtype_everything_under_mixed() {
1593 let string = Type::single(Atomic::TString);
1594 let mixed = Type::mixed();
1595 assert!(string.is_subtype_structural(&mixed));
1596 }
1597
1598 #[test]
1599 fn template_substitution() {
1600 let mut bindings = FxHashMap::default();
1601 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1602
1603 let tmpl = Type::single(Atomic::TTemplateParam {
1604 name: Name::new("T"),
1605 as_type: Box::new(Type::mixed()),
1606 defining_entity: Name::new("MyClass"),
1607 });
1608
1609 let resolved = tmpl.substitute_templates(&bindings);
1610 assert_eq!(resolved.types.len(), 1);
1611 assert!(matches!(resolved.types[0], Atomic::TString));
1612 }
1613
1614 #[test]
1615 fn intersection_is_object() {
1616 let parts = vec![
1617 Type::single(Atomic::TNamedObject {
1618 fqcn: Name::new("Iterator"),
1619 type_params: empty_type_params(),
1620 }),
1621 Type::single(Atomic::TNamedObject {
1622 fqcn: Name::new("Countable"),
1623 type_params: empty_type_params(),
1624 }),
1625 ];
1626 let atomic = Atomic::TIntersection {
1627 parts: vec_to_type_params(parts),
1628 };
1629 assert!(atomic.is_object());
1630 assert!(!atomic.can_be_falsy());
1631 assert!(atomic.can_be_truthy());
1632 }
1633
1634 #[test]
1635 fn intersection_display_two_parts() {
1636 let parts = vec![
1637 Type::single(Atomic::TNamedObject {
1638 fqcn: Name::new("Iterator"),
1639 type_params: empty_type_params(),
1640 }),
1641 Type::single(Atomic::TNamedObject {
1642 fqcn: Name::new("Countable"),
1643 type_params: empty_type_params(),
1644 }),
1645 ];
1646 let u = Type::single(Atomic::TIntersection {
1647 parts: vec_to_type_params(parts),
1648 });
1649 assert_eq!(format!("{u}"), "Iterator&Countable");
1650 }
1651
1652 #[test]
1653 fn intersection_display_three_parts() {
1654 let parts = vec![
1655 Type::single(Atomic::TNamedObject {
1656 fqcn: Name::new("A"),
1657 type_params: empty_type_params(),
1658 }),
1659 Type::single(Atomic::TNamedObject {
1660 fqcn: Name::new("B"),
1661 type_params: empty_type_params(),
1662 }),
1663 Type::single(Atomic::TNamedObject {
1664 fqcn: Name::new("C"),
1665 type_params: empty_type_params(),
1666 }),
1667 ];
1668 let u = Type::single(Atomic::TIntersection {
1669 parts: vec_to_type_params(parts),
1670 });
1671 assert_eq!(format!("{u}"), "A&B&C");
1672 }
1673
1674 #[test]
1675 fn intersection_in_nullable_union_display() {
1676 let intersection = Atomic::TIntersection {
1677 parts: vec_to_type_params(vec![
1678 Type::single(Atomic::TNamedObject {
1679 fqcn: Name::new("Iterator"),
1680 type_params: empty_type_params(),
1681 }),
1682 Type::single(Atomic::TNamedObject {
1683 fqcn: Name::new("Countable"),
1684 type_params: empty_type_params(),
1685 }),
1686 ]),
1687 };
1688 let mut u = Type::single(intersection);
1689 u.add_type(Atomic::TNull);
1690 assert!(u.is_nullable());
1691 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1692 }
1693
1694 fn t_param(name: &str) -> Type {
1697 Type::single(Atomic::TTemplateParam {
1698 name: Name::new(name),
1699 as_type: Box::new(Type::mixed()),
1700 defining_entity: Name::new("Fn"),
1701 })
1702 }
1703
1704 fn bindings_t_string() -> FxHashMap<Name, Type> {
1705 let mut b = FxHashMap::default();
1706 b.insert(Name::new("T"), Type::single(Atomic::TString));
1707 b
1708 }
1709
1710 #[test]
1711 fn substitute_non_empty_array_key_and_value() {
1712 let ty = Type::single(Atomic::TNonEmptyArray {
1713 key: Box::new(t_param("T")),
1714 value: Box::new(t_param("T")),
1715 });
1716 let result = ty.substitute_templates(&bindings_t_string());
1717 assert_eq!(result.types.len(), 1);
1718 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1719 panic!("expected TNonEmptyArray");
1720 };
1721 assert!(matches!(key.types[0], Atomic::TString));
1722 assert!(matches!(value.types[0], Atomic::TString));
1723 }
1724
1725 #[test]
1726 fn substitute_non_empty_list_value() {
1727 let ty = Type::single(Atomic::TNonEmptyList {
1728 value: Box::new(t_param("T")),
1729 });
1730 let result = ty.substitute_templates(&bindings_t_string());
1731 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1732 panic!("expected TNonEmptyList");
1733 };
1734 assert!(matches!(value.types[0], Atomic::TString));
1735 }
1736
1737 #[test]
1738 fn substitute_keyed_array_property_types() {
1739 use crate::atomic::{ArrayKey, KeyedProperty};
1740 use indexmap::IndexMap;
1741 let mut props = IndexMap::new();
1742 props.insert(
1743 ArrayKey::String(Arc::from("name")),
1744 KeyedProperty {
1745 ty: t_param("T"),
1746 optional: false,
1747 },
1748 );
1749 props.insert(
1750 ArrayKey::String(Arc::from("tag")),
1751 KeyedProperty {
1752 ty: t_param("T"),
1753 optional: true,
1754 },
1755 );
1756 let ty = Type::single(Atomic::TKeyedArray {
1757 properties: props,
1758 is_open: true,
1759 is_list: false,
1760 });
1761 let result = ty.substitute_templates(&bindings_t_string());
1762 let Atomic::TKeyedArray {
1763 properties,
1764 is_open,
1765 is_list,
1766 } = &result.types[0]
1767 else {
1768 panic!("expected TKeyedArray");
1769 };
1770 assert!(is_open);
1771 assert!(!is_list);
1772 assert!(matches!(
1773 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1774 Atomic::TString
1775 ));
1776 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1777 assert!(matches!(
1778 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1779 Atomic::TString
1780 ));
1781 }
1782
1783 #[test]
1784 fn substitute_callable_params_and_return() {
1785 use crate::atomic::FnParam;
1786 let ty = Type::single(Atomic::TCallable {
1787 params: Some(vec![FnParam {
1788 name: Name::new("x"),
1789 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1790 default: None,
1791 is_variadic: false,
1792 is_byref: false,
1793 is_optional: false,
1794 }]),
1795 return_type: Some(Box::new(t_param("T"))),
1796 });
1797 let result = ty.substitute_templates(&bindings_t_string());
1798 let Atomic::TCallable {
1799 params,
1800 return_type,
1801 } = &result.types[0]
1802 else {
1803 panic!("expected TCallable");
1804 };
1805 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1806 let param_union = param_ty.to_union();
1807 assert!(matches!(param_union.types[0], Atomic::TString));
1808 let ret = return_type.as_ref().unwrap();
1809 assert!(matches!(ret.types[0], Atomic::TString));
1810 }
1811
1812 #[test]
1813 fn substitute_callable_bare_no_panic() {
1814 let ty = Type::single(Atomic::TCallable {
1816 params: None,
1817 return_type: None,
1818 });
1819 let result = ty.substitute_templates(&bindings_t_string());
1820 assert!(matches!(
1821 result.types[0],
1822 Atomic::TCallable {
1823 params: None,
1824 return_type: None
1825 }
1826 ));
1827 }
1828
1829 #[test]
1830 fn substitute_closure_params_return_and_this() {
1831 use crate::atomic::FnParam;
1832 let ty = Type::single(Atomic::TClosure {
1833 params: vec![FnParam {
1834 name: Name::new("a"),
1835 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1836 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1837 is_variadic: true,
1838 is_byref: true,
1839 is_optional: true,
1840 }],
1841 return_type: Box::new(t_param("T")),
1842 this_type: Some(Box::new(t_param("T"))),
1843 });
1844 let result = ty.substitute_templates(&bindings_t_string());
1845 let Atomic::TClosure {
1846 params,
1847 return_type,
1848 this_type,
1849 } = &result.types[0]
1850 else {
1851 panic!("expected TClosure");
1852 };
1853 let p = ¶ms[0];
1854 let ty_union = p.ty.as_ref().unwrap().to_union();
1855 let default_union = p.default.as_ref().unwrap().to_union();
1856 assert!(matches!(ty_union.types[0], Atomic::TString));
1857 assert!(matches!(default_union.types[0], Atomic::TString));
1858 assert!(p.is_variadic);
1860 assert!(p.is_byref);
1861 assert!(p.is_optional);
1862 assert!(matches!(return_type.types[0], Atomic::TString));
1863 assert!(matches!(
1864 this_type.as_ref().unwrap().types[0],
1865 Atomic::TString
1866 ));
1867 }
1868
1869 #[test]
1870 fn substitute_conditional_all_branches() {
1871 let ty = Type::single(Atomic::TConditional {
1872 param_name: None,
1873 subject: Box::new(t_param("T")),
1874 if_true: Box::new(t_param("T")),
1875 if_false: Box::new(Type::single(Atomic::TInt)),
1876 });
1877 let result = ty.substitute_templates(&bindings_t_string());
1878 let Atomic::TConditional {
1879 param_name: _,
1880 subject,
1881 if_true,
1882 if_false,
1883 } = &result.types[0]
1884 else {
1885 panic!("expected TConditional");
1886 };
1887 assert!(matches!(subject.types[0], Atomic::TString));
1888 assert!(matches!(if_true.types[0], Atomic::TString));
1889 assert!(matches!(if_false.types[0], Atomic::TInt));
1890 }
1891
1892 #[test]
1893 fn resolve_conditional_is_null_non_null_arg() {
1894 let ty = Type::single(Atomic::TConditional {
1895 param_name: Some(Name::new("x")),
1896 subject: Box::new(Type::single(Atomic::TNull)),
1897 if_true: Box::new(Type::single(Atomic::TInt)),
1898 if_false: Box::new(Type::single(Atomic::TString)),
1899 });
1900 let result = ty.resolve_conditional_returns(|name| {
1901 if name == "x" {
1902 Some(Type::single(Atomic::TString)) } else {
1904 None
1905 }
1906 });
1907 assert!(result.types.len() == 1);
1908 assert!(matches!(result.types[0], Atomic::TString));
1909 }
1910
1911 #[test]
1912 fn resolve_conditional_is_null_null_arg() {
1913 let ty = Type::single(Atomic::TConditional {
1914 param_name: Some(Name::new("x")),
1915 subject: Box::new(Type::single(Atomic::TNull)),
1916 if_true: Box::new(Type::single(Atomic::TInt)),
1917 if_false: Box::new(Type::single(Atomic::TString)),
1918 });
1919 let result = ty.resolve_conditional_returns(|name| {
1920 if name == "x" {
1921 Some(Type::single(Atomic::TNull)) } else {
1923 None
1924 }
1925 });
1926 assert!(result.types.len() == 1);
1927 assert!(matches!(result.types[0], Atomic::TInt));
1928 }
1929
1930 #[test]
1931 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1932 let mut nullable_str = Type::single(Atomic::TString);
1933 nullable_str.add_type(Atomic::TNull);
1934 let ty = Type::single(Atomic::TConditional {
1935 param_name: Some(Name::new("x")),
1936 subject: Box::new(Type::single(Atomic::TNull)),
1937 if_true: Box::new(Type::single(Atomic::TInt)),
1938 if_false: Box::new(Type::single(Atomic::TString)),
1939 });
1940 let result = ty.resolve_conditional_returns(|name| {
1941 if name == "x" {
1942 Some(nullable_str.clone())
1943 } else {
1944 None
1945 }
1946 });
1947 assert_eq!(result.types.len(), 2);
1949 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1950 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1951 }
1952
1953 #[test]
1954 fn resolve_conditional_nested_widens_inner_branch() {
1955 let inner = Type::single(Atomic::TConditional {
1958 param_name: Some(Name::new("x")),
1959 subject: Box::new(Type::single(Atomic::TString)),
1960 if_true: Box::new(Type::single(Atomic::TString)),
1961 if_false: Box::new(Type::single(Atomic::TFloat)),
1962 });
1963 let ty = Type::single(Atomic::TConditional {
1964 param_name: Some(Name::new("x")),
1965 subject: Box::new(Type::single(Atomic::TNull)),
1966 if_true: Box::new(Type::single(Atomic::TInt)),
1967 if_false: Box::new(inner),
1968 });
1969 let result = ty.resolve_conditional_returns(|_| None);
1971 assert!(
1972 result
1973 .types
1974 .iter()
1975 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1976 "no TConditional should survive: {:?}",
1977 result.types
1978 );
1979 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1980 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1981 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
1982 }
1983
1984 #[test]
1985 fn resolve_conditional_nested_resolves_inner_branch() {
1986 let inner = Type::single(Atomic::TConditional {
1990 param_name: Some(Name::new("x")),
1991 subject: Box::new(Type::single(Atomic::TString)),
1992 if_true: Box::new(Type::single(Atomic::TString)),
1993 if_false: Box::new(Type::single(Atomic::TFloat)),
1994 });
1995 let ty = Type::single(Atomic::TConditional {
1996 param_name: Some(Name::new("x")),
1997 subject: Box::new(Type::single(Atomic::TNull)),
1998 if_true: Box::new(Type::single(Atomic::TInt)),
1999 if_false: Box::new(inner),
2000 });
2001 let result = ty.resolve_conditional_returns(|name| {
2003 if name == "x" {
2004 Some(Type::single(Atomic::TString))
2005 } else {
2006 None
2007 }
2008 });
2009 assert!(
2010 result
2011 .types
2012 .iter()
2013 .all(|t| !matches!(t, Atomic::TConditional { .. })),
2014 "no TConditional should survive: {:?}",
2015 result.types
2016 );
2017 assert_eq!(result.types.len(), 1);
2018 assert!(matches!(result.types[0], Atomic::TString));
2019 }
2020
2021 #[test]
2022 fn substitute_intersection_parts() {
2023 let ty = Type::single(Atomic::TIntersection {
2024 parts: vec_to_type_params(vec![
2025 Type::single(Atomic::TNamedObject {
2026 fqcn: Name::new("Countable"),
2027 type_params: empty_type_params(),
2028 }),
2029 t_param("T"),
2030 ]),
2031 });
2032 let result = ty.substitute_templates(&bindings_t_string());
2033 let Atomic::TIntersection { parts } = &result.types[0] else {
2034 panic!("expected TIntersection");
2035 };
2036 assert_eq!(parts.len(), 2);
2037 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
2038 assert!(matches!(parts[1].types[0], Atomic::TString));
2039 }
2040
2041 #[test]
2042 fn substitute_no_template_params_identity() {
2043 let ty = Type::single(Atomic::TInt);
2044 let result = ty.substitute_templates(&bindings_t_string());
2045 assert!(matches!(result.types[0], Atomic::TInt));
2046 }
2047}