1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::sync::Arc;
4
5use std::collections::HashSet;
6
7use foldhash::fast::FixedState;
8use mago_word::Word;
9use mago_word::ascii_lowercase_word;
10
11use crate::identifier::function_like::FunctionLikeIdentifier;
12use crate::metadata::CodebaseMetadata;
13use crate::metadata::function_like::FunctionLikeMetadata;
14use crate::ttype::TType;
15use crate::ttype::atomic::TAtomic;
16use crate::ttype::atomic::alias::TAlias;
17use crate::ttype::atomic::array::TArray;
18use crate::ttype::atomic::array::key::ArrayKey;
19use crate::ttype::atomic::callable::TCallable;
20use crate::ttype::atomic::callable::TCallableSignature;
21use crate::ttype::atomic::callable::parameter::TCallableParameter;
22use crate::ttype::atomic::derived::TDerived;
23use crate::ttype::atomic::derived::index_access::TIndexAccess;
24use crate::ttype::atomic::derived::int_mask::TIntMask;
25use crate::ttype::atomic::derived::int_mask_of::TIntMaskOf;
26use crate::ttype::atomic::derived::key_of::TKeyOf;
27use crate::ttype::atomic::derived::new::TNew;
28use crate::ttype::atomic::derived::properties_of::TPropertiesOf;
29use crate::ttype::atomic::derived::template_type::TTemplateType;
30use crate::ttype::atomic::derived::value_of::TValueOf;
31use crate::ttype::atomic::mixed::TMixed;
32use crate::ttype::atomic::object::TObject;
33use crate::ttype::atomic::object::named::TNamedObject;
34use crate::ttype::atomic::reference::TGlobalReferenceSelector;
35use crate::ttype::atomic::reference::TReference;
36use crate::ttype::atomic::reference::TReferenceMemberSelector;
37use crate::ttype::atomic::scalar::TScalar;
38use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
39use crate::ttype::atomic::scalar::int::TInteger;
40use crate::ttype::atomic::scalar::string::TString;
41use crate::ttype::atomic::scalar::string::TStringLiteral;
42use crate::ttype::combiner;
43use crate::ttype::union::TUnion;
44
45thread_local! {
46 pub(crate) static EXPANDING_ALIASES: RefCell<HashSet<(Word, Word), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
49
50 static EXPANDING_OBJECT_PARAMS: RefCell<HashSet<Word, FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
52
53 static EXPANDING_CONSTANTS: RefCell<HashSet<(Word, Word), FixedState>> = const { RefCell::new(HashSet::with_hasher(FixedState::with_seed(0))) };
57}
58
59pub(crate) struct AliasExpansionGuard {
62 class_name: Word,
63 alias_name: Word,
64}
65
66impl AliasExpansionGuard {
67 #[must_use]
68 pub(crate) fn new(class_name: Word, alias_name: Word) -> Self {
69 EXPANDING_ALIASES.with(|set| set.borrow_mut().insert((class_name, alias_name)));
70 Self { class_name, alias_name }
71 }
72}
73
74impl Drop for AliasExpansionGuard {
75 fn drop(&mut self) {
76 EXPANDING_ALIASES.with(|set| set.borrow_mut().remove(&(self.class_name, self.alias_name)));
77 }
78}
79
80struct ObjectParamsExpansionGuard {
82 object_name: Word,
83}
84
85impl ObjectParamsExpansionGuard {
86 #[must_use]
87 fn try_new(object_name: Word) -> Option<Self> {
88 EXPANDING_OBJECT_PARAMS.with(|set| {
89 let mut set = set.borrow_mut();
90 if set.contains(&object_name) {
91 None
92 } else {
93 set.insert(object_name);
94 Some(Self { object_name })
95 }
96 })
97 }
98}
99
100impl Drop for ObjectParamsExpansionGuard {
101 fn drop(&mut self) {
102 EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().remove(&self.object_name));
103 }
104}
105
106struct ConstantExpansionGuard {
113 class_name: Word,
114 constant_name: Word,
115}
116
117impl ConstantExpansionGuard {
118 #[must_use]
119 fn try_new(class_name: Word, constant_name: Word) -> Option<Self> {
120 EXPANDING_CONSTANTS.with(|set| {
121 let mut set = set.borrow_mut();
122 if set.contains(&(class_name, constant_name)) {
123 None
124 } else {
125 set.insert((class_name, constant_name));
126 Some(Self { class_name, constant_name })
127 }
128 })
129 }
130}
131
132impl Drop for ConstantExpansionGuard {
133 fn drop(&mut self) {
134 EXPANDING_CONSTANTS.with(|set| set.borrow_mut().remove(&(self.class_name, self.constant_name)));
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
139pub enum StaticClassType {
140 #[default]
141 None,
142 Name(Word),
143 Object(TObject),
144}
145
146#[derive(Debug)]
147pub struct TypeExpansionOptions {
148 pub self_class: Option<Word>,
149 pub static_class_type: StaticClassType,
150 pub parent_class: Option<Word>,
151 pub evaluate_class_constants: bool,
152 pub evaluate_conditional_types: bool,
153 pub function_is_final: bool,
154 pub expand_generic: bool,
155 pub expand_templates: bool,
156}
157
158impl Default for TypeExpansionOptions {
159 fn default() -> Self {
160 Self {
161 self_class: None,
162 static_class_type: StaticClassType::default(),
163 parent_class: None,
164 evaluate_class_constants: true,
165 evaluate_conditional_types: false,
166 function_is_final: false,
167 expand_generic: false,
168 expand_templates: true,
169 }
170 }
171}
172
173pub fn expand_union(codebase: &CodebaseMetadata, return_type: &mut TUnion, options: &TypeExpansionOptions) {
176 if !return_type.is_expandable() {
177 return;
178 }
179
180 let mut types = std::mem::take(&mut return_type.types).into_owned();
181 let mut new_return_type_parts: Vec<TAtomic> = Vec::new();
182 let mut skip_mask: u64 = 0;
183
184 for (i, return_type_part) in types.iter_mut().enumerate() {
185 let mut skip_key = false;
186 expand_atomic(return_type_part, codebase, options, &mut skip_key, &mut new_return_type_parts);
187
188 if skip_key && i < 64 {
189 skip_mask |= 1u64 << i;
190 }
191 }
192
193 if skip_mask != 0 {
194 let mut idx = 0usize;
195 types.retain(|_| {
196 let retain = idx >= 64 || (skip_mask & (1u64 << idx)) == 0;
197 idx += 1;
198 retain
199 });
200
201 new_return_type_parts.append(&mut types);
202
203 if new_return_type_parts.is_empty() {
204 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
205 }
206
207 types = if new_return_type_parts.len() > 1 {
208 combiner::combine(new_return_type_parts, codebase, combiner::CombinerOptions::default())
209 } else {
210 new_return_type_parts
211 };
212 } else if types.len() > 1 {
213 types = combiner::combine(types, codebase, combiner::CombinerOptions::default());
214 }
215
216 return_type.types = Cow::Owned(types);
217}
218
219pub(crate) fn expand_atomic(
220 return_type_part: &mut TAtomic,
221 codebase: &CodebaseMetadata,
222 options: &TypeExpansionOptions,
223 skip_key: &mut bool,
224 new_return_type_parts: &mut Vec<TAtomic>,
225) {
226 match return_type_part {
227 TAtomic::Array(array_type) => match array_type {
228 TArray::Keyed(keyed_data) => {
229 if let Some((key_parameter, value_parameter)) = &mut keyed_data.parameters {
230 expand_union(codebase, Arc::make_mut(key_parameter), options);
231 expand_union(codebase, Arc::make_mut(value_parameter), options);
232 }
233
234 if let Some(known_items) = &mut keyed_data.known_items {
235 let needs_key_resolution = known_items.keys().any(|k| k.is_class_like_constant());
237
238 if needs_key_resolution {
239 let old_items = std::mem::take(known_items);
240 for (key, (is_optional, mut value_type)) in old_items {
241 expand_union(codebase, &mut value_type, options);
242 let resolved_key = resolve_array_key(key, codebase, options);
243 known_items.insert(resolved_key, (is_optional, value_type));
244 }
245 } else {
246 for (_, item_type) in known_items.values_mut() {
247 expand_union(codebase, item_type, options);
248 }
249 }
250 }
251 }
252 TArray::List(list_data) => {
253 expand_union(codebase, Arc::make_mut(&mut list_data.element_type), options);
254
255 if let Some(known_elements) = &mut list_data.known_elements {
256 for (_, element_type) in known_elements.values_mut() {
257 expand_union(codebase, element_type, options);
258 }
259 }
260 }
261 },
262 TAtomic::Object(object) => {
263 expand_object(object, codebase, options);
264 }
265 TAtomic::Callable(TCallable::Signature(signature)) => {
266 if let Some(return_type) = signature.get_return_type_mut() {
267 expand_union(codebase, return_type, options);
268 }
269
270 for param in signature.get_parameters_mut() {
271 if let Some(param_type) = param.get_type_signature_mut() {
272 expand_union(codebase, param_type, options);
273 }
274 }
275 }
276 TAtomic::GenericParameter(parameter) => {
277 expand_union(codebase, Arc::make_mut(&mut parameter.constraint), options);
278 }
279 TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) => {
280 let mut atomic_return_type_parts = vec![];
281 expand_atomic(Arc::make_mut(constraint), codebase, options, &mut false, &mut atomic_return_type_parts);
282
283 if !atomic_return_type_parts.is_empty() {
284 *Arc::make_mut(constraint) = atomic_return_type_parts.remove(0);
285 }
286 }
287 TAtomic::Reference(TReference::Member { class_like_name, member_selector }) => {
288 *skip_key = true;
289 expand_member_reference(*class_like_name, member_selector, codebase, options, new_return_type_parts);
290 }
291 TAtomic::Reference(TReference::Global { selector }) => {
292 *skip_key = true;
293 expand_global_reference(selector, codebase, options, new_return_type_parts);
294 }
295 TAtomic::Callable(TCallable::Alias(id)) => {
296 if let Some(value) = get_atomic_of_function_like_identifier(id, codebase) {
297 *skip_key = true;
298 new_return_type_parts.push(value);
299 }
300 }
301 TAtomic::Conditional(conditional) => {
302 *skip_key = true;
303
304 let mut then = (*conditional.then).clone();
305 let mut otherwise = (*conditional.otherwise).clone();
306
307 expand_union(codebase, &mut then, options);
308 expand_union(codebase, &mut otherwise, options);
309
310 new_return_type_parts.extend(then.types.into_owned());
311 new_return_type_parts.extend(otherwise.types.into_owned());
312 }
313 TAtomic::Alias(alias) => {
314 *skip_key = true;
315 new_return_type_parts.extend(expand_alias(alias, codebase, options));
316 }
317 TAtomic::Derived(derived) => match derived {
318 TDerived::KeyOf(key_of) => {
319 *skip_key = true;
320 new_return_type_parts.extend(expand_key_of(key_of, codebase, options));
321 }
322 TDerived::ValueOf(value_of) => {
323 *skip_key = true;
324 new_return_type_parts.extend(expand_value_of(value_of, codebase, options));
325 }
326 TDerived::IndexAccess(index_access) => {
327 *skip_key = true;
328 new_return_type_parts.extend(expand_index_access(index_access, codebase, options));
329 }
330 TDerived::IntMask(int_mask) => {
331 *skip_key = true;
332 new_return_type_parts.extend(expand_int_mask(int_mask, codebase, options));
333 }
334 TDerived::IntMaskOf(int_mask_of) => {
335 *skip_key = true;
336 new_return_type_parts.extend(expand_int_mask_of(int_mask_of, codebase, options));
337 }
338 TDerived::PropertiesOf(properties_of) => {
339 *skip_key = true;
340 new_return_type_parts.extend(expand_properties_of(properties_of, codebase, options));
341 }
342 TDerived::New(new_type) => {
343 *skip_key = true;
344 new_return_type_parts.extend(expand_new(new_type, codebase, options));
345 }
346 TDerived::TemplateType(template_type) => {
347 *skip_key = true;
348 new_return_type_parts.extend(expand_template_type(template_type, codebase, options));
349 }
350 },
351 TAtomic::Iterable(iterable) => {
352 expand_union(codebase, Arc::make_mut(&mut iterable.key_type), options);
353 expand_union(codebase, Arc::make_mut(&mut iterable.value_type), options);
354 }
355 _ => {}
356 }
357}
358
359fn resolve_array_key(key: ArrayKey, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> ArrayKey {
366 let ArrayKey::ClassLikeConstant { class_like_name, constant_name } = key else {
367 return key;
368 };
369
370 let resolved_class_name = {
372 let name_lc = ascii_lowercase_word(class_like_name.as_bytes());
373 match name_lc.as_bytes() {
374 b"self" => options.self_class.unwrap_or(class_like_name),
375 b"static" | b"$this" => {
376 if let StaticClassType::Name(name) = &options.static_class_type {
377 *name
378 } else {
379 options.self_class.unwrap_or(class_like_name)
380 }
381 }
382 b"parent" => {
383 if let Some(self_class) = options.self_class
384 && let Some(class_metadata) = codebase.get_class_like(self_class.as_bytes())
385 && let Some(parent) = class_metadata.direct_parent_class
386 {
387 parent
388 } else {
389 class_like_name
390 }
391 }
392 _ => class_like_name,
393 }
394 };
395
396 let Some(class_like) = codebase.get_class_like(resolved_class_name.as_bytes()) else {
397 return ArrayKey::ClassLikeConstant { class_like_name, constant_name };
398 };
399
400 if let Some(constant) = class_like.constants.get(&constant_name)
402 && let Some(inferred) = &constant.inferred_type
403 {
404 match inferred {
405 TAtomic::Scalar(TScalar::Integer(TInteger::Literal(i))) => {
406 return ArrayKey::Integer(*i);
407 }
408 TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(s)), .. })) => {
409 return ArrayKey::String(*s);
410 }
411 _ => {}
412 }
413 }
414
415 if let Some(enum_case) = class_like.enum_cases.get(&constant_name)
417 && let Some(value_type) = &enum_case.value_type
418 {
419 match value_type {
420 TAtomic::Scalar(TScalar::Integer(TInteger::Literal(i))) => {
421 return ArrayKey::Integer(*i);
422 }
423 TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(s)), .. })) => {
424 return ArrayKey::String(*s);
425 }
426 _ => {}
427 }
428 }
429
430 ArrayKey::ClassLikeConstant { class_like_name, constant_name }
432}
433
434#[cold]
435fn expand_member_reference(
436 class_like_name: Word,
437 member_selector: &TReferenceMemberSelector,
438 codebase: &CodebaseMetadata,
439 options: &TypeExpansionOptions,
440 new_return_type_parts: &mut Vec<TAtomic>,
441) {
442 if let TReferenceMemberSelector::Identifier(member_name) = member_selector
443 && member_name.as_bytes().eq_ignore_ascii_case(b"class")
444 {
445 new_return_type_parts.push(TAtomic::Scalar(TScalar::literal_class_string(class_like_name)));
446 return;
447 }
448
449 let Some(class_like) = codebase.get_class_like(class_like_name.as_bytes()) else {
450 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
451 return;
452 };
453
454 for (constant_name, constant) in &class_like.constants {
455 if !member_selector.matches(*constant_name) {
456 continue;
457 }
458
459 if let Some(inferred_type) = constant.inferred_type.as_ref() {
460 let Some(_guard) = ConstantExpansionGuard::try_new(class_like_name, *constant_name) else {
461 new_return_type_parts.push(TAtomic::Never);
462 continue;
463 };
464
465 let mut inferred_type = inferred_type.clone();
466 let mut skip_inferred_type = false;
467 expand_atomic(&mut inferred_type, codebase, options, &mut skip_inferred_type, new_return_type_parts);
468
469 if !skip_inferred_type {
470 new_return_type_parts.push(inferred_type);
471 }
472 } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
473 let mut constant_type = type_metadata.type_union.clone();
474 expand_union(codebase, &mut constant_type, options);
475 new_return_type_parts.extend(constant_type.types.into_owned());
476 } else {
477 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
478 }
479 }
480
481 for enum_case_name in class_like.enum_cases.keys() {
482 if !member_selector.matches(*enum_case_name) {
483 continue;
484 }
485 new_return_type_parts.push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
486 }
487
488 if let TReferenceMemberSelector::Identifier(member_name) = member_selector
489 && let Some(type_alias) = class_like.type_aliases.get(member_name)
490 {
491 let mut alias_type = type_alias.type_union.clone();
492 expand_union(codebase, &mut alias_type, options);
493 new_return_type_parts.extend(alias_type.types.into_owned());
494 }
495
496 if new_return_type_parts.is_empty() {
497 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
498 }
499}
500
501fn expand_global_reference(
502 selector: &TGlobalReferenceSelector,
503 codebase: &CodebaseMetadata,
504 options: &TypeExpansionOptions,
505 new_return_type_parts: &mut Vec<TAtomic>,
506) {
507 for (constant_name, constant) in &codebase.constants {
508 if !selector.matches(*constant_name) {
509 continue;
510 }
511
512 if let Some(inferred_type) = constant.inferred_type.as_ref() {
513 let mut inferred_type = inferred_type.clone();
514 expand_union(codebase, &mut inferred_type, options);
515 new_return_type_parts.extend(inferred_type.types.into_owned());
516 } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
517 let mut constant_type = type_metadata.type_union.clone();
518 expand_union(codebase, &mut constant_type, options);
519 new_return_type_parts.extend(constant_type.types.into_owned());
520 } else {
521 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
522 }
523 }
524
525 if new_return_type_parts.is_empty() {
526 new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
527 }
528}
529
530fn expand_object(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
531 resolve_special_class_names(object, codebase, options);
532
533 if let TObject::Named(named) = object
534 && named.intersection_types.is_none()
535 && let Some(class_metadata) = codebase.get_class_like(named.name.as_bytes())
536 && class_metadata.kind.is_enum()
537 {
538 *object = TObject::new_enum(class_metadata.original_name);
539 return;
540 }
541
542 let TObject::Named(named) = object else {
543 return;
544 };
545
546 let has_params = named.type_parameters.as_ref().is_some_and(|p| !p.is_empty());
547 let class_metadata = codebase.get_class_like(named.name.as_bytes());
548 let has_required_intersections =
549 class_metadata.map(|m| !m.require_extends.is_empty() || !m.require_implements.is_empty()).unwrap_or(false);
550 let needs_default_params = !has_params && class_metadata.map(|m| !m.template_types.is_empty()).unwrap_or(false);
551
552 if !has_params && !has_required_intersections && !needs_default_params {
553 return;
554 }
555
556 let Some(_guard) = ObjectParamsExpansionGuard::try_new(named.name) else {
557 return;
558 };
559
560 if has_required_intersections && let Some(class_metadata) = class_metadata {
561 for &required in class_metadata.require_extends.iter().chain(&class_metadata.require_implements) {
562 named.add_intersection_type(TAtomic::Object(TObject::Named(TNamedObject::new(required))));
563 }
564 }
565
566 expand_or_fill_type_parameters(named, codebase, options);
567}
568
569#[derive(Copy, Clone, Eq, PartialEq)]
574enum SpecialClassName {
575 None,
576 SelfType,
577 Static,
578 Parent,
579 This,
580}
581
582#[inline]
583fn classify_special_class_name(name: &[u8]) -> SpecialClassName {
584 match name.len() {
585 4 => {
586 if name.eq_ignore_ascii_case(b"self") {
587 SpecialClassName::SelfType
588 } else {
589 SpecialClassName::None
590 }
591 }
592 5 => {
593 if name == b"$this" || name.eq_ignore_ascii_case(b"$this") {
594 SpecialClassName::This
595 } else {
596 SpecialClassName::None
597 }
598 }
599 6 => {
600 if name.eq_ignore_ascii_case(b"static") {
601 SpecialClassName::Static
602 } else if name.eq_ignore_ascii_case(b"parent") {
603 SpecialClassName::Parent
604 } else {
605 SpecialClassName::None
606 }
607 }
608 _ => SpecialClassName::None,
609 }
610}
611
612fn resolve_special_class_names(object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
614 let TObject::Named(named) = object else {
615 return;
616 };
617
618 let special = classify_special_class_name(named.name.as_bytes());
619 if matches!(special, SpecialClassName::None) && !named.is_static && !named.is_this {
620 return;
621 }
622
623 let needs_static_resolution = matches!(special, SpecialClassName::Static | SpecialClassName::This) || named.is_this;
624
625 if needs_static_resolution && let StaticClassType::Object(TObject::Enum(static_enum)) = &options.static_class_type {
626 *object = TObject::Enum(static_enum.clone());
627 return;
628 }
629
630 let TObject::Named(named) = object else {
631 return;
632 };
633
634 let was_this = named.is_this;
635 match special {
636 SpecialClassName::Static | SpecialClassName::This => {
637 resolve_static_type(named, was_this, false, codebase, options)
638 }
639 SpecialClassName::SelfType => {
640 if let Some(self_class) = options.self_class {
641 named.name = self_class;
642 }
643 }
644 SpecialClassName::Parent => {
645 if let Some(self_class) = options.self_class
646 && let Some(class_metadata) = codebase.get_class_like(self_class.as_bytes())
647 && let Some(parent) = class_metadata.direct_parent_class
648 {
649 named.name = parent;
650 }
651 }
652 SpecialClassName::None if named.is_static => resolve_static_type(named, was_this, true, codebase, options),
653 SpecialClassName::None => {}
654 }
655}
656
657fn resolve_static_type(
662 named: &mut TNamedObject,
663 is_this_type: bool,
664 check_compatibility: bool,
665 codebase: &CodebaseMetadata,
666 options: &TypeExpansionOptions,
667) {
668 match &options.static_class_type {
669 StaticClassType::Object(TObject::Named(static_obj)) => {
670 if check_compatibility && !is_static_type_compatible(named, static_obj, codebase) {
671 return;
672 }
673
674 if let Some(intersections) = &static_obj.intersection_types {
675 named.intersection_types.get_or_insert_with(Vec::new).extend(intersections.iter().cloned());
676 }
677
678 if static_obj.type_parameters.is_some() && should_use_static_type_params(named, static_obj, codebase) {
679 named.type_parameters.clone_from(&static_obj.type_parameters);
680 }
681
682 named.name = static_obj.name;
683 let effectively_final = is_effectively_final(&static_obj.name, codebase, options);
684 named.is_static = !effectively_final;
685 named.is_this = !effectively_final && is_this_type;
686 }
687 StaticClassType::Name(static_class)
688 if (!check_compatibility || codebase.is_instance_of(static_class.as_bytes(), named.name.as_bytes())) =>
689 {
690 named.name = *static_class;
691 let effectively_final = is_effectively_final(static_class, codebase, options);
692 named.is_static = !effectively_final;
693 named.is_this = !effectively_final && is_this_type;
694 }
695 _ => {}
696 }
697}
698
699fn is_effectively_final(class_name: &Word, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> bool {
707 if options.function_is_final {
708 return true;
709 }
710
711 codebase.get_class_like(class_name.as_bytes()).is_some_and(|meta| meta.name_span.is_none() || meta.flags.is_final())
712}
713
714fn is_static_type_compatible(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
716 codebase.is_instance_of(static_obj.name.as_bytes(), named.name.as_bytes())
717 || static_obj
718 .intersection_types
719 .iter()
720 .flatten()
721 .filter_map(|t| if let TAtomic::Object(obj) = t { obj.get_name() } else { None })
722 .any(|name| codebase.is_instance_of(name.as_bytes(), named.name.as_bytes()))
723}
724
725fn should_use_static_type_params(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
728 let Some(current_params) = &named.type_parameters else {
729 return true;
730 };
731
732 let Some(class_metadata) = codebase.get_class_like(static_obj.name.as_bytes()) else {
733 return false;
734 };
735
736 let templates = &class_metadata.template_types;
737
738 current_params.len() == templates.len()
739 && current_params.iter().zip(templates.values()).all(|(current, template)| {
740 current == &template.constraint || template.default.as_ref().is_some_and(|default| current == default)
741 })
742}
743
744fn expand_or_fill_type_parameters(
746 named: &mut TNamedObject,
747 codebase: &CodebaseMetadata,
748 options: &TypeExpansionOptions,
749) {
750 if let Some(class_metadata) = codebase.get_class_like(named.name.as_bytes()) {
751 let template_count = class_metadata.template_types.len();
752 let supplied_count = named.type_parameters.as_ref().map_or(0, Vec::len);
753
754 if supplied_count < template_count {
755 let mut params = named.type_parameters.take().unwrap_or_default();
756 params.extend(class_metadata.template_types.values().skip(supplied_count).map(|template| {
757 let mut fallback = template.default.clone().unwrap_or_else(|| template.constraint.clone());
758 fallback.set_from_template_default(true);
759 fallback
760 }));
761 named.type_parameters = Some(params);
762 }
763 }
764
765 if let Some(params) = &mut named.type_parameters {
766 for param in params.iter_mut() {
767 expand_union(codebase, param, options);
768 }
769 }
770}
771
772#[must_use]
773pub fn get_signature_of_function_like_identifier(
774 function_like_identifier: &FunctionLikeIdentifier,
775 codebase: &CodebaseMetadata,
776) -> Option<TCallableSignature> {
777 Some(match function_like_identifier {
778 FunctionLikeIdentifier::Function(name) => {
779 let function_like_metadata = codebase.get_function(name.as_bytes())?;
780
781 get_signature_of_function_like_metadata(
782 function_like_identifier,
783 function_like_metadata,
784 codebase,
785 &TypeExpansionOptions::default(),
786 )
787 }
788 FunctionLikeIdentifier::Closure(name) => {
789 let function_like_metadata = codebase.get_closure(name)?;
790
791 get_signature_of_function_like_metadata(
792 function_like_identifier,
793 function_like_metadata,
794 codebase,
795 &TypeExpansionOptions::default(),
796 )
797 }
798 FunctionLikeIdentifier::Method(classlike_name, method_name) => {
799 let function_like_metadata =
800 codebase.get_declaring_method(classlike_name.as_bytes(), method_name.as_bytes())?;
801
802 get_signature_of_function_like_metadata(
803 function_like_identifier,
804 function_like_metadata,
805 codebase,
806 &TypeExpansionOptions {
807 self_class: Some(*classlike_name),
808 static_class_type: StaticClassType::Name(*classlike_name),
809 ..Default::default()
810 },
811 )
812 }
813 })
814}
815
816#[must_use]
817pub fn get_atomic_of_function_like_identifier(
818 function_like_identifier: &FunctionLikeIdentifier,
819 codebase: &CodebaseMetadata,
820) -> Option<TAtomic> {
821 let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
822
823 Some(TAtomic::Callable(TCallable::Signature(signature)))
824}
825
826#[must_use]
827pub fn get_signature_of_function_like_metadata(
828 function_like_identifier: &FunctionLikeIdentifier,
829 function_like_metadata: &FunctionLikeMetadata,
830 codebase: &CodebaseMetadata,
831 options: &TypeExpansionOptions,
832) -> TCallableSignature {
833 let parameters: Vec<_> = function_like_metadata
834 .parameters
835 .iter()
836 .map(|parameter_metadata| {
837 let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
838 let mut t = t.type_union.clone();
839 expand_union(codebase, &mut t, options);
840 Some(Arc::new(t))
841 } else {
842 None
843 };
844
845 TCallableParameter::new(
846 type_signature,
847 parameter_metadata.flags.is_by_reference(),
848 parameter_metadata.flags.is_variadic(),
849 parameter_metadata.flags.has_default(),
850 )
851 })
852 .collect();
853
854 let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
855 let mut return_type = type_metadata.type_union.clone();
856 expand_union(codebase, &mut return_type, options);
857 Some(Arc::new(return_type))
858 } else {
859 None
860 };
861
862 let is_closure = matches!(function_like_identifier, FunctionLikeIdentifier::Closure(_));
863 TCallableSignature::new(function_like_metadata.flags.is_pure(), is_closure)
864 .with_parameters(parameters)
865 .with_return_type(return_type)
866 .with_source(Some(*function_like_identifier))
867}
868
869#[cold]
870fn expand_key_of(
871 return_type_key_of: &TKeyOf,
872 codebase: &CodebaseMetadata,
873 options: &TypeExpansionOptions,
874) -> Vec<TAtomic> {
875 let mut target_type = return_type_key_of.get_target_type().clone();
876 expand_union(codebase, &mut target_type, options);
877
878 let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
879 return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
880 };
881
882 new_return_types.types.into_owned()
883}
884
885#[cold]
886fn expand_value_of(
887 return_type_value_of: &TValueOf,
888 codebase: &CodebaseMetadata,
889 options: &TypeExpansionOptions,
890) -> Vec<TAtomic> {
891 let mut target_type = return_type_value_of.get_target_type().clone();
892 expand_union(codebase, &mut target_type, options);
893
894 let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
895 return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
896 };
897
898 new_return_types.types.into_owned()
899}
900
901#[cold]
902fn expand_index_access(
903 return_type_index_access: &TIndexAccess,
904 codebase: &CodebaseMetadata,
905 options: &TypeExpansionOptions,
906) -> Vec<TAtomic> {
907 let mut target_type = return_type_index_access.get_target_type().clone();
908 expand_union(codebase, &mut target_type, options);
909
910 let mut index_type = return_type_index_access.get_index_type().clone();
911 expand_union(codebase, &mut index_type, options);
912
913 let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
914 else {
915 return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
916 };
917
918 new_return_types.types.into_owned()
919}
920
921#[cold]
922fn expand_new(new_type: &TNew, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
923 let mut target_type = new_type.get_target_type().clone();
924 expand_union(codebase, &mut target_type, options);
925
926 let Some(new_return_types) = TNew::get_new_targets(&target_type.types, codebase) else {
927 return vec![TAtomic::Derived(TDerived::New(new_type.clone()))];
928 };
929
930 new_return_types.types.into_owned()
931}
932
933#[cold]
934fn expand_template_type(
935 template_type: &TTemplateType,
936 codebase: &CodebaseMetadata,
937 options: &TypeExpansionOptions,
938) -> Vec<TAtomic> {
939 let mut expanded = template_type.clone();
940 expand_union(codebase, expanded.get_object_mut(), options);
941 expand_union(codebase, expanded.get_class_name_mut(), options);
942 expand_union(codebase, expanded.get_template_name_mut(), options);
943
944 let Some(resolved) = expanded.resolve(codebase) else {
945 return vec![TAtomic::Mixed(TMixed::new())];
946 };
947
948 resolved.types.into_owned()
949}
950
951#[cold]
952fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
953 let mut literal_values = Vec::new();
954
955 for value in int_mask.get_values() {
956 let mut expanded = value.clone();
957 expand_union(codebase, &mut expanded, options);
958
959 if let Some(int_val) = expanded.get_single_literal_int_value() {
960 literal_values.push(int_val);
961 }
962 }
963
964 if literal_values.is_empty() {
965 return vec![TAtomic::Scalar(TScalar::int())];
966 }
967
968 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
969 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
970}
971
972#[cold]
973fn expand_int_mask_of(
974 int_mask_of: &TIntMaskOf,
975 codebase: &CodebaseMetadata,
976 options: &TypeExpansionOptions,
977) -> Vec<TAtomic> {
978 let mut target = int_mask_of.get_target_type().clone();
979 expand_union(codebase, &mut target, options);
980
981 let mut literal_values = Vec::new();
982 for atomic in target.types.iter() {
983 if let Some(int_val) = atomic.get_literal_int_value() {
984 literal_values.push(int_val);
985 }
986 }
987
988 if literal_values.is_empty() {
989 return vec![TAtomic::Scalar(TScalar::int())];
990 }
991
992 let combinations = TIntMask::calculate_mask_combinations(&literal_values);
993 combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
994}
995
996#[cold]
997fn expand_properties_of(
998 properties_of: &TPropertiesOf,
999 codebase: &CodebaseMetadata,
1000 options: &TypeExpansionOptions,
1001) -> Vec<TAtomic> {
1002 let mut target_type = properties_of.get_target_type().clone();
1003 expand_union(codebase, &mut target_type, options);
1004
1005 let Some(mut keyed_array) =
1006 TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
1007 else {
1008 return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
1009 };
1010
1011 let mut skip_keyed_array = false;
1012 let mut expanded_parts = vec![];
1013 expand_atomic(&mut keyed_array, codebase, options, &mut skip_keyed_array, &mut expanded_parts);
1014 if skip_keyed_array {
1015 expanded_parts
1016 } else {
1017 expanded_parts.push(keyed_array);
1018 expanded_parts
1019 }
1020}
1021
1022#[cold]
1023fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
1024 let class_name = alias.get_class_name();
1025 let alias_name = alias.get_alias_name();
1026
1027 let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
1029
1030 if is_cycle {
1031 return vec![TAtomic::Alias(alias.clone())];
1032 }
1033
1034 let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
1035 return vec![TAtomic::Alias(alias.clone())];
1036 };
1037
1038 let _guard = AliasExpansionGuard::new(class_name, alias_name);
1039
1040 expand_union(codebase, &mut expanded_union, options);
1041
1042 expanded_union.types.into_owned()
1043}
1044
1045#[cfg(test)]
1046#[allow(clippy::unwrap_used, clippy::expect_used)]
1047mod tests {
1048 use super::*;
1049 use mago_allocator::LocalArena;
1050
1051 use std::borrow::Cow;
1052 use std::collections::HashSet;
1053 use std::sync::Arc;
1054
1055 use mago_database::Database;
1056 use mago_database::DatabaseReader;
1057 use mago_database::file::File;
1058
1059 use mago_names::resolver::NameResolver;
1060
1061 use mago_syntax::parser::parse_file;
1062 use mago_word::WordSet;
1063 use mago_word::word;
1064
1065 use crate::metadata::CodebaseMetadata;
1066 use crate::misc::GenericParent;
1067 use crate::populator::populate_codebase;
1068 use crate::reference::SymbolReferences;
1069 use crate::scanner::scan_program;
1070 use crate::ttype::atomic::array::TArray;
1071 use crate::ttype::atomic::array::keyed::TKeyedArray;
1072 use crate::ttype::atomic::array::list::TList;
1073 use crate::ttype::atomic::callable::TCallable;
1074 use crate::ttype::atomic::callable::TCallableSignature;
1075 use crate::ttype::atomic::callable::parameter::TCallableParameter;
1076 use crate::ttype::atomic::conditional::TConditional;
1077 use crate::ttype::atomic::derived::TDerived;
1078 use crate::ttype::atomic::derived::index_access::TIndexAccess;
1079 use crate::ttype::atomic::derived::key_of::TKeyOf;
1080 use crate::ttype::atomic::derived::value_of::TValueOf;
1081 use crate::ttype::atomic::generic::TGenericParameter;
1082 use crate::ttype::atomic::iterable::TIterable;
1083 use crate::ttype::atomic::object::r#enum::TEnum;
1084 use crate::ttype::atomic::object::named::TNamedObject;
1085 use crate::ttype::atomic::reference::TReference;
1086 use crate::ttype::atomic::reference::TReferenceMemberSelector;
1087 use crate::ttype::atomic::scalar::TScalar;
1088 use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
1089 use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
1090 use crate::ttype::flags::UnionFlags;
1091 use crate::ttype::get_int;
1092 use crate::ttype::get_mixed;
1093 use crate::ttype::get_never;
1094 use crate::ttype::get_null;
1095 use crate::ttype::get_string;
1096 use crate::ttype::get_void;
1097
1098 fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
1099 let file = File::ephemeral(Cow::Borrowed(b"code.php"), Cow::Borrowed(code.as_bytes()));
1100 let config =
1101 mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
1102 .into_static();
1103 let database = Database::single(file, config);
1104
1105 let mut codebase = CodebaseMetadata::new();
1106 let arena = LocalArena::new();
1107 for file in database.files() {
1108 let program = parse_file(&arena, &file);
1109 assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
1110 let resolved_names = NameResolver::new(&arena).resolve(program);
1111 let program_codebase =
1112 scan_program(&arena, &file, program, &resolved_names, mago_php_version::PHPVersion::LATEST);
1113
1114 codebase.extend(program_codebase);
1115 }
1116
1117 populate_codebase(&mut codebase, &mut SymbolReferences::new(), WordSet::default(), HashSet::default());
1118
1119 codebase
1120 }
1121
1122 fn options_with_self(self_class: &str) -> TypeExpansionOptions {
1123 TypeExpansionOptions { self_class: Some(ascii_lowercase_word(self_class.as_bytes())), ..Default::default() }
1124 }
1125
1126 fn options_with_static(static_class: &str) -> TypeExpansionOptions {
1127 TypeExpansionOptions {
1128 self_class: Some(ascii_lowercase_word(static_class.as_bytes())),
1129 static_class_type: StaticClassType::Name(ascii_lowercase_word(static_class.as_bytes())),
1130 ..Default::default()
1131 }
1132 }
1133
1134 fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
1135 TypeExpansionOptions {
1136 self_class: object.get_name(),
1137 static_class_type: StaticClassType::Object(object),
1138 ..Default::default()
1139 }
1140 }
1141
1142 macro_rules! assert_expands_to {
1143 ($codebase:expr, $input:expr, $expected:expr) => {
1144 assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
1145 };
1146 ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
1147 let mut actual = $input.clone();
1148 expand_union($codebase, &mut actual, $options);
1149 assert_eq!(
1150 actual.types.as_ref(),
1151 $expected.types.as_ref(),
1152 "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
1153 $input,
1154 $expected,
1155 actual
1156 );
1157 }};
1158 }
1159
1160 fn make_self_object() -> TUnion {
1161 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))))
1162 }
1163
1164 fn make_static_object() -> TUnion {
1165 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))))
1166 }
1167
1168 fn make_parent_object() -> TUnion {
1169 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(word("parent")))))
1170 }
1171
1172 fn make_named_object(name: &str) -> TUnion {
1173 TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(name.as_bytes())))))
1174 }
1175
1176 #[test]
1177 fn test_expand_null_type() {
1178 let codebase = CodebaseMetadata::new();
1179 let null_type = get_null();
1180 assert_expands_to!(&codebase, null_type, get_null());
1181 }
1182
1183 #[test]
1184 fn test_expand_void_type() {
1185 let codebase = CodebaseMetadata::new();
1186 let void_type = get_void();
1187 assert_expands_to!(&codebase, void_type, get_void());
1188 }
1189
1190 #[test]
1191 fn test_expand_never_type() {
1192 let codebase = CodebaseMetadata::new();
1193 let never_type = get_never();
1194 assert_expands_to!(&codebase, never_type, get_never());
1195 }
1196
1197 #[test]
1198 fn test_expand_int_type() {
1199 let codebase = CodebaseMetadata::new();
1200 let int_type = get_int();
1201 assert_expands_to!(&codebase, int_type, get_int());
1202 }
1203
1204 #[test]
1205 fn test_expand_mixed_type() {
1206 let codebase = CodebaseMetadata::new();
1207 let mixed_type = get_mixed();
1208 assert_expands_to!(&codebase, mixed_type, get_mixed());
1209 }
1210
1211 #[test]
1212 fn test_expand_keyed_array_with_self_key() {
1213 let code = "<?php class Foo {}";
1214 let codebase = create_test_codebase(code);
1215
1216 let mut keyed = TKeyedArray::new();
1217 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
1218 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1219
1220 let options = options_with_self("Foo");
1221 let mut actual = input;
1222 expand_union(&codebase, &mut actual, &options);
1223
1224 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1225 && let Some((key, _)) = &keyed.parameters
1226 {
1227 assert!(key.types.iter().any(|t| {
1228 if let TAtomic::Object(TObject::Named(named)) = t {
1229 named.name == ascii_lowercase_word(b"foo")
1230 } else {
1231 false
1232 }
1233 }));
1234 }
1235 }
1236
1237 #[test]
1238 fn test_expand_keyed_array_with_self_value() {
1239 let code = "<?php class Foo {}";
1240 let codebase = create_test_codebase(code);
1241
1242 let mut keyed = TKeyedArray::new();
1243 keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
1244 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1245
1246 let options = options_with_self("Foo");
1247 let mut actual = input;
1248 expand_union(&codebase, &mut actual, &options);
1249
1250 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1251 && let Some((_, value)) = &keyed.parameters
1252 {
1253 assert!(value.types.iter().any(|t| {
1254 if let TAtomic::Object(TObject::Named(named)) = t {
1255 named.name == ascii_lowercase_word(b"foo")
1256 } else {
1257 false
1258 }
1259 }));
1260 }
1261 }
1262
1263 #[test]
1264 fn test_expand_keyed_array_known_items() {
1265 let code = "<?php class Foo {}";
1266 let codebase = create_test_codebase(code);
1267
1268 use crate::ttype::atomic::array::key::ArrayKey;
1269 use std::collections::BTreeMap;
1270
1271 let mut keyed = TKeyedArray::new();
1272 let mut known_items = BTreeMap::new();
1273 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
1274 keyed.known_items = Some(known_items);
1275 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1276
1277 let options = options_with_self("Foo");
1278 let mut actual = input;
1279 expand_union(&codebase, &mut actual, &options);
1280
1281 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1282 && let Some(items) = &keyed.known_items
1283 {
1284 let (_, item_type) = items.get(&ArrayKey::String(word("key"))).unwrap();
1285 assert!(item_type.types.iter().any(|t| {
1286 if let TAtomic::Object(TObject::Named(named)) = t {
1287 named.name == ascii_lowercase_word(b"foo")
1288 } else {
1289 false
1290 }
1291 }));
1292 }
1293 }
1294
1295 #[test]
1296 fn test_expand_list_with_self_element() {
1297 let code = "<?php class Foo {}";
1298 let codebase = create_test_codebase(code);
1299
1300 let list = TList::new(Arc::new(make_self_object()));
1301 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1302
1303 let options = options_with_self("Foo");
1304 let mut actual = input;
1305 expand_union(&codebase, &mut actual, &options);
1306
1307 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1308 assert!(list.element_type.types.iter().any(|t| {
1309 if let TAtomic::Object(TObject::Named(named)) = t {
1310 named.name == ascii_lowercase_word(b"foo")
1311 } else {
1312 false
1313 }
1314 }));
1315 }
1316 }
1317
1318 #[test]
1319 fn test_expand_list_known_elements() {
1320 let code = "<?php class Foo {}";
1321 let codebase = create_test_codebase(code);
1322
1323 use std::collections::BTreeMap;
1324
1325 let mut list = TList::new(Arc::new(get_mixed()));
1326 let mut known_elements = BTreeMap::new();
1327 known_elements.insert(0, (false, make_self_object()));
1328 list.known_elements = Some(known_elements);
1329 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1330
1331 let options = options_with_self("Foo");
1332 let mut actual = input;
1333 expand_union(&codebase, &mut actual, &options);
1334
1335 if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1336 && let Some(elements) = &list.known_elements
1337 {
1338 let (_, element_type) = elements.get(&0).unwrap();
1339 assert!(element_type.types.iter().any(|t| {
1340 if let TAtomic::Object(TObject::Named(named)) = t {
1341 named.name == ascii_lowercase_word(b"foo")
1342 } else {
1343 false
1344 }
1345 }));
1346 }
1347 }
1348
1349 #[test]
1350 fn test_expand_nested_array() {
1351 let code = "<?php class Foo {}";
1352 let codebase = create_test_codebase(code);
1353
1354 let inner_list = TList::new(Arc::new(make_self_object()));
1355 let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1356
1357 let mut outer = TKeyedArray::new();
1358 outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1359 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1360
1361 let options = options_with_self("Foo");
1362 let mut actual = input;
1363 expand_union(&codebase, &mut actual, &options);
1364
1365 if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1366 && let Some((key, value)) = &keyed.parameters
1367 {
1368 assert!(key.types.iter().any(|t| {
1369 if let TAtomic::Object(TObject::Named(named)) = t {
1370 named.name == ascii_lowercase_word(b"foo")
1371 } else {
1372 false
1373 }
1374 }));
1375 if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1376 assert!(inner.element_type.types.iter().any(|t| {
1377 if let TAtomic::Object(TObject::Named(named)) = t {
1378 named.name == ascii_lowercase_word(b"foo")
1379 } else {
1380 false
1381 }
1382 }));
1383 }
1384 }
1385 }
1386
1387 #[test]
1388 fn test_expand_empty_array() {
1389 let codebase = CodebaseMetadata::new();
1390 let keyed = TKeyedArray::new();
1391 let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1392 let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1393 assert_expands_to!(&codebase, input, expected);
1394 }
1395
1396 #[test]
1397 fn test_expand_non_empty_list() {
1398 let code = "<?php class Foo {}";
1399 let codebase = create_test_codebase(code);
1400
1401 let mut list = TList::new(Arc::new(make_self_object()));
1402 list.non_empty = true;
1403 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1404
1405 let options = options_with_self("Foo");
1406 let mut actual = input;
1407 expand_union(&codebase, &mut actual, &options);
1408
1409 if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1410 assert!(list.non_empty);
1411 assert!(list.element_type.types.iter().any(|t| {
1412 if let TAtomic::Object(TObject::Named(named)) = t {
1413 named.name == ascii_lowercase_word(b"foo")
1414 } else {
1415 false
1416 }
1417 }));
1418 }
1419 }
1420
1421 #[test]
1422 fn test_expand_self_to_class_name() {
1423 let code = "<?php class Foo {}";
1424 let codebase = create_test_codebase(code);
1425
1426 let input = make_self_object();
1427 let options = options_with_self("Foo");
1428 let mut actual = input;
1429 expand_union(&codebase, &mut actual, &options);
1430
1431 assert!(actual.types.iter().any(|t| {
1432 if let TAtomic::Object(TObject::Named(named)) = t {
1433 named.name == ascii_lowercase_word(b"foo")
1434 } else {
1435 false
1436 }
1437 }));
1438 }
1439
1440 #[test]
1441 fn test_expand_static_to_class_name() {
1442 let code = "<?php class Foo {}";
1443 let codebase = create_test_codebase(code);
1444
1445 let input = make_static_object();
1446 let options = options_with_static("Foo");
1447 let mut actual = input;
1448 expand_union(&codebase, &mut actual, &options);
1449
1450 assert!(actual.types.iter().any(|t| {
1451 if let TAtomic::Object(TObject::Named(named)) = t {
1452 named.name == ascii_lowercase_word(b"foo")
1453 } else {
1454 false
1455 }
1456 }));
1457 }
1458
1459 #[test]
1460 fn test_expand_static_with_object_type() {
1461 let code = "<?php class Foo {}";
1462 let codebase = create_test_codebase(code);
1463
1464 let input = make_static_object();
1465 let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_word(b"foo")));
1466 let options = options_with_static_object(static_obj);
1467 let mut actual = input;
1468 expand_union(&codebase, &mut actual, &options);
1469
1470 assert!(actual.types.iter().any(|t| {
1471 if let TAtomic::Object(TObject::Named(named)) = t {
1472 named.name == ascii_lowercase_word(b"foo") && named.is_static && !named.is_this
1473 } else {
1474 false
1475 }
1476 }));
1477 }
1478
1479 #[test]
1480 fn test_expand_static_with_enum_type() {
1481 let code = "<?php enum Status { case Active; case Inactive; }";
1482 let codebase = create_test_codebase(code);
1483
1484 let input = make_static_object();
1485 let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")));
1486 let options = options_with_static_object(static_enum);
1487 let mut actual = input;
1488 expand_union(&codebase, &mut actual, &options);
1489
1490 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1491 }
1492
1493 #[test]
1494 fn test_expand_parent_to_parent_class() {
1495 let code = "<?php
1496 class BaseClass {}
1497 class ChildClass extends BaseClass {}
1498 ";
1499 let codebase = create_test_codebase(code);
1500
1501 let input = make_parent_object();
1502 let options = options_with_self("ChildClass");
1503 let mut actual = input;
1504 expand_union(&codebase, &mut actual, &options);
1505
1506 assert!(actual.types.iter().any(|t| {
1507 if let TAtomic::Object(TObject::Named(named)) = t {
1508 named.name == ascii_lowercase_word(b"baseclass")
1509 } else {
1510 false
1511 }
1512 }));
1513 }
1514
1515 #[test]
1516 fn test_expand_parent_without_parent_class() {
1517 let code = "<?php class Foo {}";
1518 let codebase = create_test_codebase(code);
1519
1520 let input = make_parent_object();
1521 let options = options_with_self("Foo");
1522 let mut actual = input;
1523 expand_union(&codebase, &mut actual, &options);
1524
1525 assert!(actual.types.iter().any(|t| {
1526 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("parent") } else { false }
1527 }));
1528 }
1529
1530 #[test]
1531 fn test_expand_this_variable() {
1532 let code = "<?php class Foo {}";
1533 let codebase = create_test_codebase(code);
1534
1535 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(word("$this")))));
1536 let options = options_with_static("Foo");
1537 let mut actual = input;
1538 expand_union(&codebase, &mut actual, &options);
1539
1540 assert!(actual.types.iter().any(|t| {
1541 if let TAtomic::Object(TObject::Named(named)) = t {
1542 named.name == ascii_lowercase_word(b"foo")
1543 } else {
1544 false
1545 }
1546 }));
1547 }
1548
1549 #[test]
1550 fn test_expand_this_with_final_function() {
1551 let code = "<?php class Foo {}";
1552 let codebase = create_test_codebase(code);
1553
1554 let input = make_static_object();
1555 let options = TypeExpansionOptions {
1556 self_class: Some(ascii_lowercase_word(b"foo")),
1557 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"foo")),
1558 function_is_final: true,
1559 ..Default::default()
1560 };
1561 let mut actual = input;
1562 expand_union(&codebase, &mut actual, &options);
1563
1564 assert!(actual.types.iter().any(|t| {
1565 if let TAtomic::Object(TObject::Named(named)) = t {
1566 named.name == ascii_lowercase_word(b"foo") && !named.is_this
1567 } else {
1568 false
1569 }
1570 }));
1571 }
1572
1573 #[test]
1574 fn test_expand_object_with_type_parameters() {
1575 let code = "<?php class Container {}";
1576 let codebase = create_test_codebase(code);
1577
1578 let named =
1579 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
1580 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1581
1582 let options = options_with_self("Foo");
1583 let mut actual = input;
1584 expand_union(&codebase, &mut actual, &options);
1585
1586 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1587 && let Some(params) = &named.type_parameters
1588 {
1589 assert!(params[0].types.iter().any(|t| {
1590 if let TAtomic::Object(TObject::Named(named)) = t {
1591 named.name == ascii_lowercase_word(b"foo")
1592 } else {
1593 false
1594 }
1595 }));
1596 }
1597 }
1598
1599 #[test]
1600 fn test_expand_object_gets_default_type_params() {
1601 let code = "<?php
1602 /** @template T */
1603 class Container {}
1604 ";
1605 let codebase = create_test_codebase(code);
1606
1607 let named = TNamedObject::new(ascii_lowercase_word(b"container"));
1608 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1609
1610 let mut actual = input;
1611 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1612
1613 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1614 assert!(named.type_parameters.is_some());
1615 }
1616 }
1617
1618 #[test]
1619 fn test_expand_object_intersection_from_static() {
1620 let code = "<?php
1621 interface Stringable {}
1622 class Foo implements Stringable {}
1623 ";
1624 let codebase = create_test_codebase(code);
1625
1626 let input = make_static_object();
1627
1628 let mut static_named = TNamedObject::new(ascii_lowercase_word(b"foo"));
1629 static_named.intersection_types =
1630 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
1631 let static_obj = TObject::Named(static_named);
1632 let options = options_with_static_object(static_obj);
1633
1634 let mut actual = input;
1635 expand_union(&codebase, &mut actual, &options);
1636
1637 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1638 assert!(named.intersection_types.is_some());
1639 }
1640 }
1641
1642 #[test]
1643 fn test_expand_self_without_self_class_option() {
1644 let codebase = CodebaseMetadata::new();
1645
1646 let input = make_self_object();
1647 let mut actual = input;
1648 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1649
1650 assert!(actual.types.iter().any(|t| {
1651 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
1652 }));
1653 }
1654
1655 #[test]
1656 fn test_expand_callable_return_type() {
1657 let code = "<?php class Foo {}";
1658 let codebase = create_test_codebase(code);
1659
1660 let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1661 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1662
1663 let options = options_with_self("Foo");
1664 let mut actual = input;
1665 expand_union(&codebase, &mut actual, &options);
1666
1667 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1668 && let Some(ret) = sig.get_return_type()
1669 {
1670 assert!(ret.types.iter().any(|t| {
1671 if let TAtomic::Object(TObject::Named(named)) = t {
1672 named.name == ascii_lowercase_word(b"foo")
1673 } else {
1674 false
1675 }
1676 }));
1677 }
1678 }
1679
1680 #[test]
1681 fn test_expand_callable_parameter_types() {
1682 let code = "<?php class Foo {}";
1683 let codebase = create_test_codebase(code);
1684
1685 let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1686 let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1687 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1688
1689 let options = options_with_self("Foo");
1690 let mut actual = input;
1691 expand_union(&codebase, &mut actual, &options);
1692
1693 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1694 && let Some(param) = sig.get_parameters().first()
1695 && let Some(param_type) = param.get_type_signature()
1696 {
1697 assert!(param_type.types.iter().any(|t| {
1698 if let TAtomic::Object(TObject::Named(named)) = t {
1699 named.name == ascii_lowercase_word(b"foo")
1700 } else {
1701 false
1702 }
1703 }));
1704 }
1705 }
1706
1707 #[test]
1708 fn test_expand_callable_alias_to_function() {
1709 let code = "<?php
1710 function myFunc(): int { return 1; }
1711 ";
1712 let codebase = create_test_codebase(code);
1713
1714 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc")));
1715 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1716
1717 let mut actual = input;
1718 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1719
1720 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1721 }
1722
1723 #[test]
1724 fn test_expand_callable_alias_to_method() {
1725 let code = "<?php
1726 class Foo {
1727 public function bar(): int { return 1; }
1728 }
1729 ";
1730 let codebase = create_test_codebase(code);
1731
1732 let alias = TCallable::Alias(FunctionLikeIdentifier::Method(
1733 ascii_lowercase_word(b"foo"),
1734 ascii_lowercase_word(b"bar"),
1735 ));
1736 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1737
1738 let mut actual = input;
1739 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1740
1741 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1742 }
1743
1744 #[test]
1745 fn test_expand_callable_alias_unknown() {
1746 let codebase = CodebaseMetadata::new();
1747
1748 let alias = TCallable::Alias(FunctionLikeIdentifier::Function(word("nonexistent")));
1749 let input = TUnion::from_atomic(TAtomic::Callable(alias));
1750
1751 let mut actual = input;
1752 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1753
1754 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1755 }
1756
1757 #[test]
1758 fn test_expand_closure_signature() {
1759 let code = "<?php class Foo {}";
1760 let codebase = create_test_codebase(code);
1761
1762 let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
1763 let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1764
1765 let options = options_with_self("Foo");
1766 let mut actual = input;
1767 expand_union(&codebase, &mut actual, &options);
1768
1769 if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1770 && let Some(ret) = sig.get_return_type()
1771 {
1772 assert!(ret.types.iter().any(|t| {
1773 if let TAtomic::Object(TObject::Named(named)) = t {
1774 named.name == ascii_lowercase_word(b"foo")
1775 } else {
1776 false
1777 }
1778 }));
1779 }
1780 }
1781
1782 #[test]
1783 fn test_expand_generic_parameter_constraint() {
1784 let code = "<?php class Foo {}";
1785 let codebase = create_test_codebase(code);
1786
1787 let generic = TGenericParameter::new(
1788 word("T"),
1789 Arc::new(make_self_object()),
1790 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
1791 );
1792 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1793
1794 let options = options_with_self("Foo");
1795 let mut actual = input;
1796 expand_union(&codebase, &mut actual, &options);
1797
1798 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1799 assert!(param.constraint.types.iter().any(|t| {
1800 if let TAtomic::Object(TObject::Named(named)) = t {
1801 named.name == ascii_lowercase_word(b"foo")
1802 } else {
1803 false
1804 }
1805 }));
1806 }
1807 }
1808
1809 #[test]
1810 fn test_expand_nested_generic_constraint() {
1811 let code = "<?php class Foo {} class Bar {}";
1812 let codebase = create_test_codebase(code);
1813
1814 let container =
1815 TNamedObject::new_with_type_parameters(ascii_lowercase_word(b"container"), Some(vec![make_self_object()]));
1816 let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1817
1818 let generic = TGenericParameter::new(
1819 word("T"),
1820 Arc::new(constraint),
1821 GenericParent::ClassLike(ascii_lowercase_word(b"bar")),
1822 );
1823 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1824
1825 let options = options_with_self("Foo");
1826 let mut actual = input;
1827 expand_union(&codebase, &mut actual, &options);
1828
1829 if let TAtomic::GenericParameter(param) = &actual.types[0]
1830 && let TAtomic::Object(TObject::Named(named)) = ¶m.constraint.types[0]
1831 && let Some(params) = &named.type_parameters
1832 {
1833 assert!(params[0].types.iter().any(|t| {
1834 if let TAtomic::Object(TObject::Named(named)) = t {
1835 named.name == ascii_lowercase_word(b"foo")
1836 } else {
1837 false
1838 }
1839 }));
1840 }
1841 }
1842
1843 #[test]
1844 fn test_expand_generic_with_intersection() {
1845 let code = "<?php
1846 interface Stringable {}
1847 class Foo {}
1848 ";
1849 let codebase = create_test_codebase(code);
1850
1851 let mut generic = TGenericParameter::new(
1852 word("T"),
1853 Arc::new(make_self_object()),
1854 GenericParent::ClassLike(ascii_lowercase_word(b"foo")),
1855 );
1856 generic.intersection_types =
1857 Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_word(b"stringable"))))]);
1858 let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1859
1860 let options = options_with_self("Foo");
1861 let mut actual = input;
1862 expand_union(&codebase, &mut actual, &options);
1863
1864 if let TAtomic::GenericParameter(param) = &actual.types[0] {
1865 assert!(param.intersection_types.is_some());
1866 assert!(param.constraint.types.iter().any(|t| {
1867 if let TAtomic::Object(TObject::Named(named)) = t {
1868 named.name == ascii_lowercase_word(b"foo")
1869 } else {
1870 false
1871 }
1872 }));
1873 }
1874 }
1875
1876 #[test]
1877 fn test_expand_class_string_of_self() {
1878 let code = "<?php class Foo {}";
1879 let codebase = create_test_codebase(code);
1880
1881 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
1882 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1883 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1884
1885 let options = options_with_self("Foo");
1886 let mut actual = input;
1887 expand_union(&codebase, &mut actual, &options);
1888
1889 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1890 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1891 {
1892 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
1893 }
1894 }
1895
1896 #[test]
1897 fn test_expand_class_string_of_static() {
1898 let code = "<?php class Foo {}";
1899 let codebase = create_test_codebase(code);
1900
1901 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("static")))));
1902 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1903 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1904
1905 let options = options_with_static("Foo");
1906 let mut actual = input;
1907 expand_union(&codebase, &mut actual, &options);
1908
1909 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1910 && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1911 {
1912 assert_eq!(named.name, ascii_lowercase_word(b"foo"));
1913 }
1914 }
1915
1916 #[test]
1917 fn test_expand_interface_string_of_type() {
1918 let code = "<?php interface MyInterface {}";
1919 let codebase = create_test_codebase(code);
1920
1921 let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))));
1922 let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1923 let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1924
1925 let options = options_with_self("MyInterface");
1926 let mut actual = input;
1927 expand_union(&codebase, &mut actual, &options);
1928
1929 if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1930 &actual.types[0]
1931 {
1932 assert!(matches!(kind, TClassLikeStringKind::Interface));
1933 if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1934 assert_eq!(named.name, ascii_lowercase_word(b"myinterface"));
1935 }
1936 }
1937 }
1938
1939 #[test]
1940 fn test_expand_member_reference_wildcard_constants() {
1941 let code = "<?php
1942 class Foo {
1943 public const A = 1;
1944 public const B = 2;
1945 }
1946 ";
1947 let codebase = create_test_codebase(code);
1948
1949 let reference = TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Wildcard);
1950 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1951
1952 let mut actual = input;
1953 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1954
1955 assert!(!actual.types.is_empty());
1956 }
1957
1958 #[test]
1959 fn test_expand_member_reference_wildcard_enum_cases() {
1960 let code = "<?php
1961 enum Status {
1962 case Active;
1963 case Inactive;
1964 }
1965 ";
1966 let codebase = create_test_codebase(code);
1967
1968 let reference = TReference::new_member(ascii_lowercase_word(b"status"), TReferenceMemberSelector::Wildcard);
1969 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1970
1971 let mut actual = input;
1972 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1973
1974 assert_eq!(actual.types.len(), 2);
1975 assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1976 }
1977
1978 #[test]
1979 fn test_expand_member_reference_starts_with() {
1980 let code = "<?php
1981 class Foo {
1982 public const STATUS_ACTIVE = 1;
1983 public const STATUS_INACTIVE = 2;
1984 public const OTHER = 3;
1985 }
1986 ";
1987 let codebase = create_test_codebase(code);
1988
1989 let reference =
1990 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::StartsWith(word("STATUS_")));
1991 let input = TUnion::from_atomic(TAtomic::Reference(reference));
1992
1993 let mut actual = input;
1994 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1995
1996 assert!(!actual.types.is_empty());
1997 }
1998
1999 #[test]
2000 fn test_expand_member_reference_ends_with() {
2001 let code = "<?php
2002 class Foo {
2003 public const READ_ERROR = 1;
2004 public const WRITE_ERROR = 2;
2005 public const SUCCESS = 0;
2006 }
2007 ";
2008 let codebase = create_test_codebase(code);
2009
2010 let reference =
2011 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::EndsWith(word("_ERROR")));
2012 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2013
2014 let mut actual = input;
2015 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2016
2017 assert!(!actual.types.is_empty());
2018 }
2019
2020 #[test]
2021 fn test_expand_member_reference_identifier_constant() {
2022 let code = "<?php
2023 class Foo {
2024 public const BAR = 42;
2025 }
2026 ";
2027 let codebase = create_test_codebase(code);
2028
2029 let reference =
2030 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("BAR")));
2031 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2032
2033 let mut actual = input;
2034 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2035
2036 assert_eq!(actual.types.len(), 1);
2037 }
2038
2039 #[test]
2040 fn test_expand_member_reference_identifier_enum_case() {
2041 let code = "<?php
2042 enum Status {
2043 case Active;
2044 }
2045 ";
2046 let codebase = create_test_codebase(code);
2047
2048 let reference = TReference::new_member(
2049 ascii_lowercase_word(b"status"),
2050 TReferenceMemberSelector::Identifier(word("Active")),
2051 );
2052 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2053
2054 let mut actual = input;
2055 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2056
2057 assert_eq!(actual.types.len(), 1);
2058 assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
2059 }
2060
2061 #[test]
2062 fn test_expand_member_reference_unknown_class() {
2063 let codebase = CodebaseMetadata::new();
2064
2065 let reference = TReference::new_member(word("NonExistent"), TReferenceMemberSelector::Identifier(word("FOO")));
2066 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2067
2068 let mut actual = input;
2069 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2070
2071 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2072 }
2073
2074 #[test]
2075 fn test_expand_member_reference_unknown_member() {
2076 let code = "<?php class Foo {}";
2077 let codebase = create_test_codebase(code);
2078
2079 let reference = TReference::new_member(
2080 ascii_lowercase_word(b"foo"),
2081 TReferenceMemberSelector::Identifier(word("NONEXISTENT")),
2082 );
2083 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2084
2085 let mut actual = input;
2086 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2087
2088 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
2089 }
2090
2091 #[test]
2092 fn test_expand_member_reference_constant_with_inferred_type() {
2093 let code = r#"<?php
2094 class Foo {
2095 public const VALUE = "hello";
2096 }
2097 "#;
2098 let codebase = create_test_codebase(code);
2099
2100 let reference =
2101 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2102 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2103
2104 let mut actual = input;
2105 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2106
2107 assert_eq!(actual.types.len(), 1);
2108 }
2109
2110 #[test]
2111 fn test_expand_member_reference_constant_with_type_metadata() {
2112 let code = "<?php
2113 class Foo {
2114 /** @var int */
2115 public const VALUE = 42;
2116 }
2117 ";
2118 let codebase = create_test_codebase(code);
2119
2120 let reference =
2121 TReference::new_member(ascii_lowercase_word(b"foo"), TReferenceMemberSelector::Identifier(word("VALUE")));
2122 let input = TUnion::from_atomic(TAtomic::Reference(reference));
2123
2124 let mut actual = input;
2125 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2126
2127 assert_eq!(actual.types.len(), 1);
2128 }
2129
2130 #[test]
2131 fn test_expand_conditional_both_branches() {
2132 let code = "<?php class Foo {} class Bar {}";
2133 let codebase = create_test_codebase(code);
2134
2135 let conditional = TConditional::new(
2136 Arc::new(get_mixed()),
2137 Arc::new(get_string()),
2138 Arc::new(make_self_object()),
2139 Arc::new(make_self_object()),
2140 false,
2141 );
2142 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2143
2144 let options = options_with_self("Foo");
2145 let mut actual = input;
2146 expand_union(&codebase, &mut actual, &options);
2147
2148 assert!(actual.types.iter().any(|t| {
2149 if let TAtomic::Object(TObject::Named(named)) = t {
2150 named.name == ascii_lowercase_word(b"foo")
2151 } else {
2152 false
2153 }
2154 }));
2155 }
2156
2157 #[test]
2158 fn test_expand_conditional_with_self_in_then() {
2159 let code = "<?php class Foo {}";
2160 let codebase = create_test_codebase(code);
2161
2162 let conditional = TConditional::new(
2163 Arc::new(get_mixed()),
2164 Arc::new(get_string()),
2165 Arc::new(make_self_object()),
2166 Arc::new(get_int()),
2167 false,
2168 );
2169 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2170
2171 let options = options_with_self("Foo");
2172 let mut actual = input;
2173 expand_union(&codebase, &mut actual, &options);
2174
2175 assert!(!actual.types.is_empty());
2176 }
2177
2178 #[test]
2179 fn test_expand_conditional_with_self_in_otherwise() {
2180 let code = "<?php class Foo {}";
2181 let codebase = create_test_codebase(code);
2182
2183 let conditional = TConditional::new(
2184 Arc::new(get_mixed()),
2185 Arc::new(get_string()),
2186 Arc::new(get_int()),
2187 Arc::new(make_self_object()),
2188 false,
2189 );
2190 let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2191
2192 let options = options_with_self("Foo");
2193 let mut actual = input;
2194 expand_union(&codebase, &mut actual, &options);
2195
2196 assert!(!actual.types.is_empty());
2197 }
2198
2199 #[test]
2200 fn test_expand_simple_alias() {
2201 let code = "<?php
2202 class Foo {
2203 /** @phpstan-type MyInt = int */
2204 }
2205 ";
2206 let codebase = create_test_codebase(code);
2207
2208 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MyInt"));
2209 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2210
2211 let mut actual = input;
2212 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2213
2214 assert!(!actual.types.is_empty());
2215 }
2216
2217 #[test]
2218 fn test_expand_nested_alias() {
2219 let code = "<?php
2220 class Foo {
2221 /** @phpstan-type Inner = int */
2222 /** @phpstan-type Outer = Inner */
2223 }
2224 ";
2225 let codebase = create_test_codebase(code);
2226
2227 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("Outer"));
2228 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2229
2230 let mut actual = input;
2231 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2232
2233 assert!(!actual.types.is_empty());
2234 }
2235
2236 #[test]
2237 fn test_expand_alias_cycle_detection() {
2238 let code = "<?php
2239 /** @phpstan-type SelfRef = int|array<int, SelfRef> */
2240 class Foo {}
2241 ";
2242 let codebase = create_test_codebase(code);
2243
2244 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("SelfRef"));
2245 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2246
2247 let mut actual = input;
2248 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2249
2250 assert!(!actual.types.is_empty());
2251 }
2252
2253 #[test]
2254 fn test_expand_alias_unknown() {
2255 let codebase = CodebaseMetadata::new();
2256
2257 let alias = TAlias::new(word("NonExistent"), word("Unknown"));
2258 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2259
2260 let mut actual = input;
2261 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2262
2263 assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2264 }
2265
2266 #[test]
2267 fn test_expand_alias_direct_self_reference() {
2268 let code = "<?php
2269 /** @psalm-type SelfAlias = SelfAlias */
2270 class Foo {}
2271 ";
2272 let codebase = create_test_codebase(code);
2273
2274 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("SelfAlias"));
2275 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2276
2277 let options = TypeExpansionOptions {
2278 evaluate_conditional_types: true,
2279 expand_generic: true,
2280 expand_templates: true,
2281 ..Default::default()
2282 };
2283 let mut actual = input;
2284 expand_union(&codebase, &mut actual, &options);
2285
2286 assert!(!actual.types.is_empty());
2287 }
2288
2289 #[test]
2290 fn test_expand_alias_with_self_inside() {
2291 let code = "<?php
2292 class Foo {
2293 /** @phpstan-type MySelf = self */
2294 }
2295 ";
2296 let codebase = create_test_codebase(code);
2297
2298 let alias = TAlias::new(ascii_lowercase_word(b"foo"), word("MySelf"));
2299 let input = TUnion::from_atomic(TAtomic::Alias(alias));
2300
2301 let options = options_with_self("Foo");
2302 let mut actual = input;
2303 expand_union(&codebase, &mut actual, &options);
2304
2305 assert!(!actual.types.is_empty());
2306 }
2307
2308 #[test]
2309 fn test_expand_key_of_array() {
2310 let codebase = CodebaseMetadata::new();
2311
2312 let mut keyed = TKeyedArray::new();
2313 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2314 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2315
2316 let key_of = TKeyOf::new(Arc::new(array_type));
2317 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2318
2319 let mut actual = input;
2320 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2321
2322 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
2323 }
2324
2325 #[test]
2326 fn test_expand_key_of_with_self() {
2327 let code = "<?php class Foo {}";
2328 let codebase = create_test_codebase(code);
2329
2330 let mut keyed = TKeyedArray::new();
2331 keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2332 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2333
2334 let key_of = TKeyOf::new(Arc::new(array_type));
2335 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2336
2337 let options = options_with_self("Foo");
2338 let mut actual = input;
2339 expand_union(&codebase, &mut actual, &options);
2340
2341 assert!(!actual.types.is_empty());
2342 }
2343
2344 #[test]
2345 fn test_expand_value_of_array() {
2346 let codebase = CodebaseMetadata::new();
2347
2348 let mut keyed = TKeyedArray::new();
2349 keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2350 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2351
2352 let value_of = TValueOf::new(Arc::new(array_type));
2353 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2354
2355 let mut actual = input;
2356 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2357
2358 assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2359 }
2360
2361 #[test]
2362 fn test_expand_value_of_enum() {
2363 let code = "<?php
2364 enum Status: string {
2365 case Active = 'active';
2366 case Inactive = 'inactive';
2367 }
2368 ";
2369 let codebase = create_test_codebase(code);
2370
2371 let enum_type =
2372 TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_word(b"status")))));
2373
2374 let value_of = TValueOf::new(Arc::new(enum_type));
2375 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2376
2377 let mut actual = input;
2378 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2379
2380 assert!(!actual.types.is_empty());
2381 }
2382
2383 #[test]
2384 fn test_expand_index_access() {
2385 let codebase = CodebaseMetadata::new();
2386
2387 use crate::ttype::atomic::array::key::ArrayKey;
2388 use std::collections::BTreeMap;
2389
2390 let mut keyed = TKeyedArray::new();
2391 let mut known_items = BTreeMap::new();
2392 known_items.insert(ArrayKey::String(word("key")), (false, get_int()));
2393 keyed.known_items = Some(known_items);
2394 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2395
2396 use crate::ttype::get_literal_string;
2397 let index_type = get_literal_string(word("key"));
2398
2399 let index_access = TIndexAccess::new(array_type, index_type);
2400 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2401
2402 let mut actual = input;
2403 expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2404
2405 assert!(!actual.types.is_empty());
2406 }
2407
2408 #[test]
2409 fn test_expand_index_access_with_self() {
2410 let code = "<?php class Foo {}";
2411 let codebase = create_test_codebase(code);
2412
2413 use crate::ttype::atomic::array::key::ArrayKey;
2414 use std::collections::BTreeMap;
2415
2416 let mut keyed = TKeyedArray::new();
2417 let mut known_items = BTreeMap::new();
2418 known_items.insert(ArrayKey::String(word("key")), (false, make_self_object()));
2419 keyed.known_items = Some(known_items);
2420 let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2421
2422 use crate::ttype::get_literal_string;
2423 let index_type = get_literal_string(word("key"));
2424
2425 let index_access = TIndexAccess::new(array_type, index_type);
2426 let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2427
2428 let options = options_with_self("Foo");
2429 let mut actual = input;
2430 expand_union(&codebase, &mut actual, &options);
2431
2432 assert!(!actual.types.is_empty());
2433 }
2434
2435 #[test]
2436 fn test_expand_iterable_key_type() {
2437 let code = "<?php class Foo {}";
2438 let codebase = create_test_codebase(code);
2439
2440 let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2441 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2442
2443 let options = options_with_self("Foo");
2444 let mut actual = input;
2445 expand_union(&codebase, &mut actual, &options);
2446
2447 if let TAtomic::Iterable(iter) = &actual.types[0] {
2448 assert!(iter.get_key_type().types.iter().any(|t| {
2449 if let TAtomic::Object(TObject::Named(named)) = t {
2450 named.name == ascii_lowercase_word(b"foo")
2451 } else {
2452 false
2453 }
2454 }));
2455 }
2456 }
2457
2458 #[test]
2459 fn test_expand_iterable_value_type() {
2460 let code = "<?php class Foo {}";
2461 let codebase = create_test_codebase(code);
2462
2463 let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2464 let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2465
2466 let options = options_with_self("Foo");
2467 let mut actual = input;
2468 expand_union(&codebase, &mut actual, &options);
2469
2470 if let TAtomic::Iterable(iter) = &actual.types[0] {
2471 assert!(iter.get_value_type().types.iter().any(|t| {
2472 if let TAtomic::Object(TObject::Named(named)) = t {
2473 named.name == ascii_lowercase_word(b"foo")
2474 } else {
2475 false
2476 }
2477 }));
2478 }
2479 }
2480
2481 #[test]
2482 fn test_get_signature_of_function() {
2483 let code = r#"<?php
2484 function myFunc(int $a): string { return ""; }
2485 "#;
2486 let codebase = create_test_codebase(code);
2487
2488 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2489
2490 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2491 assert!(sig.is_some());
2492
2493 let sig = sig.unwrap();
2494 assert_eq!(sig.get_parameters().len(), 1);
2495 assert!(sig.get_return_type().is_some());
2496 }
2497
2498 #[test]
2499 fn test_get_signature_of_method() {
2500 let code = "<?php
2501 class Foo {
2502 public function bar(string $s): int { return 0; }
2503 }
2504 ";
2505 let codebase = create_test_codebase(code);
2506
2507 let id = FunctionLikeIdentifier::Method(ascii_lowercase_word(b"foo"), ascii_lowercase_word(b"bar"));
2508
2509 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2510 assert!(sig.is_some());
2511
2512 let sig = sig.unwrap();
2513 assert_eq!(sig.get_parameters().len(), 1);
2514 }
2515
2516 #[test]
2517 fn test_get_signature_of_closure() {
2518 let codebase = CodebaseMetadata::new();
2519
2520 let id = FunctionLikeIdentifier::Closure(word(b"{closure:test.php:1:1}"));
2521 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2522
2523 assert!(sig.is_none());
2524 }
2525
2526 #[test]
2527 fn test_get_atomic_of_function() {
2528 let code = "<?php
2529 function myFunc(): void {}
2530 ";
2531 let codebase = create_test_codebase(code);
2532
2533 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"myfunc"));
2534
2535 let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2536 assert!(atomic.is_some());
2537 assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2538 }
2539
2540 #[test]
2541 fn test_get_signature_with_parameters() {
2542 let code = "<?php
2543 function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2544 ";
2545 let codebase = create_test_codebase(code);
2546
2547 let id = FunctionLikeIdentifier::Function(ascii_lowercase_word(b"multiparam"));
2548
2549 let sig = get_signature_of_function_like_identifier(&id, &codebase);
2550 assert!(sig.is_some());
2551
2552 let sig = sig.unwrap();
2553 assert_eq!(sig.get_parameters().len(), 3);
2554
2555 let third_param = &sig.get_parameters()[2];
2556 assert!(third_param.has_default());
2557 }
2558
2559 #[test]
2560 fn test_expand_preserves_by_reference_flag() {
2561 let code = "<?php class Foo {}";
2562 let codebase = create_test_codebase(code);
2563
2564 let mut input = make_self_object();
2565 input.flags.insert(UnionFlags::BY_REFERENCE);
2566
2567 let options = options_with_self("Foo");
2568 let mut actual = input.clone();
2569 expand_union(&codebase, &mut actual, &options);
2570
2571 assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2572 }
2573
2574 #[test]
2575 fn test_expand_preserves_possibly_undefined_flag() {
2576 let code = "<?php class Foo {}";
2577 let codebase = create_test_codebase(code);
2578
2579 let mut input = make_self_object();
2580 input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2581
2582 let options = options_with_self("Foo");
2583 let mut actual = input.clone();
2584 expand_union(&codebase, &mut actual, &options);
2585
2586 assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2587 }
2588
2589 #[test]
2590 fn test_expand_multiple_self_in_union() {
2591 let code = "<?php class Foo {}";
2592 let codebase = create_test_codebase(code);
2593
2594 let input = TUnion::from_vec(vec![
2595 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2596 TAtomic::Object(TObject::Named(TNamedObject::new(word("self")))),
2597 ]);
2598
2599 let options = options_with_self("Foo");
2600 let mut actual = input;
2601 expand_union(&codebase, &mut actual, &options);
2602
2603 assert!(actual.types.len() <= 2);
2604 }
2605
2606 #[test]
2607 fn test_expand_deeply_nested_types() {
2608 let code = "<?php class Foo {}";
2609 let codebase = create_test_codebase(code);
2610
2611 let inner = TList::new(Arc::new(make_self_object()));
2612 let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2613 let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2614 let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2615
2616 let options = options_with_self("Foo");
2617 let mut actual = input;
2618 expand_union(&codebase, &mut actual, &options);
2619
2620 if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2621 && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2622 && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2623 {
2624 assert!(inner.element_type.types.iter().any(|t| {
2625 if let TAtomic::Object(TObject::Named(named)) = t {
2626 named.name == ascii_lowercase_word(b"foo")
2627 } else {
2628 false
2629 }
2630 }));
2631 }
2632 }
2633
2634 #[test]
2635 fn test_expand_with_all_options_disabled() {
2636 let code = "<?php class Foo {}";
2637 let codebase = create_test_codebase(code);
2638
2639 let input = make_self_object();
2640 let options = TypeExpansionOptions {
2641 self_class: None,
2642 static_class_type: StaticClassType::None,
2643 parent_class: None,
2644 evaluate_class_constants: false,
2645 evaluate_conditional_types: false,
2646 function_is_final: false,
2647 expand_generic: false,
2648 expand_templates: false,
2649 };
2650
2651 let mut actual = input;
2652 expand_union(&codebase, &mut actual, &options);
2653
2654 assert!(actual.types.iter().any(|t| {
2655 if let TAtomic::Object(TObject::Named(named)) = t { named.name == word("self") } else { false }
2656 }));
2657 }
2658
2659 #[test]
2660 fn test_expand_already_expanded_type() {
2661 let code = "<?php class Foo {}";
2662 let codebase = create_test_codebase(code);
2663
2664 let input = make_named_object("Foo");
2665 let options = options_with_self("Foo");
2666
2667 let mut actual = input;
2668 expand_union(&codebase, &mut actual, &options);
2669
2670 let mut actual2 = actual.clone();
2671 expand_union(&codebase, &mut actual2, &options);
2672
2673 assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2674 }
2675
2676 #[test]
2677 fn test_expand_complex_generic_class() {
2678 let code = "<?php
2679 /**
2680 * @template T
2681 * @template U
2682 */
2683 class Container {}
2684 ";
2685 let codebase = create_test_codebase(code);
2686
2687 let named = TNamedObject::new_with_type_parameters(
2688 ascii_lowercase_word(b"container"),
2689 Some(vec![make_self_object(), make_static_object()]),
2690 );
2691 let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2692
2693 let options = TypeExpansionOptions {
2694 self_class: Some(ascii_lowercase_word(b"foo")),
2695 static_class_type: StaticClassType::Name(ascii_lowercase_word(b"bar")),
2696 ..Default::default()
2697 };
2698
2699 let mut actual = input;
2700 expand_union(&codebase, &mut actual, &options);
2701
2702 if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2703 && let Some(params) = &named.type_parameters
2704 {
2705 assert!(params[0].types.iter().any(|t| {
2706 if let TAtomic::Object(TObject::Named(named)) = t {
2707 named.name == ascii_lowercase_word(b"foo")
2708 } else {
2709 false
2710 }
2711 }));
2712 assert!(params[1].types.iter().any(|t| {
2713 if let TAtomic::Object(TObject::Named(named)) = t {
2714 named.name == ascii_lowercase_word(b"bar")
2715 } else {
2716 false
2717 }
2718 }));
2719 }
2720 }
2721}